go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/luciexe/host/spy.go (about)

     1  // Copyright 2019 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 host
    16  
    17  import (
    18  	"bytes"
    19  	"compress/zlib"
    20  	"context"
    21  	"fmt"
    22  	"os"
    23  	"path/filepath"
    24  	"strings"
    25  
    26  	"google.golang.org/protobuf/proto"
    27  
    28  	bbpb "go.chromium.org/luci/buildbucket/proto"
    29  	"go.chromium.org/luci/logdog/client/butler"
    30  	"go.chromium.org/luci/logdog/client/butlerlib/bootstrap"
    31  	"go.chromium.org/luci/logdog/client/butlerlib/streamclient"
    32  	"go.chromium.org/luci/logdog/common/types"
    33  	"go.chromium.org/luci/luciexe"
    34  	"go.chromium.org/luci/luciexe/host/buildmerge"
    35  )
    36  
    37  // spy represents an active Spy on a Butler.
    38  //
    39  // Its job is to interpret all of the build.proto streams within the Butler into
    40  // a single, merged, 'build.proto' stream on the Butler. All merged protos are
    41  // also delivered to the MergedBuildC channel, which the owner of this spy
    42  // MUST drain as quickly as possible.
    43  //
    44  // If a protocol violation occurs within the Butler run, the spy will mark the
    45  // merged build as INFRA_FAILURE status, and report the error in the build's
    46  // SummaryMarkdown.
    47  type spy struct {
    48  	// MergedBuildC is the channel which sends EVERY merged Build message which
    49  	// this spy produces
    50  	//
    51  	// MergedBuildC will close when the spy is done processing ALL data.
    52  	//
    53  	// The owner of the spy MUST drain this channel as quickly as possible, or
    54  	// it will block the merge build process.
    55  	MergedBuildC <-chan *bbpb.Build
    56  
    57  	// Wait on this channel for the spy to drain. Will only drain after calling
    58  	// Close() at least once.
    59  	DrainC <-chan struct{}
    60  
    61  	// Close makes the spy stop processing data, and will cause MergedBuildC to
    62  	// close.
    63  	//
    64  	// Safe to call more than once.
    65  	Close func()
    66  
    67  	// The namespace under which all user build.proto streams are expected.
    68  	UserNamespace types.StreamName
    69  }
    70  
    71  // spyOn installs a Build spy on the Butler.
    72  //
    73  // Monitors '$LOGDOG_NAMESPACE/u/build.proto' datagram stream for Build
    74  // messages, merges them according to the luciexe protocol, and exports the
    75  // merged Build messages to '$LOGDOG_NAMESPACE/build.proto' as well as
    76  // spy.MergedBuildC.
    77  //
    78  // The spy should be Close()'d once the caller is no longer interested in
    79  // receiving merged builds.
    80  //
    81  // Environment: Observes logdog environment variables to determine base values
    82  // for Build.Log.Url and Build.Log.ViewUrl. Accordingly, this relies on the
    83  // Butler's environment already having been exported.
    84  //
    85  // Side-effect: Opens "$LOGDOG_NAMESPACE/build.proto" datagram stream in Butler
    86  //
    87  //	to output merged Build messages.
    88  //
    89  // Side-effect: Exports LOGDOG_NAMESPACE="$LOGDOG_NAMESPACE/u" to the
    90  //
    91  //	environment.
    92  func spyOn(ctx context.Context, b *butler.Butler, base *bbpb.Build) (*spy, error) {
    93  	curNamespace := types.StreamName(os.Getenv(luciexe.LogdogNamespaceEnv))
    94  
    95  	ldClient := streamclient.NewLoopback(b, types.StreamName(curNamespace))
    96  
    97  	// curNamespace is "$LOGDOG_NAMESPACE"
    98  	// userNamespace is "$LOGDOG_NAMESPACE/u"
    99  	// userNamespaceSlash is "$LOGDOG_NAMESPACE/u/"
   100  	userNamespace := curNamespace.AsNamespace() + "u"
   101  	if err := os.Setenv(luciexe.LogdogNamespaceEnv, string(userNamespace)); err != nil {
   102  		panic(err)
   103  	}
   104  	builds, err := buildmerge.New(ctx, userNamespace, base, mkURLCalcFn())
   105  	if err != nil {
   106  		return nil, err
   107  	}
   108  
   109  	fwdChan := teeLogdog(ctx, builds.MergedBuildC, ldClient)
   110  
   111  	builds.Attach(b)
   112  	return &spy{
   113  		MergedBuildC:  fwdChan,
   114  		DrainC:        builds.DrainC,
   115  		Close:         builds.Close,
   116  		UserNamespace: types.StreamName(userNamespace).AsNamespace(),
   117  	}, nil
   118  }
   119  
   120  // teeLogdog tees Build messages to a new "build.proto" datagram stream on the
   121  // given logdog client.
   122  func teeLogdog(ctx context.Context, in <-chan *bbpb.Build, ldClient *streamclient.Client) <-chan *bbpb.Build {
   123  	out := make(chan *bbpb.Build)
   124  
   125  	dgStream, err := ldClient.NewDatagramStream(
   126  		ctx, luciexe.BuildProtoStreamSuffix,
   127  		streamclient.WithContentType(luciexe.BuildProtoZlibContentType))
   128  	if err != nil {
   129  		panic(err)
   130  	}
   131  
   132  	go func() {
   133  		defer close(out)
   134  		defer func() {
   135  			if err := dgStream.Close(); err != nil {
   136  				panic(err)
   137  			}
   138  		}()
   139  
   140  		// keep buf and z between rounds; this means we should be able to "learn"
   141  		// how to compress build.proto's between rounds, too, since zlib.Reset()
   142  		// keeps the compressor dictionary.
   143  		buf := bytes.Buffer{}
   144  		z := zlib.NewWriter(&buf)
   145  		done := make(chan struct{})
   146  
   147  		for build := range in {
   148  			go func() {
   149  				defer func() {
   150  					done <- struct{}{}
   151  				}()
   152  				out <- build
   153  			}()
   154  
   155  			buildData, err := proto.Marshal(build)
   156  			if err != nil {
   157  				panic(err)
   158  			}
   159  
   160  			buf.Reset()
   161  			z.Reset(&buf)
   162  			if _, err := z.Write(buildData); err != nil {
   163  				panic(err)
   164  			}
   165  			if err := z.Close(); err != nil {
   166  				panic(err)
   167  			}
   168  			if err := dgStream.WriteDatagram(buf.Bytes()); err != nil {
   169  				panic(err)
   170  			}
   171  
   172  			<-done
   173  		}
   174  	}()
   175  
   176  	return out
   177  }
   178  
   179  func mkURLCalcFn() buildmerge.CalcURLFn {
   180  	// TODO(iannucci): This sort of coupling with the environment variables and
   181  	// their interpretation is pretty bad. This should be fixed so that URL
   182  	// generation is an RPC to Butler instead of string assembly by the user.
   183  	host := os.Getenv(bootstrap.EnvCoordinatorHost)
   184  
   185  	if strings.HasPrefix(host, "file://") {
   186  		hostSlash := host
   187  		if !strings.HasSuffix(hostSlash, "/") {
   188  			hostSlash += "/"
   189  		}
   190  
   191  		viewURLPrefix := filepath.FromSlash(hostSlash)
   192  
   193  		return func(ns, streamName types.StreamName) (url string, viewURL string) {
   194  			fullStreamName := string(ns + streamName)
   195  			url = hostSlash + filepath.FromSlash(fullStreamName)
   196  			// TODO(iannucci): actually implement strict types.StreamName -> (url,
   197  			// filesystem) mapping. Currently ':' is a permitted character, which is
   198  			// not legal on Windows file systems. Fortunately stream names must begin
   199  			// with an alnum character, so "." and ".." are illegal stream names.
   200  			viewURL = viewURLPrefix + filepath.ToSlash(fullStreamName)
   201  			return
   202  		}
   203  	}
   204  
   205  	project := os.Getenv(bootstrap.EnvStreamProject)
   206  	prefix := os.Getenv(bootstrap.EnvStreamPrefix)
   207  
   208  	urlPrefix := fmt.Sprintf("logdog://%s/%s/%s/+/", host, project, prefix)
   209  	viewURLPrefix := fmt.Sprintf("https://%s/logs/%s/%s/+/", host, project, prefix)
   210  
   211  	return func(ns, streamName types.StreamName) (url string, viewURL string) {
   212  		fullStreamName := string(ns + streamName)
   213  		url = urlPrefix + fullStreamName
   214  		viewURL = viewURLPrefix + fullStreamName
   215  		return
   216  	}
   217  }