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 }