blob: a27e45e896fa2ce40f92d57b6dfef3210e691969 [file] [log] [blame]
// Copyright 2021 The Chromium Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package fakelegacy
import (
"bytes"
"fmt"
"net/http"
"strings"
"sync"
"go.chromium.org/luci/common/errors"
)
// NewServer sets up a fake Pinpoint Legacy HTTP server configured to serve
// response templates from the provided templateDir. If jobData is not nil, the
// map is used as the backing store in the Server.
func NewServer(templateDir string, jobData map[string]*Job) (*Server, error) {
tmpls, err := loadTemplates(templateDir)
if err != nil {
return nil, err
}
if jobData == nil {
jobData = make(map[string]*Job)
}
return &Server{
jobData: jobData,
templates: tmpls,
}, nil
}
// Server implements a fake Legacy Pinpoint service. Use NewServer to make one.
type Server struct {
templates *Templates
mu sync.Mutex
jobData map[string]*Job
nextUUID uint64
}
// A Job represents some fake legacy pinpoint job.
type Job struct {
ID string
Status Status
UserEmail string
}
// legacyHandler represents a REST API implementation, satisfying http.Handler.
// If an error is returned, the returned code will be used as the HTTP response
// code to serve. Otherwise, the returned legacyResult will be returned to the
// user (by executing the template with the data).
type legacyHandler func(req *http.Request) (_ *legacyResult, code int, _ error)
func (h legacyHandler) ServeHTTP(wr http.ResponseWriter, req *http.Request) {
res, code, err := h(req)
if err != nil {
http.Error(wr, err.Error(), code)
return
}
// Buffer up the template output before writing to the ResponseWriter: upon
// the first Write a 200 OK status is proactively transmitted to the client,
// preventing us from being able to give an error code for a template error.
buf := new(bytes.Buffer)
if err := res.tmpl.Execute(buf, res.data); err != nil {
http.Error(wr, err.Error(), 500)
return
}
buf.WriteTo(wr)
}
type legacyResult struct {
tmpl Template
data interface{}
}
// Handler returns a Handler which implements the entire REST API for Server.
func (s *Server) Handler() http.Handler {
mux := new(http.ServeMux)
mux.Handle("/api/job/", legacyHandler(s.getJob))
mux.Handle("/api/jobs", legacyHandler(s.listJobs))
mux.Handle("/api/new", legacyHandler(s.newJob))
return mux
}
// getJob implements individual job lookup requests.
func (s *Server) getJob(req *http.Request) (*legacyResult, int, error) {
id := strings.Split(req.URL.Path, "/")[3]
s.mu.Lock()
defer s.mu.Unlock()
j, ok := s.jobData[id]
if !ok {
return nil, 404, fmt.Errorf("job %q not found", id)
}
return &legacyResult{
s.templates.Job,
j,
}, 0, nil
}
// listJobs implements listing job requests.
func (s *Server) listJobs(req *http.Request) (*legacyResult, int, error) {
args := req.URL.Query()
if args.Get("filter") != "" {
return nil, 400, errors.Reason("TODO: implement filter").Err()
}
s.mu.Lock()
defer s.mu.Unlock()
var list []Job
for _, j := range s.jobData {
// Copy in the Job data while we have the lock to avoid races after this
// function returns (while the returned data is being templated).
list = append(list, *j)
}
return &legacyResult{
s.templates.List,
list,
}, 0, nil
}
// newJob implements creating new (fake) pinpoint jobs.
// As of now it ignores all parameters other than 'user'.
func (s *Server) newJob(req *http.Request) (*legacyResult, int, error) {
if req.Method != "POST" {
return nil, 400, errors.Reason("only POST supported").Err()
}
if err := req.ParseForm(); err != nil {
return nil, 400, errors.Annotate(err, "error parsing HTTP request").Err()
}
q := req.Form
user := q.Get("user")
if user == "" {
return nil, 400, errors.Reason("must set user").Err()
}
s.mu.Lock()
defer s.mu.Unlock()
id := formatUUID(s.nextUUID)
s.nextUUID++
j := &Job{
ID: id,
Status: QueuedStatus,
UserEmail: user,
}
s.jobData[id] = j
return &legacyResult{
s.templates.New,
*j,
}, 0, nil
}
// formatUUID formats the provided integral uuid as a legacy hex ID.
// Specifically, it produces a 14 digit hex number with the first
// digit hard-coded to 1.
func formatUUID(uuid uint64) string {
return fmt.Sprintf("1%013x", uuid)
}