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 }