| OLD | NEW |
| (Empty) | |
| 1 // Copyright 2017 The LUCI Authors. All rights reserved. |
| 2 // Use of this source code is governed under the Apache License, Version 2.0 |
| 3 // that can be found in the LICENSE file. |
| 4 |
| 5 package buildbot |
| 6 |
| 7 import ( |
| 8 "fmt" |
| 9 "strconv" |
| 10 "strings" |
| 11 "unicode/utf8" |
| 12 |
| 13 "github.com/luci/luci-go/common/data/stringset" |
| 14 "github.com/luci/luci-go/common/logging" |
| 15 miloProto "github.com/luci/luci-go/common/proto/milo" |
| 16 "github.com/luci/luci-go/grpc/grpcutil" |
| 17 "github.com/luci/luci-go/logdog/client/coordinator" |
| 18 "github.com/luci/luci-go/logdog/common/types" |
| 19 "github.com/luci/luci-go/luci_config/common/cfgtypes" |
| 20 milo "github.com/luci/luci-go/milo/api/proto" |
| 21 "github.com/luci/luci-go/milo/appengine/logdog" |
| 22 |
| 23 "google.golang.org/grpc/codes" |
| 24 |
| 25 "golang.org/x/net/context" |
| 26 ) |
| 27 |
| 28 // BuildInfoProvider is a configuration that provides build information. |
| 29 // |
| 30 // In a production system, this will be completely defaults. For testing, the |
| 31 // various services and data sources may be substituted for testing stubs. |
| 32 type BuildInfoProvider struct { |
| 33 // LogdogClientFunc returns a coordinator Client instance for the suppli
ed |
| 34 // parameters. |
| 35 // |
| 36 // If nil, a production client will be generated. |
| 37 LogdogClientFunc func(c context.Context) (*coordinator.Client, error) |
| 38 } |
| 39 |
| 40 func (p *BuildInfoProvider) newLogdogClient(c context.Context) (*coordinator.Cli
ent, error) { |
| 41 if p.LogdogClientFunc != nil { |
| 42 return p.LogdogClientFunc(c) |
| 43 } |
| 44 return logdog.NewClient(c, "") |
| 45 } |
| 46 |
| 47 // GetBuildInfo resolves a Milo protobuf Step for a given BuildBot build. |
| 48 // |
| 49 // On failure, it returns a (potentially-wrapped) gRPC error. |
| 50 // |
| 51 // This: |
| 52 // |
| 53 // 1) Fetches the BuildBot build JSON from datastore. |
| 54 // 2) Resolves the LogDog annotation stream path from the BuildBot state. |
| 55 // 3) Fetches the LogDog annotation stream and resolves it into a Step. |
| 56 // 4) Merges some operational BuildBot build information into the Step. |
| 57 func (p *BuildInfoProvider) GetBuildInfo(c context.Context, req *milo.BuildInfoR
equest_BuildBot, |
| 58 projectHint cfgtypes.ProjectName) (*milo.BuildInfoResponse, error) { |
| 59 |
| 60 logging.Infof(c, "Loading build info for master %q, builder %q, build #%
d", |
| 61 req.MasterName, req.BuilderName, req.BuildNumber) |
| 62 |
| 63 // Load the BuildBot build from datastore. |
| 64 build, err := getBuild(c, req.MasterName, req.BuilderName, int(req.Build
Number)) |
| 65 if err != nil { |
| 66 if err == errBuildNotFound { |
| 67 return nil, grpcutil.Errf(codes.NotFound, "Build #%d for
master %q, builder %q was not found", |
| 68 req.BuildNumber, req.MasterName, req.BuilderName
) |
| 69 } |
| 70 |
| 71 logging.WithError(err).Errorf(c, "Failed to load build info.") |
| 72 return nil, grpcutil.Internal |
| 73 } |
| 74 |
| 75 // Create a new LogDog client. |
| 76 client, err := p.newLogdogClient(c) |
| 77 if err != nil { |
| 78 logging.WithError(err).Errorf(c, "Failed to create LogDog client
.") |
| 79 return nil, grpcutil.Internal |
| 80 } |
| 81 |
| 82 // Identify the LogDog annotation stream from the build. |
| 83 // |
| 84 // This will return a gRPC error on failure. |
| 85 as, err := getLogDogAnnotations(c, client, build, projectHint) |
| 86 if err != nil { |
| 87 return nil, err |
| 88 } |
| 89 logging.Infof(c, "Resolved annotation stream: %s / %s", as.Project, as.P
ath) |
| 90 |
| 91 // Load the annotation protobuf. |
| 92 as.Client = client |
| 93 if err := as.Normalize(); err != nil { |
| 94 logging.WithError(err).Errorf(c, "Failed to normalize annotation
stream.") |
| 95 return nil, grpcutil.Internal |
| 96 } |
| 97 |
| 98 step, err := as.Load(c) |
| 99 if err != nil { |
| 100 logging.WithError(err).Errorf(c, "Failed to load annotation stre
am.") |
| 101 return nil, grpcutil.Errf(codes.Internal, "failed to load LogDog
annotation stream from: %s", as.Path) |
| 102 } |
| 103 |
| 104 // Merge the information together. |
| 105 if err := mergeBuildIntoAnnotation(c, step, build); err != nil { |
| 106 logging.WithError(err).Errorf(c, "Failed to merge annotation wit
h build.") |
| 107 return nil, grpcutil.Errf(codes.Internal, "failed to merge annot
ation and build data") |
| 108 } |
| 109 |
| 110 prefix, name := as.Path.Split() |
| 111 return &milo.BuildInfoResponse{ |
| 112 Project: string(as.Project), |
| 113 Step: step, |
| 114 AnnotationStream: &miloProto.LogdogStream{ |
| 115 Server: client.Host, |
| 116 Prefix: string(prefix), |
| 117 Name: string(name), |
| 118 }, |
| 119 }, nil |
| 120 } |
| 121 |
| 122 // Resolve BuildBot and LogDog build information. We do this |
| 123 // |
| 124 // This returns an AnnotationStream instance with its project and path |
| 125 // populated. |
| 126 // |
| 127 // This function is messy and implementation-specific. That's the point of this |
| 128 // endpoint, though. All of the nastiness here should be replaced with something |
| 129 // more elegant once that becomes available. In the meantime... |
| 130 func getLogDogAnnotations(c context.Context, client *coordinator.Client, build *
buildbotBuild, |
| 131 projectHint cfgtypes.ProjectName) (*logdog.AnnotationStream, error) { |
| 132 |
| 133 // Modern builds will have this information in their build properties. |
| 134 var as logdog.AnnotationStream |
| 135 prefix, _ := build.getPropertyValue("logdog_prefix").(string) |
| 136 project, _ := build.getPropertyValue("logdog_project").(string) |
| 137 if prefix != "" && project != "" { |
| 138 // Construct the full annotation path. |
| 139 as.Project = cfgtypes.ProjectName(project) |
| 140 as.Path = types.StreamName(prefix).Join("annotations") |
| 141 |
| 142 logging.Debugf(c, "Resolved path/project from build properties."
) |
| 143 return &as, nil |
| 144 } |
| 145 |
| 146 // From here on out, we will need a project hint. |
| 147 if projectHint == "" { |
| 148 return nil, grpcutil.Errf(codes.NotFound, "annotation stream not
found") |
| 149 } |
| 150 as.Project = projectHint |
| 151 |
| 152 // Execute a LogDog service query to see if we can identify the stream. |
| 153 err := func() error { |
| 154 var annotationStream *coordinator.LogStream |
| 155 err := client.Query(c, as.Project, "", coordinator.QueryOptions{ |
| 156 Tags: map[string]string{ |
| 157 "buildbot.master": build.Master, |
| 158 "buildbot.builder": build.Buildername, |
| 159 "buildbot.buildnumber": strconv.Itoa(build.Numbe
r), |
| 160 }, |
| 161 ContentType: miloProto.ContentTypeAnnotations, |
| 162 }, func(ls *coordinator.LogStream) bool { |
| 163 // Only need the first (hopefully only?) result. |
| 164 annotationStream = ls |
| 165 return false |
| 166 }) |
| 167 if err != nil { |
| 168 logging.WithError(err).Errorf(c, "Failed to issue log st
ream query.") |
| 169 return grpcutil.Internal |
| 170 } |
| 171 |
| 172 if annotationStream != nil { |
| 173 as.Path = annotationStream.Path |
| 174 } |
| 175 return nil |
| 176 }() |
| 177 if err != nil { |
| 178 return nil, err |
| 179 } |
| 180 if as.Path != "" { |
| 181 logging.Debugf(c, "Resolved path/project via tag query.") |
| 182 return &as, nil |
| 183 } |
| 184 |
| 185 // Last-ditch effort: generate a prefix based on the build properties. T
his |
| 186 // re-implements the "_build_prefix" function in: |
| 187 // https://chromium.googlesource.com/chromium/tools/build/+/2d23e5284cc3
1f31c6bc07aa1d3fc5b1c454c3b4/scripts/slave/logdog_bootstrap.py#363 |
| 188 isAlnum := func(r rune) bool { |
| 189 return ((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >
= '0' && r <= '9')) |
| 190 } |
| 191 normalize := func(v string) string { |
| 192 v = strings.Map(func(r rune) rune { |
| 193 if isAlnum(r) { |
| 194 return r |
| 195 } |
| 196 switch r { |
| 197 case ':', '_', '-', '.': |
| 198 return r |
| 199 default: |
| 200 return '_' |
| 201 } |
| 202 }, v) |
| 203 if r, _ := utf8.DecodeRuneInString(v); r == utf8.RuneError || !i
sAlnum(r) { |
| 204 v = "s_" + v |
| 205 } |
| 206 return v |
| 207 } |
| 208 as.Path = types.StreamPath(fmt.Sprintf("bb/%s/%s/%s/+/annotations", |
| 209 normalize(build.Master), normalize(build.Buildername), normalize
(strconv.Itoa(build.Number)))) |
| 210 |
| 211 logging.Debugf(c, "Generated path/project algorithmically.") |
| 212 return &as, nil |
| 213 } |
| 214 |
| 215 // mergeBuildInfoIntoAnnotation merges BuildBot-specific build informtion into |
| 216 // a LogDog annotation protobuf. |
| 217 // |
| 218 // This consists of augmenting the Step's properties with BuildBot's properties, |
| 219 // favoring the Step's version of the properties if there are two with the same |
| 220 // name. |
| 221 func mergeBuildIntoAnnotation(c context.Context, step *miloProto.Step, build *bu
ildbotBuild) error { |
| 222 allProps := stringset.New(len(step.Property) + len(build.Properties)) |
| 223 for _, prop := range step.Property { |
| 224 allProps.Add(prop.Name) |
| 225 } |
| 226 for _, prop := range build.Properties { |
| 227 // Annotation protobuf overrides BuildBot properties. |
| 228 if allProps.Has(prop.Name) { |
| 229 continue |
| 230 } |
| 231 allProps.Add(prop.Name) |
| 232 |
| 233 step.Property = append(step.Property, &miloProto.Step_Property{ |
| 234 Name: prop.Name, |
| 235 Value: fmt.Sprintf("%v", prop.Value), |
| 236 }) |
| 237 } |
| 238 |
| 239 return nil |
| 240 } |
| OLD | NEW |