func (api *API) StreamEvents(ctx context.Context, w http.ResponseWriter, req *http.Request) { log, _ := ctxhelper.LoggerFromContext(ctx) httpListener := api.router.ListenerFor("http") tcpListener := api.router.ListenerFor("tcp") httpEvents := make(chan *router.Event) tcpEvents := make(chan *router.Event) sseEvents := make(chan *router.StreamEvent) go httpListener.Watch(httpEvents) go tcpListener.Watch(tcpEvents) defer httpListener.Unwatch(httpEvents) defer tcpListener.Unwatch(tcpEvents) sendEvents := func(events chan *router.Event) { for { e, ok := <-events if !ok { return } sseEvents <- &router.StreamEvent{ Event: e.Event, Route: e.Route, Error: e.Error, } } } go sendEvents(httpEvents) go sendEvents(tcpEvents) sse.ServeStream(w, sseEvents, log) }
func (api *API) GetRoutes(ctx context.Context, w http.ResponseWriter, req *http.Request) { log, _ := ctxhelper.LoggerFromContext(ctx) routes, err := api.router.HTTP.List() if err != nil { log.Error(err.Error()) httphelper.Error(w, err) return } tcpRoutes, err := api.router.TCP.List() if err != nil { log.Error(err.Error()) httphelper.Error(w, err) return } routes = append(routes, tcpRoutes...) if ref := req.URL.Query().Get("parent_ref"); ref != "" { filtered := make([]*router.Route, 0) for _, route := range routes { if route.ParentRef == ref { filtered = append(filtered, route) } } routes = filtered } sort.Sort(sortedRoutes(routes)) httphelper.JSON(w, 200, routes) }
func (api *API) DeleteRoute(ctx context.Context, w http.ResponseWriter, req *http.Request) { log, _ := ctxhelper.LoggerFromContext(ctx) params, _ := ctxhelper.ParamsFromContext(ctx) l := api.router.ListenerFor(params.ByName("route_type")) if l == nil { w.WriteHeader(404) return } err := l.RemoveRoute(params.ByName("id")) if err != nil { switch err { case ErrNotFound: w.WriteHeader(404) return case ErrInvalid: httphelper.Error(w, httphelper.JSONError{ Code: httphelper.ValidationErrorCode, Message: "Route has dependent routes", }) return default: log.Error(err.Error()) httphelper.Error(w, err) return } } w.WriteHeader(200) }
func (c *controllerAPI) Events(ctx context.Context, w http.ResponseWriter, req *http.Request) { l, _ := ctxhelper.LoggerFromContext(ctx) log := l.New("fn", "Events") var app *ct.App if appID := req.FormValue("app_id"); appID != "" { data, err := c.appRepo.Get(appID) if err != nil { respondWithError(w, err) return } app = data.(*ct.App) } if req.Header.Get("Accept") == "application/json" { if err := listEvents(ctx, w, req, app, c.eventRepo); err != nil { log.Error("error listing events", "err", err) respondWithError(w, err) } return } if err := c.maybeStartEventListener(); err != nil { log.Error("error starting event listener", "err", err) respondWithError(w, err) } if err := streamEvents(ctx, w, req, c.eventListener, app, c.eventRepo); err != nil { log.Error("error streaming events", "err", err) respondWithError(w, err) } }
func (api *API) UpdateRoute(ctx context.Context, w http.ResponseWriter, req *http.Request) { log, _ := ctxhelper.LoggerFromContext(ctx) params, _ := ctxhelper.ParamsFromContext(ctx) var route *router.Route if err := json.NewDecoder(req.Body).Decode(&route); err != nil { log.Error(err.Error()) httphelper.Error(w, err) return } route.Type = params.ByName("route_type") route.ID = params.ByName("id") l := api.router.ListenerFor(route.Type) if l == nil { httphelper.ValidationError(w, "type", "Invalid route type") return } if err := l.UpdateRoute(route); err != nil { if err == ErrNotFound { w.WriteHeader(404) return } log.Error(err.Error()) httphelper.Error(w, err) return } httphelper.JSON(w, 200, route) }
func logError(w http.ResponseWriter, err error) { if rw, ok := w.(*ResponseWriter); ok { logger, _ := ctxhelper.LoggerFromContext(rw.Context()) logger.Error(err.Error()) } else { log.Println(err) } }
func (c *controllerAPI) createAndStreamBackup(ctx context.Context, w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "application/tar") filename := "flynn-backup-" + time.Now().UTC().Format("2006-01-02_150405") + ".tar" w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename)) handleError := func(err error) { if l, ok := ctxhelper.LoggerFromContext(ctx); ok { l.Error(err.Error()) w.WriteHeader(500) } } client, err := controller.NewClient("", c.config.keys[0]) if err != nil { handleError(err) return } b := &ct.ClusterBackup{ Status: ct.ClusterBackupStatusRunning, } if err := c.backupRepo.Add(b); err != nil { handleError(err) return } h := sha512.New() hw := io.MultiWriter(h, w) sw := newSizeWriter(hw) if err := backup.Run(client, sw, nil); err != nil { b.Status = ct.ClusterBackupStatusError b.Error = err.Error() now := time.Now() b.CompletedAt = &now if err := c.backupRepo.Update(b); err != nil { handleError(err) return } handleError(err) return } b.Status = ct.ClusterBackupStatusComplete b.SHA512 = hex.EncodeToString(h.Sum(nil)) b.Size = int64(sw.Size()) now := time.Now() b.CompletedAt = &now if err := c.backupRepo.Update(b); err != nil { handleError(err) } }
func (api *API) WrapHandler(handler httphelper.HandlerFunc) httprouter.Handle { return func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { ctx := w.(*httphelper.ResponseWriter).Context() log, _ := ctxhelper.LoggerFromContext(ctx) ctx = ctxhelper.NewContextParams(ctx, params) s, err := api.conf.SessionStore.Get(req, "session") if err != nil { log.Error(err.Error()) } ctx = context.WithValue(ctx, ctxSessionKey, s) handler.ServeHTTP(ctx, w, req) } }
func (c *controllerAPI) streamFormations(ctx context.Context, w http.ResponseWriter, req *http.Request) { ch := make(chan *ct.ExpandedFormation) since, err := time.Parse(time.RFC3339, req.FormValue("since")) if err != nil { respondWithError(w, err) return } sub, err := c.formationRepo.Subscribe(ch, since, nil) if err != nil { respondWithError(w, err) return } defer c.formationRepo.Unsubscribe(sub) l, _ := ctxhelper.LoggerFromContext(ctx) sse.ServeStream(w, ch, l) }
func (api *API) ServeStatic(ctx context.Context, w http.ResponseWriter, req *http.Request, path string) { log, _ := ctxhelper.LoggerFromContext(ctx) data, t, err := AssetReader(path) if err != nil { log.Error(err.Error()) w.WriteHeader(404) return } ext := filepath.Ext(path) if mimeType := mime.TypeByExtension(ext); mimeType != "" { w.Header().Add("Content-Type", mimeType) } if ext == ".html" { w.Header().Add("Cache-Control", "max-age=0") } http.ServeContent(w, req, path, t, data) }
func (api *API) CreateRoute(ctx context.Context, w http.ResponseWriter, req *http.Request) { log, _ := ctxhelper.LoggerFromContext(ctx) var route *router.Route if err := json.NewDecoder(req.Body).Decode(&route); err != nil { log.Error(err.Error()) httphelper.Error(w, err) return } l := api.router.ListenerFor(route.Type) if l == nil { httphelper.ValidationError(w, "type", "Invalid route type") return } err := l.AddRoute(route) if err != nil { rjson, jerr := json.Marshal(&route) if jerr != nil { log.Error(jerr.Error()) httphelper.Error(w, jerr) return } jsonError := httphelper.JSONError{Detail: rjson} switch err { case ErrConflict: jsonError.Code = httphelper.ConflictErrorCode jsonError.Message = "Duplicate route" case ErrInvalid: jsonError.Code = httphelper.ValidationErrorCode jsonError.Message = "Invalid route" default: log.Error(err.Error()) httphelper.Error(w, err) return } httphelper.Error(w, jsonError) return } httphelper.JSON(w, 200, route) }
func (api *API) StreamEvents(ctx context.Context, w http.ResponseWriter, req *http.Request) { log, _ := ctxhelper.LoggerFromContext(ctx) httpListener := api.router.ListenerFor("http") tcpListener := api.router.ListenerFor("tcp") httpEvents := make(chan *router.Event) tcpEvents := make(chan *router.Event) sseEvents := make(chan *router.StreamEvent) go httpListener.Watch(httpEvents, true) go tcpListener.Watch(tcpEvents, true) defer httpListener.Unwatch(httpEvents) defer tcpListener.Unwatch(tcpEvents) reqTypes := strings.Split(req.URL.Query().Get("types"), ",") eventTypes := make(map[router.EventType]struct{}, len(reqTypes)) for _, typ := range reqTypes { eventTypes[router.EventType(typ)] = struct{}{} } sendEvents := func(events chan *router.Event) { for { e, ok := <-events if !ok { return } if _, ok := eventTypes[e.Event]; !ok { continue } sseEvents <- &router.StreamEvent{ Event: e.Event, Route: e.Route, Backend: e.Backend, Error: e.Error, } } } go sendEvents(httpEvents) go sendEvents(tcpEvents) sse.ServeStream(w, sseEvents, log) }
func (api *API) GetRoute(ctx context.Context, w http.ResponseWriter, req *http.Request) { log, _ := ctxhelper.LoggerFromContext(ctx) params, _ := ctxhelper.ParamsFromContext(ctx) l := api.router.ListenerFor(params.ByName("route_type")) if l == nil { w.WriteHeader(404) return } route, err := l.Get(params.ByName("id")) if err == ErrNotFound { w.WriteHeader(404) return } if err != nil { log.Error(err.Error()) httphelper.Error(w, err) return } httphelper.JSON(w, 200, route) }
func (api *API) ServeDashboardJs(ctx context.Context, w http.ResponseWriter, req *http.Request) { log, _ := ctxhelper.LoggerFromContext(ctx) path := filepath.Join("app", "build", "assets", filepath.Base(req.URL.Path)) data, t, err := AssetReader(path) if err != nil { log.Error(err.Error()) httphelper.Error(w, err) return } var jsConf bytes.Buffer jsConf.Write([]byte("window.DashboardConfig = ")) json.NewEncoder(&jsConf).Encode(DashboardConfig{ AppName: api.conf.AppName, ApiServer: api.conf.URL, PathPrefix: api.conf.PathPrefix, InstallCert: len(api.conf.CACert) > 0, }) jsConf.Write([]byte(";\n")) r := ioutil.NewMultiReadSeeker(bytes.NewReader(jsConf.Bytes()), data) http.ServeContent(w, req, path, t, r) }
func (c *controllerAPI) AppLog(ctx context.Context, w http.ResponseWriter, req *http.Request) { ctx, cancel := context.WithCancel(ctx) opts := logaggc.LogOpts{ Follow: req.FormValue("follow") == "true", JobID: req.FormValue("job_id"), } if vals, ok := req.Form["process_type"]; ok && len(vals) > 0 { opts.ProcessType = &vals[len(vals)-1] } if strLines := req.FormValue("lines"); strLines != "" { lines, err := strconv.Atoi(req.FormValue("lines")) if err != nil { respondWithError(w, err) return } opts.Lines = &lines } rc, err := c.logaggc.GetLog(c.getApp(ctx).ID, &opts) if err != nil { respondWithError(w, err) return } if cn, ok := w.(http.CloseNotifier); ok { go func() { select { case <-cn.CloseNotify(): rc.Close() case <-ctx.Done(): } }() } defer cancel() defer rc.Close() if !strings.Contains(req.Header.Get("Accept"), "text/event-stream") { w.Header().Set("Content-Type", "text/plain") w.WriteHeader(200) // Send headers right away if following if wf, ok := w.(http.Flusher); ok && opts.Follow { wf.Flush() } fw := httphelper.FlushWriter{Writer: w, Enabled: opts.Follow} io.Copy(fw, rc) return } ch := make(chan *sseLogChunk) l, _ := ctxhelper.LoggerFromContext(ctx) s := sse.NewStream(w, ch, l) defer s.Close() s.Serve() msgc := make(chan *json.RawMessage) go func() { defer close(msgc) dec := json.NewDecoder(rc) for { var m json.RawMessage if err := dec.Decode(&m); err != nil { if err != io.EOF { l.Error("decoding logagg stream", err) } return } msgc <- &m } }() for { select { case m := <-msgc: if m == nil { ch <- &sseLogChunk{Event: "eof"} return } // write to sse select { case ch <- &sseLogChunk{Event: "message", Data: *m}: case <-s.Done: return case <-ctx.Done(): return } case <-s.Done: return case <-ctx.Done(): return } } }
func (c *controllerAPI) streamFormations(ctx context.Context, w http.ResponseWriter, req *http.Request) (err error) { l, _ := ctxhelper.LoggerFromContext(ctx) ch := make(chan *ct.ExpandedFormation) stream := sse.NewStream(w, ch, l) stream.Serve() defer func() { if err == nil { stream.Close() } else { stream.CloseWithError(err) } }() since, err := time.Parse(time.RFC3339Nano, req.FormValue("since")) if err != nil { return err } eventListener, err := c.maybeStartEventListener() if err != nil { l.Error("error starting event listener", "err", err) return err } sub, err := eventListener.Subscribe("", []string{string(ct.EventTypeScale)}, "") if err != nil { return err } defer sub.Close() formations, err := c.formationRepo.ListSince(since) if err != nil { return err } currentUpdatedAt := since for _, formation := range formations { select { case <-stream.Done: return nil case ch <- formation: if formation.UpdatedAt.After(currentUpdatedAt) { currentUpdatedAt = formation.UpdatedAt } } } select { case <-stream.Done: return nil case ch <- &ct.ExpandedFormation{}: } for { select { case <-stream.Done: return case event, ok := <-sub.Events: if !ok { return sub.Err } var scale ct.Scale if err := json.Unmarshal(event.Data, &scale); err != nil { l.Error("error deserializing scale event", "event.id", event.ID, "err", err) continue } formation, err := c.formationRepo.GetExpanded(event.AppID, scale.ReleaseID, true) if err != nil { l.Error("error expanding formation", "app.id", event.AppID, "release.id", scale.ReleaseID, "err", err) continue } if formation.UpdatedAt.Before(currentUpdatedAt) { continue } select { case <-stream.Done: return nil case ch <- formation: } } } }
func streamEvents(ctx context.Context, w http.ResponseWriter, req *http.Request, eventListener *EventListener, app *ct.App, repo *EventRepo) (err error) { var appID string if app != nil { appID = app.ID } var lastID int64 if req.Header.Get("Last-Event-Id") != "" { lastID, err = strconv.ParseInt(req.Header.Get("Last-Event-Id"), 10, 64) if err != nil { return ct.ValidationError{Field: "Last-Event-Id", Message: "is invalid"} } } var count int if req.FormValue("count") != "" { count, err = strconv.Atoi(req.FormValue("count")) if err != nil { return ct.ValidationError{Field: "count", Message: "is invalid"} } } objectTypes := strings.Split(req.FormValue("object_types"), ",") if len(objectTypes) == 1 && objectTypes[0] == "" { objectTypes = []string{} } objectID := req.FormValue("object_id") past := req.FormValue("past") l, _ := ctxhelper.LoggerFromContext(ctx) log := l.New("fn", "Events", "object_types", objectTypes, "object_id", objectID) ch := make(chan *ct.Event) s := sse.NewStream(w, ch, log) s.Serve() defer func() { if err == nil { s.Close() } else { s.CloseWithError(err) } }() sub, err := eventListener.Subscribe(appID, objectTypes, objectID) if err != nil { return err } defer sub.Close() var currID int64 if past == "true" || lastID > 0 { list, err := repo.ListEvents(appID, objectTypes, objectID, lastID, count) if err != nil { return err } // events are in ID DESC order, so iterate in reverse for i := len(list) - 1; i >= 0; i-- { e := list[i] ch <- e currID = e.ID } } for { select { case <-s.Done: return case event, ok := <-sub.Events: if !ok { return sub.Err } if event.ID <= currID { continue } ch <- event } } }
func streamJobs(ctx context.Context, req *http.Request, w http.ResponseWriter, app *ct.App, repo *JobRepo) (err error) { var lastID int64 if req.Header.Get("Last-Event-Id") != "" { lastID, err = strconv.ParseInt(req.Header.Get("Last-Event-Id"), 10, 64) if err != nil { return ct.ValidationError{Field: "Last-Event-Id", Message: "is invalid"} } } var count int if req.FormValue("count") != "" { count, err = strconv.Atoi(req.FormValue("count")) if err != nil { return ct.ValidationError{Field: "count", Message: "is invalid"} } } ch := make(chan *ct.JobEvent) l, _ := ctxhelper.LoggerFromContext(ctx) s := sse.NewStream(w, ch, l) s.Serve() connected := make(chan struct{}) done := make(chan struct{}) listenEvent := func(ev pq.ListenerEventType, listenErr error) { switch ev { case pq.ListenerEventConnected: close(connected) case pq.ListenerEventDisconnected: if done != nil { close(done) done = nil } case pq.ListenerEventConnectionAttemptFailed: err = listenErr if done != nil { close(done) done = nil } } } listener := pq.NewListener(repo.db.DSN(), 10*time.Second, time.Minute, listenEvent) defer listener.Close() listener.Listen("job_events:" + postgres.FormatUUID(app.ID)) var currID int64 if lastID > 0 || count > 0 { events, err := repo.listEvents(app.ID, lastID, count) if err != nil { return err } // events are in ID DESC order, so iterate in reverse for i := len(events) - 1; i >= 0; i-- { e := events[i] ch <- e currID = e.ID } } select { case <-done: return case <-connected: } for { select { case <-s.Done: return case <-done: return case n := <-listener.Notify: id, err := strconv.ParseInt(n.Extra, 10, 64) if err != nil { return err } if id <= currID { continue } e, err := repo.getEvent(id) if err != nil { return err } ch <- e } } }
func logError(w http.ResponseWriter, msg string, err error) { logger, _ := ctxhelper.LoggerFromContext(w.(*httphelper.ResponseWriter).Context()) logger.Error(msg, "error", err) }