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 }