blob: 9c4e1b7eae98556c6315568f30fa0ab8c9428d9f [file] [log] [blame]
package handlers
import (
"bytes"
"encoding/json"
"infra/appengine/rotang"
"net/http"
"time"
"go.chromium.org/luci/server/router"
"go.chromium.org/luci/server/templates"
"golang.org/x/net/context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const jsonHeader = "application/json"
type jsonMember struct {
Name string
Email string
TZ string
}
type jsonRota struct {
Cfg rotang.Configuration
Members []jsonMember
}
// HandleRotaCreate handles creation of new rotations.
func (h *State) HandleRotaCreate(ctx *router.Context) {
if err := ctx.Context.Err(); err != nil {
http.Error(ctx.Writer, err.Error(), http.StatusInternalServerError)
return
}
if ctx.Request.Method == "POST" {
var res jsonRota
if err := json.NewDecoder(ctx.Request.Body).Decode(&res); err != nil {
http.Error(ctx.Writer, err.Error(), http.StatusInternalServerError)
return
}
_, err := h.configStore(ctx.Context).RotaConfig(ctx.Context, res.Cfg.Config.Name)
if status.Code(err) != codes.NotFound {
if err == nil {
http.Error(ctx.Writer, "rotation exists", http.StatusInternalServerError)
return
}
http.Error(ctx.Writer, err.Error(), http.StatusInternalServerError)
return
}
if err := h.createRota(ctx, &res); err != nil {
http.Error(ctx.Writer, err.Error(), http.StatusInternalServerError)
return
}
return
}
var genBuf bytes.Buffer
if err := json.NewEncoder(&genBuf).Encode(h.generators.List()); err != nil {
http.Error(ctx.Writer, err.Error(), http.StatusInternalServerError)
return
}
var modBuf bytes.Buffer
if err := json.NewEncoder(&modBuf).Encode(h.generators.ListModifiers()); err != nil {
http.Error(ctx.Writer, err.Error(), http.StatusInternalServerError)
return
}
templates.MustRender(ctx.Context, ctx.Writer, "pages/rotacreate.html", templates.Args{"Generators": genBuf.String(), "Modifiers": modBuf.String()})
}
func (h *State) validateConfig(ctx *router.Context, jr *jsonRota) error {
members, err := convertMembers(jr.Members)
if err != nil {
return err
}
if err := h.updateMembers(ctx.Context, members); err != nil {
return err
}
if !adminOrOwner(ctx, &jr.Cfg) {
return status.Errorf(codes.PermissionDenied, "not admin or owner")
}
// To not create empty Owners entries.
var cleanOwners []string
for _, o := range jr.Cfg.Config.Owners {
if o == "" {
continue
}
cleanOwners = append(cleanOwners, o)
}
jr.Cfg.Config.Owners = cleanOwners
if dur, ok := checkShiftDuration(&jr.Cfg); !ok {
return status.Errorf(codes.InvalidArgument, "shift durations does not add up to 24h, got %v", dur)
}
if err := validateTZFair(jr); err != nil {
return err
}
return nil
}
// validateTZFair validates that the number of TZs are equal to the number of sheriffs to schedule.
func validateTZFair(jf *jsonRota) error {
// This validation only applies to TZFair rotations.
if jf.Cfg.Config.Shifts.Generator != "TZFair" {
return nil
}
tzMap := make(map[string]int)
for _, m := range jf.Members {
tzMap[m.TZ]++
}
if len(tzMap) != jf.Cfg.Config.Shifts.ShiftMembers {
return status.Errorf(codes.InvalidArgument, "number of timezones larger than members/shift, tzMap: %v", tzMap)
}
return nil
}
func (h *State) createRota(ctx *router.Context, jr *jsonRota) error {
if err := h.validateConfig(ctx, jr); err != nil {
return err
}
return h.configStore(ctx.Context).CreateRotaConfig(ctx.Context, &jr.Cfg)
}
func (h *State) modifyRota(ctx *router.Context, cfg *rotang.Configuration, jr *jsonRota) error {
if err := h.validateConfig(ctx, jr); err != nil {
return err
}
// Want to keep the Enabled state of the config before.
// Enable/Disable of the configuration is handled elsewhere.
jr.Cfg.Config.Enabled = cfg.Config.Enabled
jr.Cfg.Config.External = cfg.Config.External
return h.configStore(ctx.Context).UpdateRotaConfig(ctx.Context, &jr.Cfg)
}
// checkShiftDuration checks if the shift durations add up to 24 hours.
func checkShiftDuration(cfg *rotang.Configuration) (time.Duration, bool) {
var totalDuration time.Duration
for _, s := range cfg.Config.Shifts.Shifts {
totalDuration += s.Duration
}
if totalDuration == fullDay {
return totalDuration, true
}
return totalDuration, false
}
// convertMembers converts between the jsonMember format to rotang.Member.
// In practice this is just changing the TZ field from string to time.Location.
func convertMembers(jm []jsonMember) ([]rotang.Member, error) {
var res []rotang.Member
for _, m := range jm {
tz, err := time.LoadLocation(m.TZ)
if err != nil {
return nil, err
}
res = append(res, rotang.Member{
Name: m.Name,
Email: m.Email,
TZ: *tz,
})
}
return res, nil
}
// updateMembers adds in members in the member list not already in the pool.
// Members already represented in the pool are updated.
func (h *State) updateMembers(ctx context.Context, members []rotang.Member) error {
ms := h.memberStore(ctx)
for _, m := range members {
currentMember, err := ms.Member(ctx, m.Email)
switch {
case err == nil:
m.OOO = currentMember.OOO
if err = ms.UpdateMember(ctx, &m); err != nil {
return err
}
case status.Code(err) == codes.NotFound:
if err = ms.CreateMember(ctx, &m); err != nil {
return err
}
default:
return err
}
}
return nil
}
// HandleRotaModify is used to modify or copy rotation configurations.
func (h *State) HandleRotaModify(ctx *router.Context) {
if err := ctx.Context.Err(); err != nil {
http.Error(ctx.Writer, err.Error(), http.StatusInternalServerError)
return
}
switch ctx.Request.Method {
case "GET":
args, err := h.modifyRotation(ctx)
if err != nil {
http.Error(ctx.Writer, err.Error(), http.StatusInternalServerError)
return
}
templates.MustRender(ctx.Context, ctx.Writer, "pages/rotamodify.html", args)
return
case "POST":
var res jsonRota
if err := json.NewDecoder(ctx.Request.Body).Decode(&res); err != nil {
http.Error(ctx.Writer, err.Error(), http.StatusInternalServerError)
return
}
rotas, err := h.configStore(ctx.Context).RotaConfig(ctx.Context, res.Cfg.Config.Name)
if err != nil {
if status.Code(err) == codes.NotFound {
if err = h.createRota(ctx, &res); err != nil {
http.Error(ctx.Writer, err.Error(), http.StatusInternalServerError)
}
return
}
http.Error(ctx.Writer, err.Error(), http.StatusInternalServerError)
return
}
if err = h.modifyRota(ctx, rotas[0], &res); err != nil {
http.Error(ctx.Writer, err.Error(), http.StatusInternalServerError)
return
}
default:
http.Error(ctx.Writer, "HandleModifyRota handles only GET and POST requests", http.StatusBadRequest)
return
}
}