go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/auth_service/impl/model/cutover.go (about) 1 // Copyright 2024 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 model 16 17 // This file contains the functionality used to validate 18 // the Go implementation of Auth Service (v2) by comparing its generated 19 // entities to those generated by the Python implemention (v1). 20 // 21 // TODO: Remove dry run and comparison code once we have fully rolled 22 // out Auth Service v2 (b/321019030). 23 24 import ( 25 "bytes" 26 "context" 27 "fmt" 28 "os" 29 30 "github.com/google/go-cmp/cmp" 31 "github.com/google/go-cmp/cmp/cmpopts" 32 "golang.org/x/exp/slices" 33 "google.golang.org/protobuf/proto" 34 35 "go.chromium.org/luci/common/errors" 36 "go.chromium.org/luci/common/logging" 37 "go.chromium.org/luci/gae/service/datastore" 38 "go.chromium.org/luci/server/auth/service/protocol" 39 40 "go.chromium.org/luci/auth_service/impl/util/zlib" 41 ) 42 43 // Names of enviroment variables which control component functionality 44 // while transitioning from Auth Service v1 (Python) to 45 // Auth Service v2 (Go). 46 // 47 // Each environment variable should be either "true" or "false". 48 const ( 49 DryRunAPIChangesEnvVar = "DRY_RUN_API_CHANGES" 50 DryRunCronConfigEnvVar = "DRY_RUN_CRON_CONFIG" 51 DryRunCronRealmsEnvVar = "DRY_RUN_CRON_REALMS" 52 DryRunCronStaleAuthEnvVar = "DRY_RUN_CRON_STALE_AUTH" 53 DryRunTQChangelogEnvVar = "DRY_RUN_TQ_CHANGELOG" 54 DryRunTQReplicationEnvVar = "DRY_RUN_TQ_REPLICATION" 55 EnableGroupImportsEnvVar = "ENABLE_GROUP_IMPORTS" 56 ) 57 58 // ParseDryRunEnvVar parses the dry run flag from the given environment 59 // variable, defaulting to true. 60 func ParseDryRunEnvVar(envVar string) bool { 61 dryRun := true 62 if os.Getenv(envVar) == "false" { 63 dryRun = false 64 } 65 return dryRun 66 } 67 68 // ParseEnableEnvVar parses the enable flag from the given environment 69 // variable, defaulting to false. 70 func ParseEnableEnvVar(envVar string) bool { 71 return os.Getenv(envVar) == "true" 72 } 73 74 // CompareV2Entities compares the entities generated by Auth Service v1 75 // (Python) to those generated by Auth Service v2 (Go) for the latest 76 // AuthDB revision. The comparison result is logged. 77 // 78 // Returns an annotated error if one occurred. 79 func CompareV2Entities(ctx context.Context) error { 80 v1Latest, err := GetAuthDBSnapshotLatest(ctx, false) 81 if err != nil { 82 return errors.Annotate(err, "error getting latest revision").Err() 83 } 84 latestRev := v1Latest.AuthDBRev 85 86 if err := compareSnapshots(ctx, latestRev); err != nil { 87 return errors.Annotate(err, "error comparing snapshots").Err() 88 } 89 90 if err := compareChangelogs(ctx, latestRev); err != nil { 91 return errors.Annotate(err, "error comparing changelogs").Err() 92 } 93 94 return nil 95 } 96 97 func compareChangelogs(ctx context.Context, authDBRev int64) error { 98 // Check if both Auth Servicev1 and v2 have processed the changes for 99 // the given revision. 100 v1LogRev, err := getAuthDBLogRev(ctx, authDBRev, false) 101 if err != nil { 102 return errors.Annotate(err, "error checking v1 changelog was processed").Err() 103 } 104 v2LogRev, err := getAuthDBLogRev(ctx, authDBRev, true) 105 if err != nil { 106 return errors.Annotate(err, "error checking v2 changelog was processed").Err() 107 } 108 if v1LogRev == nil || v2LogRev == nil { 109 logging.Infof(ctx, "changelogs are not yet processed for Rev %d", authDBRev) 110 return nil 111 } 112 113 // Get the AuthDBChanges created by Auth Service v1 and v2 for the 114 // given revision. 115 v1Changes, err := getChangesForRevision(ctx, authDBRev, false) 116 if err != nil { 117 return errors.Annotate(err, "error getting all v1 AuthDBChanges").Err() 118 } 119 v2Changes, err := getChangesForRevision(ctx, authDBRev, true) 120 if err != nil { 121 return errors.Annotate(err, "error getting all v2 AuthDBChanges").Err() 122 } 123 124 // Compare the changes. 125 diffs := diffChangelogs(v1Changes, v2Changes) 126 if len(diffs) > 0 { 127 logging.Errorf(ctx, "AuthDBChange entities for Rev %d do not match", authDBRev) 128 // Log the differences for debugging. 129 for _, diff := range diffs { 130 logging.Debugf(ctx, diff) 131 } 132 } else { 133 logging.Infof(ctx, "AuthDBChange entities for Rev %d are equivalent", authDBRev) 134 } 135 136 return nil 137 } 138 139 func getChangesForRevision(ctx context.Context, authDBRev int64, dryRun bool) ([]*AuthDBChange, error) { 140 query := datastore.NewQuery(entityKind("AuthDBChange", dryRun)).Ancestor(constructLogRevisionKey(ctx, authDBRev, dryRun)).Order("-__key__") 141 var changes []*AuthDBChange 142 if err := datastore.GetAll(ctx, query, &changes); err != nil { 143 return nil, err 144 } 145 return changes, nil 146 } 147 148 // diffChangelogs returns the "functional" differences between the given 149 // slices of AuthDBChanges. 150 // 151 // Fields ignored include: 152 // * Kind - there will be a V2 prefix; and 153 // * Parent - there will be V2 prefixes. 154 func diffChangelogs(changelogA, changelogB []*AuthDBChange) []string { 155 ignoredFields := cmpopts.IgnoreFields(AuthDBChange{}, 156 "Kind", "Parent") 157 158 diffs := []string{} 159 changeCountA := len(changelogA) 160 changeCountB := len(changelogB) 161 for i := 0; i < changeCountA && i < changeCountB; i++ { 162 diff := cmp.Diff(changelogA[i], changelogB[i], ignoredFields) 163 if diff != "" { 164 diffs = append(diffs, diff) 165 } 166 } 167 168 // Record the missing changes. 169 if changeCountA != changeCountB { 170 diffs = append(diffs, fmt.Sprintf("Total changes count: %d vs %d", 171 changeCountA, changeCountB)) 172 173 var longerChangelog []*AuthDBChange 174 var start, max int 175 if changeCountA > changeCountB { 176 longerChangelog = changelogA 177 start = changeCountB 178 max = changeCountA 179 } else { 180 longerChangelog = changelogB 181 start = changeCountA 182 max = changeCountB 183 } 184 for i := start; i < max; i++ { 185 diffs = append(diffs, fmt.Sprintf("missing AuthDBChange: %+v", longerChangelog[i])) 186 } 187 } 188 189 return diffs 190 } 191 192 func compareSnapshots(ctx context.Context, authDBRev int64) error { 193 // Get the AuthDBSnapshots created by Auth Service v1 and v2 for the 194 // given revision. 195 v1Snapshot, err := GetAuthDBSnapshot(ctx, authDBRev, false, false) 196 if err != nil { 197 if errors.Is(err, datastore.ErrNoSuchEntity) { 198 logging.Infof(ctx, "AuthDBSnapshot for Rev %d not yet created", authDBRev) 199 return nil 200 } 201 202 return errors.Annotate(err, "failed to get v1 AuthDBSnapshot").Err() 203 } 204 v2Snapshot, err := GetAuthDBSnapshot(ctx, authDBRev, false, true) 205 if err != nil { 206 if errors.Is(err, datastore.ErrNoSuchEntity) { 207 logging.Infof(ctx, "V2AuthDBSnapshot for Rev %d not yet created", authDBRev) 208 return nil 209 } 210 211 return errors.Annotate(err, "failed to get v2 AuthDBSnapshot").Err() 212 } 213 214 // Compare the snapshots. 215 diffs, err := diffSnapshots(v1Snapshot, v2Snapshot) 216 if err != nil { 217 return errors.Annotate(err, "error comparing snapshots").Err() 218 } 219 if len(diffs) > 0 { 220 logging.Errorf(ctx, "AuthDBSnapshots for Rev %d do not match", authDBRev) 221 // Log the differences for debugging. 222 for _, diff := range diffs { 223 logging.Debugf(ctx, diff) 224 } 225 } else { 226 logging.Infof(ctx, "AuthDBSnapshots for Rev %d are equivalent", authDBRev) 227 } 228 229 return nil 230 } 231 232 // processSnapshot is a helper function to get the 233 // ReplicationPushRequest from the given AuthDBSnapshot. 234 func processSnapshot(authDBSnapshot *AuthDBSnapshot) (*protocol.ReplicationPushRequest, error) { 235 authDBBlob, err := zlib.Decompress(authDBSnapshot.AuthDBDeflated) 236 if err != nil { 237 return nil, errors.Annotate(err, "error decompressing AuthDBDeflated").Err() 238 } 239 240 req := &protocol.ReplicationPushRequest{} 241 if err := proto.Unmarshal(authDBBlob, req); err != nil { 242 return nil, errors.Annotate(err, "error unmarshalling AuthDB blob").Err() 243 } 244 245 return req, nil 246 } 247 248 // diffSnapshots returns the "functional" differences between the two 249 // AuthDBSnapshots. 250 // 251 // AuthDBSnapshot fields ignored in this comparison include: 252 // - Kind; 253 // - ShardIDs; 254 // - CreatedTS; 255 // - AuthDBSha256 (expected differences due to timestamps and 256 // AuthCodeVersion); 257 // - the AuthCodeVersion within the compressed serialized 258 // ReplicationPushRequest blob (this is expected to be "1.x.x" for 259 // the Python version and "2.x.x" for Go). 260 func diffSnapshots(v1Snapshot, v2Snapshot *AuthDBSnapshot) ([]string, error) { 261 diffs := []string{} 262 diffTemplate := "field '%s': '%v' vs '%v'" 263 264 if v1Snapshot.ID != v2Snapshot.ID { 265 diffs = append(diffs, fmt.Sprintf(diffTemplate, "ID", 266 v1Snapshot.ID, v2Snapshot.ID)) 267 } 268 269 // Get the ReplicationPushRequests from the snapshots for comparison. 270 v1Req, err := processSnapshot(v1Snapshot) 271 if err != nil { 272 return diffs, errors.Annotate(err, "error processing v1 snapshot").Err() 273 } 274 v2Req, err := processSnapshot(v2Snapshot) 275 if err != nil { 276 return diffs, errors.Annotate(err, "error processing v2 snapshot").Err() 277 } 278 279 if v1Req.Revision.AuthDbRev != v2Req.Revision.AuthDbRev { 280 diffs = append(diffs, fmt.Sprintf(diffTemplate, "Revision.AuthDBRev", 281 v1Req.Revision.AuthDbRev, v2Req.Revision.AuthDbRev)) 282 } 283 if v1Req.Revision.PrimaryId != v2Req.Revision.PrimaryId { 284 diffs = append(diffs, fmt.Sprintf(diffTemplate, "Revision.PrimaryId", 285 v1Req.Revision.PrimaryId, v2Req.Revision.PrimaryId)) 286 } 287 288 // Compare AuthDB protos. 289 authDBDiffs := diffAuthDBs(v1Req.AuthDb, v2Req.AuthDb) 290 for _, authDBDiff := range authDBDiffs { 291 diffs = append(diffs, fmt.Sprintf("AuthDb.%s", authDBDiff)) 292 } 293 294 return diffs, nil 295 } 296 297 // diffAuthDBs returns the differences between the given AuthDB protos. 298 func diffAuthDBs(a, b *protocol.AuthDB) []string { 299 diffs := []string{} 300 301 // Record the fields that are different. 302 if a.OauthClientId != b.OauthClientId { 303 diffs = append(diffs, "OauthClientId") 304 } 305 if a.OauthClientSecret != b.OauthClientSecret { 306 diffs = append(diffs, "OauthClientSecret") 307 } 308 if a.TokenServerUrl != b.TokenServerUrl { 309 diffs = append(diffs, "TokenServerUrl") 310 } 311 if !bytes.Equal(a.SecurityConfig, b.SecurityConfig) { 312 diffs = append(diffs, "SecurityConfig") 313 } 314 if !proto.Equal(a.Realms, b.Realms) { 315 diffs = append(diffs, "Realms") 316 } 317 318 // Record the slice fields that are different. 319 clientIDsEqual := slices.EqualFunc(a.OauthAdditionalClientIds, b.OauthAdditionalClientIds, 320 func(clientA, clientB string) bool { 321 return clientA == clientB 322 }) 323 if !clientIDsEqual { 324 diffs = append(diffs, "OauthAdditionalClientIds") 325 } 326 ipAllowlistsEqual := slices.EqualFunc(a.IpWhitelists, b.IpWhitelists, 327 func(listA, listB *protocol.AuthIPWhitelist) bool { 328 return proto.Equal(listA, listB) 329 }) 330 if !ipAllowlistsEqual { 331 diffs = append(diffs, "IpAllowlists") 332 } 333 ipAllowlistAssignmentsEqual := slices.EqualFunc(a.IpWhitelistAssignments, b.IpWhitelistAssignments, 334 func(assignA, assignB *protocol.AuthIPWhitelistAssignment) bool { 335 return proto.Equal(assignA, assignB) 336 }) 337 if !ipAllowlistAssignmentsEqual { 338 diffs = append(diffs, "IpAllowlistsAssignments") 339 } 340 341 // Record group differences if present. 342 groupCountA := len(a.Groups) 343 groupCountB := len(b.Groups) 344 // Ignore AuthGroup unexported fields. 345 ignoredFields := cmpopts.IgnoreFields(protocol.AuthGroup{}, 346 "state", "sizeCache", "unknownFields") 347 for i := 0; i < groupCountA && i < groupCountB; i++ { 348 if !proto.Equal(a.Groups[i], b.Groups[i]) { 349 diff := cmp.Diff(a.Groups[i], b.Groups[i], ignoredFields) 350 diffs = append(diffs, fmt.Sprintf("Groups - index %d: %s", i, diff)) 351 } 352 } 353 354 // Record the names of missing groups. 355 if groupCountA != groupCountB { 356 diffs = append(diffs, fmt.Sprintf("Groups - total count: %d vs %d", groupCountA, groupCountB)) 357 358 var largerGroups []*protocol.AuthGroup 359 var start, max int 360 if groupCountA > groupCountB { 361 largerGroups = a.Groups 362 start = groupCountB 363 max = groupCountA 364 } else { 365 largerGroups = b.Groups 366 start = groupCountA 367 max = groupCountB 368 } 369 for i := start; i < max; i++ { 370 diffs = append(diffs, fmt.Sprintf("Groups - missing '%s'", largerGroups[i].Name)) 371 } 372 } 373 374 return diffs 375 }