pkg/api/v1/state.go (181 lines of code) (raw):
/*
Copyright 2021 The TestGrid 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 v1
import (
"context"
"errors"
"fmt"
"math"
"net/http"
"github.com/go-chi/chi"
"github.com/golang/protobuf/ptypes/timestamp"
"github.com/sirupsen/logrus"
"github.com/GoogleCloudPlatform/testgrid/config"
apipb "github.com/GoogleCloudPlatform/testgrid/pb/api/v1"
statepb "github.com/GoogleCloudPlatform/testgrid/pb/state"
"github.com/GoogleCloudPlatform/testgrid/pkg/tabulator"
"github.com/GoogleCloudPlatform/testgrid/util/gcs"
)
// findDashboardTab locates dashboard tab in config, given a dashboard and tab name.
func findDashboardTab(cfg *cachedConfig, dashboardInput string, tabInput string) (string, string, string, error) {
if cfg == nil || cfg.Config == nil {
return "", "", "", errors.New("empty config")
}
dashboardKey := config.Normalize(dashboardInput)
tabKey := config.Normalize(tabInput)
dashboardName, ok := cfg.NormalDashboard[dashboardKey]
if !ok {
return "", "", "", fmt.Errorf("Dashboard {%q} not found", dashboardKey)
}
tabName, ok := cfg.NormalDashboardTab[dashboardKey][tabKey]
if !ok {
return dashboardName, "", "", fmt.Errorf("Tab {%q} not found", tabKey)
}
for _, tab := range cfg.Config.Dashboards[dashboardName].DashboardTab {
if tab.Name == tabName {
return dashboardName, tabName, tab.TestGroupName, nil
}
}
return dashboardName, tabName, "", fmt.Errorf("Test group not found")
}
// Grid fetch tab and grid info (columns, rows, ..etc)
func (s Server) Grid(ctx context.Context, scope string, dashboardName, tabName, testGroupNanme string) (*statepb.Grid, error) {
configPath, _, err := s.configPath(scope)
if err != nil {
return nil, err
}
path, err := tabulator.TabStatePath(*configPath, s.TabPathPrefix, dashboardName, tabName)
if err != nil {
return nil, fmt.Errorf("tab state path: %v", err)
}
grid, _, err := gcs.DownloadGrid(ctx, s.Client, *path)
return grid, err
}
// decodeRLE decodes the run length encoded data
//
// [0, 3, 5, 4] -> [0, 0, 0, 5, 5, 5, 5]
func decodeRLE(encodedData []int32) []int32 {
var decodedResult []int32
encodedDataLength := len(encodedData)
if encodedDataLength%2 == 0 {
for encodedDataIdx := 0; encodedDataIdx < encodedDataLength; encodedDataIdx += 2 {
for cellRepeatCount := encodedData[encodedDataIdx+1]; cellRepeatCount > 0; cellRepeatCount-- {
decodedResult = append(decodedResult, encodedData[encodedDataIdx])
}
}
}
return decodedResult
}
// ListHeaders returns dashboard tab headers
func (s *Server) ListHeaders(ctx context.Context, req *apipb.ListHeadersRequest) (*apipb.ListHeadersResponse, error) {
ctx, cancel := context.WithTimeout(ctx, s.Timeout)
defer cancel()
cfg, err := s.getConfig(ctx, logrus.WithContext(ctx), req.GetScope())
if err != nil {
return nil, err
}
cfg.Mutex.RLock()
defer cfg.Mutex.RUnlock()
dashboardName, tabName, testGroupName, err := findDashboardTab(cfg, req.GetDashboard(), req.GetTab())
if err != nil {
return nil, err
}
grid, err := s.Grid(ctx, req.GetScope(), dashboardName, tabName, testGroupName)
if err != nil {
return nil, fmt.Errorf("Dashboard {%q} or tab {%q} not found", req.GetDashboard(), req.GetTab())
}
if grid == nil {
return nil, errors.New("grid not found")
}
var dashboardTabResponse apipb.ListHeadersResponse
for _, gColumn := range grid.Columns {
// TODO(#683): Remove timestamp conversion math
millis := gColumn.Started
sec := millis / 1000
nanos := math.Mod(millis, 1000) * 1e6
column := apipb.ListHeadersResponse_Header{
Name: gColumn.Name,
Build: gColumn.Build,
Started: ×tamp.Timestamp{
Seconds: int64(sec),
Nanos: int32(nanos),
},
Extra: gColumn.Extra,
HotlistIds: gColumn.HotlistIds,
}
dashboardTabResponse.Headers = append(dashboardTabResponse.Headers, &column)
}
return &dashboardTabResponse, nil
}
// ListHeadersHTTP returns dashboard tab headers
// Response json: ListHeadersResponse
func (s Server) ListHeadersHTTP(w http.ResponseWriter, r *http.Request) {
req := apipb.ListHeadersRequest{
Scope: r.URL.Query().Get(scopeParam),
Dashboard: chi.URLParam(r, "dashboard"),
Tab: chi.URLParam(r, "tab"),
}
resp, err := s.ListHeaders(r.Context(), &req)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
s.writeJSON(w, resp)
}
// ListRows returns dashboard tab rows
func (s *Server) ListRows(ctx context.Context, req *apipb.ListRowsRequest) (*apipb.ListRowsResponse, error) {
ctx, cancel := context.WithTimeout(ctx, s.Timeout)
defer cancel()
// this should be factored out of this function
cfg, err := s.getConfig(ctx, logrus.WithContext(ctx), req.GetScope())
if err != nil {
return nil, err
}
cfg.Mutex.RLock()
defer cfg.Mutex.RUnlock()
dashboardName, tabName, testGroupName, err := findDashboardTab(cfg, req.GetDashboard(), req.GetTab())
if err != nil {
return nil, err
}
grid, err := s.Grid(ctx, req.GetScope(), dashboardName, tabName, testGroupName)
if err != nil {
return nil, fmt.Errorf("Dashboard {%q} or tab {%q} not found", req.GetDashboard(), req.GetTab())
}
if grid == nil {
return nil, errors.New("grid not found")
}
dashboardTabResponse := apipb.ListRowsResponse{
Rows: make([]*apipb.ListRowsResponse_Row, 0, len(grid.Rows)),
}
for _, gRow := range grid.Rows {
gRowDecodedResults := decodeRLE(gRow.Results)
cellsCount := len(gRowDecodedResults)
row := apipb.ListRowsResponse_Row{
Name: gRow.Name,
Issues: gRow.Issues,
Alert: gRow.AlertInfo,
Cells: make([]*apipb.ListRowsResponse_Cell, 0, cellsCount),
}
var filledIdx int
// loop through CellIds, Messages, Icons slices and build cell struct objects
for cellIdx := 0; cellIdx < cellsCount; cellIdx++ {
// Cell IDs, messages, and icons are only listed for non-blank cells.
cell := apipb.ListRowsResponse_Cell{
Result: gRowDecodedResults[cellIdx],
}
if gRowDecodedResults[cellIdx] != 0 {
// Cell IDs may be omitted for subrows.
if len(gRow.CellIds) != 0 {
cell.CellId = gRow.CellIds[filledIdx]
}
if len(gRow.Messages) != 0 {
cell.Message = gRow.Messages[filledIdx]
}
if len(gRow.Icons) != 0 {
cell.Icon = gRow.Icons[filledIdx]
}
filledIdx++
}
row.Cells = append(row.Cells, &cell)
}
dashboardTabResponse.Rows = append(dashboardTabResponse.Rows, &row)
}
return &dashboardTabResponse, nil
}
// ListRowsHTTP returns dashboard tab rows
// Response json: ListRowsResponse
func (s Server) ListRowsHTTP(w http.ResponseWriter, r *http.Request) {
req := apipb.ListRowsRequest{
Scope: r.URL.Query().Get(scopeParam),
Dashboard: chi.URLParam(r, "dashboard"),
Tab: chi.URLParam(r, "tab"),
}
resp, err := s.ListRows(r.Context(), &req)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
s.writeJSON(w, resp)
}