[rotang] Adding in the legacy Sheriff rotation js/json handlers.

Files are presented at https://rota-ng.appspot.com/legacy/*

Eg. https://rota-ng.appspot.com/legacy/sheriff.js


Bug: 889221
Change-Id: I0e753d9a0f93d895f16c2ff1a8165a42cd48a292
Reviewed-on: https://chromium-review.googlesource.com/c/1272740
Commit-Queue: Ola Karlsson <olakar@chromium.org>
Reviewed-by: Tiffany Zhang <zhangtiff@chromium.org>
Cr-Commit-Position: refs/heads/master@{#18305}
diff --git a/cmd/app/templates/pages/index.html b/cmd/app/templates/pages/index.html
index 42c42d2..f0329d1 100644
--- a/cmd/app/templates/pages/index.html
+++ b/cmd/app/templates/pages/index.html
@@ -14,8 +14,9 @@
 </p>
 <p> Information about how to
 <a href="https://chromium.googlesource.com/infra/infra/+/master/go/src/infra/appengine/rotang/SWITCH.md">
-Switch to RotaNG
+  Switch to RotaNG
 </a>
+</p>
 <a href="upload">Upload Legacy JSON rota configuraton</a><br>
 <a href="list">List configurations in backend store</a><br>
 <a href="managerota">Manage rotations</a><br>
diff --git a/cmd/handlers/handle_deleterota.go b/cmd/handlers/handle_deleterota.go
index a344a99..35ec6d4 100644
--- a/cmd/handlers/handle_deleterota.go
+++ b/cmd/handlers/handle_deleterota.go
@@ -3,7 +3,6 @@
 import (
 	"net/http"
 
-	"go.chromium.org/luci/server/auth"
 	"go.chromium.org/luci/server/router"
 )
 
@@ -41,21 +40,7 @@
 	}
 	rota := rotas[0]
 
-	usr := auth.CurrentUser(ctx.Context)
-	if usr == nil {
-		http.Error(ctx.Writer, "login required", http.StatusForbidden)
-		return
-	}
-
-	isOwner := false
-	for _, o := range rota.Config.Owners {
-		if o == usr.Email {
-			isOwner = true
-			break
-		}
-	}
-
-	if !isOwner {
+	if !adminOrOwner(ctx, rota) {
 		http.Error(ctx.Writer, "not in the rotation owners", http.StatusForbidden)
 		return
 	}
diff --git a/cmd/handlers/handle_legacy.go b/cmd/handlers/handle_legacy.go
index 139e10c..930c926 100644
--- a/cmd/handlers/handle_legacy.go
+++ b/cmd/handlers/handle_legacy.go
@@ -4,8 +4,10 @@
 	"bytes"
 	"encoding/json"
 	"fmt"
+	"infra/appengine/rotang"
 	"net/http"
 	"strings"
+	"time"
 
 	"go.chromium.org/gae/service/memcache"
 	"go.chromium.org/luci/common/clock"
@@ -70,9 +72,8 @@
 				str += ", secondary: " + strings.Join(oc[1:], ", ")
 			}
 		}
-		return "document.Write('" + str + "');", nil
+		return "document.write('" + str + "');", nil
 	case "current_trooper.json":
-		var buf bytes.Buffer
 		primary := "None"
 		var secondary []string
 		if len(oc) > 0 {
@@ -81,6 +82,8 @@
 				secondary = oc[1:]
 			}
 		}
+
+		var buf bytes.Buffer
 		enc := json.NewEncoder(&buf)
 		if err := enc.Encode(&trooperJSON{
 			Primary:   primary,
@@ -99,3 +102,112 @@
 		return "", status.Errorf(codes.InvalidArgument, "legacyTrooper only handles `trooper.js` and `current_trooper.txt`")
 	}
 }
