github.com/grahambrereton-form3/tilt@v0.10.18/internal/hud/server/server.go (about) 1 package server 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io/ioutil" 9 "log" 10 "net/http" 11 "time" 12 "unsafe" 13 14 "github.com/gorilla/mux" 15 _ "github.com/gorilla/websocket" 16 "github.com/grpc-ecosystem/grpc-gateway/runtime" 17 jsoniter "github.com/json-iterator/go" 18 "github.com/pkg/errors" 19 "github.com/windmilleng/wmclient/pkg/analytics" 20 21 tiltanalytics "github.com/windmilleng/tilt/internal/analytics" 22 "github.com/windmilleng/tilt/internal/cloud" 23 "github.com/windmilleng/tilt/internal/hud/webview" 24 "github.com/windmilleng/tilt/internal/k8s" 25 "github.com/windmilleng/tilt/internal/store" 26 "github.com/windmilleng/tilt/pkg/assets" 27 "github.com/windmilleng/tilt/pkg/model" 28 proto_webview "github.com/windmilleng/tilt/pkg/webview" 29 ) 30 31 const httpTimeOut = 5 * time.Second 32 const TiltTokenCookieName = "Tilt-Token" 33 34 type analyticsPayload struct { 35 Verb string `json:"verb"` 36 Name string `json:"name"` 37 Tags map[string]string `json:"tags"` 38 } 39 40 type analyticsOptPayload struct { 41 Opt string `json:"opt"` 42 } 43 44 type triggerPayload struct { 45 ManifestNames []string `json:"manifest_names"` 46 } 47 48 type actionPayload struct { 49 Type string `json:"type"` 50 ManifestName model.ManifestName `json:"manifest_name"` 51 PodID k8s.PodID `json:"pod_id"` 52 VisibleRestarts int `json:"visible_restarts"` 53 } 54 55 type HeadsUpServer struct { 56 store *store.Store 57 router *mux.Router 58 a *tiltanalytics.TiltAnalytics 59 uploader cloud.SnapshotUploader 60 numWebsocketConns int32 61 } 62 63 func ProvideHeadsUpServer( 64 ctx context.Context, 65 store *store.Store, 66 assetServer assets.Server, 67 analytics *tiltanalytics.TiltAnalytics, 68 uploader cloud.SnapshotUploader) (*HeadsUpServer, error) { 69 r := mux.NewRouter().UseEncodedPath() 70 s := &HeadsUpServer{ 71 store: store, 72 router: r, 73 a: analytics, 74 uploader: uploader, 75 } 76 77 r.HandleFunc("/api/view", s.ViewJSON) 78 r.HandleFunc("/api/dump/engine", s.DumpEngineJSON) 79 r.HandleFunc("/api/analytics", s.HandleAnalytics) 80 r.HandleFunc("/api/analytics_opt", s.HandleAnalyticsOpt) 81 r.HandleFunc("/api/trigger", s.HandleTrigger) 82 r.HandleFunc("/api/action", s.DispatchAction).Methods("POST") 83 r.HandleFunc("/api/snapshot/new", s.HandleNewSnapshot).Methods("POST") 84 // this endpoint is only used for testing snapshots in development 85 r.HandleFunc("/api/snapshot/{snapshot_id}", s.SnapshotJSON) 86 r.HandleFunc("/ws/view", s.ViewWebsocket) 87 r.HandleFunc("/api/user_started_tilt_cloud_registration", s.userStartedTiltCloudRegistration) 88 89 r.PathPrefix("/").Handler(s.cookieWrapper(assetServer)) 90 91 return s, nil 92 } 93 94 type funcHandler struct { 95 f func(w http.ResponseWriter, r *http.Request) 96 } 97 98 func (fh funcHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 99 fh.f(w, r) 100 } 101 102 func (s *HeadsUpServer) cookieWrapper(handler http.Handler) http.Handler { 103 return funcHandler{f: func(w http.ResponseWriter, r *http.Request) { 104 state := s.store.RLockState() 105 http.SetCookie(w, &http.Cookie{Name: TiltTokenCookieName, Value: string(state.Token), Path: "/"}) 106 s.store.RUnlockState() 107 handler.ServeHTTP(w, r) 108 }} 109 } 110 111 func (s *HeadsUpServer) Router() http.Handler { 112 return s.router 113 } 114 115 func (s *HeadsUpServer) ViewJSON(w http.ResponseWriter, req *http.Request) { 116 state := s.store.RLockState() 117 view, err := webview.StateToProtoView(state) 118 s.store.RUnlockState() 119 if err != nil { 120 http.Error(w, fmt.Sprintf("Error converting view to proto: %v", err), http.StatusInternalServerError) 121 return 122 } 123 124 jsEncoder := &runtime.JSONPb{OrigName: false, EmitDefaults: true} 125 126 w.Header().Set("Content-Type", "application/json") 127 err = jsEncoder.NewEncoder(w).Encode(view) 128 if err != nil { 129 http.Error(w, fmt.Sprintf("Error rendering view payload: %v", err), http.StatusInternalServerError) 130 } 131 } 132 133 // Dump the JSON engine over http. Only intended for 'tilt dump engine'. 134 func (s *HeadsUpServer) DumpEngineJSON(w http.ResponseWriter, req *http.Request) { 135 state := s.store.RLockState() 136 defer s.store.RUnlockState() 137 138 encoder := store.CreateEngineStateEncoder(w) 139 err := encoder.Encode(state) 140 if err != nil { 141 log.Printf("Error encoding: %v", err) 142 } 143 } 144 145 func (s *HeadsUpServer) SnapshotJSON(w http.ResponseWriter, req *http.Request) { 146 state := s.store.RLockState() 147 view, err := webview.StateToProtoView(state) 148 s.store.RUnlockState() 149 if err != nil { 150 http.Error(w, fmt.Sprintf("Error converting view to proto: %v", err), http.StatusInternalServerError) 151 return 152 } 153 154 w.Header().Set("Content-Type", "application/json") 155 err = json.NewEncoder(w).Encode(&proto_webview.Snapshot{ 156 View: view, 157 }) 158 if err != nil { 159 http.Error(w, fmt.Sprintf("Error rendering view payload: %v", err), http.StatusInternalServerError) 160 } 161 } 162 163 func (s *HeadsUpServer) HandleAnalyticsOpt(w http.ResponseWriter, req *http.Request) { 164 if req.Method != http.MethodPost { 165 http.Error(w, "must be POST request", http.StatusBadRequest) 166 return 167 } 168 169 var payload analyticsOptPayload 170 171 decoder := json.NewDecoder(req.Body) 172 err := decoder.Decode(&payload) 173 if err != nil { 174 http.Error(w, fmt.Sprintf("error parsing JSON payload: %v", err), http.StatusBadRequest) 175 return 176 } 177 178 opt, err := analytics.ParseOpt(payload.Opt) 179 if err != nil { 180 http.Error(w, fmt.Sprintf("error parsing opt '%s': %v", payload.Opt, err), http.StatusBadRequest) 181 } 182 183 // only logging on opt-in, because, well, opting out means the user just told us not to report data on them! 184 if opt == analytics.OptIn { 185 s.a.IncrIfUnopted("analytics.opt.in") 186 } 187 188 s.store.Dispatch(store.AnalyticsUserOptAction{Opt: opt}) 189 } 190 191 func (s *HeadsUpServer) HandleAnalytics(w http.ResponseWriter, req *http.Request) { 192 if req.Method != http.MethodPost { 193 http.Error(w, "must be POST request", http.StatusBadRequest) 194 return 195 } 196 197 var payloads []analyticsPayload 198 199 decoder := json.NewDecoder(req.Body) 200 err := decoder.Decode(&payloads) 201 if err != nil { 202 http.Error(w, fmt.Sprintf("error parsing JSON payload: %v", err), http.StatusBadRequest) 203 return 204 } 205 206 for _, p := range payloads { 207 if p.Verb != "incr" { 208 http.Error(w, "error parsing payloads: only incr verbs are supported", http.StatusBadRequest) 209 return 210 } 211 212 s.a.Incr(p.Name, p.Tags) 213 } 214 } 215 216 func (s *HeadsUpServer) DispatchAction(w http.ResponseWriter, req *http.Request) { 217 if req.Method != http.MethodPost { 218 http.Error(w, "must be POST request", http.StatusBadRequest) 219 return 220 } 221 222 var payload actionPayload 223 decoder := json.NewDecoder(req.Body) 224 err := decoder.Decode(&payload) 225 if err != nil { 226 http.Error(w, fmt.Sprintf("error parsing JSON payload: %v", err), http.StatusBadRequest) 227 return 228 } 229 230 switch payload.Type { 231 case "PodResetRestarts": 232 s.store.Dispatch( 233 store.NewPodResetRestartsAction(payload.PodID, payload.ManifestName, payload.VisibleRestarts)) 234 default: 235 http.Error(w, fmt.Sprintf("Unknown action type: %s", payload.Type), http.StatusBadRequest) 236 } 237 238 } 239 240 func (s *HeadsUpServer) HandleTrigger(w http.ResponseWriter, req *http.Request) { 241 if req.Method != http.MethodPost { 242 http.Error(w, "must be POST request", http.StatusBadRequest) 243 return 244 } 245 246 var payload triggerPayload 247 248 decoder := json.NewDecoder(req.Body) 249 err := decoder.Decode(&payload) 250 if err != nil { 251 http.Error(w, fmt.Sprintf("error parsing JSON payload: %v", err), http.StatusBadRequest) 252 return 253 } 254 255 if len(payload.ManifestNames) != 1 { 256 http.Error(w, fmt.Sprintf("/api/trigger currently supports exactly one manifest name, got %d", len(payload.ManifestNames)), http.StatusBadRequest) 257 return 258 } 259 260 err = SendToTriggerQueue(s.store, payload.ManifestNames[0]) 261 if err != nil { 262 http.Error(w, err.Error(), http.StatusBadRequest) 263 return 264 } 265 } 266 267 func SendToTriggerQueue(st store.RStore, name string) error { 268 mName := model.ManifestName(name) 269 270 state := st.RLockState() 271 _, ok := state.Manifest(mName) 272 st.RUnlockState() 273 274 if !ok { 275 return fmt.Errorf("no manifest found with name '%s'", mName) 276 } 277 278 st.Dispatch(AppendToTriggerQueueAction{Name: mName}) 279 return nil 280 } 281 282 /* -- SNAPSHOT: SENDING SNAPSHOT TO SERVER -- */ 283 type snapshotURLJson struct { 284 Url string `json:"url"` 285 } 286 287 // the default json decoding just blows up if a time.Time field is empty 288 // this uses the default behavior, except empty string -> time.Time{} 289 type timeAllowEmptyDecoder struct{} 290 291 func (codec timeAllowEmptyDecoder) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) { 292 s := iter.ReadString() 293 var ret time.Time 294 if s != "" { 295 var err error 296 ret, err = time.Parse(time.RFC3339, s) 297 if err != nil { 298 iter.ReportError("timeAllowEmptyDecoder", errors.Wrapf(err, "decoding '%s'", s).Error()) 299 return 300 } 301 } 302 *((*time.Time)(ptr)) = ret 303 } 304 305 func (s *HeadsUpServer) HandleNewSnapshot(w http.ResponseWriter, req *http.Request) { 306 st := s.store.RLockState() 307 token := st.Token 308 teamID := st.TeamName 309 s.store.RUnlockState() 310 311 b, err := ioutil.ReadAll(req.Body) 312 if err != nil { 313 msg := fmt.Sprintf("error reading body: %v", err) 314 log.Println(msg) 315 http.Error(w, msg, http.StatusInternalServerError) 316 return 317 } 318 319 jspb := &runtime.JSONPb{OrigName: false, EmitDefaults: true} 320 decoder := jspb.NewDecoder(bytes.NewBuffer(b)) 321 var snapshot *proto_webview.Snapshot 322 323 // TODO(nick): Add more strict decoding once we have better safeguards for making 324 // sure the Go and JS types are in-sync. 325 // decoder.DisallowUnknownFields() 326 327 err = decoder.Decode(&snapshot) 328 if err != nil { 329 msg := fmt.Sprintf("Error decoding snapshot: %v\n", err) 330 log.Println(msg) 331 http.Error(w, msg, http.StatusInternalServerError) 332 return 333 } 334 335 id, err := s.uploader.Upload(token, teamID, snapshot) 336 if err != nil { 337 msg := fmt.Sprintf("Error creating snapshot: %v", err) 338 log.Println(msg) 339 http.Error(w, msg, http.StatusInternalServerError) 340 return 341 } 342 343 responsePayload := snapshotURLJson{ 344 Url: s.uploader.IDToSnapshotURL(id), 345 } 346 347 //encode URL to JSON format 348 urlJS, err := json.Marshal(responsePayload) 349 if err != nil { 350 msg := fmt.Sprintf("Error to marshal url JSON response %v", err) 351 log.Println(msg) 352 http.Error(w, msg, http.StatusInternalServerError) 353 return 354 } 355 356 //write URL to header 357 w.WriteHeader(http.StatusOK) 358 _, err = w.Write(urlJS) 359 if err != nil { 360 msg := fmt.Sprintf("Error writing URL response: %v", err) 361 log.Println(msg) 362 http.Error(w, msg, http.StatusInternalServerError) 363 return 364 } 365 366 } 367 368 func (s *HeadsUpServer) userStartedTiltCloudRegistration(w http.ResponseWriter, req *http.Request) { 369 s.store.Dispatch(store.UserStartedTiltCloudRegistrationAction{}) 370 }