Beispiel #1
0
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)
}
Beispiel #2
0
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)
}
Beispiel #3
0
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)
}
Beispiel #4
0
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)
	}
}
Beispiel #5
0
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)
}
Beispiel #6
0
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)
	}
}
Beispiel #7
0
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)
	}
}
Beispiel #8
0
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)
	}
}
Beispiel #9
0
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)
}
Beispiel #10
0
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)
}
Beispiel #11
0
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)
}
Beispiel #12
0
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)
}
Beispiel #13
0
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)
}
Beispiel #14
0
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)
}
Beispiel #15
0
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
		}
	}
}
Beispiel #16
0
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:
			}
		}
	}
}
Beispiel #17
0
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
		}
	}
}
Beispiel #18
0
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
		}
	}
}
Beispiel #19
0
func logError(w http.ResponseWriter, msg string, err error) {
	logger, _ := ctxhelper.LoggerFromContext(w.(*httphelper.ResponseWriter).Context())
	logger.Error(msg, "error", err)
}