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 }