vitess.io/vitess@v0.16.2/examples/compose/vtcompose/vtcompose.go (about)

     1  /*
     2   * Copyright 2020 The Vitess Authors.
     3  
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7  
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9  
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package main
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/json"
    22  	"fmt"
    23  	"math"
    24  	"os"
    25  	"regexp"
    26  	"strconv"
    27  	"strings"
    28  
    29  	jsonpatch "github.com/evanphx/json-patch"
    30  	yamlpatch "github.com/krishicks/yaml-patch"
    31  	"github.com/spf13/pflag"
    32  
    33  	"vitess.io/vitess/go/vt/log"
    34  )
    35  
    36  const (
    37  	DefaultWebPort        = 8080
    38  	webPortUsage          = "Web port to be used"
    39  	DefaultGrpcPort       = 15999
    40  	gRpcPortUsage         = "gRPC port to be used"
    41  	DefaultMysqlPort      = 15306
    42  	mySqlPortUsage        = "mySql port to be used"
    43  	DefaultKeyspaceData   = "test_keyspace:2:1:create_messages.sql,create_tokens.sql;unsharded_keyspace:0:0:create_dinosaurs.sql,create_eggs.sql"
    44  	keyspaceDataUsage     = "List of keyspace_name/external_db_name:num_of_shards:num_of_replica_tablets:schema_files:<optional>lookup_keyspace_name separated by ';'"
    45  	DefaultCell           = "test"
    46  	cellUsage             = "Vitess Cell name"
    47  	DefaultExternalDbData = ""
    48  	externalDbDataUsage   = "List of Data corresponding to external DBs. List of <external_db_name>,<DB_HOST>,<DB_PORT>,<DB_USER>,<DB_PASS>,<DB_CHARSET> separated by ';'"
    49  	DefaultTopologyFlags  = "--topo_implementation consul --topo_global_server_address consul1:8500 --topo_global_root vitess/global"
    50  	topologyFlagsUsage    = "Vitess Topology Flags config"
    51  )
    52  
    53  var (
    54  	tabletsUsed           = 0
    55  	tablesPath            = "tables/"
    56  	baseDockerComposeFile = pflag.String("base_yaml", "vtcompose/docker-compose.base.yml", "Starting docker-compose yaml")
    57  	baseVschemaFile       = pflag.String("base_vschema", "vtcompose/base_vschema.json", "Starting vschema json")
    58  
    59  	topologyFlags  = pflag.String("topologyFlags", DefaultTopologyFlags, topologyFlagsUsage)
    60  	webPort        = pflag.Int("webPort", DefaultWebPort, webPortUsage)
    61  	gRpcPort       = pflag.Int("gRpcPort", DefaultGrpcPort, gRpcPortUsage)
    62  	mySqlPort      = pflag.Int("mySqlPort", DefaultMysqlPort, mySqlPortUsage)
    63  	cell           = pflag.String("cell", DefaultCell, cellUsage)
    64  	keyspaceData   = pflag.String("keyspaceData", DefaultKeyspaceData, keyspaceDataUsage)
    65  	externalDbData = pflag.String("externalDbData", DefaultExternalDbData, externalDbDataUsage)
    66  )
    67  
    68  type vtOptions struct {
    69  	webPort       int
    70  	gRpcPort      int
    71  	mySqlPort     int
    72  	topologyFlags string
    73  	cell          string
    74  }
    75  
    76  type keyspaceInfo struct {
    77  	keyspace        string
    78  	shards          int
    79  	replicaTablets  int
    80  	lookupKeyspace  string
    81  	useLookups      bool
    82  	schemaFile      *os.File
    83  	schemaFileNames []string
    84  }
    85  
    86  type externalDbInfo struct {
    87  	dbName    string
    88  	dbHost    string
    89  	dbPort    string
    90  	dbUser    string
    91  	dbPass    string
    92  	dbCharset string
    93  }
    94  
    95  func newKeyspaceInfo(
    96  	keyspace string,
    97  	shards int,
    98  	replicaTablets int,
    99  	schemaFiles []string,
   100  	lookupKeyspace string,
   101  ) keyspaceInfo {
   102  	k := keyspaceInfo{
   103  		keyspace:        keyspace,
   104  		shards:          shards,
   105  		replicaTablets:  replicaTablets,
   106  		schemaFileNames: schemaFiles,
   107  		lookupKeyspace:  lookupKeyspace,
   108  	}
   109  	if len(strings.TrimSpace(lookupKeyspace)) == 0 {
   110  		k.useLookups = false
   111  	} else {
   112  		k.useLookups = true
   113  	}
   114  
   115  	k.schemaFile = nil
   116  	return k
   117  }
   118  
   119  func newExternalDbInfo(dbName, dbHost, dbPort, dbUser, dbPass, dbCharset string) externalDbInfo {
   120  	return externalDbInfo{
   121  		dbName:    dbName,
   122  		dbHost:    dbHost,
   123  		dbPort:    dbPort,
   124  		dbUser:    dbUser,
   125  		dbPass:    dbPass,
   126  		dbCharset: dbCharset,
   127  	}
   128  }
   129  
   130  func parseKeyspaceInfo(keyspaceData string) map[string]keyspaceInfo {
   131  	keyspaceInfoMap := make(map[string]keyspaceInfo)
   132  
   133  	for _, v := range strings.Split(keyspaceData, ";") {
   134  		tokens := strings.Split(v, ":")
   135  		shards, _ := strconv.Atoi(tokens[1])
   136  		replicaTablets, _ := strconv.Atoi(tokens[2])
   137  		schemaFileNames := []string{}
   138  		// Make schemafiles argument optional
   139  		if len(tokens) > 3 {
   140  			f := func(c rune) bool {
   141  				return c == ','
   142  			}
   143  			schemaFileNames = strings.FieldsFunc(tokens[3], f)
   144  		}
   145  
   146  		if len(tokens) > 4 {
   147  			keyspaceInfoMap[tokens[0]] = newKeyspaceInfo(tokens[0], shards, replicaTablets, schemaFileNames, tokens[4])
   148  		} else {
   149  			keyspaceInfoMap[tokens[0]] = newKeyspaceInfo(tokens[0], shards, replicaTablets, schemaFileNames, "")
   150  		}
   151  	}
   152  
   153  	return keyspaceInfoMap
   154  }
   155  
   156  func parseExternalDbData(externalDbData string) map[string]externalDbInfo {
   157  	externalDbInfoMap := make(map[string]externalDbInfo)
   158  	for _, v := range strings.Split(externalDbData, ";") {
   159  		tokens := strings.Split(v, ":")
   160  		if len(tokens) > 1 {
   161  			externalDbInfoMap[tokens[0]] =
   162  				newExternalDbInfo(tokens[0], tokens[1], tokens[2], tokens[3], tokens[4], tokens[5])
   163  		}
   164  	}
   165  
   166  	return externalDbInfoMap
   167  }
   168  
   169  func main() {
   170  	pflag.Parse()
   171  	keyspaceInfoMap := parseKeyspaceInfo(*keyspaceData)
   172  	externalDbInfoMap := parseExternalDbData(*externalDbData)
   173  	vtOpts := vtOptions{
   174  		webPort:       *webPort,
   175  		gRpcPort:      *gRpcPort,
   176  		mySqlPort:     *mySqlPort,
   177  		topologyFlags: *topologyFlags,
   178  		cell:          *cell,
   179  	}
   180  
   181  	// Write schemaFile.
   182  	for k, v := range keyspaceInfoMap {
   183  		if _, ok := externalDbInfoMap[k]; !ok {
   184  			v.schemaFile = createFile(fmt.Sprintf("%s%s_schema_file.sql", tablesPath, v.keyspace))
   185  			appendToSqlFile(v.schemaFileNames, v.schemaFile)
   186  			closeFile(v.schemaFile)
   187  		}
   188  	}
   189  
   190  	// Vschema Patching
   191  	for k, keyspaceData := range keyspaceInfoMap {
   192  		vSchemaFile := readFile(*baseVschemaFile)
   193  		if keyspaceData.shards == 0 {
   194  			vSchemaFile = applyJsonInMemoryPatch(vSchemaFile, `[{"op": "replace","path": "/sharded", "value": false}]`)
   195  		}
   196  
   197  		// Check if it is an external_db
   198  		if _, ok := externalDbInfoMap[k]; ok {
   199  			//This is no longer necessary, but we'll keep it for reference
   200  			//https://github.com/vitessio/vitess/pull/4868, https://github.com/vitessio/vitess/pull/5010
   201  			//vSchemaFile = applyJsonInMemoryPatch(vSchemaFile,`[{"op": "add","path": "/tables/*", "value": {}}]`)
   202  		} else {
   203  			var primaryTableColumns map[string]string
   204  			vSchemaFile, primaryTableColumns = addTablesVschemaPatch(vSchemaFile, keyspaceData.schemaFileNames)
   205  
   206  			if keyspaceData.useLookups {
   207  				lookup := keyspaceInfoMap[keyspaceData.lookupKeyspace]
   208  				vSchemaFile = addLookupDataToVschema(vSchemaFile, lookup.schemaFileNames, primaryTableColumns, lookup.keyspace)
   209  			}
   210  		}
   211  
   212  		writeVschemaFile(vSchemaFile, fmt.Sprintf("%s_vschema.json", keyspaceData.keyspace))
   213  	}
   214  
   215  	// Docker Compose File Patches
   216  	dockerComposeFile := readFile(*baseDockerComposeFile)
   217  	dockerComposeFile = applyDockerComposePatches(dockerComposeFile, keyspaceInfoMap, externalDbInfoMap, vtOpts)
   218  	writeFile(dockerComposeFile, "docker-compose.yml")
   219  }
   220  
   221  func applyFilePatch(dockerYaml []byte, patchFile string) []byte {
   222  	yamlPatch, err := os.ReadFile(patchFile)
   223  	if err != nil {
   224  		log.Fatalf("reading yaml patch file %s: %s", patchFile, err)
   225  	}
   226  
   227  	patch, err := yamlpatch.DecodePatch(yamlPatch)
   228  	if err != nil {
   229  		log.Fatalf("decoding patch failed: %s", err)
   230  	}
   231  
   232  	bs, err := patch.Apply(dockerYaml)
   233  	if err != nil {
   234  		log.Fatalf("applying patch failed: %s", err)
   235  	}
   236  	return bs
   237  }
   238  
   239  func applyJsonInMemoryPatch(vSchemaFile []byte, patchString string) []byte {
   240  	patch, err := jsonpatch.DecodePatch([]byte(patchString))
   241  	if err != nil {
   242  		log.Fatalf("decoding vschema patch failed: %s", err)
   243  	}
   244  
   245  	modified, err := patch.Apply(vSchemaFile)
   246  	if err != nil {
   247  		log.Fatalf("applying vschema patch failed: %s", err)
   248  	}
   249  	return modified
   250  }
   251  
   252  func applyInMemoryPatch(dockerYaml []byte, patchString string) []byte {
   253  	patch, err := yamlpatch.DecodePatch([]byte(patchString))
   254  	if err != nil {
   255  		log.Fatalf("decoding patch failed: %s", err)
   256  	}
   257  
   258  	bs, err := patch.Apply(dockerYaml)
   259  	if err != nil {
   260  		log.Fatalf("applying patch failed: %s", err)
   261  	}
   262  	return bs
   263  }
   264  
   265  func createFile(filePath string) *os.File {
   266  	f, err := os.Create(filePath)
   267  	if err != nil {
   268  		log.Fatalf("creating %s %s", filePath, err)
   269  	}
   270  	return f
   271  }
   272  
   273  func readFile(filePath string) []byte {
   274  	file, err := os.ReadFile(filePath)
   275  
   276  	if err != nil {
   277  		log.Fatalf("reading %s: %s", filePath, err)
   278  	}
   279  
   280  	return file
   281  }
   282  
   283  func closeFile(file *os.File) {
   284  	err := file.Close()
   285  	if err != nil {
   286  		log.Fatalf("Closing schema_file.sql %s", err)
   287  	}
   288  }
   289  
   290  func handleError(err error) {
   291  	if err != nil {
   292  		log.Fatalf("Error: %s", err)
   293  	}
   294  }
   295  
   296  func appendToSqlFile(schemaFileNames []string, f *os.File) {
   297  	for _, file := range schemaFileNames {
   298  		data, err := os.ReadFile(tablesPath + file)
   299  		if err != nil {
   300  			log.Fatalf("reading %s: %s", tablesPath+file, err)
   301  		}
   302  
   303  		_, err = f.Write(data)
   304  		handleError(err)
   305  
   306  		_, err = f.WriteString("\n\n")
   307  		handleError(err)
   308  
   309  		err = f.Sync()
   310  		handleError(err)
   311  	}
   312  }
   313  
   314  func getTableName(sqlFile string) string {
   315  	sqlFileData, err := os.ReadFile(sqlFile)
   316  	if err != nil {
   317  		log.Fatalf("reading sqlFile file %s: %s", sqlFile, err)
   318  	}
   319  
   320  	r, _ := regexp.Compile("CREATE TABLE ([a-z_-]*) \\(")
   321  	rs := r.FindStringSubmatch(string(sqlFileData))
   322  	// replace all ` from table name if exists
   323  	return strings.ReplaceAll(rs[1], "`", "")
   324  }
   325  
   326  func getPrimaryKey(sqlFile string) string {
   327  	sqlFileData, err := os.ReadFile(sqlFile)
   328  	if err != nil {
   329  		log.Fatalf("reading sqlFile file %s: %s", sqlFile, err)
   330  	}
   331  
   332  	r, _ := regexp.Compile("PRIMARY KEY \\((.*)\\).*")
   333  	rs := r.FindStringSubmatch(string(sqlFileData))
   334  
   335  	return rs[1]
   336  }
   337  
   338  func getKeyColumns(sqlFile string) string {
   339  	sqlFileData, err := os.ReadFile(sqlFile)
   340  	if err != nil {
   341  		log.Fatalf("reading sqlFile file %s: %s", sqlFile, err)
   342  	}
   343  
   344  	r, _ := regexp.Compile("[^PRIMARY] (KEY|UNIQUE KEY) .*\\((.*)\\).*")
   345  	rs := r.FindStringSubmatch(string(sqlFileData))
   346  	// replace all ` from column names if exists
   347  	return strings.ReplaceAll(rs[2], "`", "")
   348  }
   349  
   350  func addTablesVschemaPatch(vSchemaFile []byte, schemaFileNames []string) ([]byte, map[string]string) {
   351  	indexedColumns := ""
   352  	primaryTableColumns := make(map[string]string)
   353  	for _, fileName := range schemaFileNames {
   354  		tableName := getTableName(tablesPath + fileName)
   355  		indexedColumns = getPrimaryKey(tablesPath + fileName)
   356  		firstColumnName := strings.Split(indexedColumns, ", ")[0]
   357  		vSchemaFile = applyJsonInMemoryPatch(vSchemaFile, generatePrimaryVIndex(tableName, firstColumnName, "hash"))
   358  		primaryTableColumns[tableName] = firstColumnName
   359  	}
   360  
   361  	return vSchemaFile, primaryTableColumns
   362  }
   363  
   364  func addLookupDataToVschema(
   365  	vSchemaFile []byte,
   366  	schemaFileNames []string,
   367  	primaryTableColumns map[string]string,
   368  	keyspace string,
   369  ) []byte {
   370  	for _, fileName := range schemaFileNames {
   371  		tableName := fileName[7 : len(fileName)-4]
   372  		lookupTableOwner := ""
   373  
   374  		// Find owner of lookup table
   375  		for primaryTableName := range primaryTableColumns {
   376  			if strings.HasPrefix(tableName, primaryTableName) && len(primaryTableName) > len(lookupTableOwner) {
   377  				lookupTableOwner = primaryTableName
   378  			}
   379  		}
   380  
   381  		indexedColumns := getKeyColumns(tablesPath + fileName)
   382  		firstColumnName := strings.Split(indexedColumns, ", ")[0]
   383  
   384  		// Lookup patch under "tables"
   385  		vSchemaFile = applyJsonInMemoryPatch(vSchemaFile, addToColumnVIndexes(lookupTableOwner, firstColumnName, tableName))
   386  
   387  		// Generate Vschema lookup hash types
   388  		lookupHash := generateVschemaLookupHash(tableName, keyspace, firstColumnName, primaryTableColumns[lookupTableOwner], lookupTableOwner)
   389  		vSchemaFile = applyJsonInMemoryPatch(vSchemaFile, lookupHash)
   390  	}
   391  
   392  	return vSchemaFile
   393  }
   394  
   395  func writeVschemaFile(file []byte, fileName string) {
   396  	// Format json file
   397  	var buf bytes.Buffer
   398  	err := json.Indent(&buf, file, "", "\t")
   399  	handleError(err)
   400  	file = buf.Bytes()
   401  
   402  	writeFile(file, fileName)
   403  }
   404  
   405  func writeFile(file []byte, fileName string) {
   406  	err := os.WriteFile(fileName, file, 0644)
   407  	if err != nil {
   408  		log.Fatalf("writing %s %s", fileName, err)
   409  	}
   410  }
   411  
   412  func applyKeyspaceDependentPatches(
   413  	dockerComposeFile []byte,
   414  	keyspaceData keyspaceInfo,
   415  	externalDbInfoMap map[string]externalDbInfo,
   416  	opts vtOptions,
   417  ) []byte {
   418  	var externalDbInfo externalDbInfo
   419  	if val, ok := externalDbInfoMap[keyspaceData.keyspace]; ok {
   420  		externalDbInfo = val
   421  	}
   422  	tabAlias := 0 + tabletsUsed*100
   423  	shard := "-"
   424  	var primaryTablets []string
   425  	if tabletsUsed == 0 {
   426  		primaryTablets = append(primaryTablets, "101")
   427  	} else {
   428  		primaryTablets = append(primaryTablets, strconv.Itoa((tabletsUsed+1)*100+1))
   429  	}
   430  	interval := int(math.Floor(256 / float64(keyspaceData.shards)))
   431  
   432  	for i := 1; i < keyspaceData.shards; i++ {
   433  		primaryTablets = append(primaryTablets, strconv.Itoa((i+1)*100+1))
   434  	}
   435  
   436  	setKeyspaceDurabilityPolicy := generateSetKeyspaceDurabilityPolicy(primaryTablets, keyspaceData.keyspace, opts)
   437  	dockerComposeFile = applyInMemoryPatch(dockerComposeFile, setKeyspaceDurabilityPolicy)
   438  
   439  	schemaLoad := generateSchemaload(primaryTablets, "", keyspaceData.keyspace, externalDbInfo, opts)
   440  	dockerComposeFile = applyInMemoryPatch(dockerComposeFile, schemaLoad)
   441  
   442  	// Append Primary and Replica Tablets
   443  	if keyspaceData.shards < 2 {
   444  		tabAlias = tabAlias + 100
   445  		dockerComposeFile = applyTabletPatches(dockerComposeFile, tabAlias, shard, keyspaceData, externalDbInfoMap, opts)
   446  		dockerComposeFile = applyShardPatches(dockerComposeFile, tabAlias, shard, keyspaceData, externalDbInfoMap, opts)
   447  	} else {
   448  		// Determine shard range
   449  		for i := 0; i < keyspaceData.shards; i++ {
   450  			if i == 0 {
   451  				shard = fmt.Sprintf("-%x", interval)
   452  			} else if i == (keyspaceData.shards - 1) {
   453  				shard = fmt.Sprintf("%x-", interval*i)
   454  			} else {
   455  				shard = fmt.Sprintf("%x-%x", interval*(i), interval*(i+1))
   456  			}
   457  			tabAlias = tabAlias + 100
   458  			dockerComposeFile = applyTabletPatches(dockerComposeFile, tabAlias, shard, keyspaceData, externalDbInfoMap, opts)
   459  			dockerComposeFile = applyShardPatches(dockerComposeFile, tabAlias, shard, keyspaceData, externalDbInfoMap, opts)
   460  		}
   461  	}
   462  
   463  	tabletsUsed += len(primaryTablets)
   464  	return dockerComposeFile
   465  }
   466  
   467  func applyDefaultDockerPatches(
   468  	dockerComposeFile []byte,
   469  	keyspaceInfoMap map[string]keyspaceInfo,
   470  	externalDbInfoMap map[string]externalDbInfo,
   471  	opts vtOptions,
   472  ) []byte {
   473  
   474  	var dbInfo externalDbInfo
   475  	// This is a workaround to check if there are any externalDBs defined
   476  	for _, keyspaceData := range keyspaceInfoMap {
   477  		if val, ok := externalDbInfoMap[keyspaceData.keyspace]; ok {
   478  			dbInfo = val
   479  		}
   480  	}
   481  
   482  	dockerComposeFile = applyInMemoryPatch(dockerComposeFile, generateVtctld(opts))
   483  	dockerComposeFile = applyInMemoryPatch(dockerComposeFile, generateVtgate(opts))
   484  	dockerComposeFile = applyInMemoryPatch(dockerComposeFile, generateVreplication(dbInfo, opts))
   485  	dockerComposeFile = applyInMemoryPatch(dockerComposeFile, generateVTOrc(dbInfo, keyspaceInfoMap, opts))
   486  	return dockerComposeFile
   487  }
   488  
   489  func applyDockerComposePatches(
   490  	dockerComposeFile []byte,
   491  	keyspaceInfoMap map[string]keyspaceInfo,
   492  	externalDbInfoMap map[string]externalDbInfo,
   493  	vtOpts vtOptions,
   494  ) []byte {
   495  	// Vtctld, vtgate, vtwork patches.
   496  	dockerComposeFile = applyDefaultDockerPatches(dockerComposeFile, keyspaceInfoMap, externalDbInfoMap, vtOpts)
   497  	for _, keyspaceData := range keyspaceInfoMap {
   498  		dockerComposeFile = applyKeyspaceDependentPatches(dockerComposeFile, keyspaceData, externalDbInfoMap, vtOpts)
   499  	}
   500  
   501  	return dockerComposeFile
   502  }
   503  
   504  func applyShardPatches(
   505  	dockerComposeFile []byte,
   506  	tabAlias int,
   507  	shard string,
   508  	keyspaceData keyspaceInfo,
   509  	externalDbInfoMap map[string]externalDbInfo,
   510  	opts vtOptions,
   511  ) []byte {
   512  	var dbInfo externalDbInfo
   513  	if val, ok := externalDbInfoMap[keyspaceData.keyspace]; ok {
   514  		dbInfo = val
   515  	}
   516  	dockerComposeFile = applyInMemoryPatch(dockerComposeFile, generateExternalPrimary(tabAlias, shard, keyspaceData, dbInfo, opts))
   517  	return dockerComposeFile
   518  }
   519  
   520  func generateDefaultShard(tabAlias int, shard string, keyspaceData keyspaceInfo, opts vtOptions) string {
   521  	aliases := []int{tabAlias + 1} // primary alias, e.g. 201
   522  	for i := 0; i < keyspaceData.replicaTablets; i++ {
   523  		aliases = append(aliases, tabAlias+2+i) // replica aliases, e.g. 202, 203, ...
   524  	}
   525  	tabletDepends := make([]string, len(aliases))
   526  	for i, tabletId := range aliases {
   527  		tabletDepends[i] = fmt.Sprintf("vttablet%d: {condition : service_healthy}", tabletId)
   528  	}
   529  	// Wait on all shard tablets to be healthy
   530  	dependsOn := "depends_on: {" + strings.Join(tabletDepends, ", ") + "}"
   531  
   532  	return fmt.Sprintf(`
   533  - op: add
   534    path: /services/init_shard_primary%[2]d
   535    value:
   536      image: vitess/lite:v16.0.2
   537      command: ["sh", "-c", "/vt/bin/vtctlclient %[5]s InitShardPrimary -force %[4]s/%[3]s %[6]s-%[2]d "]
   538      %[1]s
   539  `, dependsOn, aliases[0], shard, keyspaceData.keyspace, opts.topologyFlags, opts.cell)
   540  }
   541  
   542  func generateExternalPrimary(
   543  	tabAlias int,
   544  	shard string,
   545  	keyspaceData keyspaceInfo,
   546  	dbInfo externalDbInfo,
   547  	opts vtOptions,
   548  ) string {
   549  
   550  	aliases := []int{tabAlias + 1} // primary alias, e.g. 201
   551  	for i := 0; i < keyspaceData.replicaTablets; i++ {
   552  		aliases = append(aliases, tabAlias+2+i) // replica aliases, e.g. 202, 203, ...
   553  	}
   554  
   555  	externalPrimaryTab := tabAlias
   556  	externalDb := "0"
   557  
   558  	if dbInfo.dbName != "" {
   559  		externalDb = "1"
   560  	} else {
   561  		return fmt.Sprintf(``)
   562  	}
   563  
   564  	return fmt.Sprintf(`
   565  - op: add
   566    path: /services/vttablet%[1]d
   567    value:
   568      image: vitess/lite:v16.0.2
   569      ports:
   570        - "15%[1]d:%[3]d"
   571        - "%[4]d"
   572        - "3306"
   573      volumes:
   574        - ".:/script"
   575      environment:
   576        - TOPOLOGY_FLAGS=%[2]s
   577        - WEB_PORT=%[3]d
   578        - GRPC_PORT=%[4]d
   579        - CELL=%[5]s
   580        - SHARD=%[6]s
   581        - KEYSPACE=%[7]s
   582        - ROLE=primary
   583        - VTHOST=vttablet%[1]d
   584        - EXTERNAL_DB=%[8]s
   585        - DB_PORT=%[9]s
   586        - DB_HOST=%[10]s
   587        - DB_USER=%[11]s
   588        - DB_PASS=%[12]s
   589        - DB_CHARSET=%[13]s
   590      command: ["sh", "-c", "[ $$EXTERNAL_DB -eq 1 ] && /script/vttablet-up.sh %[1]d || exit 0"]
   591      depends_on:
   592        - vtctld
   593      healthcheck:
   594          test: ["CMD-SHELL","curl -s --fail --show-error localhost:%[3]d/debug/health"]
   595          interval: 30s
   596          timeout: 10s
   597          retries: 15
   598  `, externalPrimaryTab, opts.topologyFlags, opts.webPort, opts.gRpcPort, opts.cell, shard, keyspaceData.keyspace, externalDb, dbInfo.dbPort, dbInfo.dbHost, dbInfo.dbUser, dbInfo.dbPass, dbInfo.dbCharset)
   599  }
   600  
   601  func applyTabletPatches(
   602  	dockerComposeFile []byte,
   603  	tabAlias int,
   604  	shard string,
   605  	keyspaceData keyspaceInfo,
   606  	externalDbInfoMap map[string]externalDbInfo,
   607  	opts vtOptions,
   608  ) []byte {
   609  	var dbInfo externalDbInfo
   610  	if val, ok := externalDbInfoMap[keyspaceData.keyspace]; ok {
   611  		dbInfo = val
   612  	}
   613  	dockerComposeFile = applyInMemoryPatch(dockerComposeFile, generateDefaultTablet(tabAlias+1, shard, "primary", keyspaceData.keyspace, dbInfo, opts))
   614  	for i := 0; i < keyspaceData.replicaTablets; i++ {
   615  		dockerComposeFile = applyInMemoryPatch(dockerComposeFile, generateDefaultTablet(tabAlias+2+i, shard, "replica", keyspaceData.keyspace, dbInfo, opts))
   616  	}
   617  	return dockerComposeFile
   618  }
   619  
   620  func generateDefaultTablet(tabAlias int, shard, role, keyspace string, dbInfo externalDbInfo, opts vtOptions) string {
   621  	externalDb := "0"
   622  	if dbInfo.dbName != "" {
   623  		externalDb = "1"
   624  	}
   625  
   626  	return fmt.Sprintf(`
   627  - op: add
   628    path: /services/vttablet%[1]d
   629    value:
   630      image: vitess/lite:v16.0.2
   631      ports:
   632      - "15%[1]d:%[4]d"
   633      - "%[5]d"
   634      - "3306"
   635      volumes:
   636      - ".:/script"
   637      environment:
   638      - TOPOLOGY_FLAGS=%[7]s
   639      - WEB_PORT=%[4]d
   640      - GRPC_PORT=%[5]d
   641      - CELL=%[8]s
   642      - KEYSPACE=%[6]s
   643      - SHARD=%[2]s
   644      - ROLE=%[3]s
   645      - VTHOST=vttablet%[1]d
   646      - EXTERNAL_DB=%[9]s
   647      - DB_PORT=%[10]s
   648      - DB_HOST=%[11]s
   649      - DB_USER=%[12]s
   650      - DB_PASS=%[13]s
   651      - DB_CHARSET=%[14]s
   652      command: ["sh", "-c", "/script/vttablet-up.sh %[1]d"]
   653      depends_on:
   654        - vtctld
   655      healthcheck:
   656          test: ["CMD-SHELL","curl -s --fail --show-error localhost:%[4]d/debug/health"]
   657          interval: 30s
   658          timeout: 10s
   659          retries: 15
   660  `, tabAlias, shard, role, opts.webPort, opts.gRpcPort, keyspace, opts.topologyFlags, opts.cell, externalDb, dbInfo.dbPort, dbInfo.dbHost, dbInfo.dbUser, dbInfo.dbPass, dbInfo.dbCharset)
   661  }
   662  
   663  func generateVtctld(opts vtOptions) string {
   664  	return fmt.Sprintf(`
   665  - op: add
   666    path: /services/vtctld
   667    value:
   668      image: vitess/lite:v16.0.2
   669      ports:
   670        - "15000:%[1]d"
   671        - "%[2]d"
   672      command: ["sh", "-c", " /vt/bin/vtctld \
   673          %[3]s \
   674          --cell %[4]s \
   675          --service_map 'grpc-vtctl,grpc-vtctld' \
   676          --backup_storage_implementation file \
   677          --file_backup_storage_root /vt/vtdataroot/backups \
   678          --logtostderr=true \
   679          --port %[1]d \
   680          --grpc_port %[2]d \
   681          "]
   682      volumes:
   683        - .:/script
   684      depends_on:
   685        - consul1
   686        - consul2
   687        - consul3
   688      depends_on:
   689        external_db_host:
   690          condition: service_healthy
   691  `, opts.webPort, opts.gRpcPort, opts.topologyFlags, opts.cell)
   692  }
   693  
   694  func generateVtgate(opts vtOptions) string {
   695  	return fmt.Sprintf(`
   696  - op: add
   697    path: /services/vtgate
   698    value:
   699      image: vitess/lite:v16.0.2
   700      ports:
   701        - "15099:%[1]d"
   702        - "%[2]d"
   703        - "15306:%[3]d"
   704      command: ["sh", "-c", "/script/run-forever.sh /vt/bin/vtgate \
   705          %[4]s \
   706          --logtostderr=true \
   707          --port %[1]d \
   708          --grpc_port %[2]d \
   709          --mysql_server_port %[3]d \
   710          --mysql_auth_server_impl none \
   711          --cell %[5]s \
   712          --cells_to_watch %[5]s \
   713          --tablet_types_to_wait PRIMARY,REPLICA,RDONLY \
   714          --service_map 'grpc-vtgateservice' \
   715          --normalize_queries=true \
   716          "]
   717      volumes:
   718        - .:/script
   719      depends_on:
   720        - vtctld
   721  `, opts.webPort, opts.gRpcPort, opts.mySqlPort, opts.topologyFlags, opts.cell)
   722  }
   723  
   724  func generateVTOrc(dbInfo externalDbInfo, keyspaceInfoMap map[string]keyspaceInfo, opts vtOptions) string {
   725  	externalDb := "0"
   726  	if dbInfo.dbName != "" {
   727  		externalDb = "1"
   728  	}
   729  
   730  	var depends []string
   731  	for _, keyspaceData := range keyspaceInfoMap {
   732  		depends = append(depends, "set_keyspace_durability_policy_"+keyspaceData.keyspace)
   733  	}
   734  	depends = append(depends, "vtctld")
   735  	dependsOn := "depends_on: [" + strings.Join(depends, ", ") + "]"
   736  
   737  	return fmt.Sprintf(`
   738  - op: add
   739    path: /services/vtorc
   740    value:
   741      image: vitess/lite:v16.0.2
   742      volumes:
   743        - ".:/script"
   744      environment:
   745        - WEB_PORT=%[1]d
   746        - TOPOLOGY_FLAGS=%[2]s
   747        - EXTERNAL_DB=%[3]s
   748        - DB_USER=%[4]s
   749        - DB_PASS=%[5]s
   750      ports:
   751        - "13000:%[1]d"
   752      command: ["sh", "-c", "/script/vtorc-up.sh"]
   753      %[6]s
   754  `, opts.webPort, opts.topologyFlags, externalDb, dbInfo.dbUser, dbInfo.dbPass, dependsOn)
   755  }
   756  
   757  func generateVreplication(dbInfo externalDbInfo, opts vtOptions) string {
   758  	externalDb := "0"
   759  	if dbInfo.dbName != "" {
   760  		externalDb = "1"
   761  	}
   762  	return fmt.Sprintf(`
   763  - op: add
   764    path: /services/vreplication
   765    value:
   766      image: vitess/lite:v16.0.2
   767      volumes:
   768        - ".:/script"
   769      environment:
   770        - TOPOLOGY_FLAGS=%[1]s
   771        - EXTERNAL_DB=%[2]s
   772      command: ["sh", "-c", "[ $$EXTERNAL_DB -eq 1 ] && /script/externaldb_vreplication.sh || exit 0"]
   773      depends_on:
   774        - vtctld
   775  `, opts.topologyFlags, externalDb)
   776  }
   777  
   778  func generateSetKeyspaceDurabilityPolicy(
   779  	tabletAliases []string,
   780  	keyspace string,
   781  	opts vtOptions,
   782  ) string {
   783  	// Formatting for list in yaml
   784  	var aliases []string
   785  	for _, tabletId := range tabletAliases {
   786  		aliases = append(aliases, "vttablet"+tabletId)
   787  	}
   788  	dependsOn := "depends_on: [" + strings.Join(aliases, ", ") + "]"
   789  
   790  	return fmt.Sprintf(`
   791  - op: add
   792    path: /services/set_keyspace_durability_policy_%[3]s
   793    value:
   794      image: vitess/lite:v16.0.2
   795      volumes:
   796        - ".:/script"
   797      environment:
   798        - GRPC_PORT=%[2]d
   799        - KEYSPACES=%[3]s
   800      command: ["sh", "-c", "/script/set_keyspace_durability_policy.sh"]
   801      %[1]s
   802  `, dependsOn, opts.gRpcPort, keyspace)
   803  }
   804  
   805  func generateSchemaload(
   806  	tabletAliases []string,
   807  	postLoadFile string,
   808  	keyspace string,
   809  	dbInfo externalDbInfo,
   810  	opts vtOptions,
   811  ) string {
   812  	targetTab := tabletAliases[0]
   813  	schemaFileName := fmt.Sprintf("%s_schema_file.sql", keyspace)
   814  	externalDb := "0"
   815  
   816  	if dbInfo.dbName != "" {
   817  		schemaFileName = ""
   818  		externalDb = "1"
   819  	}
   820  
   821  	// Formatting for list in yaml
   822  	for i, tabletId := range tabletAliases {
   823  		tabletAliases[i] = "vttablet" + tabletId + ": " + "{condition : service_healthy}"
   824  	}
   825  	dependsOn := "depends_on: {" + strings.Join(tabletAliases, ", ") + "}"
   826  
   827  	return fmt.Sprintf(`
   828  - op: add
   829    path: /services/schemaload_%[7]s
   830    value:
   831      image: vitess/lite:v16.0.2
   832      volumes:
   833        - ".:/script"
   834      environment:
   835        - TOPOLOGY_FLAGS=%[3]s
   836        - WEB_PORT=%[4]d
   837        - GRPC_PORT=%[5]d
   838        - CELL=%[6]s
   839        - KEYSPACE=%[7]s
   840        - TARGETTAB=%[6]s-0000000%[2]s
   841        - SLEEPTIME=15
   842        - VSCHEMA_FILE=%[7]s_vschema.json
   843        - SCHEMA_FILES=%[9]s
   844        - POST_LOAD_FILE=%[8]s
   845        - EXTERNAL_DB=%[10]s
   846      command: ["sh", "-c", "/script/schemaload.sh"]
   847      %[1]s
   848  `, dependsOn, targetTab, opts.topologyFlags, opts.webPort, opts.gRpcPort, opts.cell, keyspace, postLoadFile, schemaFileName, externalDb)
   849  }
   850  
   851  func generatePrimaryVIndex(tableName, column, name string) string {
   852  	return fmt.Sprintf(`
   853  [{"op": "add",
   854  "path": "/tables/%[1]s",
   855  "value":
   856    {"column_vindexes": [
   857      {
   858        "column": "%[2]s",
   859        "name":  "%[3]s"
   860      }
   861    ]}
   862  }]
   863  `, tableName, column, name)
   864  }
   865  
   866  func generateVschemaLookupHash(tableName, tableKeyspace, from, to, owner string) string {
   867  	return fmt.Sprintf(`
   868  [{"op": "add",
   869  "path": "/vindexes/%[1]s",
   870  "value":
   871    {"type": "lookup_hash",
   872      "params": {
   873        "table": "%[2]s.%[1]s",
   874        "from": "%[3]s",
   875        "to": "%[4]s",
   876        "autocommit": "true"
   877      },
   878    "owner": "%[5]s"
   879    }
   880  }]
   881  `, tableName, tableKeyspace, from, to, owner)
   882  }
   883  
   884  func addToColumnVIndexes(tableName, column, referenceName string) string {
   885  	return fmt.Sprintf(`
   886  [{"op": "add",
   887  "path": "/tables/%[1]s/column_vindexes/-",
   888  "value":
   889      {
   890        "column": "%[2]s",
   891        "name":  "%[3]s"
   892      }
   893  }]
   894  `, tableName, column, referenceName)
   895  }