github.com/GoogleCloudPlatform/testgrid@v0.0.174/pkg/api/v1/state.go (about) 1 /* 2 Copyright 2021 The TestGrid Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package v1 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "math" 24 "net/http" 25 26 "github.com/go-chi/chi" 27 "github.com/golang/protobuf/ptypes/timestamp" 28 "github.com/sirupsen/logrus" 29 30 "github.com/GoogleCloudPlatform/testgrid/config" 31 apipb "github.com/GoogleCloudPlatform/testgrid/pb/api/v1" 32 statepb "github.com/GoogleCloudPlatform/testgrid/pb/state" 33 "github.com/GoogleCloudPlatform/testgrid/pkg/tabulator" 34 "github.com/GoogleCloudPlatform/testgrid/util/gcs" 35 ) 36 37 // findDashboardTab locates dashboard tab in config, given a dashboard and tab name. 38 func findDashboardTab(cfg *cachedConfig, dashboardInput string, tabInput string) (string, string, string, error) { 39 if cfg == nil || cfg.Config == nil { 40 return "", "", "", errors.New("empty config") 41 } 42 43 dashboardKey := config.Normalize(dashboardInput) 44 tabKey := config.Normalize(tabInput) 45 46 dashboardName, ok := cfg.NormalDashboard[dashboardKey] 47 if !ok { 48 return "", "", "", fmt.Errorf("Dashboard {%q} not found", dashboardKey) 49 } 50 tabName, ok := cfg.NormalDashboardTab[dashboardKey][tabKey] 51 if !ok { 52 return dashboardName, "", "", fmt.Errorf("Tab {%q} not found", tabKey) 53 } 54 55 for _, tab := range cfg.Config.Dashboards[dashboardName].DashboardTab { 56 if tab.Name == tabName { 57 return dashboardName, tabName, tab.TestGroupName, nil 58 } 59 } 60 61 return dashboardName, tabName, "", fmt.Errorf("Test group not found") 62 } 63 64 // Grid fetch tab and grid info (columns, rows, ..etc) 65 func (s Server) Grid(ctx context.Context, scope string, dashboardName, tabName, testGroupNanme string) (*statepb.Grid, error) { 66 configPath, _, err := s.configPath(scope) 67 if err != nil { 68 return nil, err 69 } 70 path, err := tabulator.TabStatePath(*configPath, s.TabPathPrefix, dashboardName, tabName) 71 if err != nil { 72 return nil, fmt.Errorf("tab state path: %v", err) 73 } 74 grid, _, err := gcs.DownloadGrid(ctx, s.Client, *path) 75 return grid, err 76 } 77 78 // decodeRLE decodes the run length encoded data 79 // 80 // [0, 3, 5, 4] -> [0, 0, 0, 5, 5, 5, 5] 81 func decodeRLE(encodedData []int32) []int32 { 82 var decodedResult []int32 83 encodedDataLength := len(encodedData) 84 if encodedDataLength%2 == 0 { 85 for encodedDataIdx := 0; encodedDataIdx < encodedDataLength; encodedDataIdx += 2 { 86 for cellRepeatCount := encodedData[encodedDataIdx+1]; cellRepeatCount > 0; cellRepeatCount-- { 87 decodedResult = append(decodedResult, encodedData[encodedDataIdx]) 88 } 89 } 90 } 91 return decodedResult 92 } 93 94 // ListHeaders returns dashboard tab headers 95 func (s *Server) ListHeaders(ctx context.Context, req *apipb.ListHeadersRequest) (*apipb.ListHeadersResponse, error) { 96 ctx, cancel := context.WithTimeout(ctx, s.Timeout) 97 defer cancel() 98 99 cfg, err := s.getConfig(ctx, logrus.WithContext(ctx), req.GetScope()) 100 if err != nil { 101 return nil, err 102 } 103 cfg.Mutex.RLock() 104 defer cfg.Mutex.RUnlock() 105 106 dashboardName, tabName, testGroupName, err := findDashboardTab(cfg, req.GetDashboard(), req.GetTab()) 107 if err != nil { 108 return nil, err 109 } 110 111 grid, err := s.Grid(ctx, req.GetScope(), dashboardName, tabName, testGroupName) 112 if err != nil { 113 return nil, fmt.Errorf("Dashboard {%q} or tab {%q} not found", req.GetDashboard(), req.GetTab()) 114 } 115 if grid == nil { 116 return nil, errors.New("grid not found") 117 } 118 119 var dashboardTabResponse apipb.ListHeadersResponse 120 for _, gColumn := range grid.Columns { 121 // TODO(#683): Remove timestamp conversion math 122 millis := gColumn.Started 123 sec := millis / 1000 124 nanos := math.Mod(millis, 1000) * 1e6 125 column := apipb.ListHeadersResponse_Header{ 126 Name: gColumn.Name, 127 Build: gColumn.Build, 128 Started: ×tamp.Timestamp{ 129 Seconds: int64(sec), 130 Nanos: int32(nanos), 131 }, 132 Extra: gColumn.Extra, 133 HotlistIds: gColumn.HotlistIds, 134 } 135 dashboardTabResponse.Headers = append(dashboardTabResponse.Headers, &column) 136 } 137 return &dashboardTabResponse, nil 138 } 139 140 // ListHeadersHTTP returns dashboard tab headers 141 // Response json: ListHeadersResponse 142 func (s Server) ListHeadersHTTP(w http.ResponseWriter, r *http.Request) { 143 req := apipb.ListHeadersRequest{ 144 Scope: r.URL.Query().Get(scopeParam), 145 Dashboard: chi.URLParam(r, "dashboard"), 146 Tab: chi.URLParam(r, "tab"), 147 } 148 resp, err := s.ListHeaders(r.Context(), &req) 149 if err != nil { 150 http.Error(w, err.Error(), http.StatusNotFound) 151 return 152 } 153 154 s.writeJSON(w, resp) 155 } 156 157 // ListRows returns dashboard tab rows 158 func (s *Server) ListRows(ctx context.Context, req *apipb.ListRowsRequest) (*apipb.ListRowsResponse, error) { 159 ctx, cancel := context.WithTimeout(ctx, s.Timeout) 160 defer cancel() 161 162 // this should be factored out of this function 163 cfg, err := s.getConfig(ctx, logrus.WithContext(ctx), req.GetScope()) 164 if err != nil { 165 return nil, err 166 } 167 cfg.Mutex.RLock() 168 defer cfg.Mutex.RUnlock() 169 170 dashboardName, tabName, testGroupName, err := findDashboardTab(cfg, req.GetDashboard(), req.GetTab()) 171 if err != nil { 172 return nil, err 173 } 174 175 grid, err := s.Grid(ctx, req.GetScope(), dashboardName, tabName, testGroupName) 176 if err != nil { 177 return nil, fmt.Errorf("Dashboard {%q} or tab {%q} not found", req.GetDashboard(), req.GetTab()) 178 } 179 if grid == nil { 180 return nil, errors.New("grid not found") 181 } 182 183 dashboardTabResponse := apipb.ListRowsResponse{ 184 Rows: make([]*apipb.ListRowsResponse_Row, 0, len(grid.Rows)), 185 } 186 for _, gRow := range grid.Rows { 187 gRowDecodedResults := decodeRLE(gRow.Results) 188 cellsCount := len(gRowDecodedResults) 189 row := apipb.ListRowsResponse_Row{ 190 Name: gRow.Name, 191 Issues: gRow.Issues, 192 Alert: gRow.AlertInfo, 193 Cells: make([]*apipb.ListRowsResponse_Cell, 0, cellsCount), 194 } 195 var filledIdx int 196 // loop through CellIds, Messages, Icons slices and build cell struct objects 197 for cellIdx := 0; cellIdx < cellsCount; cellIdx++ { 198 // Cell IDs, messages, and icons are only listed for non-blank cells. 199 cell := apipb.ListRowsResponse_Cell{ 200 Result: gRowDecodedResults[cellIdx], 201 } 202 if gRowDecodedResults[cellIdx] != 0 { 203 // Cell IDs may be omitted for subrows. 204 if len(gRow.CellIds) != 0 { 205 cell.CellId = gRow.CellIds[filledIdx] 206 } 207 if len(gRow.Messages) != 0 { 208 cell.Message = gRow.Messages[filledIdx] 209 } 210 if len(gRow.Icons) != 0 { 211 cell.Icon = gRow.Icons[filledIdx] 212 } 213 filledIdx++ 214 } 215 row.Cells = append(row.Cells, &cell) 216 } 217 dashboardTabResponse.Rows = append(dashboardTabResponse.Rows, &row) 218 } 219 return &dashboardTabResponse, nil 220 } 221 222 // ListRowsHTTP returns dashboard tab rows 223 // Response json: ListRowsResponse 224 func (s Server) ListRowsHTTP(w http.ResponseWriter, r *http.Request) { 225 req := apipb.ListRowsRequest{ 226 Scope: r.URL.Query().Get(scopeParam), 227 Dashboard: chi.URLParam(r, "dashboard"), 228 Tab: chi.URLParam(r, "tab"), 229 } 230 resp, err := s.ListRows(r.Context(), &req) 231 if err != nil { 232 http.Error(w, err.Error(), http.StatusNotFound) 233 return 234 } 235 236 s.writeJSON(w, resp) 237 }