blob: 38751d4f3909470faa75bd43fcdbb8d99c891a7f [file]
// Copyright 2017 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package buildbot
import (
"fmt"
"strconv"
"strings"
"unicode/utf8"
"github.com/luci/luci-go/common/data/stringset"
"github.com/luci/luci-go/common/logging"
miloProto "github.com/luci/luci-go/common/proto/milo"
"github.com/luci/luci-go/grpc/grpcutil"
"github.com/luci/luci-go/logdog/client/coordinator"
"github.com/luci/luci-go/logdog/common/types"
"github.com/luci/luci-go/luci_config/common/cfgtypes"
milo "github.com/luci/luci-go/milo/api/proto"
"github.com/luci/luci-go/milo/buildsource/rawpresentation"
"github.com/luci/luci-go/milo/common"
"google.golang.org/grpc/codes"
"golang.org/x/net/context"
)
// BuildInfoProvider is a configuration that provides build information.
//
// In a production system, this will be completely defaults. For testing, the
// various services and data sources may be substituted for testing stubs.
type BuildInfoProvider struct {
// LogdogClientFunc returns a coordinator Client instance for the supplied
// parameters.
//
// If nil, a production client will be generated.
LogdogClientFunc func(c context.Context) (*coordinator.Client, error)
}
func (p *BuildInfoProvider) newLogdogClient(c context.Context) (*coordinator.Client, error) {
if p.LogdogClientFunc != nil {
return p.LogdogClientFunc(c)
}
return rawpresentation.NewClient(c, "")
}
// GetBuildInfo resolves a Milo protobuf Step for a given BuildBot build.
//
// On failure, it returns a (potentially-wrapped) gRPC error.
//
// This:
//
// 1) Fetches the BuildBot build JSON from datastore.
// 2) Resolves the LogDog annotation stream path from the BuildBot state.
// 3) Fetches the LogDog annotation stream and resolves it into a Step.
// 4) Merges some operational BuildBot build information into the Step.
func (p *BuildInfoProvider) GetBuildInfo(c context.Context, req *milo.BuildInfoRequest_BuildBot,
projectHint cfgtypes.ProjectName) (*milo.BuildInfoResponse, error) {
logging.Infof(c, "Loading build info for master %q, builder %q, build #%d",
req.MasterName, req.BuilderName, req.BuildNumber)
// Load the BuildBot build from datastore.
build, err := getBuild(c, req.MasterName, req.BuilderName, int(req.BuildNumber))
if err != nil {
switch common.ErrorTag.In(err) {
case common.CodeNotFound:
return nil, grpcutil.Errf(codes.NotFound, "Build #%d for master %q, builder %q was not found",
req.BuildNumber, req.MasterName, req.BuilderName)
case common.CodeUnauthorized:
return nil, grpcutil.Unauthenticated
default:
logging.WithError(err).Errorf(c, "Failed to load build info.")
return nil, grpcutil.Internal
}
}
// Create a new LogDog client.
client, err := p.newLogdogClient(c)
if err != nil {
logging.WithError(err).Errorf(c, "Failed to create LogDog client.")
return nil, grpcutil.Internal
}
// Identify the LogDog annotation stream from the build.
//
// This will return a gRPC error on failure.
addr, err := getLogDogAnnotationAddr(c, client, build, projectHint)
if err != nil {
return nil, err
}
logging.Infof(c, "Resolved annotation stream: %s / %s", addr.Project, addr.Path)
// Load the annotation protobuf.
as := rawpresentation.AnnotationStream{
Client: client,
Path: addr.Path,
Project: addr.Project,
}
if err := as.Normalize(); err != nil {
logging.WithError(err).Errorf(c, "Failed to normalize annotation stream.")
return nil, grpcutil.Internal
}
step, err := as.Fetch(c)
if err != nil {
logging.WithError(err).Errorf(c, "Failed to load annotation stream.")
return nil, grpcutil.Errf(codes.Internal, "failed to load LogDog annotation stream from: %s", as.Path)
}
// Merge the information together.
if err := mergeBuildIntoAnnotation(c, step, build); err != nil {
logging.WithError(err).Errorf(c, "Failed to merge annotation with build.")
return nil, grpcutil.Errf(codes.Internal, "failed to merge annotation and build data")
}
prefix, name := as.Path.Split()
return &milo.BuildInfoResponse{
Project: string(as.Project),
Step: step,
AnnotationStream: &miloProto.LogdogStream{
Server: client.Host,
Prefix: string(prefix),
Name: string(name),
},
}, nil
}
// Resolve BuildBot and LogDog build information. We do this
//
// This returns an AnnotationStream instance with its project and path
// populated.
//
// This function is messy and implementation-specific. That's the point of this
// endpoint, though. All of the nastiness here should be replaced with something
// more elegant once that becomes available. In the meantime...
func getLogDogAnnotationAddr(c context.Context, client *coordinator.Client, build *buildbotBuild,
projectHint cfgtypes.ProjectName) (*types.StreamAddr, error) {
if v, ok := build.getPropertyValue("log_location").(string); ok && v != "" {
addr, err := types.ParseURL(v)
if err == nil {
return addr, nil
}
logging.Fields{
logging.ErrorKey: err,
"log_location": v,
}.Debugf(c, "'log_location' property did not parse as LogDog URL.")
}
// logdog_annotation_url (if present, must be valid)
if v, ok := build.getPropertyValue("logdog_annotation_url").(string); ok && v != "" {
addr, err := types.ParseURL(v)
if err != nil {
logging.Fields{
logging.ErrorKey: err,
"url": v,
}.Errorf(c, "Failed to parse 'logdog_annotation_url' property.")
return nil, grpcutil.Errf(codes.FailedPrecondition, "build has invalid annotation URL")
}
return addr, nil
}
// Modern builds will have this information in their build properties.
var addr types.StreamAddr
prefix, _ := build.getPropertyValue("logdog_prefix").(string)
project, _ := build.getPropertyValue("logdog_project").(string)
if prefix != "" && project != "" {
// Construct the full annotation path.
addr.Project = cfgtypes.ProjectName(project)
addr.Path = types.StreamName(prefix).Join("annotations")
logging.Debugf(c, "Resolved path/project from build properties.")
return &addr, nil
}
// From here on out, we will need a project hint.
if projectHint == "" {
return nil, grpcutil.Errf(codes.NotFound, "annotation stream not found")
}
addr.Project = projectHint
// Execute a LogDog service query to see if we can identify the stream.
err := func() error {
var annotationStream *coordinator.LogStream
err := client.Query(c, addr.Project, "", coordinator.QueryOptions{
Tags: map[string]string{
"buildbot.master": build.Master,
"buildbot.builder": build.Buildername,
"buildbot.buildnumber": strconv.Itoa(build.Number),
},
ContentType: miloProto.ContentTypeAnnotations,
}, func(ls *coordinator.LogStream) bool {
// Only need the first (hopefully only?) result.
annotationStream = ls
return false
})
if err != nil {
logging.WithError(err).Errorf(c, "Failed to issue log stream query.")
return grpcutil.Internal
}
if annotationStream != nil {
addr.Path = annotationStream.Path
}
return nil
}()
if err != nil {
return nil, err
}
if addr.Path != "" {
logging.Debugf(c, "Resolved path/project via tag query.")
return &addr, nil
}
// Last-ditch effort: generate a prefix based on the build properties. This
// re-implements the "_build_prefix" function in:
// https://chromium.googlesource.com/chromium/tools/build/+/2d23e5284cc31f31c6bc07aa1d3fc5b1c454c3b4/scripts/slave/logdog_bootstrap.py#363
isAlnum := func(r rune) bool {
return ((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9'))
}
normalize := func(v string) string {
v = strings.Map(func(r rune) rune {
if isAlnum(r) {
return r
}
switch r {
case ':', '_', '-', '.':
return r
default:
return '_'
}
}, v)
if r, _ := utf8.DecodeRuneInString(v); r == utf8.RuneError || !isAlnum(r) {
v = "s_" + v
}
return v
}
addr.Path = types.StreamPath(fmt.Sprintf("bb/%s/%s/%s/+/annotations",
normalize(build.Master), normalize(build.Buildername), normalize(strconv.Itoa(build.Number))))
logging.Debugf(c, "Generated path/project algorithmically.")
return &addr, nil
}
// mergeBuildInfoIntoAnnotation merges BuildBot-specific build informtion into
// a LogDog annotation protobuf.
//
// This consists of augmenting the Step's properties with BuildBot's properties,
// favoring the Step's version of the properties if there are two with the same
// name.
func mergeBuildIntoAnnotation(c context.Context, step *miloProto.Step, build *buildbotBuild) error {
allProps := stringset.New(len(step.Property) + len(build.Properties))
for _, prop := range step.Property {
allProps.Add(prop.Name)
}
for _, prop := range build.Properties {
// Annotation protobuf overrides BuildBot properties.
if allProps.Has(prop.Name) {
continue
}
allProps.Add(prop.Name)
step.Property = append(step.Property, &miloProto.Step_Property{
Name: prop.Name,
Value: fmt.Sprintf("%v", prop.Value),
})
}
return nil
}