vitess.io/vitess@v0.16.2/go/vt/vtorc/server/api.go (about)

     1  /*
     2  Copyright 2022 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 server
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/json"
    22  	"fmt"
    23  	"net/http"
    24  
    25  	"vitess.io/vitess/go/acl"
    26  	"vitess.io/vitess/go/vt/log"
    27  	"vitess.io/vitess/go/vt/vtorc/inst"
    28  	"vitess.io/vitess/go/vt/vtorc/logic"
    29  	"vitess.io/vitess/go/vt/vtorc/process"
    30  )
    31  
    32  // vtorcAPI struct is created to implement the Handler interface to register
    33  // the API endpoints for VTOrc. Instead, we could have used the HandleFunc method
    34  // of registering the endpoints, but this approach seems cleaner and easier to unit test
    35  // as it abstracts the acl check code into a single place
    36  type vtorcAPI struct{}
    37  
    38  const (
    39  	problemsAPI                = "/api/problems"
    40  	disableGlobalRecoveriesAPI = "/api/disable-global-recoveries"
    41  	enableGlobalRecoveriesAPI  = "/api/enable-global-recoveries"
    42  	replicationAnalysisAPI     = "/api/replication-analysis"
    43  	healthAPI                  = "/debug/health"
    44  
    45  	shardWithoutKeyspaceFilteringErrorStr = "Filtering by shard without keyspace isn't supported"
    46  )
    47  
    48  var (
    49  	apiHandler    = &vtorcAPI{}
    50  	vtorcAPIPaths = []string{
    51  		problemsAPI,
    52  		disableGlobalRecoveriesAPI,
    53  		enableGlobalRecoveriesAPI,
    54  		replicationAnalysisAPI,
    55  		healthAPI,
    56  	}
    57  )
    58  
    59  // ServeHTTP implements the http.Handler interface. This is the entry point for all the api commands of VTOrc
    60  func (v *vtorcAPI) ServeHTTP(response http.ResponseWriter, request *http.Request) {
    61  	apiPath := request.URL.Path
    62  	log.Infof("HTTP API Request received: %v", apiPath)
    63  	if err := acl.CheckAccessHTTP(request, getACLPermissionLevelForAPI(apiPath)); err != nil {
    64  		acl.SendError(response, err)
    65  		return
    66  	}
    67  
    68  	switch apiPath {
    69  	case disableGlobalRecoveriesAPI:
    70  		disableGlobalRecoveriesAPIHandler(response)
    71  	case enableGlobalRecoveriesAPI:
    72  		enableGlobalRecoveriesAPIHandler(response)
    73  	case healthAPI:
    74  		healthAPIHandler(response, request)
    75  	case problemsAPI:
    76  		problemsAPIHandler(response, request)
    77  	case replicationAnalysisAPI:
    78  		replicationAnalysisAPIHandler(response, request)
    79  	default:
    80  		// This should be unreachable. Any endpoint which isn't registered is automatically redirected to /debug/status.
    81  		// This code will only be reachable if we register an API but don't handle it here. That will be a bug.
    82  		http.Error(response, "API registered but not handled. Please open an issue at https://github.com/vitessio/vitess/issues/new/choose", http.StatusInternalServerError)
    83  	}
    84  }
    85  
    86  // getACLPermissionLevelForAPI returns the acl permission level that is required to run a given API
    87  func getACLPermissionLevelForAPI(apiEndpoint string) string {
    88  	switch apiEndpoint {
    89  	case problemsAPI:
    90  		return acl.MONITORING
    91  	case disableGlobalRecoveriesAPI, enableGlobalRecoveriesAPI:
    92  		return acl.ADMIN
    93  	case replicationAnalysisAPI:
    94  		return acl.MONITORING
    95  	case healthAPI:
    96  		return acl.MONITORING
    97  	}
    98  	return acl.ADMIN
    99  }
   100  
   101  // RegisterVTOrcAPIEndpoints is used to register the VTOrc API endpoints
   102  func RegisterVTOrcAPIEndpoints() {
   103  	for _, apiPath := range vtorcAPIPaths {
   104  		http.Handle(apiPath, apiHandler)
   105  	}
   106  }
   107  
   108  // returnAsJSON returns the argument received on the resposeWriter as a json object
   109  func returnAsJSON(response http.ResponseWriter, code int, stuff any) {
   110  	response.Header().Set("Content-Type", "application/json; charset=utf-8")
   111  	response.WriteHeader(code)
   112  	buf, err := json.MarshalIndent(stuff, "", " ")
   113  	if err != nil {
   114  		_, _ = response.Write([]byte(err.Error()))
   115  		return
   116  	}
   117  	ebuf := bytes.NewBuffer(nil)
   118  	json.HTMLEscape(ebuf, buf)
   119  	_, _ = response.Write(ebuf.Bytes())
   120  }
   121  
   122  // problemsAPIHandler is the handler for the problemsAPI endpoint
   123  func problemsAPIHandler(response http.ResponseWriter, request *http.Request) {
   124  	// This api also supports filtering by shard and keyspace provided.
   125  	shard := request.URL.Query().Get("shard")
   126  	keyspace := request.URL.Query().Get("keyspace")
   127  	if shard != "" && keyspace == "" {
   128  		http.Error(response, shardWithoutKeyspaceFilteringErrorStr, http.StatusBadRequest)
   129  		return
   130  	}
   131  	instances, err := inst.ReadProblemInstances(keyspace, shard)
   132  	if err != nil {
   133  		http.Error(response, err.Error(), http.StatusInternalServerError)
   134  		return
   135  	}
   136  	returnAsJSON(response, http.StatusOK, instances)
   137  }
   138  
   139  // disableGlobalRecoveriesAPIHandler is the handler for the disableGlobalRecoveriesAPI endpoint
   140  func disableGlobalRecoveriesAPIHandler(response http.ResponseWriter) {
   141  	err := logic.DisableRecovery()
   142  	if err != nil {
   143  		http.Error(response, err.Error(), http.StatusInternalServerError)
   144  		return
   145  	}
   146  	writePlainTextResponse(response, "Global recoveries disabled", http.StatusOK)
   147  }
   148  
   149  // enableGlobalRecoveriesAPIHandler is the handler for the enableGlobalRecoveriesAPI endpoint
   150  func enableGlobalRecoveriesAPIHandler(response http.ResponseWriter) {
   151  	err := logic.EnableRecovery()
   152  	if err != nil {
   153  		http.Error(response, err.Error(), http.StatusInternalServerError)
   154  		return
   155  	}
   156  	writePlainTextResponse(response, "Global recoveries enabled", http.StatusOK)
   157  }
   158  
   159  // replicationAnalysisAPIHandler is the handler for the replicationAnalysisAPI endpoint
   160  func replicationAnalysisAPIHandler(response http.ResponseWriter, request *http.Request) {
   161  	// This api also supports filtering by shard and keyspace provided.
   162  	shard := request.URL.Query().Get("shard")
   163  	keyspace := request.URL.Query().Get("keyspace")
   164  	if shard != "" && keyspace == "" {
   165  		http.Error(response, shardWithoutKeyspaceFilteringErrorStr, http.StatusBadRequest)
   166  		return
   167  	}
   168  	analysis, err := inst.GetReplicationAnalysis(keyspace, shard, &inst.ReplicationAnalysisHints{})
   169  	if err != nil {
   170  		http.Error(response, err.Error(), http.StatusInternalServerError)
   171  		return
   172  	}
   173  
   174  	// TODO: We can also add filtering for a specific instance too based on the tablet alias.
   175  	// Currently inst.ReplicationAnalysis doesn't store the tablet alias, but once it does we can filter on that too
   176  	returnAsJSON(response, http.StatusOK, analysis)
   177  }
   178  
   179  // healthAPIHandler is the handler for the healthAPI endpoint
   180  func healthAPIHandler(response http.ResponseWriter, request *http.Request) {
   181  	health, err := process.HealthTest()
   182  	if err != nil {
   183  		http.Error(response, err.Error(), http.StatusInternalServerError)
   184  		return
   185  	}
   186  	code := http.StatusOK
   187  	if !health.Healthy {
   188  		code = http.StatusInternalServerError
   189  	}
   190  	returnAsJSON(response, code, health)
   191  }
   192  
   193  // writePlainTextResponse writes a plain text response to the writer.
   194  func writePlainTextResponse(w http.ResponseWriter, message string, code int) {
   195  	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
   196  	w.Header().Set("X-Content-Type-Options", "nosniff")
   197  	w.WriteHeader(code)
   198  	_, _ = fmt.Fprintln(w, message)
   199  }