blob: 186ad4212c68be94fbba9b1f8aea7181ac229e70 [file] [log] [blame]
package cd
import (
"context"
"fmt"
"regexp"
"strings"
"go.skia.org/infra/cd/go/stages"
"go.skia.org/infra/go/auth"
"go.skia.org/infra/go/docker"
"go.skia.org/infra/go/exec"
"go.skia.org/infra/go/gerrit/rubberstamper"
"go.skia.org/infra/go/git"
"go.skia.org/infra/go/gitiles"
"go.skia.org/infra/go/httputils"
"go.skia.org/infra/go/louhi"
"go.skia.org/infra/go/louhi/pubsub"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/vcsinfo"
"go.skia.org/infra/task_driver/go/td"
"golang.org/x/oauth2/google"
)
var uploadedCLRegex = regexp.MustCompile(`https://.*review\.googlesource\.com.*\d+`)
// MaybeUploadCL uploads a CL if there are any diffs in checkoutDir. It builds
// the commit message starting with the given commitSubject. If srcRepo and
// srcCommit are provided, a link back to the source commit is added to the
// commit message. If louhiPubsubProject and louhiExecutionID are provided,
// a pub/sub message is sent after the CL is uploaded.
func MaybeUploadCL(ctx context.Context, checkoutDir, commitSubject, srcRepo, srcCommit, louhiPubsubProject, louhiExecutionID string) error {
ctx = td.StartStep(ctx, td.Props("MaybeUploadCL"))
defer td.EndStep(ctx)
gitExec, err := git.Executable(ctx)
if err != nil {
return skerr.Wrap(err)
}
// Did we change anything?
if _, err := exec.RunCwd(ctx, checkoutDir, gitExec, "diff", "HEAD", "--exit-code"); err == nil {
return nil // No diffs, no CL
}
// There were diffs, so create a CL.
// Build the commit message.
commitMsg := commitSubject
if srcCommit != "" {
shortCommit := srcCommit
if len(shortCommit) > 12 {
shortCommit = shortCommit[:12]
}
commitMsg += " for " + shortCommit
}
commitMsg += "\n\n"
if srcRepo != "" && srcCommit != "" {
ts, err := google.DefaultTokenSource(ctx, auth.ScopeUserinfoEmail)
if err != nil {
return skerr.Wrap(err)
}
client := httputils.DefaultClientConfig().WithTokenSource(ts).Client()
gitilesRepo := gitiles.NewRepo(srcRepo, client)
commitDetails, err := gitilesRepo.Details(ctx, srcCommit)
if err != nil {
return skerr.Wrap(err)
}
commitMsg += fmt.Sprintf("%s/+/%s\n\n", srcRepo, srcCommit)
commitMsg += commitDetails.Subject
commitMsg += "\n\n"
}
commitMsg += rubberstamper.RandomChangeID(ctx)
// Commit and push.
if _, err := exec.RunCwd(ctx, checkoutDir, gitExec, "commit", "-a", "-m", commitMsg); err != nil {
return skerr.Wrap(err)
}
output, err := exec.RunCwd(ctx, checkoutDir, gitExec, "push", git.DefaultRemote, rubberstamper.PushRequestAutoSubmit)
if err != nil {
return skerr.Wrap(err)
}
// Send a pub/sub message.
if louhiPubsubProject != "" && louhiExecutionID != "" {
match := uploadedCLRegex.FindString(output)
if match == "" {
return skerr.Fmt("Failed to parse CL link from:\n%s", output)
}
sender, err := pubsub.NewPubSubSender(ctx, louhiPubsubProject)
if err != nil {
return skerr.Wrap(err)
}
if err := sender.Send(ctx, &louhi.Notification{
EventAction: louhi.EventAction_CREATED_ARTIFACT,
GeneratedCls: []string{match},
PipelineExecutionId: louhiExecutionID,
}); err != nil {
return skerr.Wrap(err)
}
}
return nil
}
// DockerImageWithGitCommit pairs a Docker image instance with the Git commit at
// which the image was built.
type DockerImageWithGitCommit struct {
Digest string
Commit *vcsinfo.LongCommit
Tags []string
}
// MatchDockerImagesToGitCommits retrieves all versions of a given Docker image
// and maps them to the Git commits at which they were built.
func MatchDockerImagesToGitCommits(ctx context.Context, dockerClient docker.Client, repo gitiles.GitilesRepo, image string, limit int) ([]*DockerImageWithGitCommit, error) {
registry, repository, _, err := docker.SplitImage(image)
if err != nil {
return nil, skerr.Wrap(err)
}
instances, err := dockerClient.ListInstances(ctx, registry, repository)
if err != nil {
return nil, skerr.Wrap(err)
}
instancesByCommit := make(map[string]*DockerImageWithGitCommit, len(instances))
for _, instance := range instances {
var commitHash string
for _, tag := range instance.Tags {
if strings.HasPrefix(tag, stages.GitTagPrefix) {
commitHash = strings.TrimPrefix(tag, stages.GitTagPrefix)
break
}
}
if !git.IsFullCommitHash(commitHash) {
continue
}
instancesByCommit[commitHash] = &DockerImageWithGitCommit{
Digest: instance.Digest,
Tags: instance.Tags,
}
}
rv := make([]*DockerImageWithGitCommit, 0, len(instances))
if err := repo.LogFnBatch(ctx, git.MainBranch, func(ctx context.Context, commits []*vcsinfo.LongCommit) error {
for _, c := range commits {
instance, ok := instancesByCommit[c.Hash]
if ok {
instance.Commit = c
rv = append(rv, instance)
delete(instancesByCommit, c.Hash)
if len(instancesByCommit) == 0 || len(rv) >= limit {
return gitiles.ErrStopIteration
}
}
}
return nil
}, gitiles.LogBatchSize(limit)); err != nil {
return nil, skerr.Wrap(err)
}
return rv, nil
}