| package handlers |
| |
| import ( |
| "bytes" |
| "encoding/json" |
| "fmt" |
| "infra/appengine/rotang" |
| "net/http" |
| "regexp" |
| "sort" |
| "strings" |
| "time" |
| |
| "go.chromium.org/gae/service/memcache" |
| "go.chromium.org/luci/common/clock" |
| "go.chromium.org/luci/common/logging" |
| "go.chromium.org/luci/server/router" |
| "google.golang.org/grpc/codes" |
| "google.golang.org/grpc/status" |
| ) |
| |
| // HandleLegacy serves the /legacy endpoint. |
| func (h *State) HandleLegacy(ctx *router.Context) { |
| if err := ctx.Context.Err(); err != nil { |
| http.Error(ctx.Writer, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| |
| name := ctx.Params.ByName("name") |
| |
| vf, ok := h.legacyMap[name] |
| if !ok { |
| http.Error(ctx.Writer, "not found", http.StatusNotFound) |
| return |
| } |
| |
| item := memcache.NewItem(ctx.Context, name) |
| doCORS(ctx) |
| if err := memcache.Get(ctx.Context, item); err != nil { |
| logging.Warningf(ctx.Context, "%q not in the cache", name) |
| val, err := vf(ctx, name) |
| if err != nil { |
| http.Error(ctx.Writer, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| fmt.Fprint(ctx.Writer, val) |
| return |
| } |
| fmt.Fprint(ctx.Writer, string(item.Value())) |
| } |
| |
| func doCORS(ctx *router.Context) { |
| ctx.Writer.Header().Add("Access-Control-Allow-Origin", "*") |
| } |
| |
| const ( |
| trooperCal = "google.com_3aov6uidfjscpj2hrpsd8i4e7o@group.calendar.google.com" |
| matchSummary = "CCI-Trooper:" |
| ) |
| |
| var trooperRotationNamePattern = regexp.MustCompile("(chrome-ops-([[:word:]]|-)+).json") |
| var matchSummaryByRotation = map[string]string{ |
| "chrome-ops-client-infra": "CCI-Trooper:", |
| "chrome-ops-devx": "DevX-Trooper:", |
| "chrome-ops-foundation": "Foundation-Trooper:", |
| "chrome-ops-sre": "SRE-Trooper:", |
| } |
| |
| type trooperJSON struct { |
| Primary string `json:"primary"` |
| Secondary []string `json:"secondaries"` |
| UnixTS int64 `json:"updated_unix_timestamp"` |
| } |
| |
| func (h *State) legacyTrooperByRotation(ctx *router.Context, file string) (string, error) { |
| patternMatches := trooperRotationNamePattern.FindStringSubmatch(file) |
| if len(patternMatches) == 0 { |
| return "", status.Errorf(codes.NotFound, "Invalid trooper rotation name.") |
| } |
| |
| rotation := patternMatches[1] |
| summary, ok := matchSummaryByRotation[rotation] |
| if !ok { |
| return "", status.Errorf(codes.InvalidArgument, rotation) |
| } |
| |
| // look up entries until it finds at least one trooper shift |
| // in the following 7 days. |
| var buf bytes.Buffer |
| enc := json.NewEncoder(&buf) |
| start := clock.Now(ctx.Context) |
| end := start.Add(fullDay * 8) |
| shifts, err := h.legacyCalendar.TrooperShifts(ctx, trooperCal, summary, start, end) |
| if err != nil && status.Code(err) != codes.NotFound { |
| return "", err |
| } |
| for _, shift := range shifts { |
| members := shift.OnCall |
| if len(members) > 0 { |
| var secondaries []string |
| for _, secondary := range members[1:] { |
| secondaries = append(secondaries, secondary.Email) |
| } |
| |
| if err := enc.Encode(&trooperJSON{ |
| Primary: members[0].Email, |
| Secondary: secondaries, |
| UnixTS: shift.StartTime.Unix(), |
| }); err != nil { |
| return "", err |
| } |
| return buf.String(), nil |
| } |
| } |
| primary := "None" |
| secondary := []string{} |
| enc.Encode(&trooperJSON{ |
| Primary: primary, |
| Secondary: secondary, |
| UnixTS: start.Unix(), |
| }) |
| return buf.String(), nil |
| } |
| |
| func (h *State) legacyTrooper(ctx *router.Context, file string) (string, error) { |
| updated := clock.Now(ctx.Context) |
| oc, err := h.legacyCalendar.TrooperOncall(ctx, trooperCal, matchSummary, updated) |
| if err != nil && status.Code(err) != codes.NotFound { |
| return "", err |
| } |
| switch file { |
| case "trooper.js": |
| str := "None" |
| if len(oc) > 0 { |
| str = oc[0] |
| if len(oc) > 1 { |
| str += ", secondary: " + strings.Join(oc[1:], ", ") |
| } |
| } |
| return "document.write('" + str + "');", nil |
| case "current_trooper.json", "trooper.json": |
| primary := "None" |
| secondary := make([]string, 0) |
| if len(oc) > 0 { |
| primary = oc[0] |
| if len(oc) > 1 { |
| secondary = oc[1:] |
| } |
| } |
| |
| var buf bytes.Buffer |
| enc := json.NewEncoder(&buf) |
| if err := enc.Encode(&trooperJSON{ |
| Primary: primary, |
| Secondary: secondary, |
| UnixTS: updated.Unix(), |
| }); err != nil { |
| return "", err |
| } |
| return buf.String(), nil |
| case "current_trooper.txt": |
| if len(oc) == 0 { |
| return "None", nil |
| } |
| return strings.Join(oc, ","), nil |
| default: |
| 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_webgl_bug_triage.js": {"WebGL Bug Triage", ""}, |
| |
| "sheriff.json": {"Build Sheriff", ""}, |
| // "sheriff_webkit.json": "", // In the cron file but does not have a configuration. |
| // "sheriff_memory.json": "", // In the cron file but does not have a configuration. |
| "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", ""}, |
| "sheriff_webgl_bug_triage.json": {"WebGL Bug Triage", ""}, |
| //"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] |
| } |
| |
| cal := h.legacyCalendar |
| if cfg.Config.Enabled { |
| cal = h.calendar |
| } |
| |
| updated := clock.Now(ctx.Context) |
| events, err := cal.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": |
| // This makes the JSON encoder produce `[]` instead of `null` |
| // for empty lists. |
| 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") |
| } |
| } |
| |
| var rotaToName = map[string][2]string{ |
| "angle": {"The ANGLE Wrangle", ""}, |
| "ios_internal_roll": {"Bling Piper Roll", ""}, |
| "bling": {"Chrome iOS Build Sheriff", ""}, |
| "blink_bug_triage": {"Blink Bug Triage", ""}, |
| "blink_media_triage": {"Blink Media Bug Triage Rotation", ""}, |
| "chromeosgardener": {"Chrome on ChromeOS Gardening", ""}, |
| "chromeosgardener.shadow": {"Chrome on ChromeOS Gardening Shadow", ""}, |
| "chromeos.other": {"Chrome OS Build Sheriff - Other", "Chrome OS Build Sheriff"}, |
| "chromeos": {"Chrome OS Build Sheriff", ""}, |
| "chrome": {"Build Sheriff", ""}, |
| "android": {"Chrome on Android Build Sheriff", ""}, |
| "android_stability": {"Chrome on Android Stability Sheriff", "Clank Stability Sheriff (go/clankss)"}, |
| "codesearch": {"ChOps DevX Codesearch Triage Rotation", ""}, |
| "ecosystem_infra": {"Ecosystem Infra rotation", ""}, |
| "fizzlon_bugcop": {"Fizz London Bug Cop", ""}, |
| "gitadmin": {"Chrome Infra Git Admin Rotation", ""}, |
| "gpu": {"Chrome GPU Pixel Wrangling", ""}, |
| "headless_roll": {"Headless Chrome roll sheriff", ""}, |
| "infra_platform": {"Chops Foundation Triage", ""}, |
| "infra_triage": {"Chrome Infra Bug Triage Rotation", ""}, |
| "media_ux_triage": {"Chrome Media UX Bug Triage Rotation", ""}, |
| "monorail": {"Chrome Infra Monorail Triage Rotation", ""}, |
| "network": {"Chrome Network Bug Triage", ""}, |
| "perfbot": {"Chromium Perf Bot Sheriff Rotation", ""}, |
| "ios": {"Chrome iOS Build Sheriff", ""}, |
| "perf": {"Chromium Perf Regression Sheriff Rotation", ""}, |
| "sdk": {"ChOps DevX SDK Triage Rotation", ""}, |
| "sheriff-o-matic": {"Sheriff-o-Matic Bug Triage Rotation", ""}, |
| "stability": {"Chromium Stability Sheriff", ""}, |
| "v8_infra_triage": {"V8 Infra Bug Triage Rotation", ""}, |
| // "v8": {"V8 Sheriff", ""}, // Nothing in their calendar. |
| "webview_bugcop": {"WebView Bug Cop", ""}, |
| "flutter_engine": {"Flutter Engine Rotation", ""}, |
| //"troopers": {"", ""} // Handled in it's own way. |
| "webgl_bug_triage": {"WebGL Bug Triage", ""}, |
| } |
| |
| const ( |
| fullDay = 24 * time.Hour |
| timeDelta = 90 * fullDay |
| trooperRota = "troopers" |
| ) |
| |
| type allRotations struct { |
| Rotations []string `json:"rotations"` |
| Calendar []dayEntry `json:"calendar"` |
| } |
| |
| type dayEntry struct { |
| Date string `json:"date"` |
| Participants [][]string `json:"participants"` |
| } |
| |
| func (h *State) legacyAllRotations(ctx *router.Context, _ string) (string, error) { |
| start := clock.Now(ctx.Context).In(mtvTime) |
| end := start.Add(timeDelta) |
| cs := h.configStore(ctx.Context) |
| |
| var res allRotations |
| dateMap := make(map[string]map[string][]string) |
| |
| // The Sheriff rotations. |
| for k, v := range rotaToName { |
| rs, err := cs.RotaConfig(ctx.Context, v[0]) |
| if err != nil { |
| logging.Errorf(ctx.Context, "Getting configuration for: %q failed: %v", v, err) |
| continue |
| } |
| if len(rs) != 1 { |
| return "", status.Errorf(codes.Internal, "RotaConfig did not return 1 configuration") |
| } |
| cfg := rs[0] |
| if v[1] != "" { |
| cfg.Config.Name = v[1] |
| } |
| cal := h.legacyCalendar |
| if cfg.Config.Enabled { |
| cal = h.calendar |
| } |
| shifts, err := cal.Events(ctx, cfg, start, end) |
| if err != nil { |
| logging.Errorf(ctx.Context, "Fetching calendar events for: %q failed: %v", v[0], err) |
| continue |
| } |
| res.Rotations = append(res.Rotations, k) |
| //buildSheriffRotation(ctx.Context, dateMap, k, start, shifts) |
| buildLegacyRotation(dateMap, k, shifts) |
| } |
| // Troopers rotation. |
| ts, err := h.legacyCalendar.TrooperShifts(ctx, trooperCal, matchSummary, start, end) |
| if err != nil { |
| return "", err |
| } |
| buildLegacyRotation(dateMap, trooperRota, ts) |
| res.Rotations = append(res.Rotations, trooperRota) |
| |
| for k, v := range dateMap { |
| entry := dayEntry{ |
| Date: k, |
| } |
| for _, r := range res.Rotations { |
| p, ok := v[r] |
| // When JSON encoding slices creating a slice with. |
| // var bla []slice -> Produces `null` in the json output. |
| // If instead creating the slice with. |
| // make([]string,0) -> Will produce `[]`. |
| if !ok || len(p) == 0 { |
| p = make([]string, 0) |
| } |
| entry.Participants = append(entry.Participants, p) |
| } |
| res.Calendar = append(res.Calendar, entry) |
| } |
| |
| sort.Slice(res.Calendar, func(i, j int) bool { |
| return res.Calendar[i].Date < res.Calendar[j].Date |
| }) |
| |
| var buf bytes.Buffer |
| enc := json.NewEncoder(&buf) |
| if err := enc.Encode(res); err != nil { |
| return "", err |
| } |
| |
| return buf.String(), nil |
| } |
| |
| func buildLegacyRotation(dateMap map[string]map[string][]string, rota string, shifts []rotang.ShiftEntry) { |
| if len(shifts) < 1 { |
| return |
| } |
| for _, s := range shifts { |
| // Truncade to fulldays for go/chromecals |
| date := time.Date(s.StartTime.Year(), s.StartTime.Month(), s.StartTime.Day(), 0, 0, 0, 0, time.UTC) |
| oc := make([]string, 0) |
| for _, o := range s.OnCall { |
| oc = append(oc, strings.Split(o.Email, "@")[0]) |
| } |
| for ; ; date = date.Add(fullDay) { |
| s.StartTime = time.Date(s.StartTime.Year(), s.StartTime.Month(), s.StartTime.Day(), 0, 0, 0, 0, time.UTC) |
| s.EndTime = time.Date(s.EndTime.Year(), s.EndTime.Month(), s.EndTime.Day(), 0, 0, 0, 0, time.UTC) |
| if !((date.After(s.StartTime) || date.Equal(s.StartTime)) && date.Before(s.EndTime)) { |
| break |
| } |
| if _, ok := dateMap[date.Format(elementTimeFormat)]; !ok { |
| dateMap[date.Format(elementTimeFormat)] = make(map[string][]string) |
| } |
| dateMap[date.Format(elementTimeFormat)][rota] = append(dateMap[date.Format(elementTimeFormat)][rota], oc...) |
| } |
| } |
| } |