go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/internal/buildsource/rawpresentation/build.go (about) 1 // Copyright 2016 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package rawpresentation 16 17 import ( 18 "bytes" 19 "compress/zlib" 20 "context" 21 "fmt" 22 "io" 23 "time" 24 25 "google.golang.org/protobuf/proto" 26 "google.golang.org/protobuf/types/known/timestamppb" 27 28 bbpb "go.chromium.org/luci/buildbucket/proto" 29 "go.chromium.org/luci/common/clock" 30 "go.chromium.org/luci/common/errors" 31 log "go.chromium.org/luci/common/logging" 32 "go.chromium.org/luci/config" 33 "go.chromium.org/luci/grpc/grpcutil" 34 "go.chromium.org/luci/hardcoded/chromeinfra" 35 "go.chromium.org/luci/logdog/api/logpb" 36 "go.chromium.org/luci/logdog/client/coordinator" 37 "go.chromium.org/luci/logdog/common/types" 38 "go.chromium.org/luci/logdog/common/viewer" 39 "go.chromium.org/luci/luciexe" 40 annopb "go.chromium.org/luci/luciexe/legacy/annotee/proto" 41 "go.chromium.org/luci/milo/frontend/ui" 42 ) 43 44 const ( 45 // DefaultLogDogHost is the default LogDog host, if one isn't specified via 46 // query string. 47 DefaultLogDogHost = chromeinfra.LogDogHost 48 ) 49 50 // AnnotationStream represents a LogDog annotation protobuf stream. 51 type AnnotationStream struct { 52 Project string 53 Path types.StreamPath 54 55 // Client is the HTTP client to use for LogDog communication. 56 Client *coordinator.Client 57 58 // The cached Step object 59 step *annopb.Step 60 61 // Build is the build.proto, if this annotation stream is Build messages 62 // instead of Step messages. 63 build *bbpb.Build 64 65 finished bool 66 } 67 68 // normalize validates and normalizes the stream's parameters. 69 func (as *AnnotationStream) normalize() error { 70 if err := config.ValidateProjectName(as.Project); err != nil { 71 return errors.Annotate(err, "Invalid project name: %s", as.Project).Tag(grpcutil.InvalidArgumentTag).Err() 72 } 73 74 if err := as.Path.Validate(); err != nil { 75 return errors.Annotate(err, "Invalid log stream path %q", as.Path).Tag(grpcutil.InvalidArgumentTag).Err() 76 } 77 78 return nil 79 } 80 81 var errNotMilo = errors.New("Requested stream is not a recognized protobuf") 82 var errNotDatagram = errors.New("Requested stream is not a datagram stream") 83 var errNoEntries = errors.New("Log stream has no annotation entries") 84 85 // populateCache loads the annotation stream from LogDog and caches it on this 86 // AnnotationStream. 87 // 88 // If the stream does not exist, or is invalid, populateCache will return a Milo error. 89 func (as *AnnotationStream) populateCache(c context.Context) error { 90 // Cached? 91 if as.step != nil || as.build != nil { 92 return nil 93 } 94 95 // Load from LogDog directly. 96 log.Fields{ 97 "host": as.Client.Host, 98 "project": as.Project, 99 "path": as.Path, 100 }.Infof(c, "Making tail request to LogDog to populateCache annotation stream.") 101 102 var ( 103 state coordinator.LogStream 104 stream = as.Client.Stream(as.Project, as.Path) 105 ) 106 107 le, err := stream.Tail(c, coordinator.WithState(&state), coordinator.Complete()) 108 if err != nil { 109 log.WithError(err).Errorf(c, "Failed to load stream.") 110 return err 111 } 112 113 // Make sure that this is an annotation stream. 114 switch { 115 case state.Desc.StreamType != logpb.StreamType_DATAGRAM: 116 return errNotDatagram 117 118 case le == nil: 119 // No annotation stream data, so render a minimal page. 120 return errNoEntries 121 } 122 123 var toUnmarshal proto.Message 124 var compressed bool 125 var followup func() 126 127 switch state.Desc.ContentType { 128 case annopb.ContentTypeAnnotations: 129 var step annopb.Step 130 toUnmarshal = &step 131 followup = func() { 132 var latestEndedTime time.Time 133 for _, sub := range step.Substep { 134 switch t := sub.Substep.(type) { 135 case *annopb.Step_Substep_AnnotationStream: 136 // TODO(hinoka,dnj): Implement recursive / embedded substream fetching if 137 // specified. 138 log.Warningf(c, "Annotation stream links LogDog substream [%+v], not supported!", t.AnnotationStream) 139 140 case *annopb.Step_Substep_Step: 141 endedTime := t.Step.Ended.AsTime() 142 if t.Step.Ended != nil && endedTime.After(latestEndedTime) { 143 latestEndedTime = endedTime 144 } 145 } 146 } 147 if latestEndedTime.IsZero() { 148 // No substep had an ended time :( 149 latestEndedTime = step.Started.AsTime() 150 } 151 as.step = &step 152 } 153 154 case luciexe.BuildProtoZlibContentType: 155 var build bbpb.Build 156 toUnmarshal = &build 157 compressed = true 158 followup = func() { 159 as.build = &build 160 } 161 162 default: 163 return errNotMilo 164 } 165 166 // Get the last log entry in the stream. In reality, this will be index 0, 167 // since the "Tail" call should only return one log entry. 168 // 169 // Because we supplied the "Complete" flag to Tail and succeeded, this 170 // datagram will be complete even if its source datagram(s) are fragments. 171 dg := le.GetDatagram() 172 if dg == nil { 173 return errors.New("Datagram stream does not have datagram data") 174 } 175 176 data := dg.Data 177 if compressed { 178 z, err := zlib.NewReader(bytes.NewBuffer(dg.Data)) 179 if err != nil { 180 return errors.Annotate( 181 err, "Datagram is marked as compressed, but failed to open zlib stream", 182 ).Err() 183 } 184 185 if data, err = io.ReadAll(z); err != nil { 186 return errors.Annotate( 187 err, "Datagram is marked as compressed, but failed to decompress", 188 ).Err() 189 } 190 } 191 192 // Attempt to decode the protobuf. 193 if err := proto.Unmarshal(data, toUnmarshal); err != nil { 194 return err 195 } 196 followup() 197 198 as.finished = (state.State.TerminalIndex >= 0 && 199 le.StreamIndex == uint64(state.State.TerminalIndex)) 200 return nil 201 } 202 203 func (as *AnnotationStream) toMiloBuild(c context.Context) *ui.MiloBuildLegacy { 204 prefix, name := as.Path.Split() 205 206 // Prepare a Streams object with only one stream. 207 streams := Streams{ 208 MainStream: &Stream{ 209 Server: as.Client.Host, 210 Prefix: string(prefix), 211 Path: string(name), 212 IsDatagram: true, 213 Data: as.step, 214 Closed: as.finished, 215 }, 216 } 217 218 var ( 219 build ui.MiloBuildLegacy 220 ub = ViewerURLBuilder{ 221 Host: as.Client.Host, 222 Project: as.Project, 223 Prefix: prefix, 224 } 225 ) 226 AddLogDogToBuild(c, &ub, streams.MainStream.Data, &build) 227 return &build 228 } 229 230 // ViewerURLBuilder is a URL builder that constructs LogDog viewer URLs. 231 type ViewerURLBuilder struct { 232 Host string 233 Prefix types.StreamName 234 Project string 235 } 236 237 // NewURLBuilder creates a new URLBuilder that can generate links to LogDog 238 // pages given a LogDog StreamAddr. 239 func NewURLBuilder(addr *types.StreamAddr) *ViewerURLBuilder { 240 prefix, _ := addr.Path.Split() 241 return &ViewerURLBuilder{ 242 Host: addr.Host, 243 Prefix: prefix, 244 Project: addr.Project, 245 } 246 } 247 248 // BuildLink implements URLBuilder. 249 func (b *ViewerURLBuilder) BuildLink(l *annopb.AnnotationLink) *ui.Link { 250 switch t := l.Value.(type) { 251 case *annopb.AnnotationLink_LogdogStream: 252 ls := t.LogdogStream 253 254 server := ls.Server 255 if server == "" { 256 server = b.Host 257 } 258 259 prefix := types.StreamName(ls.Prefix) 260 if prefix == "" { 261 prefix = b.Prefix 262 } 263 264 u := viewer.GetURL(server, b.Project, prefix.Join(types.StreamName(ls.Name))) 265 link := ui.NewLink(l.Label, u, fmt.Sprintf("logdog link for %s", l.Label)) 266 if link.Label == "" { 267 link.Label = ls.Name 268 } 269 return link 270 271 case *annopb.AnnotationLink_Url: 272 link := ui.NewLink(l.Label, t.Url, fmt.Sprintf("step link for %s", l.Label)) 273 if link.Label == "" { 274 link.Label = "unnamed" 275 } 276 return link 277 278 default: 279 // Don't know how to render. 280 return nil 281 } 282 } 283 284 // GetBuild returns either a MiloBuildLegacy or a Build from a raw datagram 285 // stream. 286 // 287 // The type of return value is determined by the content type of the stream. 288 func GetBuild(c context.Context, host string, project string, path types.StreamPath) (*ui.MiloBuildLegacy, *ui.BuildPage, error) { 289 as := AnnotationStream{ 290 Project: project, 291 Path: path, 292 } 293 if err := as.normalize(); err != nil { 294 return nil, nil, err 295 } 296 297 // Setup our LogDog client. 298 var err error 299 if as.Client, err = NewClient(c, host); err != nil { 300 return nil, nil, errors.Annotate(err, "generating LogDog Client").Err() 301 } 302 303 // Load the Milo annotation protobuf from the annotation stream. 304 switch err := as.populateCache(c); errors.Unwrap(err) { 305 case nil, errNoEntries: 306 307 case coordinator.ErrNoSuchStream: 308 return nil, nil, grpcutil.NotFoundTag.Apply(err) 309 310 case coordinator.ErrNoAccess: 311 return nil, nil, grpcutil.PermissionDeniedTag.Apply(err) 312 313 case errNotMilo, errNotDatagram: 314 // The user requested a LogDog url that isn't a Milo annotation. 315 return nil, nil, grpcutil.InvalidArgumentTag.Apply(err) 316 317 default: 318 return nil, nil, errors.Annotate(err, "failed to load stream").Err() 319 } 320 321 if as.step != nil { 322 return as.toMiloBuild(c), nil, nil 323 } 324 now := timestamppb.New(clock.Now(c)) 325 return nil, &ui.BuildPage{Build: ui.Build{Build: as.build, Now: now}}, nil 326 } 327 328 // ReadAnnotations synchronously reads and decodes the latest Step information 329 // from the provided StreamAddr. 330 func ReadAnnotations(c context.Context, addr *types.StreamAddr) (*annopb.Step, error) { 331 log.Infof(c, "Loading build from LogDog stream at: %s", addr) 332 333 client, err := NewClient(c, addr.Host) 334 if err != nil { 335 return nil, errors.Annotate(err, "failed to create LogDog client").Err() 336 } 337 338 as := AnnotationStream{ 339 Client: client, 340 Project: addr.Project, 341 Path: addr.Path, 342 } 343 if err := as.normalize(); err != nil { 344 return nil, errors.Annotate(err, "failed to normalize annotation stream parameters").Err() 345 } 346 347 if err := as.populateCache(c); err != nil { 348 return nil, err 349 } 350 if as.step == nil { 351 return nil, errors.New("stream does not contain annopb.Step") 352 } 353 return as.step, nil 354 }