go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/testresults/gerritchangelists/span.go (about)

     1  // Copyright 2023 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 gerritchangelists contains methods for maintaining a cache
    16  // of whether gerrit changelists were authored by humans or automation.
    17  package gerritchangelists
    18  
    19  import (
    20  	"context"
    21  	"sort"
    22  	"time"
    23  
    24  	"cloud.google.com/go/spanner"
    25  
    26  	"go.chromium.org/luci/common/errors"
    27  	"go.chromium.org/luci/server/span"
    28  
    29  	spanutil "go.chromium.org/luci/analysis/internal/span"
    30  	"go.chromium.org/luci/analysis/internal/testutil"
    31  	"go.chromium.org/luci/analysis/pbutil"
    32  	pb "go.chromium.org/luci/analysis/proto/v1"
    33  )
    34  
    35  // GerritChangelist is a record used to cache whether a gerrit changelist
    36  // was authored by a human or by automation.
    37  //
    38  // The cache is per-project to avoid confused deputy problems. Each project
    39  // will use its own project-scoped service account to access to gerrit to
    40  // retrieve change owner information and store this data in its own cache.
    41  type GerritChangelist struct {
    42  	// Project is the name of the LUCI Project. This is the project
    43  	// for which the cache is being maintained and the project we authenticated
    44  	// as to gerrit.
    45  	Project string
    46  	// Host is the gerrit hostname. E.g. "chromium-review.googlesource.com".
    47  	Host string
    48  	// Change is the gerrit change number.
    49  	Change int64
    50  	// The kind of owner that created the changelist. This could be
    51  	// a human (user) or automation.
    52  	OwnerKind pb.ChangelistOwnerKind
    53  	// The time the record was created in Spanner.
    54  	CreationTime time.Time
    55  }
    56  
    57  // Key represents the key fields of a GerritChangelist record.
    58  type Key struct {
    59  	// Project is the name of the LUCI Project.
    60  	Project string
    61  	// Host is the gerrit hostname. E.g. "chromium-review.googlesource.com".
    62  	Host string
    63  	// Change is the gerrit change number.
    64  	Change int64
    65  }
    66  
    67  // Read reads the gerrit changelists with the given keys.
    68  //
    69  // ctx must be a Spanner transaction context.
    70  func Read(ctx context.Context, keys map[Key]struct{}) (map[Key]*GerritChangelist, error) {
    71  	var ks []spanner.Key
    72  	for key := range keys {
    73  		ks = append(ks, spanner.Key{key.Project, key.Host, key.Change})
    74  	}
    75  	return readByKeys(ctx, spanner.KeySetFromKeys(ks...))
    76  }
    77  
    78  // ReadAll reads all gerrit changelists. Provided for testing only.
    79  func ReadAll(ctx context.Context) ([]*GerritChangelist, error) {
    80  	entries, err := readByKeys(ctx, spanner.AllKeys())
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  	var results []*GerritChangelist
    85  	for _, cl := range entries {
    86  		results = append(results, cl)
    87  	}
    88  	// Sort changelists by key.
    89  	sort.Slice(results, func(i, j int) bool {
    90  		if results[i].Project != results[j].Project {
    91  			return results[i].Project < results[j].Project
    92  		}
    93  		if results[i].Host != results[j].Host {
    94  			return results[i].Host < results[j].Host
    95  		}
    96  		return results[i].Change < results[j].Change
    97  	})
    98  	return results, nil
    99  }
   100  
   101  // readByKeys reads gerrit changelists for the given keyset.
   102  func readByKeys(ctx context.Context, keys spanner.KeySet) (map[Key]*GerritChangelist, error) {
   103  	results := make(map[Key]*GerritChangelist)
   104  	var b spanutil.Buffer
   105  
   106  	it := span.Read(ctx, "GerritChangelists", keys, []string{"Project", "Host", "Change", "OwnerKind", "CreationTime"})
   107  	err := it.Do(func(r *spanner.Row) error {
   108  		var changelist GerritChangelist
   109  
   110  		err := b.FromSpanner(r,
   111  			&changelist.Project,
   112  			&changelist.Host,
   113  			&changelist.Change,
   114  			&changelist.OwnerKind,
   115  			&changelist.CreationTime,
   116  		)
   117  		if err != nil {
   118  			return errors.Annotate(err, "read row").Err()
   119  		}
   120  		key := Key{
   121  			Project: changelist.Project,
   122  			Host:    changelist.Host,
   123  			Change:  changelist.Change,
   124  		}
   125  		results[key] = &changelist
   126  		return nil
   127  	})
   128  	if err != nil {
   129  		return nil, errors.Annotate(err, "read gerrit changelists").Err()
   130  	}
   131  	return results, nil
   132  }
   133  
   134  // CreateOrUpdate returns a Spanner mutation that inserts the given
   135  // gerrit changelist record.
   136  func CreateOrUpdate(g *GerritChangelist) (*spanner.Mutation, error) {
   137  	if err := validateGerritChangelist(g); err != nil {
   138  		return nil, err
   139  	}
   140  
   141  	// Row not found. Create it.
   142  	row := map[string]any{
   143  		"Project":      g.Project,
   144  		"Host":         g.Host,
   145  		"Change":       g.Change,
   146  		"OwnerKind":    g.OwnerKind,
   147  		"CreationTime": spanner.CommitTimestamp,
   148  	}
   149  	return spanner.InsertOrUpdateMap("GerritChangelists", spanutil.ToSpannerMap(row)), nil
   150  }
   151  
   152  // validateGerritChangelist validates that the GerritChangelist is valid.
   153  func validateGerritChangelist(g *GerritChangelist) error {
   154  	if err := pbutil.ValidateProject(g.Project); err != nil {
   155  		return errors.Annotate(err, "project").Err()
   156  	}
   157  	if g.Host == "" || len(g.Host) > 255 {
   158  		return errors.Reason("host: must have a length between 1 and 255").Err()
   159  	}
   160  	if g.Change <= 0 {
   161  		return errors.Reason("change: must be set and positive").Err()
   162  	}
   163  	return nil
   164  }
   165  
   166  // SetGerritChangelistsForTesting replaces the set of stored gerrit changelists
   167  // to match the given set. Provided for testing only.
   168  func SetGerritChangelistsForTesting(ctx context.Context, gs []*GerritChangelist) error {
   169  	testutil.MustApply(ctx,
   170  		spanner.Delete("GerritChangelists", spanner.AllKeys()))
   171  	// Insert some GerritChangelists.
   172  	_, err := span.ReadWriteTransaction(ctx, func(ctx context.Context) error {
   173  		for _, g := range gs {
   174  			row := map[string]any{
   175  				"Project":      g.Project,
   176  				"Host":         g.Host,
   177  				"Change":       g.Change,
   178  				"OwnerKind":    g.OwnerKind,
   179  				"CreationTime": g.CreationTime,
   180  			}
   181  			span.BufferWrite(ctx, spanner.InsertMap("GerritChangelists", spanutil.ToSpannerMap(row)))
   182  		}
   183  		return nil
   184  	})
   185  	return err
   186  }