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 }