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  }