bosun.org@v0.0.0-20210513094433-e25bc3e69a1f/annotate/web/web.go (about)

     1  package web
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"log"
    10  	"net/http"
    11  	"strconv"
    12  	"strings"
    13  	"time"
    14  
    15  	"bosun.org/annotate"
    16  	"bosun.org/annotate/backend"
    17  
    18  	"github.com/gorilla/mux"
    19  	"github.com/twinj/uuid"
    20  )
    21  
    22  // esc -o static.go -pkg web static
    23  
    24  //AddRoutes will add annotate routes to the given router, using the specified prefix
    25  func AddRoutes(router *mux.Router, prefix string, b []backend.Backend, enableUI, useLocalAssets bool) error {
    26  	return AddRoutesWithMiddleware(router, prefix, b, enableUI, useLocalAssets, nil, nil)
    27  }
    28  
    29  func noopMiddleware(h http.HandlerFunc) http.Handler { return h }
    30  
    31  //AddRoutesWithMiddleware will add annotate routes to the given router, using the specified prefix. It accepts two middleware functions that will be applied to each route,
    32  //depending on whether they are a "read" operation, or a "write" operation
    33  func AddRoutesWithMiddleware(router *mux.Router, prefix string, b []backend.Backend, enableUI, useLocalAssets bool, readMiddleware, modifyMiddleware func(http.HandlerFunc) http.Handler) error {
    34  	if readMiddleware == nil {
    35  		readMiddleware = noopMiddleware
    36  	}
    37  	if modifyMiddleware == nil {
    38  		modifyMiddleware = noopMiddleware
    39  	}
    40  	backends = b
    41  	router.Handle(prefix+"/annotation", modifyMiddleware(InsertAnnotation)).Methods("POST", "PUT")
    42  	router.Handle(prefix+"/annotation/query", readMiddleware(GetAnnotations)).Methods("GET")
    43  	router.Handle(prefix+"/annotation/{id}", readMiddleware(GetAnnotation)).Methods("GET")
    44  	router.Handle(prefix+"/annotation/{id}", modifyMiddleware(InsertAnnotation)).Methods("PUT")
    45  	router.Handle(prefix+"/annotation/{id}", modifyMiddleware(DeleteAnnotation)).Methods("DELETE")
    46  	router.Handle(prefix+"/annotation/values/{field}", readMiddleware(GetFieldValues)).Methods("GET")
    47  	if !enableUI {
    48  		return nil
    49  	}
    50  	webFS := FS(useLocalAssets)
    51  	index, err := webFS.Open("/static/index.html")
    52  	if err != nil {
    53  		return fmt.Errorf("Error opening static file: %v", err)
    54  	}
    55  	indexHTML, err = ioutil.ReadAll(index)
    56  	if err != nil {
    57  		return err
    58  	}
    59  	router.PathPrefix("/static/").Handler(http.FileServer(webFS))
    60  	router.PathPrefix("/").Handler(readMiddleware(Index)).Methods("GET")
    61  	return nil
    62  }
    63  
    64  func Index(w http.ResponseWriter, r *http.Request) {
    65  	w.Write(indexHTML)
    66  }
    67  
    68  //Web Section
    69  var (
    70  	indexHTML []byte
    71  	backends  = []backend.Backend{}
    72  )
    73  
    74  func InsertAnnotation(w http.ResponseWriter, req *http.Request) {
    75  	var a annotate.Annotation
    76  	var ea annotate.EpochAnnotation
    77  	epochFmt := false
    78  	id := mux.Vars(req)["id"]
    79  
    80  	// Need to read the request body twice to try both formats, so tee the
    81  	b := bytes.NewBuffer(make([]byte, 0))
    82  	tee := io.TeeReader(req.Body, b)
    83  	d := json.NewDecoder(tee)
    84  	errRegFmt := d.Decode(&a)
    85  	if errRegFmt != nil {
    86  		d := json.NewDecoder(b)
    87  		errEpochFmt := d.Decode(&ea)
    88  		if errEpochFmt != nil {
    89  			serveError(w, fmt.Errorf("Could not unmarhsal json in RFC3339 fmt or Epoch fmt: %v, %v", errRegFmt, errEpochFmt))
    90  			return
    91  		}
    92  		a = ea.AsAnnotation()
    93  		epochFmt = true
    94  	}
    95  	if a.Id != "" && id != "" && a.Id != id {
    96  		serveError(w, fmt.Errorf("conflicting ids in request: url id %v, body id %v", id, a.Id))
    97  		return
    98  	}
    99  	if id != "" { // If we got the id in the url
   100  		a.Id = id
   101  	}
   102  	if a.IsOneTimeSet() {
   103  		a.MatchTimes()
   104  	}
   105  	if a.IsTimeNotSet() {
   106  		a.SetNow()
   107  	}
   108  	err := a.ValidateTime()
   109  	if err != nil {
   110  		serveError(w, err)
   111  		return
   112  	}
   113  	if a.Id == "" { //if Id isn't set, this is a new Annotation
   114  		a.Id = uuid.NewV4().String()
   115  	} else { // Make sure annotation exists if not new
   116  		for _, b := range backends {
   117  			//TODO handle multiple backends
   118  			_, found, err := b.GetAnnotation(a.Id)
   119  			if err == nil && !found {
   120  				serveError(w, fmt.Errorf("could not find annotation with id %v to update: %v", a.Id, err))
   121  				return
   122  			}
   123  			if err != nil {
   124  				serveError(w, err)
   125  				return
   126  			}
   127  		}
   128  	}
   129  	for _, b := range backends {
   130  		log.Println("Inserting", a)
   131  		err := b.InsertAnnotation(&a)
   132  		//TODO Collect errors and insert into the backends that we can
   133  		if err != nil {
   134  			serveError(w, err)
   135  			return
   136  		}
   137  	}
   138  	if err = format(&a, w, epochFmt); err != nil {
   139  		serveError(w, err)
   140  		return
   141  	}
   142  	w.Header().Set("Content-Type", "application/json")
   143  	return
   144  }
   145  
   146  func format(a *annotate.Annotation, w http.ResponseWriter, epochFmt bool) (e error) {
   147  	if epochFmt {
   148  		e = json.NewEncoder(w).Encode(a.AsEpochAnnotation())
   149  	} else {
   150  		e = json.NewEncoder(w).Encode(a)
   151  	}
   152  	return
   153  }
   154  
   155  func formatPlural(a annotate.Annotations, w http.ResponseWriter, epochFmt bool) (e error) {
   156  	if epochFmt {
   157  		e = json.NewEncoder(w).Encode(a.AsEpochAnnotations())
   158  	} else {
   159  		e = json.NewEncoder(w).Encode(a)
   160  	}
   161  	return
   162  }
   163  
   164  func GetAnnotation(w http.ResponseWriter, req *http.Request) {
   165  	var a *annotate.Annotation
   166  	var err error
   167  	w.Header().Set("Content-Type", "application/json")
   168  	id := mux.Vars(req)["id"]
   169  	for _, b := range backends {
   170  		var found bool
   171  		a, found, err = b.GetAnnotation(id)
   172  		//TODO Collect errors and insert into the backends that we can
   173  		if err == nil && !found {
   174  			serve404(w)
   175  			return
   176  		}
   177  		if err != nil {
   178  			serveError(w, err)
   179  			return
   180  		}
   181  	}
   182  	err = format(a, w, req.URL.Query().Get("Epoch") == "1")
   183  	if err != nil {
   184  		serveError(w, err)
   185  		return
   186  	}
   187  	return
   188  }
   189  
   190  func DeleteAnnotation(w http.ResponseWriter, req *http.Request) {
   191  	id := mux.Vars(req)["id"]
   192  	if id == "" {
   193  		serveError(w, fmt.Errorf("id required"))
   194  	}
   195  	for _, b := range backends {
   196  		err := b.DeleteAnnotation(id)
   197  		//TODO Make sure it is deleted from at least one backend?
   198  		if err != nil {
   199  			serveError(w, err)
   200  			return
   201  		}
   202  	}
   203  }
   204  
   205  func GetFieldValues(w http.ResponseWriter, req *http.Request) {
   206  	values := []string{}
   207  	var err error
   208  	w.Header().Set("Content-Type", "application/json")
   209  	field := mux.Vars(req)["field"]
   210  	for _, b := range backends {
   211  		values, err = b.GetFieldValues(field)
   212  		//TODO Collect errors and insert into the backends that we can
   213  		//TODO Unique Results from all backends
   214  		if err != nil {
   215  			serveError(w, err)
   216  			return
   217  		}
   218  	}
   219  	err = json.NewEncoder(w).Encode(values)
   220  	if err != nil {
   221  		serveError(w, err)
   222  		return
   223  	}
   224  	return
   225  }
   226  
   227  func GetAnnotations(w http.ResponseWriter, req *http.Request) {
   228  	var a annotate.Annotations
   229  	var startT time.Time
   230  	var endT time.Time
   231  	var err error
   232  	values := req.URL.Query()
   233  	for param := range values {
   234  		sp := strings.Split(param, ":")
   235  		switch sp[0] {
   236  		case annotate.StartDate:
   237  		case annotate.EndDate:
   238  		case annotate.Source:
   239  		case annotate.Host:
   240  		case annotate.CreationUser:
   241  		case annotate.Owner:
   242  		case annotate.Category:
   243  		case annotate.Url:
   244  		case annotate.Message:
   245  		case "Epoch":
   246  		default:
   247  			serveError(w, fmt.Errorf("%v is not a valid query field", param))
   248  			return
   249  		}
   250  	}
   251  	w.Header().Set("Content-Type", "application/json")
   252  	// Time
   253  	start := values.Get(annotate.StartDate)
   254  	end := values.Get(annotate.EndDate)
   255  	if start != "" {
   256  		s, rfcErr := time.Parse(time.RFC3339, start)
   257  		if rfcErr != nil {
   258  			epoch, epochErr := strconv.ParseInt(start, 10, 64)
   259  			if epochErr != nil {
   260  				serveError(w, fmt.Errorf("couldn't parse StartDate as RFC3339 or epoch: %v, %v", rfcErr, epochErr))
   261  				return
   262  			}
   263  			s = time.Unix(epoch, 0)
   264  		}
   265  		startT = s
   266  	}
   267  	if end != "" {
   268  		s, rfcErr := time.Parse(time.RFC3339, end)
   269  		if rfcErr != nil {
   270  			epoch, epochErr := strconv.ParseInt(end, 10, 64)
   271  			if epochErr != nil {
   272  				serveError(w, fmt.Errorf("couldn't parse EndDate as RFC3339 or epoch: %v, %v", rfcErr, epochErr))
   273  				return
   274  			}
   275  			s = time.Unix(epoch, 0)
   276  		}
   277  		endT = s
   278  	}
   279  	if end == "" {
   280  		endT = time.Now().UTC()
   281  	}
   282  	if start == "" {
   283  		startT = time.Now().Add(-time.Hour * 24)
   284  	}
   285  
   286  	// Queryable Fields
   287  	filters := []backend.FieldFilter{}
   288  	for param := range values {
   289  		sp := strings.Split(param, ":")
   290  		switch sp[0] {
   291  		case annotate.Source, annotate.Host, annotate.CreationUser, annotate.Owner, annotate.Category, annotate.Message:
   292  		default:
   293  			continue
   294  		}
   295  		filter := backend.FieldFilter{Field: sp[0], Value: values.Get(param)}
   296  		if len(sp) > 1 {
   297  			filter.Verb = sp[1]
   298  		}
   299  		if len(sp) > 2 {
   300  			filter.Not = true
   301  		}
   302  		filters = append(filters, filter)
   303  	}
   304  
   305  	// Execute
   306  	for _, b := range backends {
   307  		a, err = b.GetAnnotations(&startT, &endT, filters...)
   308  		//TODO Collect errors and insert into the backends that we can
   309  		if err != nil {
   310  			serveError(w, err)
   311  			return
   312  		}
   313  	}
   314  
   315  	// Encode
   316  	if err := formatPlural(a, w, values.Get("Epoch") == "1"); err != nil {
   317  		serveError(w, err)
   318  		return
   319  	}
   320  	return
   321  }
   322  
   323  func serveError(w http.ResponseWriter, err error) {
   324  	jsonError := struct {
   325  		Error string `json:"error"`
   326  	}{
   327  		err.Error(),
   328  	}
   329  	b, _ := json.Marshal(jsonError)
   330  	http.Error(w, string(b), http.StatusInternalServerError)
   331  }
   332  
   333  func serve404(w http.ResponseWriter) {
   334  	jsonError := struct {
   335  		Error string `json:"error"`
   336  	}{
   337  		"not found",
   338  	}
   339  	b, _ := json.Marshal(jsonError)
   340  	http.Error(w, string(b), http.StatusNotFound)
   341  }