github.com/GoogleCloudPlatform/testgrid@v0.0.174/util/gcs/read.go (about) 1 /* 2 Copyright 2019 The Kubernetes 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 gcs 18 19 import ( 20 "context" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "net/url" 25 "path" 26 "regexp" 27 "strconv" 28 "strings" 29 30 "cloud.google.com/go/storage" 31 "github.com/fvbommel/sortorder" 32 "google.golang.org/api/iterator" 33 core "k8s.io/api/core/v1" 34 35 "github.com/GoogleCloudPlatform/testgrid/metadata" 36 "github.com/GoogleCloudPlatform/testgrid/metadata/junit" 37 ) 38 39 // PodInfo holds podinfo.json (data about the pod). 40 type PodInfo struct { 41 Pod *core.Pod `json:"pod,omitempty"` 42 // ignore unused events 43 } 44 45 const ( 46 // MissingPodInfo appears when builds complete without a podinfo.json report. 47 MissingPodInfo = "podinfo.json not found in job artifacts (has not uploaded, or Prow's GCS reporter is not enabled)." 48 // NoPodUtils appears when builds run without decoration. 49 NoPodUtils = "Decoration is not enabled; set `decorate: true` on Prowjob." 50 ) 51 52 func truncate(s string, max int) string { 53 if max <= 0 { 54 return s 55 } 56 l := len(s) 57 if l < max { 58 return s 59 } 60 h := max / 2 61 return s[:h] + "..." + s[l-h:] 62 } 63 64 func checkContainerStatus(status core.ContainerStatus) (bool, string) { 65 name := status.Name 66 if status.State.Waiting != nil { 67 return false, fmt.Sprintf("%s still waiting: %s", name, status.State.Waiting.Message) 68 } 69 if status.State.Running != nil { 70 return false, fmt.Sprintf("%s still running", name) 71 } 72 if status.State.Terminated != nil && status.State.Terminated.ExitCode != 0 { 73 return false, fmt.Sprintf("%s exited %d: %s", name, status.State.Terminated.ExitCode, truncate(status.State.Terminated.Message, 140)) 74 } 75 return true, "" 76 } 77 78 // Summarize returns if the pod completed successfully and a diagnostic message. 79 func (pi PodInfo) Summarize() (bool, string) { 80 if pi.Pod == nil { 81 return false, MissingPodInfo 82 } 83 84 if pi.Pod.Status.Phase == core.PodSucceeded { 85 return true, "" 86 } 87 88 conditions := make(map[core.PodConditionType]core.PodCondition, len(pi.Pod.Status.Conditions)) 89 90 for _, cond := range pi.Pod.Status.Conditions { 91 conditions[cond.Type] = cond 92 } 93 94 if cond, ok := conditions[core.PodScheduled]; ok && cond.Status != core.ConditionTrue { 95 return false, fmt.Sprintf("pod did not schedule: %s", cond.Message) 96 } 97 98 if cond, ok := conditions[core.PodInitialized]; ok && cond.Status != core.ConditionTrue { 99 return false, fmt.Sprintf("pod could not initialize: %s", cond.Message) 100 } 101 102 for _, status := range pi.Pod.Status.InitContainerStatuses { 103 if pass, msg := checkContainerStatus(status); !pass { 104 return pass, fmt.Sprintf("init container %s", msg) 105 } 106 } 107 108 var foundSidecar bool 109 for _, status := range pi.Pod.Status.ContainerStatuses { 110 if status.Name == "sidecar" { 111 foundSidecar = true 112 } 113 pass, msg := checkContainerStatus(status) 114 if pass { 115 continue 116 } 117 if status.Name == "sidecar" { 118 return pass, msg 119 } 120 if status.State.Terminated == nil { 121 return pass, msg 122 } 123 } 124 125 if !foundSidecar { 126 return true, NoPodUtils 127 } 128 return true, "" 129 } 130 131 // Started holds started.json data. 132 type Started struct { 133 metadata.Started 134 // Pending when the job has not started yet 135 Pending bool 136 } 137 138 // Finished holds finished.json data. 139 type Finished struct { 140 metadata.Finished 141 // Running when the job hasn't finished and finished.json doesn't exist 142 Running bool 143 } 144 145 // Build points to a build stored under a particular gcs prefix. 146 type Build struct { 147 Path Path 148 baseName string 149 } 150 151 func (build Build) object() string { 152 o := build.Path.Object() 153 if strings.HasSuffix(o, "/") { 154 return o[0 : len(o)-1] 155 } 156 return o 157 } 158 159 // Build is the unique invocation id of the job. 160 func (build Build) Build() string { 161 return path.Base(build.object()) 162 } 163 164 // Job is the name of the job for this build 165 func (build Build) Job() string { 166 return path.Base(path.Dir(build.object())) 167 } 168 169 func (build Build) String() string { 170 return build.Path.String() 171 } 172 173 func readLink(objAttrs *storage.ObjectAttrs) string { 174 if link, ok := objAttrs.Metadata["x-goog-meta-link"]; ok { 175 return link 176 } 177 if link, ok := objAttrs.Metadata["link"]; ok { 178 return link 179 } 180 return "" 181 } 182 183 // hackOffset handles tot's sequential names, which GCS handles poorly 184 // AKA asking GCS to return results after 6 will never find 10 185 // So we always have to list everything for these types of numbers. 186 func hackOffset(offset *string) string { 187 if *offset == "" { 188 return "" 189 } 190 if strings.HasSuffix(*offset, "/") { 191 *offset = (*offset)[:len(*offset)-1] 192 } 193 dir, offsetBaseName := path.Split(*offset) 194 const first = 1000000000000000000 195 if n, err := strconv.Atoi(offsetBaseName); err == nil && n < first { 196 *offset = path.Join(dir, "0") 197 } 198 return offsetBaseName 199 } 200 201 // ListBuilds returns the array of builds under path, sorted in monotonically decreasing order. 202 func ListBuilds(parent context.Context, lister Lister, gcsPath Path, after *Path) ([]Build, error) { 203 ctx, cancel := context.WithCancel(parent) 204 defer cancel() 205 var offset string 206 if after != nil { 207 offset = after.Object() 208 } 209 offsetBaseName := hackOffset(&offset) 210 if !strings.HasSuffix(offset, "/") { 211 offset += "/" 212 } 213 it := lister.Objects(ctx, gcsPath, "/", offset) 214 var all []Build 215 for { 216 objAttrs, err := it.Next() 217 if errors.Is(err, iterator.Done) { 218 break 219 } 220 if err != nil { 221 return nil, fmt.Errorf("list objects: %w", err) 222 } 223 224 // if this is a link under directory/, resolve the build value 225 // This is used for PR type jobs which we store in a PR specific prefix. 226 // The directory prefix contains a link header to the result 227 // under the PR specific prefix. 228 if link := readLink(objAttrs); link != "" { 229 // links created by bootstrap.py have a space 230 link = strings.TrimSpace(link) 231 u, err := url.Parse(link) 232 if err != nil { 233 return nil, fmt.Errorf("parse %s link: %v", objAttrs.Name, err) 234 } 235 if !strings.HasSuffix(u.Path, "/") { 236 u.Path += "/" 237 } 238 var linkPath Path 239 if err := linkPath.SetURL(u); err != nil { 240 return nil, fmt.Errorf("bad %s link path %s: %w", objAttrs.Name, u, err) 241 } 242 all = append(all, Build{ 243 Path: linkPath, 244 baseName: path.Base(objAttrs.Name), 245 }) 246 continue 247 } 248 249 if objAttrs.Prefix == "" { 250 continue // not a symlink to a directory 251 } 252 253 loc := "gs://" + gcsPath.Bucket() + "/" + objAttrs.Prefix 254 gcsPath, err := NewPath(loc) 255 if err != nil { 256 return nil, fmt.Errorf("bad path %q: %w", loc, err) 257 } 258 259 all = append(all, Build{ 260 Path: *gcsPath, 261 baseName: path.Base(objAttrs.Prefix), 262 }) 263 } 264 265 Sort(all) 266 267 if offsetBaseName != "" { 268 // GCS will return 200 2000 30 for a prefix of 100 269 // testgrid expects this as 2000 200 (dropping 30) 270 for i, b := range all { 271 if sortorder.NaturalLess(b.baseName, offsetBaseName) || b.baseName == offsetBaseName { 272 return all[:i], nil // b <= offsetBaseName, so skip this one 273 } 274 } 275 } 276 return all, nil 277 } 278 279 // junit_CONTEXT_TIMESTAMP_THREAD.xml 280 var re = regexp.MustCompile(`.+/(?:junit((_[^_]+)?(_\d+-\d+)?(_\d+)?|.+)?\.xml|test.xml)$`) 281 282 // dropPrefix removes the _ in _CONTEXT to help keep the regexp simple 283 func dropPrefix(name string) string { 284 if len(name) == 0 { 285 return name 286 } 287 return name[1:] 288 } 289 290 // parseSuitesMeta returns the metadata for this junit file (nil for a non-junit file). 291 // 292 // Expected format: junit_context_20180102-1256_07.xml 293 // Results in { 294 // "Context": "context", 295 // "Timestamp": "20180102-1256", 296 // "Thread": "07", 297 // } 298 func parseSuitesMeta(name string) map[string]string { 299 mat := re.FindStringSubmatch(name) 300 if mat == nil { 301 return nil 302 } 303 c, ti, th := dropPrefix(mat[2]), dropPrefix(mat[3]), dropPrefix(mat[4]) 304 if c == "" && ti == "" && th == "" { 305 c = mat[1] 306 } 307 return map[string]string{ 308 "Context": c, 309 "Timestamp": ti, 310 "Thread": th, 311 } 312 313 } 314 315 // readJSON will decode the json object stored in GCS. 316 func readJSON(ctx context.Context, opener Opener, p Path, i interface{}) error { 317 reader, _, err := opener.Open(ctx, p) 318 if errors.Is(err, storage.ErrObjectNotExist) { 319 return err 320 } 321 if err != nil { 322 return fmt.Errorf("open: %w", err) 323 } 324 defer reader.Close() 325 if err = json.NewDecoder(reader).Decode(i); err != nil { 326 return fmt.Errorf("decode: %w", err) 327 } 328 if err := reader.Close(); err != nil { 329 return fmt.Errorf("close: %w", err) 330 } 331 return nil 332 } 333 334 // PodInfo parses the build's pod state. 335 func (build Build) PodInfo(ctx context.Context, opener Opener) (*PodInfo, error) { 336 path, err := build.Path.ResolveReference(&url.URL{Path: "podinfo.json"}) 337 if err != nil { 338 return nil, fmt.Errorf("resolve: %w", err) 339 } 340 var podInfo PodInfo 341 err = readJSON(ctx, opener, *path, &podInfo) 342 if errors.Is(err, storage.ErrObjectNotExist) { 343 return nil, nil 344 } 345 if err != nil { 346 return nil, fmt.Errorf("read: %w", err) 347 } 348 return &podInfo, nil 349 } 350 351 // Started parses the build's started metadata. 352 func (build Build) Started(ctx context.Context, opener Opener) (*Started, error) { 353 path, err := build.Path.ResolveReference(&url.URL{Path: "started.json"}) 354 if err != nil { 355 return nil, fmt.Errorf("resolve: %w", err) 356 } 357 var started Started 358 err = readJSON(ctx, opener, *path, &started) 359 if errors.Is(err, storage.ErrObjectNotExist) { 360 started.Pending = true 361 return &started, nil 362 } 363 if err != nil { 364 return nil, fmt.Errorf("read: %w", err) 365 } 366 return &started, nil 367 } 368 369 // Finished parses the build's finished metadata. 370 func (build Build) Finished(ctx context.Context, opener Opener) (*Finished, error) { 371 path, err := build.Path.ResolveReference(&url.URL{Path: "finished.json"}) 372 if err != nil { 373 return nil, fmt.Errorf("resolve: %w", err) 374 } 375 var finished Finished 376 err = readJSON(ctx, opener, *path, &finished) 377 if errors.Is(err, storage.ErrObjectNotExist) { 378 finished.Running = true 379 return &finished, nil 380 } 381 if err != nil { 382 return nil, fmt.Errorf("read: %w", err) 383 } 384 return &finished, nil 385 } 386 387 // Artifacts writes the object name of all paths under the build's artifact dir to the output channel. 388 func (build Build) Artifacts(ctx context.Context, lister Lister, artifacts chan<- string) error { 389 objs := lister.Objects(ctx, build.Path, "", "") // no delim or offset so we get all objects. 390 for { 391 obj, err := objs.Next() 392 if err == iterator.Done { 393 break 394 } 395 if err != nil { 396 return fmt.Errorf("list %s: %w", build.Path, err) 397 } 398 select { 399 case <-ctx.Done(): 400 return ctx.Err() 401 case artifacts <- obj.Name: 402 } 403 } 404 return nil 405 } 406 407 // SuitesMeta holds testsuites xml and metadata from the filename 408 type SuitesMeta struct { 409 Suites *junit.Suites // suites data extracted from file contents 410 Metadata map[string]string // metadata extracted from path name 411 Path string 412 Err error 413 } 414 415 const ( 416 maxSize int64 = 100e6 // 100 million, coarce to int not float 417 ) 418 419 func readSuites(ctx context.Context, opener Opener, p Path) (*junit.Suites, error) { 420 r, attrs, err := opener.Open(ctx, p) 421 if err != nil { 422 return nil, fmt.Errorf("open: %w", err) 423 } 424 defer r.Close() 425 if attrs != nil && attrs.Size > maxSize { 426 return nil, fmt.Errorf("too large: %d bytes > %d bytes max", attrs.Size, maxSize) 427 } 428 suitesMeta, err := junit.ParseStream(r) 429 if err != nil { 430 return nil, fmt.Errorf("parse: %w", err) 431 } 432 return suitesMeta, nil 433 } 434 435 // Suites takes a channel of artifact names, parses those representing junit suites, sending the result to the suites channel. 436 // 437 // Truncates xml results when set to a positive number of max bytes. 438 func (build Build) Suites(ctx context.Context, opener Opener, artifacts <-chan string, suites chan<- SuitesMeta, max int) error { 439 for { 440 var art string 441 var more bool 442 select { 443 case <-ctx.Done(): 444 return ctx.Err() 445 case art, more = <-artifacts: 446 if !more { 447 return nil 448 } 449 } 450 meta := parseSuitesMeta(art) 451 if meta == nil { 452 continue // not a junit file ignore it, ignore it 453 } 454 if art != "" && art[0] != '/' { 455 art = "/" + art 456 } 457 path, err := build.Path.ResolveReference(&url.URL{Path: art}) 458 if err != nil { 459 return fmt.Errorf("resolve %q: %v", art, err) 460 } 461 out := SuitesMeta{ 462 Metadata: meta, 463 Path: path.String(), 464 } 465 out.Suites, err = readSuites(ctx, opener, *path) 466 if err != nil { 467 out.Err = err 468 } else { 469 out.Suites.Truncate(max) 470 } 471 select { 472 case <-ctx.Done(): 473 return ctx.Err() 474 case suites <- out: 475 } 476 } 477 }