vitess.io/vitess@v0.16.2/go/vt/vtctld/api.go (about) 1 /* 2 Copyright 2019 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 vtctld 18 19 import ( 20 "context" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "io" 25 "net/http" 26 "strings" 27 "time" 28 29 "github.com/spf13/pflag" 30 31 "vitess.io/vitess/go/acl" 32 "vitess.io/vitess/go/netutil" 33 "vitess.io/vitess/go/vt/discovery" 34 "vitess.io/vitess/go/vt/log" 35 "vitess.io/vitess/go/vt/logutil" 36 "vitess.io/vitess/go/vt/mysqlctl" 37 "vitess.io/vitess/go/vt/proto/vttime" 38 "vitess.io/vitess/go/vt/schema" 39 "vitess.io/vitess/go/vt/schemamanager" 40 "vitess.io/vitess/go/vt/servenv" 41 "vitess.io/vitess/go/vt/topo" 42 "vitess.io/vitess/go/vt/topo/topoproto" 43 "vitess.io/vitess/go/vt/vtctl" 44 "vitess.io/vitess/go/vt/vttablet/tmclient" 45 "vitess.io/vitess/go/vt/wrangler" 46 47 logutilpb "vitess.io/vitess/go/vt/proto/logutil" 48 querypb "vitess.io/vitess/go/vt/proto/query" 49 topodatapb "vitess.io/vitess/go/vt/proto/topodata" 50 ) 51 52 var ( 53 localCell string 54 proxyTablets bool 55 showTopologyCRUD = true 56 ) 57 58 // This file implements a REST-style API for the vtctld web interface. 59 60 const ( 61 apiPrefix = "/api/" 62 jsonContentType = "application/json; charset=utf-8" 63 ) 64 65 // TabletStats represents realtime stats from a discovery.TabletHealth struct. 66 type TabletStats struct { 67 LastError string `json:"last_error,omitempty"` 68 Realtime *querypb.RealtimeStats `json:"realtime,omitempty"` 69 Serving bool `json:"serving"` 70 Up bool `json:"up"` 71 } 72 73 // TabletWithStatsAndURL wraps topo.Tablet, adding a URL property and optional realtime stats. 74 type TabletWithStatsAndURL struct { 75 Alias *topodatapb.TabletAlias `json:"alias,omitempty"` 76 Hostname string `json:"hostname,omitempty"` 77 PortMap map[string]int32 `json:"port_map,omitempty"` 78 Keyspace string `json:"keyspace,omitempty"` 79 Shard string `json:"shard,omitempty"` 80 KeyRange *topodatapb.KeyRange `json:"key_range,omitempty"` 81 Type topodatapb.TabletType `json:"type,omitempty"` 82 DbNameOverride string `json:"db_name_override,omitempty"` 83 Tags map[string]string `json:"tags,omitempty"` 84 MysqlHostname string `json:"mysql_hostname,omitempty"` 85 MysqlPort int32 `json:"mysql_port,omitempty"` 86 PrimaryTermStartTime *vttime.Time `json:"primary_term_start_time,omitempty"` 87 Stats *TabletStats `json:"stats,omitempty"` 88 URL string `json:"url,omitempty"` 89 } 90 91 func init() { 92 for _, cmd := range []string{"vtcombo", "vtctld"} { 93 servenv.OnParseFor(cmd, registerVtctldAPIFlags) 94 } 95 } 96 97 func registerVtctldAPIFlags(fs *pflag.FlagSet) { 98 fs.StringVar(&localCell, "cell", localCell, "cell to use") 99 fs.BoolVar(&proxyTablets, "proxy_tablets", proxyTablets, "Setting this true will make vtctld proxy the tablet status instead of redirecting to them") 100 fs.BoolVar(&showTopologyCRUD, "vtctld_show_topology_crud", showTopologyCRUD, "Controls the display of the CRUD topology actions in the vtctld UI.") 101 fs.MarkDeprecated("vtctld_show_topology_crud", "It is no longer applicable because vtctld no longer provides a UI.") 102 } 103 104 func newTabletWithStatsAndURL(t *topodatapb.Tablet, healthcheck discovery.HealthCheck) *TabletWithStatsAndURL { 105 tablet := &TabletWithStatsAndURL{ 106 Alias: t.Alias, 107 Hostname: t.Hostname, 108 PortMap: t.PortMap, 109 Keyspace: t.Keyspace, 110 Shard: t.Shard, 111 KeyRange: t.KeyRange, 112 Type: t.Type, 113 DbNameOverride: t.DbNameOverride, 114 Tags: t.Tags, 115 MysqlHostname: t.MysqlHostname, 116 MysqlPort: t.MysqlPort, 117 PrimaryTermStartTime: t.PrimaryTermStartTime, 118 } 119 120 if proxyTablets { 121 tablet.URL = fmt.Sprintf("/vttablet/%s-%d/debug/status", t.Alias.Cell, t.Alias.Uid) 122 } else { 123 tablet.URL = "http://" + netutil.JoinHostPort(t.Hostname, t.PortMap["vt"]) 124 } 125 126 if healthcheck != nil { 127 if health, err := healthcheck.GetTabletHealth(discovery.KeyFromTablet(t), tablet.Alias); err == nil { 128 tablet.Stats = &TabletStats{ 129 Realtime: health.Stats, 130 Serving: health.Serving, 131 Up: true, 132 } 133 if health.LastError != nil { 134 tablet.Stats.LastError = health.LastError.Error() 135 } 136 } 137 } 138 139 return tablet 140 } 141 142 func httpErrorf(w http.ResponseWriter, r *http.Request, format string, args ...any) { 143 errMsg := fmt.Sprintf(format, args...) 144 log.Errorf("HTTP error on %v: %v, request: %#v", r.URL.Path, errMsg, r) 145 http.Error(w, errMsg, http.StatusInternalServerError) 146 } 147 148 func handleAPI(apiPath string, handlerFunc func(w http.ResponseWriter, r *http.Request) error) { 149 http.HandleFunc(apiPrefix+apiPath, func(w http.ResponseWriter, r *http.Request) { 150 defer func() { 151 if x := recover(); x != nil { 152 httpErrorf(w, r, "uncaught panic: %v", x) 153 } 154 }() 155 if err := handlerFunc(w, r); err != nil { 156 httpErrorf(w, r, "%v", err) 157 } 158 }) 159 } 160 161 func handleCollection(collection string, getFunc func(*http.Request) (any, error)) { 162 handleAPI(collection+"/", func(w http.ResponseWriter, r *http.Request) error { 163 // Get the requested object. 164 obj, err := getFunc(r) 165 if err != nil { 166 if topo.IsErrType(err, topo.NoNode) { 167 http.NotFound(w, r) 168 return nil 169 } 170 return fmt.Errorf("can't get %v: %v", collection, err) 171 } 172 173 // JSON encode response. 174 data, err := vtctl.MarshalJSON(obj) 175 log.Flush() 176 if err != nil { 177 return fmt.Errorf("cannot marshal data: %v", err) 178 } 179 w.Header().Set("Content-Type", jsonContentType) 180 w.Write(data) 181 return nil 182 }) 183 } 184 185 func getItemPath(url string) string { 186 // Strip API prefix. 187 if !strings.HasPrefix(url, apiPrefix) { 188 return "" 189 } 190 url = url[len(apiPrefix):] 191 192 // Strip collection name. 193 parts := strings.SplitN(url, "/", 2) 194 if len(parts) != 2 { 195 return "" 196 } 197 return parts[1] 198 } 199 200 func unmarshalRequest(r *http.Request, v any) error { 201 data, err := io.ReadAll(r.Body) 202 if err != nil { 203 return err 204 } 205 return json.Unmarshal(data, v) 206 } 207 208 func initAPI(ctx context.Context, ts *topo.Server, actions *ActionRepository, healthcheck discovery.HealthCheck) { 209 tabletHealthCache := newTabletHealthCache(ts) 210 tmClient := tmclient.NewTabletManagerClient() 211 212 // Cells 213 handleCollection("cells", func(r *http.Request) (any, error) { 214 if getItemPath(r.URL.Path) != "" { 215 return nil, errors.New("cells can only be listed, not retrieved") 216 } 217 return ts.GetKnownCells(ctx) 218 }) 219 220 // Keyspaces 221 handleCollection("keyspaces", func(r *http.Request) (any, error) { 222 keyspace := getItemPath(r.URL.Path) 223 switch r.Method { 224 case "GET": 225 // List all keyspaces. 226 if keyspace == "" { 227 return ts.GetKeyspaces(ctx) 228 } 229 // Get the keyspace record. 230 k, err := ts.GetKeyspace(ctx, keyspace) 231 if err != nil { 232 return nil, err 233 } 234 // Pass the embedded proto directly or jsonpb will panic. 235 return k.Keyspace, err 236 // Perform an action on a keyspace. 237 case "POST": 238 if keyspace == "" { 239 return nil, errors.New("a POST request needs a keyspace in the URL") 240 } 241 if err := r.ParseForm(); err != nil { 242 return nil, err 243 } 244 245 action := r.FormValue("action") 246 if action == "" { 247 return nil, errors.New("a POST request must specify action") 248 } 249 return actions.ApplyKeyspaceAction(ctx, action, keyspace), nil 250 default: 251 return nil, fmt.Errorf("unsupported HTTP method: %v", r.Method) 252 } 253 }) 254 255 handleCollection("keyspace", func(r *http.Request) (any, error) { 256 // Valid requests: api/keyspace/my_ks/tablets (all shards) 257 // Valid requests: api/keyspace/my_ks/tablets/-80 (specific shard) 258 itemPath := getItemPath(r.URL.Path) 259 parts := strings.SplitN(itemPath, "/", 3) 260 261 malformedRequestError := fmt.Errorf("invalid keyspace path: %q expected path: /keyspace/<keyspace>/tablets or /keyspace/<keyspace>/tablets/<shard>", itemPath) 262 if len(parts) < 2 { 263 return nil, malformedRequestError 264 } 265 if parts[1] != "tablets" { 266 return nil, malformedRequestError 267 } 268 269 keyspace := parts[0] 270 if keyspace == "" { 271 return nil, errors.New("keyspace is required") 272 } 273 var shardNames []string 274 if len(parts) > 2 && parts[2] != "" { 275 shardNames = []string{parts[2]} 276 } else { 277 var err error 278 shardNames, err = ts.GetShardNames(ctx, keyspace) 279 if err != nil { 280 return nil, err 281 } 282 } 283 284 if err := r.ParseForm(); err != nil { 285 return nil, err 286 } 287 cell := r.FormValue("cell") 288 cells := r.FormValue("cells") 289 filterCells := []string{} // empty == all cells 290 if cell != "" { 291 filterCells = []string{cell} // single cell 292 } else if cells != "" { 293 filterCells = strings.Split(cells, ",") // list of cells 294 } 295 296 tablets := [](*TabletWithStatsAndURL){} 297 for _, shard := range shardNames { 298 // Get tablets for this shard. 299 tabletAliases, err := ts.FindAllTabletAliasesInShardByCell(ctx, keyspace, shard, filterCells) 300 if err != nil && !topo.IsErrType(err, topo.PartialResult) { 301 return nil, err 302 } 303 for _, tabletAlias := range tabletAliases { 304 t, err := ts.GetTablet(ctx, tabletAlias) 305 if err != nil { 306 return nil, err 307 } 308 tablet := newTabletWithStatsAndURL(t.Tablet, healthcheck) 309 tablets = append(tablets, tablet) 310 } 311 } 312 return tablets, nil 313 }) 314 315 // Shards 316 handleCollection("shards", func(r *http.Request) (any, error) { 317 shardPath := getItemPath(r.URL.Path) 318 if !strings.Contains(shardPath, "/") { 319 return nil, fmt.Errorf("invalid shard path: %q", shardPath) 320 } 321 parts := strings.SplitN(shardPath, "/", 2) 322 keyspace := parts[0] 323 shard := parts[1] 324 325 // List the shards in a keyspace. 326 if shard == "" { 327 return ts.GetShardNames(ctx, keyspace) 328 } 329 330 // Perform an action on a shard. 331 if r.Method == "POST" { 332 if err := r.ParseForm(); err != nil { 333 return nil, err 334 } 335 action := r.FormValue("action") 336 if action == "" { 337 return nil, errors.New("must specify action") 338 } 339 return actions.ApplyShardAction(ctx, action, keyspace, shard), nil 340 } 341 342 // Get the shard record. 343 si, err := ts.GetShard(ctx, keyspace, shard) 344 if err != nil { 345 return nil, err 346 } 347 // Pass the embedded proto directly or jsonpb will panic. 348 return si.Shard, err 349 }) 350 351 // SrvKeyspace 352 handleCollection("srv_keyspace", func(r *http.Request) (any, error) { 353 keyspacePath := getItemPath(r.URL.Path) 354 parts := strings.SplitN(keyspacePath, "/", 2) 355 356 // Request was incorrectly formatted. 357 if len(parts) != 2 { 358 return nil, fmt.Errorf("invalid srvkeyspace path: %q expected path: /srv_keyspace/<cell>/<keyspace>", keyspacePath) 359 } 360 361 cell := parts[0] 362 keyspace := parts[1] 363 364 if cell == "local" { 365 if localCell == "" { 366 cells, err := ts.GetCellInfoNames(ctx) 367 if err != nil { 368 return nil, fmt.Errorf("could not fetch cell info: %v", err) 369 } 370 if len(cells) == 0 { 371 return nil, fmt.Errorf("no local cells have been created yet") 372 } 373 cell = cells[0] 374 } else { 375 cell = localCell 376 } 377 } 378 379 // If a keyspace is provided then return the specified srvkeyspace. 380 if keyspace != "" { 381 srvKeyspace, err := ts.GetSrvKeyspace(ctx, cell, keyspace) 382 if err != nil { 383 return nil, fmt.Errorf("can't get server keyspace: %v", err) 384 } 385 return srvKeyspace, nil 386 } 387 388 // Else return the srvKeyspace from all keyspaces. 389 srvKeyspaces := make(map[string]any) 390 keyspaceNamesList, err := ts.GetSrvKeyspaceNames(ctx, cell) 391 if err != nil { 392 return nil, fmt.Errorf("can't get list of SrvKeyspaceNames for cell %q: GetSrvKeyspaceNames returned: %v", cell, err) 393 } 394 for _, keyspaceName := range keyspaceNamesList { 395 srvKeyspace, err := ts.GetSrvKeyspace(ctx, cell, keyspaceName) 396 if err != nil { 397 // If a keyspace is in the process of being set up, it exists 398 // in the list of keyspaces but GetSrvKeyspace fails. 399 // 400 // Instead of returning this error, simply skip it in the 401 // loop so we still return the other valid keyspaces. 402 continue 403 } 404 srvKeyspaces[keyspaceName] = srvKeyspace 405 } 406 return srvKeyspaces, nil 407 408 }) 409 410 // Tablets 411 handleCollection("tablets", func(r *http.Request) (any, error) { 412 tabletPath := getItemPath(r.URL.Path) 413 414 // List tablets based on query params. 415 if tabletPath == "" { 416 if err := r.ParseForm(); err != nil { 417 return nil, err 418 } 419 shardRef := r.FormValue("shard") 420 cell := r.FormValue("cell") 421 422 if shardRef != "" { 423 // Look up by keyspace/shard, and optionally cell. 424 keyspace, shard, err := topoproto.ParseKeyspaceShard(shardRef) 425 if err != nil { 426 return nil, err 427 } 428 if cell != "" { 429 result, err := ts.FindAllTabletAliasesInShardByCell(ctx, keyspace, shard, []string{cell}) 430 if err != nil && !topo.IsErrType(err, topo.PartialResult) { 431 return result, err 432 } 433 return result, nil 434 } 435 result, err := ts.FindAllTabletAliasesInShard(ctx, keyspace, shard) 436 if err != nil && !topo.IsErrType(err, topo.PartialResult) { 437 return result, err 438 } 439 return result, nil 440 } 441 442 // Get all tablets in a cell. 443 if cell == "" { 444 return nil, errors.New("cell param required") 445 } 446 return ts.GetTabletAliasesByCell(ctx, cell) 447 } 448 449 // Get tablet health. 450 if parts := strings.Split(tabletPath, "/"); len(parts) == 2 && parts[1] == "health" { 451 tabletAlias, err := topoproto.ParseTabletAlias(parts[0]) 452 if err != nil { 453 return nil, err 454 } 455 return tabletHealthCache.Get(ctx, tabletAlias) 456 } 457 458 tabletAlias, err := topoproto.ParseTabletAlias(tabletPath) 459 if err != nil { 460 return nil, err 461 } 462 463 // Perform an action on a tablet. 464 if r.Method == "POST" { 465 if err := r.ParseForm(); err != nil { 466 return nil, err 467 } 468 action := r.FormValue("action") 469 if action == "" { 470 return nil, errors.New("must specify action") 471 } 472 return actions.ApplyTabletAction(ctx, action, tabletAlias, r), nil 473 } 474 475 // Get the tablet record. 476 t, err := ts.GetTablet(ctx, tabletAlias) 477 if err != nil { 478 return nil, err 479 } 480 481 return newTabletWithStatsAndURL(t.Tablet, nil), nil 482 }) 483 484 // Healthcheck real time status per (cell, keyspace, tablet type, metric). 485 handleCollection("tablet_statuses", func(r *http.Request) (any, error) { 486 targetPath := getItemPath(r.URL.Path) 487 488 // Get the heatmap data based on query parameters. 489 if targetPath == "" { 490 if err := r.ParseForm(); err != nil { 491 return nil, err 492 } 493 keyspace := r.FormValue("keyspace") 494 cell := r.FormValue("cell") 495 tabletType := r.FormValue("type") 496 _, err := topoproto.ParseTabletType(tabletType) 497 // Excluding the case where parse fails because all tabletTypes was chosen. 498 if err != nil && tabletType != "all" { 499 return nil, fmt.Errorf("invalid tablet type: %v ", err) 500 } 501 metric := r.FormValue("metric") 502 503 // Setting default values if none was specified in the query params. 504 if keyspace == "" { 505 keyspace = "all" 506 } 507 if cell == "" { 508 cell = "all" 509 } 510 if tabletType == "" { 511 tabletType = "all" 512 } 513 if metric == "" { 514 metric = "health" 515 } 516 517 if healthcheck == nil { 518 return nil, fmt.Errorf("healthcheck not initialized") 519 } 520 521 heatmap, err := heatmapData(healthcheck, keyspace, cell, tabletType, metric) 522 if err != nil { 523 return nil, fmt.Errorf("couldn't get heatmap data: %v", err) 524 } 525 return heatmap, nil 526 } 527 528 return nil, fmt.Errorf("invalid target path: %q expected path: ?keyspace=<keyspace>&cell=<cell>&type=<type>&metric=<metric>", targetPath) 529 }) 530 531 handleCollection("tablet_health", func(r *http.Request) (any, error) { 532 tabletPath := getItemPath(r.URL.Path) 533 parts := strings.SplitN(tabletPath, "/", 2) 534 535 // Request was incorrectly formatted. 536 if len(parts) != 2 { 537 return nil, fmt.Errorf("invalid tablet_health path: %q expected path: /tablet_health/<cell>/<uid>", tabletPath) 538 } 539 540 if healthcheck == nil { 541 return nil, fmt.Errorf("healthcheck not initialized") 542 } 543 544 cell := parts[0] 545 uidStr := parts[1] 546 uid, err := topoproto.ParseUID(uidStr) 547 if err != nil { 548 return nil, fmt.Errorf("incorrect uid: %v", err) 549 } 550 551 tabletAlias := topodatapb.TabletAlias{ 552 Cell: cell, 553 Uid: uid, 554 } 555 tabletStat, err := healthcheck.GetTabletHealthByAlias(&tabletAlias) 556 if err != nil { 557 return nil, fmt.Errorf("could not get tabletStats: %v", err) 558 } 559 return tabletStat, nil 560 }) 561 562 handleCollection("topology_info", func(r *http.Request) (any, error) { 563 targetPath := getItemPath(r.URL.Path) 564 565 // Retrieving topology information (keyspaces, cells, and types) based on query params. 566 if targetPath == "" { 567 if err := r.ParseForm(); err != nil { 568 return nil, err 569 } 570 keyspace := r.FormValue("keyspace") 571 cell := r.FormValue("cell") 572 573 // Setting default values if none was specified in the query params. 574 if keyspace == "" { 575 keyspace = "all" 576 } 577 if cell == "" { 578 cell = "all" 579 } 580 581 if healthcheck == nil { 582 return nil, fmt.Errorf("realtimeStats not initialized") 583 } 584 585 return getTopologyInfo(healthcheck, keyspace, cell), nil 586 } 587 return nil, fmt.Errorf("invalid target path: %q expected path: ?keyspace=<keyspace>&cell=<cell>", targetPath) 588 }) 589 590 // Vtctl Command 591 handleAPI("vtctl/", func(w http.ResponseWriter, r *http.Request) error { 592 if err := acl.CheckAccessHTTP(r, acl.ADMIN); err != nil { 593 http.Error(w, "403 Forbidden", http.StatusForbidden) 594 return nil 595 } 596 var args []string 597 resp := struct { 598 Error string 599 Output string 600 }{} 601 if err := unmarshalRequest(r, &args); err != nil { 602 return fmt.Errorf("can't unmarshal request: %v", err) 603 } 604 605 logstream := logutil.NewMemoryLogger() 606 607 wr := wrangler.New(logstream, ts, tmClient) 608 err := vtctl.RunCommand(r.Context(), wr, args) 609 if err != nil { 610 resp.Error = err.Error() 611 } 612 resp.Output = logstream.String() 613 data, err := json.MarshalIndent(resp, "", " ") 614 if err != nil { 615 return fmt.Errorf("json error: %v", err) 616 } 617 w.Header().Set("Content-Type", jsonContentType) 618 w.Write(data) 619 return nil 620 }) 621 622 // Schema Change 623 handleAPI("schema/apply", func(w http.ResponseWriter, r *http.Request) error { 624 if err := acl.CheckAccessHTTP(r, acl.ADMIN); err != nil { 625 http.Error(w, "403 Forbidden", http.StatusForbidden) 626 return nil 627 } 628 req := struct { 629 Keyspace, SQL string 630 ReplicaTimeoutSeconds int 631 DDLStrategy string `json:"ddl_strategy,omitempty"` 632 }{} 633 if err := unmarshalRequest(r, &req); err != nil { 634 return fmt.Errorf("can't unmarshal request: %v", err) 635 } 636 if req.ReplicaTimeoutSeconds <= 0 { 637 req.ReplicaTimeoutSeconds = 10 638 } 639 640 logger := logutil.NewCallbackLogger(func(ev *logutilpb.Event) { 641 w.Write([]byte(logutil.EventString(ev))) 642 }) 643 wr := wrangler.New(logger, ts, tmClient) 644 645 apiCallUUID, err := schema.CreateUUID() 646 if err != nil { 647 return err 648 } 649 650 requestContext := fmt.Sprintf("vtctld/api:%s", apiCallUUID) 651 executor := schemamanager.NewTabletExecutor(requestContext, wr.TopoServer(), wr.TabletManagerClient(), wr.Logger(), time.Duration(req.ReplicaTimeoutSeconds)*time.Second) 652 if err := executor.SetDDLStrategy(req.DDLStrategy); err != nil { 653 return fmt.Errorf("error setting DDL strategy: %v", err) 654 } 655 656 _, err = schemamanager.Run(ctx, 657 schemamanager.NewUIController(req.SQL, req.Keyspace, w), executor) 658 return err 659 }) 660 661 // Features 662 handleAPI("features", func(w http.ResponseWriter, r *http.Request) error { 663 if err := acl.CheckAccessHTTP(r, acl.ADMIN); err != nil { 664 http.Error(w, "403 Forbidden", http.StatusForbidden) 665 return nil 666 } 667 668 resp := make(map[string]any) 669 resp["activeReparents"] = !mysqlctl.DisableActiveReparents 670 resp["showStatus"] = enableRealtimeStats 671 data, err := json.MarshalIndent(resp, "", " ") 672 if err != nil { 673 return fmt.Errorf("json error: %v", err) 674 } 675 w.Header().Set("Content-Type", jsonContentType) 676 w.Write(data) 677 return nil 678 }) 679 }