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  }