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  }