+
+var fileToRota = map[string][2]string{
+	"sheriff.js": {"Build Sheriff", ""},
+	// "sheriff_webkit.js":              "",
+	// "sheriff_memory.js":              "",
+	"sheriff_cros_mtv.js":          {"Chrome OS Build Sheriff", ""},
+	"sheriff_cros_nonmtv.js":       {"Chrome OS Build Sheriff - Other", "Chrome OS Build Sheriff"},
+	"sheriff_perf.js":              {"Chromium Perf Regression Sheriff Rotation", ""},
+	"sheriff_cr_cros_gardeners.js": {"Chrome on ChromeOS Gardening", ""},
+	"sheriff_gpu.js":               {"Chrome GPU Pixel Wrangling", ""},
+	"sheriff_angle.js":             {"The ANGLE Wrangle", ""},
+	"sheriff_android.js":           {"Chrome on Android Build Sheriff", ""},
+	"sheriff_ios.js":               {"Chrome iOS Build Sheriff", ""},
+	"sheriff_v8.js":                {"V8 Sheriff", ""},
+	"sheriff_perfbot.js":           {"Chromium Perf Bot Sheriff Rotation", ""},
+
+	"sheriff.json": {"Build Sheriff", ""},
+	// "sheriff_webkit.json":            "",
+	// "sheriff_memory.json":            "",
+	"sheriff_cros_mtv.json":          {"Chrome OS Build Sheriff", ""},
+	"sheriff_cros_nonmtv.json":       {"Chrome OS Build Sheriff - Other", "Chrome OS Build Sheriff"},
+	"sheriff_perf.json":              {"Chromium Perf Regression Sheriff Rotation", ""},
+	"sheriff_cr_cros_gardeners.json": {"Chrome on ChromeOS Gardening", ""},
+	"sheriff_gpu.json":               {"Chrome GPU Pixel Wrangling", ""},
+	"sheriff_angle.json":             {"The ANGLE Wrangle", ""},
+	"sheriff_android.json":           {"Chrome on Android Build Sheriff", ""},
+	"sheriff_ios.json":               {"Chrome iOS Build Sheriff", ""},
+	"sheriff_v8.json":                {"V8 Sheriff", ""},
+	"sheriff_perfbot.json":           {"Chromium Perf Bot Sheriff Rotation", ""},
+	//"all_rotations.js":               "",
+	//"all_rotations.js":               "",
+}
+
+const week = 7 * 24 * time.Hour
+
+type sheriffJSON struct {
+	UnixTS int64    `json:"updated_unix_timestamp"`
+	Emails []string `json:"emails"`
+}
+
+// legacySheriff produces the legacy cron created sherriff oncall files.
+func (h *State) legacySheriff(ctx *router.Context, file string) (string, error) {
+	rota, ok := fileToRota[file]
+	if !ok {
+		return "", status.Errorf(codes.InvalidArgument, "file: %q not handled by legacySheriff", file)
+	}
+	r, err := h.configStore(ctx.Context).RotaConfig(ctx.Context, rota[0])
+	if err != nil {
+		return "", err
+	}
+	if len(r) != 1 {
+		return "", status.Errorf(codes.Internal, "RotaConfig did not return 1 configuration")
+	}
+	cfg := r[0]
+	// As a workaround to handle split shifts some users create multiple configurations with different
+	// calendars but the same Event Name. The new service use the rota name as a key in the datastore.
+	if rota[1] != "" {
+		cfg.Config.Name = rota[1]
+	}
+
+	updated := clock.Now(ctx.Context)
+	events, err := h.legacyCalendar.Events(ctx, cfg, updated.Add(-week), updated.Add(week))
+	if err != nil {
+		return "", err
+	}
+
+	var entry rotang.ShiftEntry
+	for _, e := range events {
+		if (updated.After(e.StartTime) || updated.Equal(e.StartTime)) &&
+			updated.Before(e.EndTime) {
+			entry = e
+		}
+	}
+
+	sp := strings.Split(file, ".")
+	if len(sp) != 2 {
+		return "", status.Errorf(codes.InvalidArgument, "filename in wrong format")
+	}
+
+	switch sp[1] {
+	case "js":
+		var oc []string
+		for _, o := range entry.OnCall {
+			oc = append(oc, strings.Split(o.Email, "@")[0])
+		}
+		str := "None"
+		if len(oc) > 0 {
+			str = strings.Join(oc, ", ")
+		}
+		return "document.write('" + str + "');", nil
+	case "json":
+		oc := make([]string, 0)
+		for _, o := range entry.OnCall {
+			oc = append(oc, o.Email)
+		}
+		var buf bytes.Buffer
+		enc := json.NewEncoder(&buf)
+		if err := enc.Encode(&sheriffJSON{
+			UnixTS: updated.Unix(),
+			Emails: oc,
+		}); err != nil {
+			return "", err
+		}
+		return buf.String(), nil
+
+	default:
+		return "", status.Errorf(codes.InvalidArgument, "filename in wrong format")
+	}
+}
diff --git a/cmd/handlers/handle_legacy_test.go b/cmd/handlers/handle_legacy_test.go
index 2114d2c..43924d1 100644
--- a/cmd/handlers/handle_legacy_test.go
+++ b/cmd/handlers/handle_legacy_test.go
@@ -1,6 +1,7 @@
 package handlers
 
 import (
+	"infra/appengine/rotang"
 	"net/http"
 	"net/http/httptest"
 	"testing"
@@ -104,6 +105,173 @@
 
 }
 
