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

     1  // Copyright 2022 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 rules contains methods to read and write failure association rules.
    16  package rules
    17  
    18  import (
    19  	"context"
    20  	"crypto/rand"
    21  	"encoding/hex"
    22  	"regexp"
    23  	"time"
    24  
    25  	"cloud.google.com/go/spanner"
    26  	"google.golang.org/protobuf/proto"
    27  
    28  	"go.chromium.org/luci/common/errors"
    29  	"go.chromium.org/luci/server/span"
    30  
    31  	"go.chromium.org/luci/analysis/internal/bugs"
    32  	bugspb "go.chromium.org/luci/analysis/internal/bugs/proto"
    33  	"go.chromium.org/luci/analysis/internal/clustering"
    34  	"go.chromium.org/luci/analysis/internal/clustering/rules/lang"
    35  	spanutil "go.chromium.org/luci/analysis/internal/span"
    36  	"go.chromium.org/luci/analysis/pbutil"
    37  )
    38  
    39  // RuleIDRe is the regular expression pattern that matches validly
    40  // formed rule IDs.
    41  const RuleIDRePattern = `[0-9a-f]{32}`
    42  
    43  // PolicyIDRePattern is the regular expression pattern that matches
    44  // validly formed bug management policy IDs.
    45  // Designed to conform to google.aip.dev/122 resource ID requirements.
    46  const PolicyIDRePattern = `[a-z]([a-z0-9-]{0,62}[a-z0-9])?`
    47  
    48  // MaxRuleDefinitionLength is the maximum length of a rule definition.
    49  const MaxRuleDefinitionLength = 65536
    50  
    51  // RuleIDRe matches validly formed rule IDs.
    52  var RuleIDRe = regexp.MustCompile(`^` + RuleIDRePattern + `$`)
    53  
    54  // PolicyIDRe matches validly formed bug management policy IDs.
    55  var PolicyIDRe = regexp.MustCompile(`^` + PolicyIDRePattern + `$`)
    56  
    57  // UserRe matches valid users. These are email addresses or the special
    58  // value "system".
    59  var UserRe = regexp.MustCompile(`^system|([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)$`)
    60  
    61  // LUCIAnalysisSystem is the special user that identifies changes made by the
    62  // LUCI Analysis system itself in audit fields.
    63  const LUCIAnalysisSystem = "system"
    64  
    65  // StartingEpoch is the rule last updated time used for projects that have
    66  // no rules (active or otherwise). It is deliberately different from the
    67  // timestamp zero value to be discernible from "timestamp not populated"
    68  // programming errors.
    69  var StartingEpoch = time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC)
    70  
    71  // StartingEpoch is the rule version used for projects that have
    72  // no rules (active or otherwise).
    73  var StartingVersion = Version{
    74  	Predicates: StartingEpoch,
    75  	Total:      StartingEpoch,
    76  }
    77  
    78  // NotExistsErr is returned by Read methods for a single failure
    79  // association rule, if no matching rule exists.
    80  var NotExistsErr = errors.New("no matching rule exists")
    81  
    82  // Entry represents a failure association rule (a rule which associates
    83  // failures with a bug). When the rule is used to match incoming test
    84  // failures, the resultant cluster is known as a 'bug cluster' because
    85  // the cluster is associated with a bug (via the failure association rule).
    86  type Entry struct {
    87  	// Identity fields.
    88  
    89  	// The LUCI Project for which this rule is defined.
    90  	Project string
    91  	// The unique identifier for the failure association rule,
    92  	// as 32 lowercase hexadecimal characters.
    93  	RuleID string
    94  
    95  	// Failure predicate fields (which failures are matched by the rule).
    96  
    97  	// The rule predicate, defining which failures are being associated.
    98  	RuleDefinition string
    99  	// Whether the bug should be updated by LUCI Analysis, and whether failures
   100  	// should still be matched against the rule.
   101  	IsActive bool
   102  	// The time the rule was last updated in a way that caused the
   103  	// matched failures to change, i.e. because of a change to RuleDefinition
   104  	// or IsActive. (By contrast, updating BugID does NOT change
   105  	// the matched failures, so does NOT update this field.)
   106  	// When this value changes, it triggers re-clustering.
   107  	// Compare with RulesVersion on ReclusteringRuns to identify
   108  	// reclustering state.
   109  	// Output only.
   110  	PredicateLastUpdateTime time.Time
   111  
   112  	// Bug fields.
   113  
   114  	// BugID is the identifier of the bug that the failures are
   115  	// associated with.
   116  	BugID bugs.BugID
   117  	// Whether this rule should manage the priority and verified status
   118  	// of the associated bug based on the impact of the cluster defined
   119  	// by this rule.
   120  	IsManagingBug bool
   121  	// Whether the bug priority should be updated based on the cluster's impact.
   122  	// This flag is effective only if the IsManagingBug is true.
   123  	// The default value will be false.
   124  	IsManagingBugPriority bool
   125  	// Tracks the last time the field `IsManagingBugPriority` was updated.
   126  	// Defaults to nil which means the field was never updated.
   127  	IsManagingBugPriorityLastUpdateTime time.Time
   128  
   129  	// Immutable data.
   130  
   131  	// The suggested cluster this rule was created from (if any).
   132  	// Until re-clustering is complete and has reduced the residual impact
   133  	// of the source cluster, this cluster ID tells bug filing to ignore
   134  	// the source cluster when determining whether new bugs need to be filed.
   135  	SourceCluster clustering.ClusterID
   136  
   137  	// System-controlled data.
   138  
   139  	// State used to control automatic bug management.
   140  	// Invariant: BugManagementState != nil
   141  	BugManagementState *bugspb.BugManagementState
   142  
   143  	// Audit fields.
   144  
   145  	// The time the rule was created. Output only.
   146  	CreateTime time.Time
   147  	// The user which created the rule. Output only.
   148  	CreateUser string
   149  	// The last time an auditable field was updated. An auditable field
   150  	// is any field other than a system controlled data field. Output only.
   151  	LastAuditableUpdateTime time.Time
   152  	// The last user who updated an auditable field. An auditable field
   153  	// is any field other than a system controlled data field. Output only.
   154  	LastAuditableUpdateUser string
   155  	// The time the rule was last updated. Output only.
   156  	LastUpdateTime time.Time
   157  }
   158  
   159  // Clone makes a deep copy of the rule.
   160  func (r Entry) Clone() *Entry {
   161  	result := &Entry{}
   162  	*result = r
   163  	result.BugManagementState = proto.Clone(result.BugManagementState).(*bugspb.BugManagementState)
   164  	return result
   165  }
   166  
   167  // Read reads the failure association rule with the given rule ID.
   168  // If no rule exists, NotExistsErr will be returned.
   169  func Read(ctx context.Context, project string, id string) (*Entry, error) {
   170  	whereClause := `Project = @project AND RuleId = @ruleId`
   171  	params := map[string]any{
   172  		"project": project,
   173  		"ruleId":  id,
   174  	}
   175  	rs, err := readWhere(ctx, whereClause, params)
   176  	if err != nil {
   177  		return nil, errors.Annotate(err, "query rule by id").Err()
   178  	}
   179  	if len(rs) == 0 {
   180  		return nil, NotExistsErr
   181  	}
   182  	return rs[0], nil
   183  }
   184  
   185  // ReadAllForTesting reads all LUCI Analysis failure association rules.
   186  // This method is not expected to scale -- for testing use only.
   187  func ReadAllForTesting(ctx context.Context) ([]*Entry, error) {
   188  	whereClause := `TRUE`
   189  	params := map[string]any{}
   190  	rs, err := readWhere(ctx, whereClause, params)
   191  	if err != nil {
   192  		return nil, errors.Annotate(err, "query all rules").Err()
   193  	}
   194  	return rs, nil
   195  }
   196  
   197  // ReadActive reads all active LUCI Analysis failure association rules in
   198  // the given LUCI project.
   199  func ReadActive(ctx context.Context, project string) ([]*Entry, error) {
   200  	whereClause := `Project = @project AND IsActive`
   201  	params := map[string]any{
   202  		"project": project,
   203  	}
   204  	rs, err := readWhere(ctx, whereClause, params)
   205  	if err != nil {
   206  		return nil, errors.Annotate(err, "query active rules").Err()
   207  	}
   208  	return rs, nil
   209  }
   210  
   211  // ReadWithMonorailForProject reads all LUCI Analysis failure association rules
   212  // using monorail in the given LUCI Project.
   213  //
   214  // This RPC was introduced for the specific purpose of supporting monorail
   215  // migration for projects. As rules are never deleted by LUCI Analysis, and
   216  // this method does no pagination, this style of method will cease to scale
   217  // at some point.
   218  //
   219  // It has implemented this way only because monorail has a limited remaining life
   220  // and we seem able to get away with a pagination-free approach for now.
   221  func ReadWithMonorailForProject(ctx context.Context, project string) ([]*Entry, error) {
   222  	whereClause := `Project = @project AND BugSystem = 'monorail'`
   223  	params := map[string]any{
   224  		"project": project,
   225  	}
   226  	rs, err := readWhere(ctx, whereClause, params)
   227  	if err != nil {
   228  		return nil, errors.Annotate(err, "query monorail rules for project").Err()
   229  	}
   230  	return rs, nil
   231  }
   232  
   233  // ReadByBug reads the failure association rules associated with the given bug.
   234  // At most one rule will be returned per project.
   235  func ReadByBug(ctx context.Context, bugID bugs.BugID) ([]*Entry, error) {
   236  	whereClause := `BugSystem = @bugSystem and BugId = @bugId`
   237  	params := map[string]any{
   238  		"bugSystem": bugID.System,
   239  		"bugId":     bugID.ID,
   240  	}
   241  	rs, err := readWhere(ctx, whereClause, params)
   242  	if err != nil {
   243  		return nil, errors.Annotate(err, "query rule by bug").Err()
   244  	}
   245  	return rs, nil
   246  }
   247  
   248  // ReadDelta reads the changed failure association rules since the given
   249  // timestamp, in the given LUCI project.
   250  func ReadDelta(ctx context.Context, project string, sinceTime time.Time) ([]*Entry, error) {
   251  	if sinceTime.Before(StartingEpoch) {
   252  		return nil, errors.New("cannot query rule deltas from before project inception")
   253  	}
   254  	whereClause := `Project = @project AND LastUpdated > @sinceTime`
   255  	params := map[string]any{
   256  		"project":   project,
   257  		"sinceTime": sinceTime,
   258  	}
   259  	rs, err := readWhere(ctx, whereClause, params)
   260  	if err != nil {
   261  		return nil, errors.Annotate(err, "query rules since").Err()
   262  	}
   263  	return rs, nil
   264  }
   265  
   266  // ReadDeltaAllProjects reads the changed failure association rules since the given
   267  // timestamp for all LUCI projects.
   268  func ReadDeltaAllProjects(ctx context.Context, sinceTime time.Time) ([]*Entry, error) {
   269  	if sinceTime.Before(StartingEpoch) {
   270  		return nil, errors.New("cannot query rule deltas from before project inception")
   271  	}
   272  	whereClause := `LastUpdated > @sinceTime`
   273  	params := map[string]any{"sinceTime": sinceTime}
   274  
   275  	rs, err := readWhere(ctx, whereClause, params)
   276  	if err != nil {
   277  		return nil, errors.Annotate(err, "query rules since").Err()
   278  	}
   279  	return rs, nil
   280  }
   281  
   282  // ReadMany reads the failure association rules with the given rule IDs.
   283  // The returned slice of rules will correspond one-to-one the IDs requested
   284  // (so returned[i].RuleId == ids[i], assuming the rule exists, else
   285  // returned[i] == nil). If a rule does not exist, a value of nil will be
   286  // returned for that ID. The same rule can be requested multiple times.
   287  func ReadMany(ctx context.Context, project string, ids []string) ([]*Entry, error) {
   288  	whereClause := `Project = @project AND RuleId IN UNNEST(@ruleIds)`
   289  	params := map[string]any{
   290  		"project": project,
   291  		"ruleIds": ids,
   292  	}
   293  	rs, err := readWhere(ctx, whereClause, params)
   294  	if err != nil {
   295  		return nil, errors.Annotate(err, "query rules by id").Err()
   296  	}
   297  	ruleByID := make(map[string]Entry)
   298  	for _, r := range rs {
   299  		ruleByID[r.RuleID] = *r
   300  	}
   301  	var result []*Entry
   302  	for _, id := range ids {
   303  		var entry *Entry
   304  		rule, ok := ruleByID[id]
   305  		if ok {
   306  			// Copy the rule to ensure the rules in the result
   307  			// are not aliased, even if the same rule ID is requested
   308  			// multiple times.
   309  			entry = rule.Clone()
   310  		}
   311  		result = append(result, entry)
   312  	}
   313  	return result, nil
   314  }
   315  
   316  // readWhere failure association rules matching the given where clause,
   317  // substituting params for any SQL parameters used in that clause.
   318  func readWhere(ctx context.Context, whereClause string, params map[string]any) ([]*Entry, error) {
   319  	stmt := spanner.NewStatement(`
   320  		SELECT
   321  			Project, RuleId,
   322  			RuleDefinition,
   323  			IsActive,
   324  			PredicateLastUpdated,
   325  			BugSystem, BugId,
   326  		  IsManagingBug,
   327  			IsManagingBugPriority,
   328  			IsManagingBugPriorityLastUpdated,
   329  		  SourceClusterAlgorithm, SourceClusterId,
   330  			BugManagementState,
   331  			CreationTime, LastUpdated,
   332  		  CreationUser,
   333  			LastAuditableUpdate,
   334  			LastAuditableUpdateUser
   335  		FROM FailureAssociationRules
   336  		WHERE (` + whereClause + `)
   337  		ORDER BY BugSystem, BugId, Project
   338  	`)
   339  	stmt.Params = params
   340  
   341  	it := span.Query(ctx, stmt)
   342  	rs := []*Entry{}
   343  	var b spanutil.Buffer
   344  	err := it.Do(func(r *spanner.Row) error {
   345  		var project, ruleID string
   346  		var ruleDefinition string
   347  		var isActive spanner.NullBool
   348  		var predicateLastUpdated time.Time
   349  		var bugSystem, bugID string
   350  		var isManagingBug spanner.NullBool
   351  		var isManagingBugPriority bool
   352  		var isManagingBugPriorityLastUpdated spanner.NullTime
   353  		var sourceClusterAlgorithm, sourceClusterID string
   354  		var bugManagementStateCompressed spanutil.Compressed
   355  		var creationTime, lastUpdated time.Time
   356  		var creationUser string
   357  		var lastAuditableUpdate spanner.NullTime
   358  		var lastAuditableUpdateUser spanner.NullString
   359  
   360  		err := b.FromSpanner(r,
   361  			&project, &ruleID,
   362  			&ruleDefinition,
   363  			&isActive,
   364  			&predicateLastUpdated,
   365  			&bugSystem, &bugID,
   366  			&isManagingBug,
   367  			&isManagingBugPriority,
   368  			&isManagingBugPriorityLastUpdated,
   369  			&sourceClusterAlgorithm, &sourceClusterID,
   370  			&bugManagementStateCompressed,
   371  			&creationTime, &lastUpdated,
   372  			&creationUser,
   373  			&lastAuditableUpdate,
   374  			&lastAuditableUpdateUser,
   375  		)
   376  		if err != nil {
   377  			return errors.Annotate(err, "read rule row").Err()
   378  		}
   379  
   380  		bugManagementState := &bugspb.BugManagementState{}
   381  		if len(bugManagementStateCompressed) > 0 {
   382  			if err := proto.Unmarshal(bugManagementStateCompressed, bugManagementState); err != nil {
   383  				return errors.Annotate(err, "unmarshal bug management state").Err()
   384  			}
   385  		}
   386  
   387  		lastAuditableUpdateTime := lastAuditableUpdate.Time
   388  		if !lastAuditableUpdate.Valid {
   389  			// Some rows may have been created before this field existed. Treat
   390  			// all previous updates as auditable updates.
   391  			lastAuditableUpdateTime = lastUpdated
   392  		}
   393  
   394  		rule := &Entry{
   395  			Project:                             project,
   396  			RuleID:                              ruleID,
   397  			RuleDefinition:                      ruleDefinition,
   398  			IsActive:                            isActive.Valid && isActive.Bool,
   399  			PredicateLastUpdateTime:             predicateLastUpdated,
   400  			BugID:                               bugs.BugID{System: bugSystem, ID: bugID},
   401  			IsManagingBug:                       isManagingBug.Valid && isManagingBug.Bool,
   402  			IsManagingBugPriority:               isManagingBugPriority,
   403  			IsManagingBugPriorityLastUpdateTime: isManagingBugPriorityLastUpdated.Time,
   404  			SourceCluster: clustering.ClusterID{
   405  				Algorithm: sourceClusterAlgorithm,
   406  				ID:        sourceClusterID,
   407  			},
   408  			BugManagementState:      bugManagementState,
   409  			CreateTime:              creationTime,
   410  			CreateUser:              creationUser,
   411  			LastAuditableUpdateTime: lastAuditableUpdateTime,
   412  			LastAuditableUpdateUser: lastAuditableUpdateUser.StringVal,
   413  			LastUpdateTime:          lastUpdated,
   414  		}
   415  		rs = append(rs, rule)
   416  		return nil
   417  	})
   418  	return rs, err
   419  }
   420  
   421  // Version captures version information about a project's rules.
   422  type Version struct {
   423  	// Predicates is the last time any rule changed its
   424  	// rule predicate (RuleDefinition or IsActive).
   425  	// Also known as "Rules Version" in clustering contexts.
   426  	Predicates time.Time
   427  	// Total is the last time any rule was updated in any way.
   428  	// Pass to ReadDelta when seeking to read changed rules.
   429  	Total time.Time
   430  }
   431  
   432  // ReadVersion reads information about when rules in the given project
   433  // were last updated. This is used to version the set of rules retrieved
   434  // by ReadActive and is typically called in the same transaction.
   435  // It is also used to implement change detection on rule predicates
   436  // for the purpose of triggering re-clustering.
   437  //
   438  // Simply reading the last LastUpdated time of the rules read by ReadActive
   439  // is not sufficient to version the set of rules read, as the most recent
   440  // update may have been to mark a rule inactive (removing it from the set
   441  // that is read).
   442  //
   443  // If the project has no failure association rules, the timestamp
   444  // StartingEpoch is returned.
   445  func ReadVersion(ctx context.Context, projectID string) (Version, error) {
   446  	stmt := spanner.NewStatement(`
   447  		SELECT
   448  		  Max(PredicateLastUpdated) as PredicateLastUpdated,
   449  		  MAX(LastUpdated) as LastUpdated
   450  		FROM FailureAssociationRules
   451  		WHERE Project = @projectID
   452  	`)
   453  	stmt.Params = map[string]any{
   454  		"projectID": projectID,
   455  	}
   456  	var predicateLastUpdated, lastUpdated spanner.NullTime
   457  	it := span.Query(ctx, stmt)
   458  	err := it.Do(func(r *spanner.Row) error {
   459  		err := r.Columns(&predicateLastUpdated, &lastUpdated)
   460  		if err != nil {
   461  			return errors.Annotate(err, "read last updated row").Err()
   462  		}
   463  		return nil
   464  	})
   465  	if err != nil {
   466  		return Version{}, errors.Annotate(err, "query last updated").Err()
   467  	}
   468  	result := Version{
   469  		Predicates: StartingEpoch,
   470  		Total:      StartingEpoch,
   471  	}
   472  	// predicateLastUpdated / lastUpdated are only invalid if there
   473  	// are no failure association rules.
   474  	if predicateLastUpdated.Valid {
   475  		result.Predicates = predicateLastUpdated.Time
   476  	}
   477  	if lastUpdated.Valid {
   478  		result.Total = lastUpdated.Time
   479  	}
   480  	return result, nil
   481  }
   482  
   483  // ReadTotalActiveRules reads the number active rules, for each LUCI Project.
   484  // Only returns entries for projects that have any rules (at all). Combine
   485  // with config if you need zero entries for projects that are defined but
   486  // have no rules.
   487  func ReadTotalActiveRules(ctx context.Context) (map[string]int64, error) {
   488  	stmt := spanner.NewStatement(`
   489  		SELECT
   490  		  project,
   491  		  COUNTIF(IsActive) as active_rules,
   492  		FROM FailureAssociationRules
   493  		GROUP BY project
   494  	`)
   495  	result := make(map[string]int64)
   496  	it := span.Query(ctx, stmt)
   497  	err := it.Do(func(r *spanner.Row) error {
   498  		var project string
   499  		var activeRules int64
   500  		err := r.Columns(&project, &activeRules)
   501  		if err != nil {
   502  			return errors.Annotate(err, "read row").Err()
   503  		}
   504  		result[project] = activeRules
   505  		return nil
   506  	})
   507  	if err != nil {
   508  		return nil, errors.Annotate(err, "query total active rules by project").Err()
   509  	}
   510  	return result, nil
   511  }
   512  
   513  // Create creates a mutation to insert a new failure association rule.
   514  func Create(rule *Entry, user string) (*spanner.Mutation, error) {
   515  	if err := validateRule(rule); err != nil {
   516  		return nil, err
   517  	}
   518  	if err := validateUser(user); err != nil {
   519  		return nil, err
   520  	}
   521  
   522  	bugManagementStateBuf, err := proto.Marshal(rule.BugManagementState)
   523  	if err != nil {
   524  		return nil, errors.Annotate(err, "marshal bug management state").Err()
   525  	}
   526  
   527  	ms := spanutil.InsertMap("FailureAssociationRules", map[string]any{
   528  		"Project":        rule.Project,
   529  		"RuleId":         rule.RuleID,
   530  		"RuleDefinition": rule.RuleDefinition,
   531  		// IsActive uses the value NULL to indicate false, and TRUE to indicate true.
   532  		"IsActive":             spanner.NullBool{Bool: rule.IsActive, Valid: rule.IsActive},
   533  		"PredicateLastUpdated": spanner.CommitTimestamp,
   534  		"BugSystem":            rule.BugID.System,
   535  		"BugId":                rule.BugID.ID,
   536  		// IsManagingBug uses the value NULL to indicate false, and TRUE to indicate true.
   537  		"IsManagingBug":                    spanner.NullBool{Bool: rule.IsManagingBug, Valid: rule.IsManagingBug},
   538  		"IsManagingBugPriority":            rule.IsManagingBugPriority,
   539  		"IsManagingBugPriorityLastUpdated": spanner.CommitTimestamp,
   540  		"SourceClusterAlgorithm":           rule.SourceCluster.Algorithm,
   541  		"SourceClusterId":                  rule.SourceCluster.ID,
   542  		"BugManagementState":               spanutil.Compress(bugManagementStateBuf),
   543  		"CreationTime":                     spanner.CommitTimestamp,
   544  		"CreationUser":                     user,
   545  		"LastAuditableUpdate":              spanner.CommitTimestamp,
   546  		"LastAuditableUpdateUser":          user,
   547  		"LastUpdated":                      spanner.CommitTimestamp,
   548  	})
   549  	return ms, nil
   550  }
   551  
   552  // UpdateOptions are the options that are using during
   553  // the update of a rule.
   554  type UpdateOptions struct {
   555  	// IsAuditableUpdate should be set if any auditable field
   556  	// (that is, any field other than a system-controlled data field)
   557  	// has been updated.
   558  	// If set, the values of these fields will be saved and
   559  	// LastAuditableUpdate and LastAuditableUpdateUser will be updated.
   560  	IsAuditableUpdate bool
   561  	// PredicateUpdated should be set if IsActive and/or RuleDefinition
   562  	// have been updated.
   563  	// If set, these new values of these fields will be saved and
   564  	// PredicateLastUpdated will be updated.
   565  	PredicateUpdated bool
   566  	// IsManagingBugPriorityUpdated should be set if IsManagingBugPriority
   567  	// has been updated.
   568  	// If set, the IsManagingBugPriorityLastUpdated will be updated.
   569  	IsManagingBugPriorityUpdated bool
   570  }
   571  
   572  // Update creates a mutation to update an existing failure association rule.
   573  // Correctly specify the nature of your changes in UpdateOptions to ensure
   574  // those changes are saved and that audit timestamps are updated.
   575  func Update(rule *Entry, options UpdateOptions, user string) (*spanner.Mutation, error) {
   576  	if err := validateRule(rule); err != nil {
   577  		return nil, err
   578  	}
   579  	if err := validateUser(user); err != nil {
   580  		return nil, err
   581  	}
   582  	if options.PredicateUpdated && !options.IsAuditableUpdate {
   583  		return nil, errors.Reason("predicate updates are auditable updates, did you forget to set IsAuditableUpdate?").Err()
   584  	}
   585  	if options.IsManagingBugPriorityUpdated && !options.IsAuditableUpdate {
   586  		return nil, errors.Reason("is managing bug priority updates are auditable updates, did you forget to set IsAuditableUpdate?").Err()
   587  	}
   588  
   589  	bugManagementStateBuf, err := proto.Marshal(rule.BugManagementState)
   590  	if err != nil {
   591  		return nil, errors.Annotate(err, "marshal bug management state").Err()
   592  	}
   593  
   594  	update := map[string]any{
   595  		"Project":            rule.Project,
   596  		"RuleId":             rule.RuleID,
   597  		"BugManagementState": spanutil.Compress(bugManagementStateBuf),
   598  		"LastUpdated":        spanner.CommitTimestamp,
   599  	}
   600  	if options.IsAuditableUpdate {
   601  		update["BugSystem"] = rule.BugID.System
   602  		update["BugId"] = rule.BugID.ID
   603  		// IsManagingBug uses the value NULL to indicate false, and TRUE to indicate true.
   604  		update["IsManagingBug"] = spanner.NullBool{Bool: rule.IsManagingBug, Valid: rule.IsManagingBug}
   605  		update["LastAuditableUpdate"] = spanner.CommitTimestamp
   606  		update["LastAuditableUpdateUser"] = user
   607  
   608  		if options.PredicateUpdated {
   609  			update["RuleDefinition"] = rule.RuleDefinition
   610  			// IsActive uses the value NULL to indicate false, and TRUE to indicate true.
   611  			update["IsActive"] = spanner.NullBool{Bool: rule.IsActive, Valid: rule.IsActive}
   612  			update["PredicateLastUpdated"] = spanner.CommitTimestamp
   613  		}
   614  		if options.IsManagingBugPriorityUpdated {
   615  			update["IsManagingBugPriority"] = rule.IsManagingBugPriority
   616  			update["IsManagingBugPriorityLastUpdated"] = spanner.CommitTimestamp
   617  		}
   618  	}
   619  	ms := spanutil.UpdateMap("FailureAssociationRules", update)
   620  	return ms, nil
   621  }
   622  
   623  func validateRule(r *Entry) error {
   624  	if err := pbutil.ValidateProject(r.Project); err != nil {
   625  		return errors.Annotate(err, "project").Err()
   626  	}
   627  	if !RuleIDRe.MatchString(r.RuleID) {
   628  		return errors.Reason("rule ID: must match %s", RuleIDRe).Err()
   629  	}
   630  	if err := r.BugID.Validate(); err != nil {
   631  		return errors.Annotate(r.BugID.Validate(), "bug ID").Err()
   632  	}
   633  	if r.SourceCluster.Validate() != nil && !r.SourceCluster.IsEmpty() {
   634  		return errors.Annotate(r.SourceCluster.Validate(), "source cluster ID").Err()
   635  	}
   636  	if len(r.RuleDefinition) > MaxRuleDefinitionLength {
   637  		return errors.Reason("rule definition: exceeds maximum length of %v", MaxRuleDefinitionLength).Err()
   638  	}
   639  	_, err := lang.Parse(r.RuleDefinition)
   640  	if err != nil {
   641  		return errors.Annotate(err, "rule definition").Err()
   642  	}
   643  	if err := validateBugManagementState(r.BugManagementState); err != nil {
   644  		return errors.Annotate(err, "bug management state").Err()
   645  	}
   646  	return nil
   647  }
   648  
   649  func validateBugManagementState(state *bugspb.BugManagementState) error {
   650  	if state == nil {
   651  		return errors.Reason("must be set").Err()
   652  	}
   653  	for policy, state := range state.PolicyState {
   654  		if !PolicyIDRe.MatchString(policy) {
   655  			return errors.Reason("policy_state[%q]: key must match pattern %s", policy, PolicyIDRe).Err()
   656  		}
   657  		if state.LastActivationTime != nil {
   658  			if err := state.LastActivationTime.CheckValid(); err != nil {
   659  				return errors.Annotate(err, "policy_state[%q]: last_activation_time", policy).Err()
   660  			}
   661  		}
   662  		if state.LastDeactivationTime != nil {
   663  			if err := state.LastDeactivationTime.CheckValid(); err != nil {
   664  				return errors.Annotate(err, "policy_state[%q]: last_deactivation_time", policy).Err()
   665  			}
   666  		}
   667  	}
   668  	return nil
   669  }
   670  
   671  func validateUser(u string) error {
   672  	if !UserRe.MatchString(u) {
   673  		return errors.New("user must be valid")
   674  	}
   675  	return nil
   676  }
   677  
   678  // GenerateID returns a random 128-bit rule ID, encoded as
   679  // 32 lowercase hexadecimal characters.
   680  func GenerateID() (string, error) {
   681  	randomBytes := make([]byte, 16)
   682  	_, err := rand.Read(randomBytes)
   683  	if err != nil {
   684  		return "", err
   685  	}
   686  	return hex.EncodeToString(randomBytes), nil
   687  }