vitess.io/vitess@v0.16.2/web/vtadmin/src/api/http.ts (about)

     1  /**
     2   * Copyright 2021 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  import { vtadmin as pb, vtadmin, vtctldata } from '../proto/vtadmin';
    18  import * as errorHandler from '../errors/errorHandler';
    19  import { HttpFetchError, HttpResponseNotOkError, MalformedHttpResponseError } from '../errors/errorTypes';
    20  import { HttpOkResponse } from './responseTypes';
    21  import { TabletDebugVars } from '../util/tabletDebugVars';
    22  import { env, isReadOnlyMode } from '../util/env';
    23  
    24  /**
    25   * vtfetch makes HTTP requests against the given vtadmin-api endpoint
    26   * and returns the parsed response.
    27   *
    28   * HttpResponse envelope types are not defined in vtadmin.proto (nor should they be)
    29   * thus we have to validate the shape of the API response with more care.
    30   *
    31   * Note that this only validates the HttpResponse envelope; it does not
    32   * do any type checking or validation on the result.
    33   */
    34  export const vtfetch = async (endpoint: string, options: RequestInit = {}): Promise<HttpOkResponse> => {
    35      try {
    36          if (isReadOnlyMode() && options.method && options.method.toLowerCase() !== 'get') {
    37              // Any UI controls that ultimately trigger a write request should be hidden when in read-only mode,
    38              // so getting to this point (where we actually execute a write request) is an error.
    39              // So: we fail obnoxiously, as failing silently (e.g, logging and returning an empty "ok" response)
    40              // could imply to the user that a write action succeeded.
    41              throw new Error(`Cannot execute write request in read-only mode: ${options.method} ${endpoint}`);
    42          }
    43  
    44          const url = `${env().REACT_APP_VTADMIN_API_ADDRESS}${endpoint}`;
    45          const opts = { ...vtfetchOpts(), ...options };
    46  
    47          let response = null;
    48          try {
    49              response = await global.fetch(url, opts);
    50          } catch (error) {
    51              // Capture fetch() promise rejections and rethrow as HttpFetchError.
    52              // fetch() promises will reject with a TypeError when a network error is
    53              // encountered or CORS is misconfigured, in which case the request never
    54              // makes it to the server.
    55              // See https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#checking_that_the_fetch_was_successful
    56              throw new HttpFetchError(url);
    57          }
    58  
    59          let json = null;
    60          try {
    61              json = await response.json();
    62          } catch (error) {
    63              throw new MalformedHttpResponseError((error as Error).message, endpoint, json, response);
    64          }
    65  
    66          if (!('ok' in json)) {
    67              throw new MalformedHttpResponseError('invalid HTTP envelope', endpoint, json, response);
    68          }
    69  
    70          if (!json.ok) {
    71              throw new HttpResponseNotOkError(endpoint, json, response);
    72          }
    73  
    74          return json as HttpOkResponse;
    75      } catch (error) {
    76          // Most commonly, react-query is the downstream consumer of
    77          // errors thrown in vtfetch. Because react-query "handles" errors
    78          // by propagating them to components (as it should!), any errors thrown
    79          // from vtfetch are _not_ automatically logged as "unhandled errors".
    80          // Instead, we catch errors and manually notify our error handling serivce(s),
    81          // and then rethrow the error for react-query to propagate the usual way.
    82          // See https://react-query.tanstack.com/guides/query-functions#handling-and-throwing-errors
    83          errorHandler.notify(error as Error);
    84          throw error;
    85      }
    86  };
    87  
    88  export const vtfetchOpts = (): RequestInit => {
    89      const credentials = env().REACT_APP_FETCH_CREDENTIALS;
    90      if (credentials && credentials !== 'omit' && credentials !== 'same-origin' && credentials !== 'include') {
    91          throw Error(
    92              `Invalid fetch credentials property: ${credentials}. Must be undefined or one of omit, same-origin, include`
    93          );
    94      }
    95  
    96      return { credentials };
    97  };
    98  
    99  // vtfetchEntities is a helper function for querying vtadmin-api endpoints
   100  // that return a list of protobuf entities.
   101  export const vtfetchEntities = async <T>(opts: {
   102      endpoint: string;
   103      // Extract the list of entities from the response. We can't (strictly)
   104      // guarantee type safety for API responses, hence the `any` return type.
   105      extract: (res: HttpOkResponse) => any;
   106      // Transform an individual entity in the array to its (proto)typed form.
   107      // This will almost always be a `.verify` followed by a `.create`,
   108      // but because of how protobufjs structures its generated types,
   109      // writing this in a generic way is... unpleasant, and difficult to read.
   110      transform: (e: object) => T;
   111  }): Promise<T[]> => {
   112      const res = await vtfetch(opts.endpoint);
   113  
   114      const entities = opts.extract(res);
   115      if (!Array.isArray(entities)) {
   116          // Since react-query is the downstream consumer of vtfetch + vtfetchEntities,
   117          // errors thrown in either function will be "handled" and will not automatically
   118          // propagate as "unhandled" errors, meaning we have to log them manually.
   119          const error = Error(`expected entities to be an array, got ${entities}`);
   120          errorHandler.notify(error);
   121          throw error;
   122      }
   123  
   124      return entities.map(opts.transform);
   125  };
   126  
   127  export const fetchBackups = async () =>
   128      vtfetchEntities({
   129          endpoint: '/api/backups',
   130          extract: (res) => res.result.backups,
   131          transform: (e) => {
   132              const err = pb.ClusterBackup.verify(e);
   133              if (err) throw Error(err);
   134              return pb.ClusterBackup.create(e);
   135          },
   136      });
   137  
   138  export const fetchClusters = async () =>
   139      vtfetchEntities({
   140          endpoint: '/api/clusters',
   141          extract: (res) => res.result.clusters,
   142          transform: (e) => {
   143              const err = pb.Cluster.verify(e);
   144              if (err) throw Error(err);
   145              return pb.Cluster.create(e);
   146          },
   147      });
   148  
   149  export const fetchGates = async () =>
   150      vtfetchEntities({
   151          endpoint: '/api/gates',
   152          extract: (res) => res.result.gates,
   153          transform: (e) => {
   154              const err = pb.VTGate.verify(e);
   155              if (err) throw Error(err);
   156              return pb.VTGate.create(e);
   157          },
   158      });
   159  
   160  export const fetchVtctlds = async () =>
   161      vtfetchEntities({
   162          endpoint: '/api/vtctlds',
   163          extract: (res) => res.result.vtctlds,
   164          transform: (e) => {
   165              const err = pb.Vtctld.verify(e);
   166              if (err) throw Error(err);
   167              return pb.Vtctld.create(e);
   168          },
   169      });
   170  
   171  export interface FetchKeyspaceParams {
   172      clusterID: string;
   173      name: string;
   174  }
   175  
   176  export const fetchKeyspace = async ({ clusterID, name }: FetchKeyspaceParams) => {
   177      const { result } = await vtfetch(`/api/keyspace/${clusterID}/${name}`);
   178  
   179      const err = pb.Keyspace.verify(result);
   180      if (err) throw Error(err);
   181  
   182      return pb.Keyspace.create(result);
   183  };
   184  
   185  export const fetchKeyspaces = async () =>
   186      vtfetchEntities({
   187          endpoint: '/api/keyspaces',
   188          extract: (res) => res.result.keyspaces,
   189          transform: (e) => {
   190              const err = pb.Keyspace.verify(e);
   191              if (err) throw Error(err);
   192              return pb.Keyspace.create(e);
   193          },
   194      });
   195  
   196  export interface CreateKeyspaceParams {
   197      clusterID: string;
   198      options: vtctldata.ICreateKeyspaceRequest;
   199  }
   200  
   201  export const createKeyspace = async (params: CreateKeyspaceParams) => {
   202      const { result } = await vtfetch(`/api/keyspace/${params.clusterID}`, {
   203          body: JSON.stringify(params.options),
   204          method: 'post',
   205      });
   206  
   207      const err = pb.CreateKeyspaceResponse.verify(result);
   208      if (err) throw Error(err);
   209  
   210      return pb.CreateKeyspaceResponse.create(result);
   211  };
   212  
   213  export const fetchSchemas = async () =>
   214      vtfetchEntities({
   215          endpoint: '/api/schemas',
   216          extract: (res) => res.result.schemas,
   217          transform: (e) => {
   218              const err = pb.Schema.verify(e);
   219              if (err) throw Error(err);
   220              return pb.Schema.create(e);
   221          },
   222      });
   223  
   224  export interface FetchSchemaParams {
   225      clusterID: string;
   226      keyspace: string;
   227      table: string;
   228  }
   229  
   230  export const fetchSchema = async ({ clusterID, keyspace, table }: FetchSchemaParams) => {
   231      const { result } = await vtfetch(`/api/schema/${clusterID}/${keyspace}/${table}`);
   232  
   233      const err = pb.Schema.verify(result);
   234      if (err) throw Error(err);
   235  
   236      return pb.Schema.create(result);
   237  };
   238  
   239  export interface FetchTabletParams {
   240      clusterID: string;
   241      alias: string;
   242  }
   243  
   244  export const fetchTablet = async ({ clusterID, alias }: FetchTabletParams) => {
   245      const { result } = await vtfetch(`/api/tablet/${alias}?cluster=${clusterID}`);
   246  
   247      const err = pb.Tablet.verify(result);
   248      if (err) throw Error(err);
   249  
   250      return pb.Tablet.create(result);
   251  };
   252  
   253  export interface DeleteTabletParams {
   254      allowPrimary?: boolean;
   255      clusterID: string;
   256      alias: string;
   257  }
   258  
   259  export const deleteTablet = async ({ allowPrimary, clusterID, alias }: DeleteTabletParams) => {
   260      const req = new URLSearchParams();
   261      req.append('cluster', clusterID);
   262  
   263      // Do not append `allow_primary` if undefined in order to fall back to server default
   264      if (typeof allowPrimary === 'boolean') {
   265          req.append('allow_primary', allowPrimary.toString());
   266      }
   267  
   268      const { result } = await vtfetch(`/api/tablet/${alias}?${req}`, { method: 'delete' });
   269  
   270      const err = pb.DeleteTabletResponse.verify(result);
   271      if (err) throw Error(err);
   272  
   273      return pb.DeleteTabletResponse.create(result);
   274  };
   275  
   276  export interface RefreshTabletReplicationSourceParams {
   277      clusterID: string;
   278      alias: string;
   279  }
   280  
   281  export const refreshTabletReplicationSource = async ({ clusterID, alias }: RefreshTabletReplicationSourceParams) => {
   282      const { result } = await vtfetch(`/api/tablet/${alias}/refresh_replication_source`, { method: 'put' });
   283  
   284      const err = pb.RefreshTabletReplicationSourceResponse.verify(result);
   285      if (err) throw Error(err);
   286  
   287      return pb.RefreshTabletReplicationSourceResponse.create(result);
   288  };
   289  
   290  export interface PingTabletParams {
   291      clusterID?: string;
   292      alias: string;
   293  }
   294  
   295  export const pingTablet = async ({ clusterID, alias }: PingTabletParams) => {
   296      const { result } = await vtfetch(`/api/tablet/${alias}/ping?cluster=${clusterID}`);
   297      const err = pb.PingTabletResponse.verify(result);
   298      if (err) throw Error(err);
   299  
   300      return pb.PingTabletResponse.create(result);
   301  };
   302  
   303  export interface RefreshStateParams {
   304      clusterID?: string;
   305      alias: string;
   306  }
   307  
   308  export const refreshState = async ({ clusterID, alias }: RefreshStateParams) => {
   309      const { result } = await vtfetch(`/api/tablet/${alias}/refresh?cluster=${clusterID}`, { method: 'put' });
   310      const err = pb.RefreshStateResponse.verify(result);
   311      if (err) throw Error(err);
   312  
   313      return pb.RefreshStateResponse.create(result);
   314  };
   315  
   316  export interface RunHealthCheckParams {
   317      clusterID?: string;
   318      alias: string;
   319  }
   320  
   321  export const runHealthCheck = async ({ clusterID, alias }: RunHealthCheckParams) => {
   322      const { result } = await vtfetch(`/api/tablet/${alias}/healthcheck?cluster=${clusterID}`);
   323      const err = pb.RunHealthCheckResponse.verify(result);
   324      if (err) throw Error(err);
   325  
   326      return pb.RunHealthCheckResponse.create(result);
   327  };
   328  
   329  export interface SetReadOnlyParams {
   330      clusterID?: string;
   331      alias: string;
   332  }
   333  
   334  export const setReadOnly = async ({ clusterID, alias }: SetReadOnlyParams) => {
   335      const { result } = await vtfetch(`/api/tablet/${alias}/set_read_only?cluster=${clusterID}`, { method: 'put' });
   336      const err = pb.SetReadOnlyResponse.verify(result);
   337      if (err) throw Error(err);
   338  
   339      return pb.SetReadOnlyResponse.create(result);
   340  };
   341  
   342  export interface SetReadWriteParams {
   343      clusterID?: string;
   344      alias: string;
   345  }
   346  
   347  export const setReadWrite = async ({ clusterID, alias }: SetReadWriteParams) => {
   348      const { result } = await vtfetch(`/api/tablet/${alias}/set_read_write?cluster=${clusterID}`, { method: 'put' });
   349      const err = pb.SetReadWriteResponse.verify(result);
   350      if (err) throw Error(err);
   351  
   352      return pb.SetReadWriteResponse.create(result);
   353  };
   354  
   355  export interface StartReplicationParams {
   356      clusterID?: string;
   357      alias: string;
   358  }
   359  
   360  export const startReplication = async ({ clusterID, alias }: StartReplicationParams) => {
   361      const { result } = await vtfetch(`/api/tablet/${alias}/start_replication?cluster=${clusterID}`, { method: 'put' });
   362      const err = pb.StartReplicationResponse.verify(result);
   363      if (err) throw Error(err);
   364  
   365      return pb.StartReplicationResponse.create(result);
   366  };
   367  
   368  export interface StopReplicationParams {
   369      clusterID?: string;
   370      alias: string;
   371  }
   372  
   373  export const stopReplication = async ({ clusterID, alias }: StopReplicationParams) => {
   374      const { result } = await vtfetch(`/api/tablet/${alias}/stop_replication?cluster=${clusterID}`, { method: 'put' });
   375      const err = pb.StopReplicationResponse.verify(result);
   376      if (err) throw Error(err);
   377  
   378      return pb.StopReplicationResponse.create(result);
   379  };
   380  export interface TabletDebugVarsResponse {
   381      params: FetchTabletParams;
   382      data?: TabletDebugVars;
   383  }
   384  
   385  export const fetchExperimentalTabletDebugVars = async (params: FetchTabletParams): Promise<TabletDebugVarsResponse> => {
   386      if (!env().REACT_APP_ENABLE_EXPERIMENTAL_TABLET_DEBUG_VARS) {
   387          return Promise.resolve({ params });
   388      }
   389  
   390      const { clusterID, alias } = params;
   391      const { result } = await vtfetch(`/api/experimental/tablet/${alias}/debug/vars?cluster=${clusterID}`);
   392  
   393      // /debug/vars doesn't contain cluster/tablet information, so we
   394      // return that as part of the response.
   395      return { params, data: result };
   396  };
   397  
   398  export const fetchTablets = async () =>
   399      vtfetchEntities({
   400          endpoint: '/api/tablets',
   401          extract: (res) => res.result.tablets,
   402          transform: (e) => {
   403              const err = pb.Tablet.verify(e);
   404              if (err) throw Error(err);
   405              return pb.Tablet.create(e);
   406          },
   407      });
   408  export interface FetchVSchemaParams {
   409      clusterID: string;
   410      keyspace: string;
   411  }
   412  
   413  export const fetchVSchema = async ({ clusterID, keyspace }: FetchVSchemaParams) => {
   414      const { result } = await vtfetch(`/api/vschema/${clusterID}/${keyspace}`);
   415  
   416      const err = pb.VSchema.verify(result);
   417      if (err) throw Error(err);
   418  
   419      return pb.VSchema.create(result);
   420  };
   421  
   422  export const fetchWorkflows = async () => {
   423      const { result } = await vtfetch(`/api/workflows`);
   424  
   425      const err = pb.GetWorkflowsResponse.verify(result);
   426      if (err) throw Error(err);
   427  
   428      return pb.GetWorkflowsResponse.create(result);
   429  };
   430  
   431  export const fetchWorkflow = async (params: { clusterID: string; keyspace: string; name: string }) => {
   432      const { result } = await vtfetch(`/api/workflow/${params.clusterID}/${params.keyspace}/${params.name}`);
   433  
   434      const err = pb.Workflow.verify(result);
   435      if (err) throw Error(err);
   436  
   437      return pb.Workflow.create(result);
   438  };
   439  
   440  export const fetchVTExplain = async <R extends pb.IVTExplainRequest>({ cluster, keyspace, sql }: R) => {
   441      // As an easy enhancement for later, we can also validate the request parameters on the front-end
   442      // instead of defaulting to '', to save a round trip.
   443      const req = new URLSearchParams();
   444      req.append('cluster', cluster || '');
   445      req.append('keyspace', keyspace || '');
   446      req.append('sql', sql || '');
   447  
   448      const { result } = await vtfetch(`/api/vtexplain?${req}`);
   449  
   450      const err = pb.VTExplainResponse.verify(result);
   451      if (err) throw Error(err);
   452  
   453      return pb.VTExplainResponse.create(result);
   454  };
   455  
   456  export interface ValidateKeyspaceParams {
   457      clusterID: string;
   458      keyspace: string;
   459      pingTablets: boolean;
   460  }
   461  
   462  export const validateKeyspace = async ({ clusterID, keyspace, pingTablets }: ValidateKeyspaceParams) => {
   463      const body = JSON.stringify({ pingTablets });
   464  
   465      const { result } = await vtfetch(`/api/keyspace/${clusterID}/${keyspace}/validate`, { method: 'put', body });
   466      const err = vtctldata.ValidateKeyspaceResponse.verify(result);
   467      if (err) throw Error(err);
   468  
   469      return vtctldata.ValidateKeyspaceResponse.create(result);
   470  };
   471  
   472  export interface ValidateSchemaKeyspaceParams {
   473      clusterID: string;
   474      keyspace: string;
   475  }
   476  
   477  export const validateSchemaKeyspace = async ({ clusterID, keyspace }: ValidateSchemaKeyspaceParams) => {
   478      const { result } = await vtfetch(`/api/keyspace/${clusterID}/${keyspace}/validate/schema`, { method: 'put' });
   479      const err = vtctldata.ValidateSchemaKeyspaceResponse.verify(result);
   480      if (err) throw Error(err);
   481  
   482      return vtctldata.ValidateSchemaKeyspaceResponse.create(result);
   483  };
   484  
   485  export interface ValidateVersionKeyspaceParams {
   486      clusterID: string;
   487      keyspace: string;
   488  }
   489  
   490  export const validateVersionKeyspace = async ({ clusterID, keyspace }: ValidateVersionKeyspaceParams) => {
   491      const { result } = await vtfetch(`/api/keyspace/${clusterID}/${keyspace}/validate/version`, { method: 'put' });
   492      const err = vtctldata.ValidateVersionKeyspaceResponse.verify(result);
   493      if (err) throw Error(err);
   494  
   495      return vtctldata.ValidateVersionKeyspaceResponse.create(result);
   496  };
   497  
   498  export interface FetchShardReplicationPositionsParams {
   499      clusterIDs?: (string | null | undefined)[];
   500      keyspaces?: (string | null | undefined)[];
   501      keyspaceShards?: (string | null | undefined)[];
   502  }
   503  
   504  export const fetchShardReplicationPositions = async ({
   505      clusterIDs = [],
   506      keyspaces = [],
   507      keyspaceShards = [],
   508  }: FetchShardReplicationPositionsParams) => {
   509      const req = new URLSearchParams();
   510      clusterIDs.forEach((c) => c && req.append('cluster', c));
   511      keyspaces.forEach((k) => k && req.append('keyspace', k));
   512      keyspaceShards.forEach((s) => s && req.append('keyspace_shard', s));
   513  
   514      const { result } = await vtfetch(`/api/shard_replication_positions?${req}`);
   515      const err = pb.GetShardReplicationPositionsResponse.verify(result);
   516      if (err) throw Error(err);
   517  
   518      return pb.GetShardReplicationPositionsResponse.create(result);
   519  };
   520  
   521  export interface ReloadSchemaParams {
   522      clusterIDs?: (string | null | undefined)[];
   523      concurrency?: number;
   524      includePrimary?: boolean;
   525      keyspaces?: (string | null | undefined)[];
   526  
   527      // e.g., ["commerce/0"]
   528      keyspaceShards?: (string | null | undefined)[];
   529  
   530      // A list of tablet aliases; e.g., ["zone1-101", "zone1-102"]
   531      tablets?: (string | null | undefined)[];
   532  
   533      waitPosition?: string;
   534  }
   535  
   536  export const reloadSchema = async (params: ReloadSchemaParams) => {
   537      const req = new URLSearchParams();
   538  
   539      (params.clusterIDs || []).forEach((c) => c && req.append('cluster', c));
   540      (params.keyspaces || []).forEach((k) => k && req.append('keyspace', k));
   541      (params.keyspaceShards || []).forEach((k) => k && req.append('keyspaceShard', k));
   542      (params.tablets || []).forEach((t) => t && req.append('tablet', t));
   543  
   544      if (typeof params.concurrency === 'number') {
   545          req.append('concurrency', params.concurrency.toString());
   546      }
   547  
   548      if (typeof params.includePrimary === 'boolean') {
   549          req.append('include_primary', params.includePrimary.toString());
   550      }
   551  
   552      if (typeof params.waitPosition === 'string') {
   553          req.append('wait_position', params.waitPosition);
   554      }
   555  
   556      const { result } = await vtfetch(`/api/schemas/reload?${req}`, { method: 'put' });
   557  
   558      const err = pb.ReloadSchemasResponse.verify(result);
   559      if (err) throw Error(err);
   560  
   561      return pb.ReloadSchemasResponse.create(result);
   562  };
   563  
   564  export interface DeleteShardParams {
   565      clusterID: string;
   566      keyspaceShard: string;
   567      evenIfServing: boolean;
   568      recursive: boolean;
   569  }
   570  
   571  export const deleteShard = async (params: DeleteShardParams) => {
   572      const req = new URLSearchParams();
   573      req.append('keyspace_shard', params.keyspaceShard);
   574      req.append('even_if_serving', String(params.evenIfServing));
   575      req.append('recursive', String(params.recursive));
   576  
   577      const { result } = await vtfetch(`/api/shards/${params.clusterID}?${req}`, { method: 'delete' });
   578  
   579      const err = vtctldata.DeleteShardsResponse.verify(result);
   580      if (err) throw Error(err);
   581  
   582      return vtctldata.DeleteShardsResponse.create(result);
   583  };
   584  
   585  export interface ReloadSchemaShardParams {
   586      clusterID: string;
   587      keyspace: string;
   588      shard: string;
   589  
   590      waitPosition?: string;
   591      includePrimary: boolean;
   592      concurrency?: number;
   593  }
   594  
   595  export const reloadSchemaShard = async (params: ReloadSchemaShardParams) => {
   596      const body: Record<string, string | boolean | number> = {
   597          include_primary: params.includePrimary,
   598      };
   599  
   600      if (params.waitPosition) {
   601          body.wait_position = params.waitPosition;
   602      }
   603  
   604      if (params.concurrency) {
   605          body.concurrency = params.concurrency;
   606      }
   607  
   608      const { result } = await vtfetch(
   609          `/api/shard/${params.clusterID}/${params.keyspace}/${params.shard}/reload_schema_shard`,
   610          {
   611              method: 'put',
   612              body: JSON.stringify(body),
   613          }
   614      );
   615  
   616      const err = pb.ReloadSchemaShardResponse.verify(result);
   617      if (err) throw Error(err);
   618  
   619      return pb.ReloadSchemaShardResponse.create(result);
   620  };
   621  
   622  export interface TabletExternallyPromotedParams {
   623      alias?: string;
   624      clusterIDs: string[];
   625  }
   626  
   627  export const tabletExternallyPromoted = async (params: TabletExternallyPromotedParams) => {
   628      const req = new URLSearchParams();
   629      req.append('cluster', params.clusterIDs[0]);
   630  
   631      const { result } = await vtfetch(`/api/tablet/${params.alias}/externally_promoted?${req}`, {
   632          method: 'post',
   633      });
   634  
   635      const err = pb.TabletExternallyPromotedResponse.verify(result);
   636      if (err) throw Error(err);
   637  
   638      return pb.TabletExternallyPromotedResponse.create(result);
   639  };
   640  
   641  export interface PlannedFailoverShardParams {
   642      clusterID: string;
   643      keyspace: string;
   644      shard: string;
   645      new_primary?: vtadmin.Tablet;
   646  }
   647  
   648  export const plannedFailoverShard = async (params: PlannedFailoverShardParams) => {
   649      const body: Partial<pb.PlannedFailoverShardRequest['options']> = {};
   650      if (params.new_primary) body['new_primary'] = params.new_primary.tablet?.alias;
   651  
   652      const { result } = await vtfetch(
   653          `/api/shard/${params.clusterID}/${params.keyspace}/${params.shard}/planned_failover`,
   654          {
   655              method: 'post',
   656              body: JSON.stringify(body),
   657          }
   658      );
   659  
   660      const err = pb.PlannedFailoverShardResponse.verify(result);
   661      if (err) throw Error(err);
   662  
   663      return pb.PlannedFailoverShardResponse.create(result);
   664  };
   665  
   666  export interface EmergencyFailoverShardParams {
   667      clusterID: string;
   668      keyspace: string;
   669      shard: string;
   670      new_primary?: vtadmin.Tablet;
   671  }
   672  
   673  export const emergencyFailoverShard = async (params: EmergencyFailoverShardParams) => {
   674      const body: Partial<pb.PlannedFailoverShardRequest['options']> = {};
   675      if (params.new_primary && params.new_primary.tablet?.alias) body['new_primary'] = params.new_primary.tablet?.alias;
   676  
   677      const { result } = await vtfetch(
   678          `/api/shard/${params.clusterID}/${params.keyspace}/${params.shard}/emergency_failover`,
   679          {
   680              method: 'post',
   681              body: JSON.stringify(body),
   682          }
   683      );
   684  
   685      const err = pb.EmergencyFailoverShardResponse.verify(result);
   686      if (err) throw Error(err);
   687  
   688      return pb.EmergencyFailoverShardResponse.create(result);
   689  };
   690  
   691  export interface RebuildKeyspaceGraphParams {
   692      clusterID: string;
   693      keyspace: string;
   694  
   695      // A comma-separated list of cells, eg. "zone1,zone2"
   696      cells?: string;
   697  
   698      allowPartial?: boolean;
   699  }
   700  
   701  export const rebuildKeyspaceGraph = async (params: RebuildKeyspaceGraphParams) => {
   702      const { result } = await vtfetch(`/api/keyspace/${params.clusterID}/${params.keyspace}/rebuild_keyspace_graph`, {
   703          method: 'put',
   704          body: JSON.stringify({ cells: params.cells, allow_partial: params.allowPartial }),
   705      });
   706      const err = pb.RebuildKeyspaceGraphRequest.verify(result);
   707      if (err) throw Error(err);
   708  
   709      return pb.RebuildKeyspaceGraphResponse.create(result);
   710  };
   711  
   712  export interface RemoveKeyspaceCellParams {
   713      clusterID: string;
   714      keyspace: string;
   715      cell: string;
   716      force: boolean;
   717      recursive: boolean;
   718  }
   719  
   720  export const removeKeyspaceCell = async (params: RemoveKeyspaceCellParams) => {
   721      const { result } = await vtfetch(`/api/keyspace/${params.clusterID}/${params.keyspace}/remove_keyspace_cell`, {
   722          method: 'put',
   723          body: JSON.stringify({ cell: params.cell, force: params.force, recursive: params.recursive }),
   724      });
   725      const err = pb.RemoveKeyspaceCellRequest.verify(result);
   726      if (err) throw Error(err);
   727  
   728      return pb.RemoveKeyspaceCellResponse.create(result);
   729  };
   730  
   731  export interface CreateShardParams {
   732      keyspace: string;
   733      clusterID: string;
   734  
   735      // shardName is the name of the shard to create. E.g. "-" or "-80".
   736      shard_name: string;
   737  
   738      // force treats an attempt to create a shard that already exists as a
   739      // non-error.
   740      force?: boolean;
   741  
   742      // IncludeParent creates the parent keyspace as an empty BASE keyspace, if it
   743      // doesn't already exist.
   744      include_parent?: boolean;
   745  }
   746  
   747  export const createShard = async (params: CreateShardParams) => {
   748      const { result } = await vtfetch(`/api/shards/${params.clusterID}`, {
   749          method: 'post',
   750          body: JSON.stringify(params),
   751      });
   752      const err = vtctldata.CreateShardResponse.verify(result);
   753      if (err) throw Error(err);
   754  
   755      return vtctldata.CreateShardResponse.create(result);
   756  };
   757  
   758  export interface GetTopologyPathParams {
   759      clusterID: string;
   760      path: string;
   761  }
   762  
   763  export const getTopologyPath = async (params: GetTopologyPathParams) => {
   764      const req = new URLSearchParams({ path: params.path });
   765      const { result } = await vtfetch(`/api/cluster/${params.clusterID}/topology?${req}`);
   766  
   767      const err = vtctldata.GetTopologyPathResponse.verify(result);
   768      if (err) throw Error(err);
   769  
   770      return vtctldata.GetTopologyPathResponse.create(result);
   771  };
   772  export interface ValidateParams {
   773      clusterID: string;
   774      pingTablets: boolean;
   775  }
   776  
   777  export const validate = async (params: ValidateParams) => {
   778      const { result } = await vtfetch(`/api/cluster/${params.clusterID}/validate`, {
   779          method: 'put',
   780          body: JSON.stringify({ ping_tablets: params.pingTablets }),
   781      });
   782      const err = pb.ValidateRequest.verify(result);
   783      if (err) throw Error(err);
   784  
   785      return vtctldata.ValidateResponse.create(result);
   786  };
   787  
   788  export interface ValidateShardParams {
   789      clusterID: string;
   790      keyspace: string;
   791      shard: string;
   792      pingTablets: boolean;
   793  }
   794  
   795  export const validateShard = async (params: ValidateShardParams) => {
   796      const { result } = await vtfetch(`/api/shard/${params.clusterID}/${params.keyspace}/${params.shard}/validate`, {
   797          method: 'put',
   798          body: JSON.stringify({ ping_tablets: params.pingTablets }),
   799      });
   800  
   801      const err = vtctldata.ValidateShardResponse.verify(result);
   802      if (err) throw Error(err);
   803  
   804      return vtctldata.ValidateShardResponse.create(result);
   805  };
   806  
   807  export interface GetFullStatusParams {
   808      clusterID: string;
   809      alias: string;
   810  }
   811  
   812  export const getFullStatus = async (params: GetFullStatusParams) => {
   813      const req = new URLSearchParams();
   814      req.append('cluster', params.clusterID);
   815  
   816      const { result } = await vtfetch(`/api/tablet/${params.alias}/full_status?${req.toString()}`);
   817  
   818      const err = vtctldata.GetFullStatusResponse.verify(result);
   819      if (err) throw Error(err);
   820  
   821      return vtctldata.GetFullStatusResponse.create(result);
   822  };
   823  
   824  export interface ValidateVersionShardParams {
   825      clusterID: string;
   826      keyspace: string;
   827      shard: string;
   828  }
   829  
   830  export const validateVersionShard = async (params: ValidateVersionShardParams) => {
   831      const { result } = await vtfetch(
   832          `/api/shard/${params.clusterID}/${params.keyspace}/${params.shard}/validate_version`,
   833          {
   834              method: 'put',
   835          }
   836      );
   837  
   838      const err = vtctldata.ValidateVersionShardResponse.verify(result);
   839      if (err) throw Error(err);
   840  
   841      return vtctldata.ValidateVersionShardResponse.create(result);
   842  };