+func TestLegacySheriff(t *testing.T) {
+	ctx := newTestContext()
+
+	tests := []struct {
+		name       string
+		fail       bool
+		calFail    bool
+		time       time.Time
+		calShifts  []rotang.ShiftEntry
+		ctx        *router.Context
+		file       string
+		memberPool []rotang.Member
+		cfgs       []*rotang.Configuration
+		want       string
+	}{{
+		name: "Success JS",
+		ctx: &router.Context{
+			Context: ctx,
+			Writer:  httptest.NewRecorder(),
+		},
+		file: "sheriff.js",
+		time: midnight,
+		cfgs: []*rotang.Configuration{
+			{
+				Config: rotang.Config{
+					Name: "Build Sheriff",
+				},
+			},
+		},
+		calShifts: []rotang.ShiftEntry{
+			{
+				StartTime: midnight,
+				EndTime:   midnight.Add(5 * fullDay),
+				OnCall: []rotang.ShiftMember{
+					{
+						Email: "test1@oncall.com",
+					}, {
+						Email: "test2@oncall.com",
+					},
+				},
+			}, {
+				StartTime: midnight.Add(5 * fullDay),
+				EndTime:   midnight.Add(10 * fullDay),
+				OnCall: []rotang.ShiftMember{
+					{
+						Email: "test3@oncall.com",
+					}, {
+						Email: "test4@oncall.com",
+					},
+				},
+			},
+		},
+		want: "document.write('test1, test2');",
+	}, {
+		name: "Success JSON",
+		ctx: &router.Context{
+			Context: ctx,
+			Writer:  httptest.NewRecorder(),
+		},
+		file: "sheriff.json",
+		time: midnight.Add(6 * fullDay),
+		cfgs: []*rotang.Configuration{
+			{
+				Config: rotang.Config{
+					Name: "Build Sheriff",
+				},
+			},
+		},
+		calShifts: []rotang.ShiftEntry{
+			{
+				StartTime: midnight,
+				EndTime:   midnight.Add(5 * fullDay),
+				OnCall: []rotang.ShiftMember{
+					{
+						Email: "test1@oncall.com",
+					}, {
+						Email: "test2@oncall.com",
+					},
+				},
+			}, {
+				StartTime: midnight.Add(5 * fullDay),
+				EndTime:   midnight.Add(10 * fullDay),
+				OnCall: []rotang.ShiftMember{
+					{
+						Email: "test3@oncall.com",
+					}, {
+						Email: "test4@oncall.com",
+					},
+				},
+			},
+		},
+		want: `{"updated_unix_timestamp":1144454400,"emails":["test3@oncall.com","test4@oncall.com"]}
+`,
+	}, {
+		name: "File not supported",
+		fail: true,
+		ctx: &router.Context{
+			Context: ctx,
+			Writer:  httptest.NewRecorder(),
+		},
+		file: "sheriff_not_supported.js",
+		time: midnight,
+	}, {
+		name: "Config not found",
+		fail: true,
+		ctx: &router.Context{
+			Context: ctx,
+			Writer:  httptest.NewRecorder(),
+		},
+		file: "sheriff.js",
+		time: midnight,
+	}, {
+		name:    "Calendar fail",
+		fail:    true,
+		calFail: true,
+		ctx: &router.Context{
+			Context: ctx,
+			Writer:  httptest.NewRecorder(),
+		},
+		file: "sheriff.json",
+		time: midnight.Add(6 * fullDay),
+		cfgs: []*rotang.Configuration{
+			{
+				Config: rotang.Config{
+					Name: "Build Sheriff",
+				},
+			},
+		},
+	},
+	}
+
+	h := testSetup(t)
+
+	for _, tst := range tests {
+		t.Run(tst.name, func(t *testing.T) {
+			for _, m := range tst.memberPool {
+				if err := h.memberStore(ctx).CreateMember(ctx, &m); err != nil {
+					t.Fatalf("%s: AddMember(ctx, _) failed: %v", tst.name, err)
+				}
+				defer h.memberStore(ctx).DeleteMember(ctx, m.Email)
+			}
+			for _, cfg := range tst.cfgs {
+				if err := h.configStore(ctx).CreateRotaConfig(ctx, cfg); err != nil {
+					t.Fatalf("%s: CreateRotaConfig(ctx, _) failed: %v", tst.name, err)
+				}
+				defer h.configStore(ctx).DeleteRotaConfig(ctx, cfg.Config.Name)
+			}
+
+			h.legacyCalendar.(*fakeCal).Set(tst.calShifts, tst.calFail, false, 0)
+
+			tst.ctx.Context = clock.Set(tst.ctx.Context, testclock.New(tst.time))
+
+			res, err := h.legacySheriff(tst.ctx, tst.file)
+			if got, want := (err != nil), tst.fail; got != want {
+				t.Fatalf("%s: h.legacySheriff(ctx, %q) = %t want: %t, err: %v", tst.name, tst.file, got, want, err)
+			}
+			if err != nil {
+				return
+			}
+
+			if diff := pretty.Compare(tst.want, res); diff != "" {
+				t.Fatalf("%s: h.legacySheriff(ctx, %q) differ -want +got, \n%s", tst.name, tst.file, diff)
+			}
+		})
+	}
+}
+
 func TestLegacyTroopers(t *testing.T) {
 	ctx := newTestContext()
 
@@ -126,7 +294,7 @@
 		file:       "trooper.js",
 		oncallers:  []string{"primary1", "secondary1", "secondary2"},
 		updateTime: midnight,
-		want:       "document.Write('primary1, secondary: secondary1, secondary2');",
+		want:       "document.write('primary1, secondary: secondary1, secondary2');",
 	}, {
 		name: "Success JSON",
 		ctx: &router.Context{
diff --git a/cmd/handlers/handlers.go b/cmd/handlers/handlers.go
index f091a6d..70f976e 100644
--- a/cmd/handlers/handlers.go
+++ b/cmd/handlers/handlers.go
@@ -14,9 +14,10 @@
 	"golang.org/x/net/context"
 	"golang.org/x/oauth2"
 	"google.golang.org/appengine"
-	aeuser "google.golang.org/appengine/user"
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
+
+	aeuser "google.golang.org/appengine/user"
 )
 
 var mtvTime = func() *time.Location {
@@ -45,9 +46,33 @@
 
 func buildLegacyMap(h *State) map[string]func(ctx *router.Context, file string) (string, error) {
 	return map[string]func(ctx *router.Context, file string) (string, error){
+		// Trooper files.
 		"trooper.js":           h.legacyTrooper,
 		"current_trooper.json": h.legacyTrooper,
 		"current_trooper.txt":  h.legacyTrooper,
+		// Sheriff files.
+		"sheriff.js":                     h.legacySheriff,
+		"sheriff_cros_mtv.js":            h.legacySheriff,
+		"sheriff_cros_nonmtv.js":         h.legacySheriff,
+		"sheriff_perf.js":                h.legacySheriff,
+		"sheriff_cr_cros_gardeners.js":   h.legacySheriff,
+		"sheriff_gpu.js":                 h.legacySheriff,
+		"sheriff_angle.js":               h.legacySheriff,
+		"sheriff_android.js":             h.legacySheriff,
+		"sheriff_ios.js":                 h.legacySheriff,
+		"sheriff_v8.js":                  h.legacySheriff,
+		"sheriff_perfbot.js":             h.legacySheriff,
+		"sheriff.json":                   h.legacySheriff,
+		"sheriff_cros_mtv.json":          h.legacySheriff,
+		"sheriff_cros_nonmtv.json":       h.legacySheriff,
+		"sheriff_perf.json":              h.legacySheriff,
+		"sheriff_cr_cros_gardeners.json": h.legacySheriff,
+		"sheriff_gpu.json":               h.legacySheriff,
+		"sheriff_angle.json":             h.legacySheriff,
+		"sheriff_android.json":           h.legacySheriff,
+		"sheriff_ios.json":               h.legacySheriff,
+		"sheriff_v8.json":                h.legacySheriff,
+		"sheriff_perfbot.json":           h.legacySheriff,
 	}
 }
 
diff --git a/cmd/handlers/handlers_test.go b/cmd/handlers/handlers_test.go
index 0a28b46..7548d52 100644
--- a/cmd/handlers/handlers_test.go
+++ b/cmd/handlers/handlers_test.go
@@ -124,6 +124,13 @@
 	return f.oncallers, nil
 }
 
+func (f *fakeCal) TrooperShifts(_ *router.Context, _, _ string, _ time.Time) ([]rotang.ShiftEntry, error) {
+	if f.fail {
+		return nil, status.Errorf(codes.Internal, "fake is failing as requested")
+	}
+	return f.ret, nil
+}
+
 func TestNew(t *testing.T) {
 
 	tests := []struct {