github.com/filecoin-project/bacalhau@v0.3.23-0.20230228154132-45c989550ace/dashboard/api/pkg/server/server.go (about)

     1  package server
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"net/http"
     8  	"strings"
     9  	"time"
    10  
    11  	bacmodel "github.com/filecoin-project/bacalhau/pkg/model"
    12  	"github.com/rs/zerolog/log"
    13  
    14  	"github.com/filecoin-project/bacalhau/dashboard/api/pkg/model"
    15  	"github.com/filecoin-project/bacalhau/dashboard/api/pkg/types"
    16  	"github.com/filecoin-project/bacalhau/pkg/localdb"
    17  	"github.com/filecoin-project/bacalhau/pkg/system"
    18  	"github.com/gorilla/mux"
    19  )
    20  
    21  type ServerOptions struct {
    22  	Host        string
    23  	Port        int
    24  	SwarmPort   int
    25  	PeerConnect string
    26  	JWTSecret   string
    27  }
    28  
    29  type DashboardAPIServer struct {
    30  	Options ServerOptions
    31  	API     *model.ModelAPI
    32  }
    33  
    34  func NewServer(
    35  	options ServerOptions,
    36  	api *model.ModelAPI,
    37  ) (*DashboardAPIServer, error) {
    38  	if options.Host == "" {
    39  		return nil, fmt.Errorf("host is required")
    40  	}
    41  	if options.Port == 0 {
    42  		return nil, fmt.Errorf("port is required")
    43  	}
    44  	if options.JWTSecret == "" {
    45  		return nil, fmt.Errorf("jwt secret is required")
    46  	}
    47  	return &DashboardAPIServer{
    48  		Options: options,
    49  		API:     api,
    50  	}, nil
    51  }
    52  
    53  func (apiServer *DashboardAPIServer) ListenAndServe(ctx context.Context, cm *system.CleanupManager) error {
    54  	router := mux.NewRouter()
    55  	subrouter := router.PathPrefix("/api/v1").Subrouter()
    56  	subrouter.HandleFunc("/nodes", apiServer.nodes).Methods("GET")
    57  	subrouter.HandleFunc("/run", apiServer.run).Methods("POST")
    58  	subrouter.HandleFunc("/stablediffusion", apiServer.stablediffusion).Methods("POST")
    59  	subrouter.HandleFunc("/jobs", apiServer.jobs).Methods("POST")
    60  	subrouter.HandleFunc("/jobs/count", apiServer.jobsCount).Methods("POST")
    61  	subrouter.HandleFunc("/job/{id}", apiServer.job).Methods("GET")
    62  	subrouter.HandleFunc("/job/{id}/info", apiServer.jobInfo).Methods("GET")
    63  	subrouter.HandleFunc("/summary/annotations", apiServer.annotations).Methods("GET")
    64  	subrouter.HandleFunc("/summary/jobmonths", apiServer.jobmonths).Methods("GET")
    65  	subrouter.HandleFunc("/summary/jobexecutors", apiServer.jobexecutors).Methods("GET")
    66  	subrouter.HandleFunc("/summary/totaljobs", apiServer.totaljobs).Methods("GET")
    67  	subrouter.HandleFunc("/summary/totaljobevents", apiServer.totaljobevents).Methods("GET")
    68  	subrouter.HandleFunc("/summary/totalusers", apiServer.totalusers).Methods("GET")
    69  	subrouter.HandleFunc("/summary/totalexecutors", apiServer.totalexecutors).Methods("GET")
    70  
    71  	subrouter.HandleFunc("/admin/login", apiServer.adminlogin).Methods("POST")
    72  	subrouter.HandleFunc("/admin/status", apiServer.adminstatus).Methods("GET")
    73  	subrouter.HandleFunc("/admin/moderate", apiServer.adminmoderate).Methods("POST")
    74  
    75  	srv := &http.Server{
    76  		Addr:              fmt.Sprintf("%s:%d", apiServer.Options.Host, apiServer.Options.Port),
    77  		WriteTimeout:      time.Minute * 15,
    78  		ReadTimeout:       time.Minute * 15,
    79  		ReadHeaderTimeout: time.Minute * 15,
    80  		IdleTimeout:       time.Minute * 60,
    81  		Handler:           router,
    82  	}
    83  	return srv.ListenAndServe()
    84  }
    85  
    86  type PromptParam struct {
    87  	Prompt string `json:"prompt"`
    88  }
    89  
    90  // TODO: factor commonality from following two funcs
    91  func (apiServer *DashboardAPIServer) run(res http.ResponseWriter, req *http.Request) {
    92  	// any crazy mofo on the planet can build this into their web apps
    93  	res.Header().Set("Access-Control-Allow-Origin", "*")
    94  
    95  	spec := bacmodel.Spec{}
    96  	err := json.NewDecoder(req.Body).Decode(&spec)
    97  	if err != nil {
    98  		_, _ = res.Write([]byte(fmt.Sprintf(`{"error": "%s"}`, strings.Trim(err.Error(), "\n"))))
    99  		return
   100  	}
   101  
   102  	cid, err := runGenericJob(spec)
   103  	if err != nil {
   104  		log.Ctx(req.Context()).Error().Err(err).Send()
   105  		_, _ = res.Write([]byte(fmt.Sprintf(`{"error": "%s"}`, strings.Trim(err.Error(), "\n"))))
   106  	} else {
   107  		log.Ctx(req.Context()).Info().Str("CID", cid).Send()
   108  		_, _ = res.Write([]byte(fmt.Sprintf(`{"cid": "%s"}`, strings.Trim(cid, "\n"))))
   109  	}
   110  }
   111  
   112  func (apiServer *DashboardAPIServer) stablediffusion(res http.ResponseWriter, req *http.Request) {
   113  	// any crazy mofo on the planet can build this into their web apps
   114  	res.Header().Set("Access-Control-Allow-Origin", "*")
   115  
   116  	promptParam := PromptParam{}
   117  	err := json.NewDecoder(req.Body).Decode(&promptParam)
   118  	if err != nil {
   119  		_, _ = res.Write([]byte(fmt.Sprintf(`{"error": "%s"}`, strings.Trim(err.Error(), "\n"))))
   120  		return
   121  	}
   122  	prompt := promptParam.Prompt
   123  
   124  	// user can pass ?testing=1 to bypass GPU and just return the prompt
   125  	testing := len(req.URL.Query()["testing"]) > 0
   126  
   127  	log.Ctx(req.Context()).Info().Msgf("--> testing=%t", testing)
   128  
   129  	cid, err := runStableDiffusion(prompt, testing)
   130  	if err != nil {
   131  		log.Ctx(req.Context()).Error().Err(err).Send()
   132  		_, _ = res.Write([]byte(fmt.Sprintf(`{"error": "%s"}`, strings.Trim(err.Error(), "\n"))))
   133  	} else {
   134  		log.Ctx(req.Context()).Info().Str("CID", cid).Send()
   135  		_, _ = res.Write([]byte(fmt.Sprintf(`{"cid": "%s"}`, strings.Trim(cid, "\n"))))
   136  	}
   137  }
   138  
   139  func (apiServer *DashboardAPIServer) annotations(res http.ResponseWriter, req *http.Request) {
   140  	data, err := apiServer.API.GetAnnotationSummary(context.Background())
   141  	if err != nil {
   142  		log.Ctx(req.Context()).Error().Msgf("error for annotations route: %s", err.Error())
   143  		http.Error(res, err.Error(), http.StatusInternalServerError)
   144  		return
   145  	}
   146  	err = json.NewEncoder(res).Encode(data)
   147  	if err != nil {
   148  		log.Ctx(req.Context()).Error().Msgf("error for annotations route: %s", err.Error())
   149  		http.Error(res, err.Error(), http.StatusInternalServerError)
   150  		return
   151  	}
   152  }
   153  
   154  func (apiServer *DashboardAPIServer) jobmonths(res http.ResponseWriter, req *http.Request) {
   155  	data, err := apiServer.API.GetJobMonthSummary(context.Background())
   156  	if err != nil {
   157  		log.Ctx(req.Context()).Error().Msgf("error for job months route: %s", err.Error())
   158  		http.Error(res, err.Error(), http.StatusInternalServerError)
   159  		return
   160  	}
   161  	err = json.NewEncoder(res).Encode(data)
   162  	if err != nil {
   163  		log.Ctx(req.Context()).Error().Msgf("error for job months route: %s", err.Error())
   164  		http.Error(res, err.Error(), http.StatusInternalServerError)
   165  		return
   166  	}
   167  }
   168  
   169  func (apiServer *DashboardAPIServer) jobexecutors(res http.ResponseWriter, req *http.Request) {
   170  	data, err := apiServer.API.GetJobExecutorSummary(context.Background())
   171  	if err != nil {
   172  		log.Ctx(req.Context()).Error().Msgf("error for job executors route: %s", err.Error())
   173  		http.Error(res, err.Error(), http.StatusInternalServerError)
   174  		return
   175  	}
   176  	err = json.NewEncoder(res).Encode(data)
   177  	if err != nil {
   178  		log.Ctx(req.Context()).Error().Msgf("error for job executors route: %s", err.Error())
   179  		http.Error(res, err.Error(), http.StatusInternalServerError)
   180  		return
   181  	}
   182  }
   183  
   184  func (apiServer *DashboardAPIServer) totaljobs(res http.ResponseWriter, req *http.Request) {
   185  	data, err := apiServer.API.GetTotalJobsCount(context.Background())
   186  	if err != nil {
   187  		log.Ctx(req.Context()).Error().Msgf("error for job totals route: %s", err.Error())
   188  		http.Error(res, err.Error(), http.StatusInternalServerError)
   189  		return
   190  	}
   191  	err = json.NewEncoder(res).Encode(data)
   192  	if err != nil {
   193  		log.Ctx(req.Context()).Error().Msgf("error for job totals route: %s", err.Error())
   194  		http.Error(res, err.Error(), http.StatusInternalServerError)
   195  		return
   196  	}
   197  }
   198  
   199  func (apiServer *DashboardAPIServer) totaljobevents(res http.ResponseWriter, req *http.Request) {
   200  	data, err := apiServer.API.GetTotalEventCount(context.Background())
   201  	if err != nil {
   202  		log.Ctx(req.Context()).Error().Msgf("error for job event totals route: %s", err.Error())
   203  		http.Error(res, err.Error(), http.StatusInternalServerError)
   204  		return
   205  	}
   206  	err = json.NewEncoder(res).Encode(data)
   207  	if err != nil {
   208  		log.Ctx(req.Context()).Error().Msgf("error for job event totals route: %s", err.Error())
   209  		http.Error(res, err.Error(), http.StatusInternalServerError)
   210  		return
   211  	}
   212  }
   213  
   214  func (apiServer *DashboardAPIServer) totalusers(res http.ResponseWriter, req *http.Request) {
   215  	data, err := apiServer.API.GetTotalUserCount(context.Background())
   216  	if err != nil {
   217  		log.Ctx(req.Context()).Error().Msgf("error for job user totals route: %s", err.Error())
   218  		http.Error(res, err.Error(), http.StatusInternalServerError)
   219  		return
   220  	}
   221  	err = json.NewEncoder(res).Encode(data)
   222  	if err != nil {
   223  		log.Ctx(req.Context()).Error().Msgf("error for job user totals route: %s", err.Error())
   224  		http.Error(res, err.Error(), http.StatusInternalServerError)
   225  		return
   226  	}
   227  }
   228  
   229  func (apiServer *DashboardAPIServer) totalexecutors(res http.ResponseWriter, req *http.Request) {
   230  	data, err := apiServer.API.GetTotalExecutorCount(context.Background())
   231  	if err != nil {
   232  		log.Ctx(req.Context()).Error().Msgf("error for job executors totals route: %s", err.Error())
   233  		http.Error(res, err.Error(), http.StatusInternalServerError)
   234  		return
   235  	}
   236  	err = json.NewEncoder(res).Encode(data)
   237  	if err != nil {
   238  		log.Ctx(req.Context()).Error().Msgf("error for job executors totals route: %s", err.Error())
   239  		http.Error(res, err.Error(), http.StatusInternalServerError)
   240  		return
   241  	}
   242  }
   243  
   244  func (apiServer *DashboardAPIServer) nodes(res http.ResponseWriter, req *http.Request) {
   245  	nodes, err := apiServer.API.GetNodes(context.Background())
   246  	if err == nil {
   247  		err = json.NewEncoder(res).Encode(nodes)
   248  	}
   249  	if err != nil {
   250  		log.Ctx(req.Context()).Error().Msgf("error for nodes route: %s", err.Error())
   251  		http.Error(res, err.Error(), http.StatusInternalServerError)
   252  		return
   253  	}
   254  }
   255  
   256  func (apiServer *DashboardAPIServer) jobs(res http.ResponseWriter, req *http.Request) {
   257  	query, err := GetRequestBody[localdb.JobQuery](res, req)
   258  	if err != nil {
   259  		log.Ctx(req.Context()).Error().Msgf("error for jobs route: %s", err.Error())
   260  		http.Error(res, err.Error(), http.StatusInternalServerError)
   261  		return
   262  	}
   263  
   264  	results, err := apiServer.API.GetJobs(context.Background(), *query)
   265  	if err != nil {
   266  		log.Ctx(req.Context()).Error().Msgf("error for jobs route: %s", err.Error())
   267  		http.Error(res, err.Error(), http.StatusInternalServerError)
   268  		return
   269  	}
   270  
   271  	err = json.NewEncoder(res).Encode(results)
   272  	if err != nil {
   273  		log.Ctx(req.Context()).Error().Msgf("error for jobs route: %s", err.Error())
   274  		http.Error(res, err.Error(), http.StatusInternalServerError)
   275  		return
   276  	}
   277  }
   278  
   279  type jobsCountResponse struct {
   280  	Count int `json:"count"`
   281  }
   282  
   283  func (apiServer *DashboardAPIServer) jobsCount(res http.ResponseWriter, req *http.Request) {
   284  	query, err := GetRequestBody[localdb.JobQuery](res, req)
   285  	if err != nil {
   286  		log.Ctx(req.Context()).Error().Msgf("error for jobs route: %s", err.Error())
   287  		http.Error(res, err.Error(), http.StatusInternalServerError)
   288  		return
   289  	}
   290  
   291  	count, err := apiServer.API.GetJobsCount(context.Background(), *query)
   292  	if err != nil {
   293  		log.Ctx(req.Context()).Error().Msgf("error for jobsCount route: %s", err.Error())
   294  		http.Error(res, err.Error(), http.StatusInternalServerError)
   295  		return
   296  	}
   297  
   298  	err = json.NewEncoder(res).Encode(jobsCountResponse{
   299  		Count: count,
   300  	})
   301  	if err != nil {
   302  		log.Ctx(req.Context()).Error().Msgf("error for jobsCount route: %s", err.Error())
   303  		http.Error(res, err.Error(), http.StatusInternalServerError)
   304  		return
   305  	}
   306  }
   307  
   308  func (apiServer *DashboardAPIServer) job(res http.ResponseWriter, req *http.Request) {
   309  	vars := mux.Vars(req)
   310  	id := vars["id"]
   311  
   312  	data, err := apiServer.API.GetJob(context.Background(), id)
   313  	if err != nil {
   314  		log.Ctx(req.Context()).Error().Msgf("error for job route: %s", err.Error())
   315  		http.Error(res, err.Error(), http.StatusInternalServerError)
   316  		return
   317  	}
   318  
   319  	err = json.NewEncoder(res).Encode(data)
   320  	if err != nil {
   321  		log.Ctx(req.Context()).Error().Msgf("error for job route: %s", err.Error())
   322  		http.Error(res, err.Error(), http.StatusInternalServerError)
   323  		return
   324  	}
   325  }
   326  
   327  func (apiServer *DashboardAPIServer) jobInfo(res http.ResponseWriter, req *http.Request) {
   328  	vars := mux.Vars(req)
   329  	id := vars["id"]
   330  
   331  	data, err := apiServer.API.GetJobInfo(context.Background(), id)
   332  	if err != nil {
   333  		log.Ctx(req.Context()).Error().Msgf("error for jobInfo route: %s", err.Error())
   334  		http.Error(res, err.Error(), http.StatusInternalServerError)
   335  		return
   336  	}
   337  
   338  	err = json.NewEncoder(res).Encode(data)
   339  	if err != nil {
   340  		log.Ctx(req.Context()).Error().Msgf("error for jobInfo route: %s", err.Error())
   341  		http.Error(res, err.Error(), http.StatusInternalServerError)
   342  		return
   343  	}
   344  }
   345  
   346  type loginResponse struct {
   347  	Token string `json:"token"`
   348  }
   349  
   350  func (apiServer *DashboardAPIServer) adminlogin(res http.ResponseWriter, req *http.Request) {
   351  	// decode the request body into a LoginRequest struct
   352  	var loginRequest types.LoginRequest
   353  	err := json.NewDecoder(req.Body).Decode(&loginRequest)
   354  	if err != nil {
   355  		log.Ctx(req.Context()).Error().Msgf("error for login route: %s", err.Error())
   356  		http.Error(res, err.Error(), http.StatusInternalServerError)
   357  		return
   358  	}
   359  	user, err := apiServer.API.Login(context.Background(), loginRequest)
   360  	if err != nil {
   361  		log.Ctx(req.Context()).Error().Msgf("error for login route: %s", err.Error())
   362  		http.Error(res, err.Error(), http.StatusInternalServerError)
   363  		return
   364  	}
   365  	token, err := generateJWT(apiServer.Options.JWTSecret, user.Username)
   366  	if err != nil {
   367  		log.Ctx(req.Context()).Error().Msgf("error for login route: %s", err.Error())
   368  		http.Error(res, err.Error(), http.StatusInternalServerError)
   369  		return
   370  	}
   371  	err = json.NewEncoder(res).Encode(loginResponse{
   372  		Token: token,
   373  	})
   374  	if err != nil {
   375  		log.Ctx(req.Context()).Error().Msgf("error for login route: %s", err.Error())
   376  		http.Error(res, err.Error(), http.StatusInternalServerError)
   377  		return
   378  	}
   379  }
   380  
   381  func (apiServer *DashboardAPIServer) adminstatus(res http.ResponseWriter, req *http.Request) {
   382  	user, err := getUserFromRequest(apiServer.API, req, apiServer.Options.JWTSecret)
   383  	if err != nil {
   384  		log.Ctx(req.Context()).Error().Msgf("error for adminstatus route: %s", err.Error())
   385  		http.Error(res, fmt.Sprintf("error for adminstatus route: %s", err.Error()), http.StatusUnauthorized)
   386  		return
   387  	}
   388  	err = json.NewEncoder(res).Encode(user)
   389  	if err != nil {
   390  		log.Ctx(req.Context()).Error().Msgf("error for status route: %s", err.Error())
   391  		http.Error(res, err.Error(), http.StatusInternalServerError)
   392  		return
   393  	}
   394  }
   395  
   396  func (apiServer *DashboardAPIServer) adminmoderate(res http.ResponseWriter, req *http.Request) {
   397  	user, err := getUserFromRequest(apiServer.API, req, apiServer.Options.JWTSecret)
   398  	if err != nil || user == nil {
   399  		log.Ctx(req.Context()).Error().Msgf("access denied: %s", err.Error())
   400  		http.Error(res, fmt.Sprintf("access denied: %s", err.Error()), http.StatusUnauthorized)
   401  		return
   402  	}
   403  	data, err := GetRequestBody[types.JobModeration](res, req)
   404  	if err != nil {
   405  		log.Ctx(req.Context()).Error().Msgf("error for adminmoderate route: %s", err.Error())
   406  		http.Error(res, err.Error(), http.StatusInternalServerError)
   407  		return
   408  	}
   409  	err = apiServer.API.CreateJobModeration(context.Background(), *data)
   410  	if err != nil {
   411  		log.Ctx(req.Context()).Error().Msgf("error for adminmoderate route: %s", err.Error())
   412  		http.Error(res, err.Error(), http.StatusInternalServerError)
   413  		return
   414  	}
   415  
   416  	err = json.NewEncoder(res).Encode(struct {
   417  		Success bool `json:"success"`
   418  	}{
   419  		Success: true,
   420  	})
   421  	if err != nil {
   422  		log.Ctx(req.Context()).Error().Msgf("error for adminmoderate route: %s", err.Error())
   423  		http.Error(res, err.Error(), http.StatusInternalServerError)
   424  		return
   425  	}
   426  }