go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/luci_notify/notify/srcman.go (about)

     1  // Copyright 2018 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 notify
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"net/http"
    21  	"path"
    22  	"strings"
    23  
    24  	"github.com/golang/protobuf/proto"
    25  
    26  	"go.chromium.org/luci/common/api/gitiles"
    27  	"go.chromium.org/luci/common/errors"
    28  	"go.chromium.org/luci/common/logging"
    29  	"go.chromium.org/luci/common/proto/srcman"
    30  	"go.chromium.org/luci/common/retry/transient"
    31  	"go.chromium.org/luci/grpc/grpcutil"
    32  	"go.chromium.org/luci/grpc/prpc"
    33  	"go.chromium.org/luci/logdog/client/coordinator"
    34  	"go.chromium.org/luci/logdog/common/renderer"
    35  	"go.chromium.org/luci/server/auth"
    36  )
    37  
    38  // CheckoutFunc is a function that given a Build, produces a source checkout
    39  // related to that build.
    40  type CheckoutFunc func(context.Context, *Build) (Checkout, error)
    41  
    42  // srcmanCheckout is a CheckoutFunc which retrieves a source checkout related
    43  // to a build by querying LogDog for a source manifest stream associated with
    44  // that build. It assumes that the build has exactly one source manifest.
    45  func srcmanCheckout(c context.Context, build *Build) (Checkout, error) {
    46  	if build.Infra == nil || build.Infra.Logdog == nil || build.Infra.Logdog.Hostname == "" {
    47  		return nil, errors.Reason("logdog hostname is not set in the build proto").Err()
    48  	}
    49  	transport, err := auth.GetRPCTransport(c, auth.AsSelf)
    50  	if err != nil {
    51  		return nil, errors.Annotate(err, "getting RPC Transport").Err()
    52  	}
    53  	client := coordinator.NewClient(&prpc.Client{
    54  		C:       &http.Client{Transport: transport},
    55  		Host:    build.Infra.Logdog.Hostname,
    56  		Options: prpc.DefaultOptions(),
    57  	})
    58  	qo := coordinator.QueryOptions{
    59  		ContentType: srcman.ContentTypeSourceManifest,
    60  	}
    61  	logProject := build.Infra.Logdog.Project
    62  	logPath := path.Join(build.Infra.Logdog.Prefix, "+", "**")
    63  
    64  	// Perform the query, capturing exactly one log stream and erroring otherwise.
    65  	var log *coordinator.LogStream
    66  	err = client.Query(c, logProject, logPath, qo, func(s *coordinator.LogStream) bool {
    67  		log = s
    68  		return false
    69  	})
    70  	switch {
    71  	case err != nil:
    72  		return nil, grpcutil.WrapIfTransient(err)
    73  	case log == nil:
    74  		logging.Infof(c, "unable to find source manifest in project %s at path %s",
    75  			build.Infra.Logdog.Project, logPath)
    76  		return nil, nil
    77  	}
    78  
    79  	// Read the source manifest from the log stream.
    80  	var buf bytes.Buffer
    81  	_, err = buf.ReadFrom(&renderer.Renderer{
    82  		Source: client.Stream(logProject, log.Path).Fetcher(c, nil),
    83  		Raw:    true,
    84  	})
    85  	if err != nil {
    86  		return nil, errors.Annotate(err, "failed to read stream").Tag(transient.Tag).Err()
    87  	}
    88  
    89  	// Unmarshal the source manifest from the bytes.
    90  	var manifest srcman.Manifest
    91  	if err := proto.Unmarshal(buf.Bytes(), &manifest); err != nil {
    92  		return nil, err
    93  	}
    94  
    95  	results := make(Checkout)
    96  	for dirname, dir := range manifest.Directories {
    97  		gitCheckout := dir.GetGitCheckout()
    98  		if gitCheckout == nil {
    99  			continue
   100  		}
   101  
   102  		url, err := gitiles.NormalizeRepoURL(gitCheckout.RepoUrl, false)
   103  		if err != nil {
   104  			logging.WithError(err).Warningf(c, "could not parse RepoURL %q for dir %q", gitCheckout.RepoUrl, dirname)
   105  			continue
   106  		}
   107  
   108  		if !strings.HasSuffix(url.Host, ".googlesource.com") {
   109  			logging.WithError(err).Warningf(c, "unsupported git host %q for dir %q", gitCheckout.RepoUrl, dirname)
   110  			continue
   111  		}
   112  		results[url.String()] = gitCheckout.Revision
   113  	}
   114  	return results, nil
   115  }