github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/boskos/boskos.go (about) 1 /* 2 Copyright 2017 The Kubernetes 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 main 18 19 import ( 20 "bytes" 21 "encoding/json" 22 "flag" 23 "fmt" 24 "io" 25 "net/http" 26 "strings" 27 "time" 28 29 "github.com/sirupsen/logrus" 30 31 "k8s.io/test-infra/boskos/common" 32 "k8s.io/test-infra/boskos/crds" 33 "k8s.io/test-infra/boskos/ranch" 34 ) 35 36 var ( 37 configPath = flag.String("config", "config.yaml", "Path to init resource file") 38 storagePath = flag.String("storage", "", "Path to persistent volume to load the state") 39 kubeClientOptions crds.KubernetesClientOptions 40 ) 41 42 func main() { 43 kubeClientOptions.AddFlags(flag.CommandLine) 44 flag.Parse() 45 kubeClientOptions.Validate() 46 47 logrus.SetFormatter(&logrus.JSONFormatter{}) 48 49 rc, err := kubeClientOptions.Client(crds.ResourceType) 50 if err != nil { 51 logrus.WithError(err).Fatal("unable to create a CRD client") 52 } 53 54 resourceStorage := crds.NewCRDStorage(rc) 55 storage, err := ranch.NewStorage(resourceStorage, *storagePath) 56 if err != nil { 57 logrus.WithError(err).Fatal("failed to create storage") 58 } 59 60 r, err := ranch.NewRanch(*configPath, storage) 61 if err != nil { 62 logrus.WithError(err).Fatalf("failed to create ranch! Config: %v", *configPath) 63 } 64 65 boskos := http.Server{ 66 Handler: NewBoskosHandler(r), 67 Addr: ":8080", 68 } 69 70 go func() { 71 logTick := time.NewTicker(time.Minute).C 72 configTick := time.NewTicker(time.Minute * 10).C 73 for { 74 select { 75 case <-logTick: 76 r.LogStatus() 77 case <-configTick: 78 r.SyncConfig(*configPath) 79 } 80 } 81 }() 82 83 logrus.Info("Start Service") 84 logrus.WithError(boskos.ListenAndServe()).Fatal("ListenAndServe returned.") 85 } 86 87 //NewBoskosHandler constructs the boskos handler. 88 func NewBoskosHandler(r *ranch.Ranch) *http.ServeMux { 89 mux := http.NewServeMux() 90 mux.Handle("/", handleDefault(r)) 91 mux.Handle("/acquire", handleAcquire(r)) 92 mux.Handle("/acquirebystate", handleAcquireByState(r)) 93 mux.Handle("/release", handleRelease(r)) 94 mux.Handle("/reset", handleReset(r)) 95 mux.Handle("/update", handleUpdate(r)) 96 mux.Handle("/metric", handleMetric(r)) 97 return mux 98 } 99 100 // ErrorToStatus translates error into http code 101 func ErrorToStatus(err error) int { 102 switch err.(type) { 103 default: 104 return http.StatusInternalServerError 105 case *ranch.OwnerNotMatch: 106 return http.StatusUnauthorized 107 case *ranch.ResourceNotFound: 108 return http.StatusNotFound 109 case *ranch.StateNotMatch: 110 return http.StatusConflict 111 } 112 } 113 114 // handleDefault: Handler for /, always pass with 200 115 func handleDefault(r *ranch.Ranch) http.HandlerFunc { 116 return func(res http.ResponseWriter, req *http.Request) { 117 logrus.WithField("handler", "handleDefault").Infof("From %v", req.RemoteAddr) 118 } 119 } 120 121 // handleAcquire: Handler for /acquire 122 // Method: POST 123 // URLParams: 124 // Required: type=[string] : type of requested resource 125 // Required: state=[string] : current state of the requested resource 126 // Required: dest=[string] : destination state of the requested resource 127 // Required: owner=[string] : requester of the resource 128 func handleAcquire(r *ranch.Ranch) http.HandlerFunc { 129 return func(res http.ResponseWriter, req *http.Request) { 130 logrus.WithField("handler", "handleStart").Infof("From %v", req.RemoteAddr) 131 132 if req.Method != http.MethodPost { 133 msg := fmt.Sprintf("Method %v, /acquire only accepts POST.", req.Method) 134 logrus.Warning(msg) 135 http.Error(res, msg, http.StatusMethodNotAllowed) 136 return 137 } 138 139 // TODO(krzyzacy) - sanitize user input 140 rtype := req.URL.Query().Get("type") 141 state := req.URL.Query().Get("state") 142 dest := req.URL.Query().Get("dest") 143 owner := req.URL.Query().Get("owner") 144 if rtype == "" || state == "" || dest == "" || owner == "" { 145 msg := fmt.Sprintf("Type: %v, state: %v, dest: %v, owner: %v, all of them must be set in the request.", rtype, state, dest, owner) 146 logrus.Warning(msg) 147 http.Error(res, msg, http.StatusBadRequest) 148 return 149 } 150 151 logrus.Infof("Request for a %v %v from %v, dest %v", state, rtype, owner, dest) 152 153 resource, err := r.Acquire(rtype, state, dest, owner) 154 155 if err != nil { 156 logrus.WithError(err).Errorf("No available resource") 157 http.Error(res, err.Error(), ErrorToStatus(err)) 158 return 159 } 160 161 resJSON, err := json.Marshal(resource) 162 if err != nil { 163 logrus.WithError(err).Errorf("json.Marshal failed: %v, resource will be released", resource) 164 http.Error(res, err.Error(), ErrorToStatus(err)) 165 // release the resource, though this is not expected to happen. 166 err = r.Release(resource.Name, state, owner) 167 if err != nil { 168 logrus.WithError(err).Warningf("unable to release resource %s", resource.Name) 169 } 170 return 171 } 172 logrus.Infof("Resource leased: %v", string(resJSON)) 173 fmt.Fprint(res, string(resJSON)) 174 } 175 } 176 177 // handleAcquireByState: Handler for /acquirebystate 178 // Method: POST 179 // URLParams: 180 // Required: state=[string] : current state of the requested resource 181 // Required: dest=[string] : destination state of the requested resource 182 // Required: owner=[string] : requester of the resource 183 // Required: names=[string] : expected resources names 184 func handleAcquireByState(r *ranch.Ranch) http.HandlerFunc { 185 return func(res http.ResponseWriter, req *http.Request) { 186 logrus.WithField("handler", "handleStart").Infof("From %v", req.RemoteAddr) 187 188 if req.Method != http.MethodPost { 189 msg := fmt.Sprintf("Method %v, /acquire only accepts POST.", req.Method) 190 logrus.Warning(msg) 191 http.Error(res, msg, http.StatusMethodNotAllowed) 192 return 193 } 194 195 // TODO(krzyzacy) - sanitize user input 196 state := req.URL.Query().Get("state") 197 dest := req.URL.Query().Get("dest") 198 owner := req.URL.Query().Get("owner") 199 names := req.URL.Query().Get("names") 200 if state == "" || dest == "" || owner == "" || names == "" { 201 msg := fmt.Sprintf( 202 "state: %v, dest: %v, owner: %v, names: %v - all of them must be set in the request.", 203 state, dest, owner, names) 204 logrus.Warning(msg) 205 http.Error(res, msg, http.StatusBadRequest) 206 return 207 } 208 rNames := strings.Split(names, ",") 209 logrus.Infof("Request resources %s at state %v from %v, to state %v", 210 strings.Join(rNames, ", "), state, owner, dest) 211 212 resources, err := r.AcquireByState(state, dest, owner, rNames) 213 214 if err != nil { 215 logrus.WithError(err).Errorf("No available resources") 216 http.Error(res, err.Error(), ErrorToStatus(err)) 217 return 218 } 219 220 resBytes := new(bytes.Buffer) 221 222 if err := json.NewEncoder(resBytes).Encode(resources); err != nil { 223 logrus.WithError(err).Errorf("json.Marshal failed: %v, resources will be released", resources) 224 http.Error(res, err.Error(), ErrorToStatus(err)) 225 for _, resource := range resources { 226 err := r.Release(resource.Name, state, owner) 227 if err != nil { 228 logrus.WithError(err).Warningf("unable to release resource %s", resource.Name) 229 } 230 } 231 return 232 } 233 logrus.Infof("Resource leased: %v", resBytes.String()) 234 fmt.Fprint(res, resBytes.String()) 235 } 236 } 237 238 // handleRelease: Handler for /release 239 // Method: POST 240 // URL Params: 241 // Required: name=[string] : name of finished resource 242 // Required: owner=[string] : owner of the resource 243 // Required: dest=[string] : dest state 244 func handleRelease(r *ranch.Ranch) http.HandlerFunc { 245 return func(res http.ResponseWriter, req *http.Request) { 246 logrus.WithField("handler", "handleDone").Infof("From %v", req.RemoteAddr) 247 248 if req.Method != http.MethodPost { 249 msg := fmt.Sprintf("Method %v, /release only accepts POST.", req.Method) 250 logrus.Warning(msg) 251 http.Error(res, msg, http.StatusMethodNotAllowed) 252 return 253 } 254 255 name := req.URL.Query().Get("name") 256 dest := req.URL.Query().Get("dest") 257 owner := req.URL.Query().Get("owner") 258 if name == "" || dest == "" || owner == "" { 259 msg := fmt.Sprintf("Name: %v, dest: %v, owner: %v, all of them must be set in the request.", name, dest, owner) 260 logrus.Warning(msg) 261 http.Error(res, msg, http.StatusBadRequest) 262 return 263 } 264 265 if err := r.Release(name, dest, owner); err != nil { 266 logrus.WithError(err).Errorf("Done failed: %v - %v (from %v)", name, dest, owner) 267 http.Error(res, err.Error(), ErrorToStatus(err)) 268 return 269 } 270 271 logrus.Infof("Done with resource %v, set to state %v", name, dest) 272 } 273 } 274 275 // handleReset: Handler for /reset 276 // Method: POST 277 // URL Params: 278 // Required: type=[string] : type of resource in interest 279 // Required: state=[string] : original state 280 // Required: dest=[string] : dest state, for expired resource 281 // Required: expire=[durationStr*] resource has not been updated since before {expire}. 282 func handleReset(r *ranch.Ranch) http.HandlerFunc { 283 return func(res http.ResponseWriter, req *http.Request) { 284 logrus.WithField("handler", "handleReset").Infof("From %v", req.RemoteAddr) 285 286 if req.Method != http.MethodPost { 287 msg := fmt.Sprintf("Method %v, /reset only accepts POST.", req.Method) 288 logrus.Warning(msg) 289 http.Error(res, msg, http.StatusMethodNotAllowed) 290 return 291 } 292 293 rtype := req.URL.Query().Get("type") 294 state := req.URL.Query().Get("state") 295 expireStr := req.URL.Query().Get("expire") 296 dest := req.URL.Query().Get("dest") 297 298 logrus.Infof("%v, %v, %v, %v", rtype, state, expireStr, dest) 299 300 if rtype == "" || state == "" || expireStr == "" || dest == "" { 301 msg := fmt.Sprintf("Type: %v, state: %v, expire: %v, dest: %v, all of them must be set in the request.", rtype, state, expireStr, dest) 302 logrus.Warning(msg) 303 http.Error(res, msg, http.StatusBadRequest) 304 return 305 } 306 307 expire, err := time.ParseDuration(expireStr) 308 if err != nil { 309 logrus.WithError(err).Errorf("Invalid expiration: %v", expireStr) 310 http.Error(res, err.Error(), http.StatusBadRequest) 311 return 312 } 313 314 rmap, err := r.Reset(rtype, state, expire, dest) 315 if err != nil { 316 logrus.WithError(err).Errorf("could not reset states") 317 http.Error(res, err.Error(), http.StatusBadRequest) 318 return 319 } 320 resJSON, err := json.Marshal(rmap) 321 if err != nil { 322 logrus.WithError(err).Errorf("json.Marshal failed: %v", rmap) 323 http.Error(res, err.Error(), ErrorToStatus(err)) 324 return 325 } 326 logrus.Infof("Resource %v reset successful, %d items moved to state %v", rtype, len(rmap), dest) 327 fmt.Fprint(res, string(resJSON)) 328 } 329 } 330 331 // handleUpdate: Handler for /update 332 // Method: POST 333 // URLParams 334 // Required: name=[string] : name of target resource 335 // Required: owner=[string] : owner of the resource 336 // Required: state=[string] : current state of the resource 337 // Optional: userData=[common.UserData] : user data id to update 338 func handleUpdate(r *ranch.Ranch) http.HandlerFunc { 339 return func(res http.ResponseWriter, req *http.Request) { 340 logrus.WithField("handler", "handleUpdate").Infof("From %v", req.RemoteAddr) 341 342 if req.Method != http.MethodPost { 343 msg := fmt.Sprintf("Method %v, /update only accepts POST.", req.Method) 344 logrus.Warning(msg) 345 http.Error(res, msg, http.StatusMethodNotAllowed) 346 return 347 } 348 349 name := req.URL.Query().Get("name") 350 owner := req.URL.Query().Get("owner") 351 state := req.URL.Query().Get("state") 352 353 if name == "" || owner == "" || state == "" { 354 msg := fmt.Sprintf("Name: %v, owner: %v, state : %v, all of them must be set in the request.", name, owner, state) 355 logrus.Warning(msg) 356 http.Error(res, msg, http.StatusBadRequest) 357 return 358 } 359 360 var userData common.UserData 361 362 if req.Body != nil { 363 err := json.NewDecoder(req.Body).Decode(&userData) 364 switch { 365 case err == io.EOF: 366 // empty body 367 case err != nil: 368 logrus.WithError(err).Warning("Unable to read from response body") 369 http.Error(res, err.Error(), http.StatusBadRequest) 370 return 371 } 372 } 373 374 if err := r.Update(name, owner, state, &userData); err != nil { 375 logrus.WithError(err).Errorf("Update failed: %v - %v (%v)", name, state, owner) 376 http.Error(res, err.Error(), ErrorToStatus(err)) 377 return 378 } 379 380 logrus.Infof("Updated resource %v", name) 381 } 382 } 383 384 // handleMetric: Handler for /metric 385 // Method: GET 386 func handleMetric(r *ranch.Ranch) http.HandlerFunc { 387 return func(res http.ResponseWriter, req *http.Request) { 388 logrus.WithField("handler", "handleMetric").Infof("From %v", req.RemoteAddr) 389 390 if req.Method != http.MethodGet { 391 logrus.Warningf("[BadRequest]method %v, expect GET", req.Method) 392 http.Error(res, "/metric only accepts GET", http.StatusMethodNotAllowed) 393 return 394 } 395 396 rtype := req.URL.Query().Get("type") 397 if rtype == "" { 398 msg := "Type must be set in the request." 399 logrus.Warning(msg) 400 http.Error(res, msg, http.StatusBadRequest) 401 return 402 } 403 404 metric, err := r.Metric(rtype) 405 if err != nil { 406 logrus.WithError(err).Errorf("Metric for %s failed", rtype) 407 http.Error(res, err.Error(), ErrorToStatus(err)) 408 return 409 } 410 411 js, err := json.Marshal(metric) 412 if err != nil { 413 logrus.WithError(err).Error("Fail to marshal metric") 414 http.Error(res, err.Error(), ErrorToStatus(err)) 415 return 416 } 417 418 res.Header().Set("Content-Type", "application/json") 419 res.Write(js) 420 } 421 }