Chromium Code Reviews| 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. | |
|
hinoka
2017/02/03 02:16:44
Add to comment:
1. Fetches the buildbot build json
dnj
2017/02/03 23:54:04
Done.
| |
| 48 // | |
| 49 // On failure, it returns a (potentially-wrapped) gRPC error. | |
| 50 func (p *BuildInfoProvider) GetBuildInfo(c context.Context, req *milo.BuildInfoR equest_BuildBot, | |
| 51 projectHint cfgtypes.ProjectName) (*milo.BuildInfoResponse, error) { | |
| 52 | |
| 53 logging.Infof(c, "Loading build info for master %q, builder %q, build #% d", | |
| 54 req.MasterName, req.BuilderName, req.BuildNumber) | |
| 55 | |
| 56 // Load the BuildBot build from datastore. | |
| 57 build, err := getBuild(c, req.MasterName, req.BuilderName, int(req.Build Number)) | |
| 58 if err != nil { | |
| 59 if err == errBuildNotFound { | |
| 60 return nil, grpcutil.Errf(codes.NotFound, "Build #%d for master %q, builder %q was not found", | |
| 61 req.BuildNumber, req.MasterName, req.BuilderName ) | |
| 62 } | |
| 63 | |
| 64 logging.WithError(err).Errorf(c, "Failed to load build info.") | |
| 65 return nil, grpcutil.Internal | |
| 66 } | |
| 67 | |
| 68 // Create a new LogDog client. | |
| 69 client, err := p.newLogdogClient(c) | |
| 70 if err != nil { | |
| 71 logging.WithError(err).Errorf(c, "Failed to create LogDog client .") | |
| 72 return nil, grpcutil.Internal | |
| 73 } | |
| 74 | |
| 75 // Identify the LogDog annotation stream from the build. | |
| 76 // | |
| 77 // This will return a gRPC error on failure. | |
| 78 as, err := getLogDogAnnotations(c, client, build, projectHint) | |
| 79 if err != nil { | |
| 80 return nil, err | |
|
hinoka
2017/02/03 02:16:44
We should resolve 404s and give a better error mes
dnj
2017/02/03 23:53:38
This here will error if, given a build, we can't f
| |
| 81 } | |
| 82 logging.Infof(c, "Resolved annotation stream: %s / %s", as.Project, as.P ath) | |
| 83 | |
| 84 // Load the annotation protobuf. | |
| 85 as.Client = client | |
| 86 if err := as.Normalize(); err != nil { | |
| 87 logging.WithError(err).Errorf(c, "Failed to normalize annotation stream.") | |
| 88 return nil, grpcutil.Internal | |
| 89 } | |
| 90 if err := as.Load(c); err != nil { | |
| 91 logging.WithError(err).Errorf(c, "Failed to load annotation stre am.") | |
| 92 return nil, grpcutil.Internal | |
| 93 } | |
| 94 | |
| 95 // Merge the information together. | |
| 96 step := as.Step() | |
| 97 if err := mergeBuildIntoAnnotation(c, step, build); err != nil { | |
| 98 logging.WithError(err).Errorf(c, "Failed to merge annotation wit h build.") | |
| 99 return nil, grpcutil.Internal | |
| 100 } | |
| 101 | |
| 102 prefix, name := as.Path.Split() | |
| 103 return &milo.BuildInfoResponse{ | |
| 104 Project: string(as.Project), | |
| 105 Step: step, | |
| 106 AnnotationStream: &miloProto.LogdogStream{ | |
| 107 Server: client.Host, | |
| 108 Prefix: string(prefix), | |
| 109 Name: string(name), | |
| 110 }, | |
| 111 }, nil | |
| 112 } | |
| 113 | |
| 114 // Resolve BuildBot and LogDog build information. We do this | |
| 115 // | |
| 116 // This returns an AnnotationStream instance with its project and path | |
| 117 // populated. | |
| 118 // | |
| 119 // This function is messy and implementation-specific. That's the point of this | |
| 120 // endpoint, though. All of the nastiness here should be replaced with something | |
| 121 // more elegant once that becomes available. In the meantime... | |
| 122 func getLogDogAnnotations(c context.Context, client *coordinator.Client, build * buildbotBuild, projectHint cfgtypes.ProjectName) ( | |
|
hinoka
2017/02/03 02:16:44
Try to fit this in under 100char
dnj
2017/02/03 23:53:38
Done.
| |
| 123 *logdog.AnnotationStream, error) { | |
| 124 | |
| 125 // Modern builds will have this information in their build properties. | |
| 126 var as logdog.AnnotationStream | |
| 127 prefix, _ := build.getPropertyValue("logdog_prefix").(string) | |
| 128 project, _ := build.getPropertyValue("logdog_project").(string) | |
| 129 if prefix != "" && project != "" { | |
| 130 // Construct the full annotation path. | |
| 131 as.Project = cfgtypes.ProjectName(project) | |
| 132 as.Path = types.StreamName(prefix).Join("annotations") | |
| 133 | |
| 134 logging.Debugf(c, "Resolved path/project from build properties." ) | |
| 135 return &as, nil | |
| 136 } | |
| 137 | |
| 138 // From here on out, we will need a project hint. | |
| 139 if projectHint == "" { | |
|
hinoka
2017/02/03 02:16:44
I thought this was optional, will buildbot fetches
dnj
2017/02/03 23:53:38
It is optional, but some resolution paths require
| |
| 140 return nil, grpcutil.Errf(codes.NotFound, "annotation stream not found") | |
| 141 } | |
| 142 as.Project = projectHint | |
| 143 | |
| 144 // Execute a LogDog service query to see if we can identify the stream. | |
| 145 err := func() error { | |
| 146 var annotationStream *coordinator.LogStream | |
| 147 err := client.Query(c, as.Project, "", coordinator.QueryOptions{ | |
| 148 Tags: map[string]string{ | |
| 149 "buildbot.master": build.Master, | |
| 150 "buildbot.builder": build.Buildername, | |
| 151 "buildbot.buildnumber": strconv.Itoa(build.Numbe r), | |
| 152 }, | |
| 153 ContentType: miloProto.ContentTypeAnnotations, | |
| 154 }, func(ls *coordinator.LogStream) bool { | |
| 155 // Only need the first (hopefully only?) result. | |
| 156 annotationStream = ls | |
| 157 return false | |
| 158 }) | |
| 159 if err != nil { | |
| 160 logging.WithError(err).Errorf(c, "Failed to issue log st ream query.") | |
| 161 return grpcutil.Internal | |
| 162 } | |
| 163 | |
| 164 if annotationStream != nil { | |
| 165 as.Path = annotationStream.Path | |
| 166 } | |
| 167 return nil | |
| 168 }() | |
| 169 if err != nil { | |
| 170 return nil, err | |
| 171 } | |
| 172 if as.Path != "" { | |
| 173 logging.Debugf(c, "Resolved path/project via tag query.") | |
|
hinoka
2017/02/03 02:16:44
Resolved path/project into %s .....
dnj
2017/02/03 23:53:38
The calling site for getLogDogAnnotations follows
| |
| 174 return &as, nil | |
| 175 } | |
| 176 | |
| 177 // Last-ditch effort: generate a prefix based on the build properties. T his | |
| 178 // re-implements the "_build_prefix" function in: | |
| 179 // https://chromium.googlesource.com/chromium/tools/build/+/2d23e5284cc3 1f31c6bc07aa1d3fc5b1c454c3b4/scripts/slave/logdog_bootstrap.py#363 | |
| 180 isAlnum := func(r rune) bool { | |
| 181 return ((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r > = '0' && r <= '9')) | |
| 182 } | |
| 183 normalize := func(v string) string { | |
| 184 v = strings.Map(func(r rune) rune { | |
| 185 if isAlnum(r) { | |
| 186 return r | |
| 187 } | |
| 188 switch r { | |
| 189 case ':', '_', '-', '.': | |
| 190 return r | |
| 191 default: | |
| 192 return '_' | |
| 193 } | |
| 194 }, v) | |
| 195 if r, _ := utf8.DecodeRuneInString(v); r == utf8.RuneError || !i sAlnum(r) { | |
| 196 v = "s_" + v | |
| 197 } | |
| 198 return v | |
| 199 } | |
| 200 as.Path = types.StreamPath(fmt.Sprintf("bb/%s/%s/%s/+/annotations", | |
| 201 normalize(build.Master), normalize(build.Buildername), normalize (strconv.Itoa(build.Number)))) | |
| 202 | |
| 203 logging.Debugf(c, "Generated path/project algorithmically.") | |
| 204 return &as, nil | |
| 205 } | |
| 206 | |
| 207 func mergeBuildIntoAnnotation(c context.Context, step *miloProto.Step, build *bu ildbotBuild) error { | |
|
hinoka
2017/02/03 02:16:44
Add comment. This looks like it just extract buil
dnj
2017/02/03 23:53:38
I was thinking that this might be expanded in the
| |
| 208 allProps := stringset.New(len(step.Property) + len(build.Properties)) | |
| 209 for _, prop := range step.Property { | |
| 210 allProps.Add(prop.Name) | |
| 211 } | |
| 212 for _, prop := range build.Properties { | |
| 213 // Annotation protobuf overrides BuildBot properties. | |
| 214 if allProps.Has(prop.Name) { | |
| 215 continue | |
| 216 } | |
| 217 allProps.Add(prop.Name) | |
| 218 | |
| 219 step.Property = append(step.Property, &miloProto.Step_Property{ | |
| 220 Name: prop.Name, | |
| 221 Value: fmt.Sprintf("%v", prop.Value), | |
| 222 }) | |
| 223 } | |
| 224 | |
| 225 return nil | |
| 226 } | |
| OLD | NEW |