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 };