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  }