go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/config/validate.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 config 16 17 import ( 18 "fmt" 19 "math" 20 "regexp" 21 "strings" 22 "unicode" 23 "unicode/utf8" 24 25 "go.chromium.org/luci/common/errors" 26 luciproto "go.chromium.org/luci/common/proto" 27 "go.chromium.org/luci/config/validation" 28 29 "go.chromium.org/luci/analysis/internal/analysis/metrics" 30 "go.chromium.org/luci/analysis/internal/bugs" 31 "go.chromium.org/luci/analysis/internal/clustering/algorithms/testname/rules" 32 "go.chromium.org/luci/analysis/pbutil" 33 configpb "go.chromium.org/luci/analysis/proto/config" 34 ) 35 36 const maxHysteresisPercent = 1000 37 38 var ( 39 // https://cloud.google.com/storage/docs/naming-buckets 40 bucketRE = regexp.MustCompile(`^[a-z0-9][a-z0-9\-_.]{1,220}[a-z0-9]$`) 41 bucketMaxLengthBytes = 222 42 43 // From https://source.chromium.org/chromium/infra/infra/+/main:appengine/monorail/project/project_constants.py;l=13. 44 monorailProjectRE = regexp.MustCompile(`^[a-z0-9][-a-z0-9]{0,61}[a-z0-9]$`) 45 monorailProjectMaxLengthBytes = 63 46 47 // https://source.chromium.org/chromium/infra/infra/+/main:luci/appengine/auth_service/proto/realms_config.proto;l=85;drc=04e290f764a293d642d287b0118e9880df4afb35 48 realmRE = regexp.MustCompile(`^[a-z0-9_\.\-/]{1,400}$`) 49 realmMaxLengthBytes = 400 50 51 // Matches valid prefixes to use when displaying bugs. 52 // E.g. "crbug.com", "fxbug.dev". 53 prefixRE = regexp.MustCompile(`^[a-z0-9\-.]{0,64}$`) 54 prefixMaxLengthBytes = 64 55 56 // hostnameRE excludes most invalid hostnames. 57 hostnameRE = regexp.MustCompile(`^[a-z][a-z9-9\-.]{0,62}[a-z]$`) 58 hostnameMaxLengthBytes = 64 59 60 // policyIDRE matches valid bug management policy identifiers. 61 policyIDRE = regexp.MustCompile(`^[a-z]([a-z0-9-]{0,62}[a-z0-9])?$`) 62 policyIDMaxLengthBytes = 64 63 64 // policyHumanReadableNameRE matches a valid bug management policy short descriptions. 65 policyHumanReadableNameRE = regexp.MustCompile("^[[:print:]]{1,100}$") 66 policyHumanReadableMaxLengthBytes = 100 67 68 // nameRE matches valid rule names. 69 ruleNameRE = regexp.MustCompile(`^[a-zA-Z0-9\-(), ]+$`) 70 ruleNameMaxLengthBytes = 100 71 72 // See RFC 3696 Part 3. The syntax below does not allow for spaces 73 // or quoted local parts, which are technically allowed by the spec 74 // but shouldn't be necessary here. 75 ownerEmailRE = regexp.MustCompile("^[A-Za-z0-9!#$%&'*+-/=?^_`.{|}~]{1,64}@google\\.com$") 76 ownerEmailMaxLengthBytes = 64 + len("@google.com") 77 78 // printableASCIIRE matches any input consisting only of printable ASCII 79 // characters. 80 printableASCIIRE = regexp.MustCompile(`^[[:print:]]+$`) 81 82 // Standard maximum lengths, in bytes. For fields, where there 83 // is no obvious maximum length for the input type. 84 longMaxLengthBytes = 10000 85 standardMaxLengthBytes = 100 86 87 // Patterns for BigQuery table. 88 // https://cloud.google.com/resource-manager/docs/creating-managing-projects 89 cloudProjectRE = regexp.MustCompile(`^[a-z][a-z0-9\-]{4,28}[a-z0-9]$`) 90 cloudProjectMaxLengthBytes = 30 91 92 // https://cloud.google.com/bigquery/docs/datasets#dataset-naming 93 datasetRE = regexp.MustCompile(`^[a-zA-Z0-9_]*$`) 94 datasetMaxLengthBytes = 1024 95 96 // https://cloud.google.com/bigquery/docs/tables#table_naming 97 tableRE = regexp.MustCompile(`^[\p{L}\p{M}\p{N}\p{Pc}\p{Pd}\p{Zs}]*$`) 98 tableMaxLengthBytes = 1024 99 100 // labelRE matches valid monorail labels. Note that label comparison 101 // is case-insensitive. Could not find exact validation criteria 102 // in monorail, so supporting a conservative subset of labels. 103 monorailLabelRE = regexp.MustCompile(`^[a-zA-Z0-9\-]+$`) 104 monorailLabelMaxLengthBytes = 60 105 106 unspecifiedMessage = "must be specified" 107 ) 108 109 func validateConfig(ctx *validation.Context, cfg *configpb.Config) { 110 validateStringConfig(ctx, "monorail_hostname", cfg.MonorailHostname, hostnameRE, hostnameMaxLengthBytes) 111 validateStringConfig(ctx, "chunk_gcs_bucket", cfg.ChunkGcsBucket, bucketRE, bucketMaxLengthBytes) 112 // Limit to default max_concurrent_requests of 1000. 113 // https://cloud.google.com/appengine/docs/standard/go111/config/queueref 114 validateIntegerConfig(ctx, "reclustering_workers", cfg.ReclusteringWorkers, 1, 1000) 115 } 116 117 func validateStringConfig(ctx *validation.Context, name, cfg string, re *regexp.Regexp, maxLengthBytes int) { 118 ctx.Enter(name) 119 defer ctx.Exit() 120 if len(cfg) > maxLengthBytes { 121 ctx.Errorf("exceeds maximum allowed length of %v bytes", maxLengthBytes) 122 return 123 } 124 switch err := pbutil.ValidateWithRe(re, cfg); err { 125 case pbutil.Unspecified: 126 ctx.Errorf(unspecifiedMessage) 127 case pbutil.DoesNotMatch: 128 ctx.Errorf("does not match pattern %q", re) 129 } 130 } 131 132 // validateIntegerConfig validates that an integer field is within the 133 // range [minInclusive, maxInclusive]. 134 func validateIntegerConfig(ctx *validation.Context, name string, cfg, minInclusive, maxInclusive int64) { 135 ctx.Enter(name) 136 defer ctx.Exit() 137 138 if cfg < minInclusive || cfg > maxInclusive { 139 if cfg == 0 { 140 ctx.Errorf(unspecifiedMessage) 141 } else { 142 ctx.Errorf("must be in the range [%v, %v]", minInclusive, maxInclusive) 143 } 144 } 145 } 146 147 // validateFloat64Config validates that a float64 field is within the 148 // range [minInclusive, maxInclusive]. 149 func validateFloat64Config(ctx *validation.Context, name string, cfg, minInclusive, maxInclusive float64) { 150 ctx.Enter(name) 151 defer ctx.Exit() 152 153 if math.IsInf(cfg, 0) || math.IsNaN(cfg) { 154 ctx.Errorf("must be a finite number") 155 return 156 } 157 if cfg == 0.0 && (cfg < minInclusive || cfg > maxInclusive) { 158 ctx.Errorf(unspecifiedMessage) 159 return 160 } 161 if cfg < minInclusive || cfg > maxInclusive { 162 ctx.Errorf("must be in the range [%f, %f]", minInclusive, maxInclusive) 163 } 164 } 165 166 // validateProjectConfigRaw deserializes the project-level config message 167 // and passes it through the validator. 168 func validateProjectConfigRaw(ctx *validation.Context, project, content string) *configpb.ProjectConfig { 169 msg := &configpb.ProjectConfig{} 170 if err := luciproto.UnmarshalTextML(content, msg); err != nil { 171 ctx.Errorf("failed to unmarshal as text proto: %s", err) 172 return nil 173 } 174 ValidateProjectConfig(ctx, project, msg) 175 return msg 176 } 177 178 func ValidateProjectConfig(ctx *validation.Context, project string, cfg *configpb.ProjectConfig) { 179 validateClustering(ctx, cfg.Clustering) 180 validateMetrics(ctx, cfg.Metrics) 181 validateBugManagement(ctx, cfg.BugManagement) 182 validateTestStabilityCriteria(ctx, cfg.TestStabilityCriteria) 183 } 184 185 func validateBuganizerDefaultComponent(ctx *validation.Context, component *configpb.BuganizerComponent) { 186 ctx.Enter("default_component") 187 defer ctx.Exit() 188 189 if component == nil { 190 ctx.Errorf(unspecifiedMessage) 191 return 192 } 193 194 ctx.Enter("id") 195 defer ctx.Exit() 196 197 if component.Id == 0 { 198 ctx.Errorf(unspecifiedMessage) 199 } else if component.Id < 0 { 200 ctx.Errorf("must be positive") 201 } 202 } 203 204 func validateBuganizerPriority(ctx *validation.Context, priority configpb.BuganizerPriority) { 205 ctx.Enter("priority") 206 defer ctx.Exit() 207 208 if priority == configpb.BuganizerPriority_BUGANIZER_PRIORITY_UNSPECIFIED { 209 ctx.Errorf(unspecifiedMessage) 210 return 211 } 212 } 213 214 func validateDefaultFieldValues(ctx *validation.Context, fvs []*configpb.MonorailFieldValue) { 215 ctx.Enter("default_field_values") 216 defer ctx.Exit() 217 218 if len(fvs) > 50 { 219 ctx.Errorf("at most 50 field values may be specified") 220 return 221 } 222 for i, fv := range fvs { 223 validateFieldValue(ctx, fmt.Sprintf("[%v]", i), fv) 224 } 225 } 226 227 func validateFieldValue(ctx *validation.Context, name string, fv *configpb.MonorailFieldValue) { 228 ctx.Enter(name) 229 defer ctx.Exit() 230 231 if fv == nil { 232 ctx.Errorf(unspecifiedMessage) 233 return 234 } 235 236 validateFieldID(ctx, "field_id", fv.FieldId) 237 validateStringConfig(ctx, "value", fv.Value, printableASCIIRE, standardMaxLengthBytes) 238 } 239 240 func validateFieldID(ctx *validation.Context, fieldName string, fieldID int64) { 241 ctx.Enter(fieldName) 242 defer ctx.Exit() 243 244 if fieldID == 0 { 245 ctx.Errorf(unspecifiedMessage) 246 } else if fieldID < 0 { 247 ctx.Errorf("must be positive") 248 } 249 } 250 251 // validateMetricThreshold a metric threshold message. If mustBeSatisfiable is set, 252 // the metric threshold must have at least one of one_day, three_day or seven_day set. 253 func validateMetricThreshold(ctx *validation.Context, fieldName string, t *configpb.MetricThreshold, mustBeSatisfiable bool) { 254 ctx.Enter(fieldName) 255 defer ctx.Exit() 256 257 if t == nil { 258 if mustBeSatisfiable { 259 // To be satisfiable, a threshold must be set. 260 ctx.Errorf(unspecifiedMessage) 261 } 262 // Not specified. 263 return 264 } 265 266 if mustBeSatisfiable && (t.OneDay == nil && t.ThreeDay == nil && t.SevenDay == nil) { 267 // To be satisfiable, a threshold must be set. 268 ctx.Errorf("at least one of one_day, three_day and seven_day must be set") 269 } 270 271 validateThresholdValue(ctx, t.OneDay, "one_day") 272 validateThresholdValue(ctx, t.ThreeDay, "three_day") 273 validateThresholdValue(ctx, t.SevenDay, "seven_day") 274 } 275 276 func validateThresholdValue(ctx *validation.Context, value *int64, fieldName string) { 277 ctx.Enter(fieldName) 278 defer ctx.Exit() 279 280 if value != nil && *value <= 0 { 281 ctx.Errorf("value must be positive") 282 } 283 if value != nil && *value >= 1000*1000 { 284 ctx.Errorf("value must be less than one million") 285 } 286 } 287 288 type validateImplicationOptions struct { 289 // A human-readable description of the LHS threshold, e.g. 290 // "the bug-filing threshold", for use in error messages. 291 lhsDescription string 292 // A human-readable statement motivating the threshold implication 293 // check, for use in error messages. E.g. 294 // "this ensures that bugs which are filed meet the criteria to stay open". 295 implicationDescription string 296 } 297 298 // validateMetricThresholdImpliedBy verifies the threshold `lhs` implies 299 // the threshold `rhs` is satisfied, i.e. lhs => rhs. 300 // fieldName is the name of the field that contains the `rhs` threshold. 301 func validateMetricThresholdImpliedBy(ctx *validation.Context, rhsFieldName string, rhs *configpb.MetricThreshold, lhs *configpb.MetricThreshold, opts validateImplicationOptions) { 302 ctx.Enter(rhsFieldName) 303 defer ctx.Exit() 304 305 if rhs == nil { 306 rhs = &configpb.MetricThreshold{} 307 } 308 if lhs == nil { 309 // Bugs are not filed based on this metric. So 310 // we do not need to check that bugs filed 311 // based on this metric will stay open. 312 return 313 } 314 315 // Thresholds are met if ANY of the 1, 3 or 7-day thresholds are met 316 // (i.e. semantically they are an 'OR' of the day sub-thresholds). 317 // 318 // This means checking lhs => rhs actually means checking: 319 // value(1-day) >= lhs(1-day) OR value(3-day) >= lhs(3-day) OR value(7-day) >= lhs(7-day) => 320 // value(1-day) >= rhs(1-day) OR value(3-day) >= rhs(3-day) OR value(7-day) >= rhs(7-day) 321 // (equation (1)). 322 // 323 // Where: 324 // - value(X-day) is the time-changing variable identifying the metric calculated on X days of data, 325 // - lhs(X-day) is the LHS's X-day threshold, 326 // - rhs(X-day) is the RHS's X-day threshold. 327 // Where lhs or rhs do not have a threshold set for a given day, e.g. lhs.OneDay = nil, 328 // this can be taken instead as infinity being the threshold (i.e. lhs(1-day) = infinity). 329 // 330 // If lhs(1-day) >= rhs(1-day) AND lhs(3-day) >= rhs(3-day) AND lhs(7-day) >= rhs(7-day), 331 // i.e. all LHS are strictly stronger than the corresponding RHS thresholds, `lhs => rhs` 332 // is trivially shown. However, this is not the only case where `lhs => rhs` can be 333 // shown. For example, consider if the LHS is: 334 // - User CLs Failed Presubmit (1-day) >= 10 335 // and the RHS is: 336 // - User CLs Failed Presubmit (3-day) >= 5 337 // 338 // The LHS still implies the RHS is true. This is because User CLs Failed Presubmit (3-day) 339 // >= User CLs Failed Presubmit (1-day). To derive more precise conditions for testing if 340 // `lhs => rhs`, we follow a systematic approach, explained below. 341 // 342 // To show equation (1) above is true, we must show: 343 // value(X-day) >= lhs(X-day) 344 // => 345 // value(1-day) >= rhs(1-day) OR value(3-day) >= rhs(3-day) OR value(7-day) >= rhs(7-day) 346 // for ALL of X = 1, 3, 7 (equation (2)). 347 // 348 // Let us consider the case for X = 3 days. 349 // 350 // From the LHS of the implication in equation (2) we are given that `value(3-day) >= lhs(3-day)` (context 1). 351 // We must show one of `value(1-day) >= rhs(1-day)` OR `value(3-day) >= rhs(3-day)` OR `value(7-day) >= rhs(7-day)`. 352 // 353 // - We cannot show value(1-day) >= rhs(1-day) as we only have a bound on the value of value(3-day), 354 // so we proceed to the second 'OR' case. 355 // - We can show value(3-day) >= rhs(3-day) if lhs(3-day) >= rhs(3-day). 356 // This follows as we have: 357 // - value(3-day) >= lhs(3-day) // (from context 1) 358 // - lhs(3-day) >= rhs(3-day) // IF condition 359 // Alternatively, 360 // - We also can show value(7-day) >= rhs(7-day) is true if lhs(3-day) >= rhs(7-day), 361 // This follows as we have: 362 // - value(7-day) >= value(3-day) // property of the metric values 363 // - value(3-day) >= lhs(3-day) // (context 1) 364 // - lhs(3-day) >= rhs(7-day) // IF condition 365 // 366 // In summary, to prove implication for the case of X = 3 days, we need 367 // lhs(3-day) >= rhs(3-day) OR lhs(3-day) >= rhs(7-day). 368 // 369 // The same pattern applies to X = 1 days and X = 7 days. The criteria is: 370 // For X = 1, lhs(1-day) >= rhs(1-day) OR lhs(1-day) >= rhs(3-day) OR lhs(1-day) >= rhs(7-day). 371 // For X = 7, lhs(7-day) >= rhs(7-day). 372 // 373 // Note that for X = 1, this is the same as 374 // `lhs(1-day) >= min(rhs(1-day), rhs(3-day), rhs(7-day))`. 375 // 376 // Using this simplification, we get we must check: 377 // - lhs(1-day) >= min(rhs(1-day), rhs(3-day), rhs(7-day)) AND 378 // - lhs(3-day) >= min(rhs(3-day), rhs(7-day)) AND 379 // - lhs(7-day) >= rhs(7-day) 380 // To show LHS => RHS. 381 382 oneDayRhsThreshold := minOfThresholds(rhs.OneDay, rhs.ThreeDay, rhs.SevenDay) 383 threeDayRhsThreshold := minOfThresholds(rhs.ThreeDay, rhs.SevenDay) 384 385 // Attribute the failure of the 1-day criteria to rhs(1-day), even 386 // if rhs(3-day) or rhs(7-day) can fix it, as this is more understandable 387 // for users. 388 validateBugFilingThresholdSatisfiesThresold(ctx, oneDayRhsThreshold, lhs.OneDay, "one_day", opts.lhsDescription, opts.implicationDescription) 389 // Attribute the failure of the 3-day criteria to rhs(3-day), even 390 // if rhs(7-day) can fix it, as this is more understandable 391 // for users. 392 validateBugFilingThresholdSatisfiesThresold(ctx, threeDayRhsThreshold, lhs.ThreeDay, "three_day", opts.lhsDescription, opts.implicationDescription) 393 validateBugFilingThresholdSatisfiesThresold(ctx, rhs.SevenDay, lhs.SevenDay, "seven_day", opts.lhsDescription, opts.implicationDescription) 394 } 395 396 func minOfThresholds(thresholds ...*int64) *int64 { 397 var result *int64 398 for _, t := range thresholds { 399 if t != nil && (result == nil || *t < *result) { 400 result = t 401 } 402 } 403 return result 404 } 405 406 func validateBugFilingThresholdSatisfiesThresold(ctx *validation.Context, rhsThreshold *int64, lhsThres *int64, fieldName, lhsDescription, implicationDescription string) { 407 ctx.Enter(fieldName) 408 defer ctx.Exit() 409 410 if lhsThres == nil { 411 // Bugs are not filed based on this threshold. 412 return 413 } 414 if *lhsThres <= 0 { 415 // The bug-filing threshold is invalid. This is already reported as an 416 // error elsewhere. 417 return 418 } 419 if rhsThreshold == nil { 420 ctx.Errorf("%s threshold must be set, with a value of at most %v (%s); %s", fieldName, *lhsThres, lhsDescription, implicationDescription) 421 } else if *rhsThreshold > *lhsThres { 422 ctx.Errorf("value must be at most %v (%s); %s", *lhsThres, lhsDescription, implicationDescription) 423 } 424 } 425 426 func validateClustering(ctx *validation.Context, ca *configpb.Clustering) { 427 ctx.Enter("clustering") 428 defer ctx.Exit() 429 430 if ca == nil { 431 return 432 } 433 ctx.Enter("test_name_rules") 434 for i, r := range ca.TestNameRules { 435 ctx.Enter("[%v]", i) 436 validateTestNameRule(ctx, r) 437 ctx.Exit() 438 } 439 ctx.Exit() 440 ctx.Enter("reason_mask_patterns") 441 for i, p := range ca.ReasonMaskPatterns { 442 ctx.Enter("[%v]", i) 443 validateReasonMaskPattern(ctx, p) 444 ctx.Exit() 445 } 446 ctx.Exit() 447 } 448 449 func validateTestNameRule(ctx *validation.Context, r *configpb.TestNameClusteringRule) { 450 validateStringConfig(ctx, "name", r.Name, ruleNameRE, ruleNameMaxLengthBytes) 451 452 // Check the fields are non-empty. Their structure will be checked 453 // by "Compile" below. 454 validateStringConfig(ctx, "like_template", r.LikeTemplate, printableASCIIRE, 1024) 455 validateStringConfig(ctx, "pattern", r.Pattern, printableASCIIRE, 1024) 456 457 _, err := rules.Compile(r) 458 if err != nil { 459 ctx.Error(err) 460 } 461 } 462 463 func validateReasonMaskPattern(ctx *validation.Context, p string) { 464 if p == "" { 465 ctx.Errorf("empty pattern is not allowed") 466 } 467 re, err := regexp.Compile(p) 468 if err != nil { 469 ctx.Errorf("could not compile pattern: %s", err) 470 } else { 471 if re.NumSubexp() != 1 { 472 ctx.Errorf("pattern must contain exactly one parenthesised capturing subexpression indicating the text to mask") 473 } 474 } 475 } 476 477 func validateMetrics(ctx *validation.Context, m *configpb.Metrics) { 478 ctx.Enter("metrics") 479 defer ctx.Exit() 480 481 if m == nil { 482 // Allow non-existent metrics section. 483 return 484 } 485 seenIDs := map[string]struct{}{} 486 for i, o := range m.Overrides { 487 ctx.Enter("[%v]", i) 488 validateMetricOverride(ctx, o, seenIDs) 489 ctx.Exit() 490 } 491 } 492 493 func validateMetricOverride(ctx *validation.Context, o *configpb.Metrics_MetricOverride, seenIDs map[string]struct{}) { 494 validateMetricID(ctx, o.MetricId, seenIDs) 495 if o.SortPriority != nil { 496 validateSortPriority(ctx, int64(*o.SortPriority)) 497 } 498 } 499 500 func validateMetricID(ctx *validation.Context, metricID string, seenIDs map[string]struct{}) { 501 ctx.Enter("metric_id") 502 defer ctx.Exit() 503 504 if _, err := metrics.ByID(metrics.ID(metricID)); err != nil { 505 ctx.Error(err) 506 } else { 507 if _, ok := seenIDs[metricID]; ok { 508 ctx.Errorf("metric with ID %q appears in collection more than once", metricID) 509 } 510 seenIDs[metricID] = struct{}{} 511 } 512 } 513 514 func validateSortPriority(ctx *validation.Context, value int64) { 515 ctx.Enter("sort_priority") 516 defer ctx.Exit() 517 518 if value <= 0 { 519 ctx.Errorf("value must be positive") 520 } 521 } 522 523 func validateBugManagement(ctx *validation.Context, bm *configpb.BugManagement) { 524 ctx.Enter("bug_management") 525 defer ctx.Exit() 526 527 if bm == nil { 528 // Allow non-existent bug managment section. 529 return 530 } 531 532 validateBugManagementPolicies(ctx, bm.Policies) 533 534 if bm.DefaultBugSystem == configpb.BugSystem_MONORAIL && bm.Monorail == nil { 535 ctx.Errorf("monorail section is required when the default_bug_system is Monorail") 536 return 537 } 538 if bm.DefaultBugSystem == configpb.BugSystem_BUGANIZER && bm.Buganizer == nil { 539 ctx.Errorf("buganizer section is required when the default_bug_system is Buganizer") 540 return 541 } 542 if bm.Buganizer != nil || bm.Monorail != nil { 543 // Default bug system must be specified if either Buganizer or Monorail is configured. 544 validateDefaultBugSystem(ctx, bm.DefaultBugSystem) 545 } 546 validateBuganizer(ctx, bm.Buganizer) 547 validateMonorail(ctx, bm.Monorail) 548 } 549 550 func validateDefaultBugSystem(ctx *validation.Context, value configpb.BugSystem) { 551 ctx.Enter("default_bug_system") 552 defer ctx.Exit() 553 if value == configpb.BugSystem_BUG_SYSTEM_UNSPECIFIED { 554 ctx.Errorf(unspecifiedMessage) 555 } 556 } 557 558 func validateBuganizer(ctx *validation.Context, cfg *configpb.BuganizerProject) { 559 ctx.Enter("buganizer") 560 defer ctx.Exit() 561 562 if cfg == nil { 563 // Allow non-existent buganizer section. 564 return 565 } 566 validateBuganizerDefaultComponent(ctx, cfg.DefaultComponent) 567 } 568 569 func validateMonorail(ctx *validation.Context, cfg *configpb.MonorailProject) { 570 ctx.Enter("monorail") 571 defer ctx.Exit() 572 573 if cfg == nil { 574 // Allow non-existent monorail section. 575 return 576 } 577 578 validateStringConfig(ctx, "project", cfg.Project, monorailProjectRE, monorailProjectMaxLengthBytes) 579 validateDefaultFieldValues(ctx, cfg.DefaultFieldValues) 580 validateFieldID(ctx, "priority_field_id", cfg.PriorityFieldId) 581 validateStringConfig(ctx, "display_prefix", cfg.DisplayPrefix, prefixRE, prefixMaxLengthBytes) 582 validateStringConfig(ctx, "monorail_hostname", cfg.MonorailHostname, hostnameRE, hostnameMaxLengthBytes) 583 } 584 585 func validateBugManagementPolicies(ctx *validation.Context, policies []*configpb.BugManagementPolicy) { 586 ctx.Enter("policies") 587 defer ctx.Exit() 588 589 if len(policies) > 50 { 590 ctx.Errorf("exceeds maximum of 50 policies") 591 return 592 } 593 594 policyIDs := make(map[string]struct{}) 595 for i, policy := range policies { 596 validateBugManagementPolicy(ctx, fmt.Sprintf("[%v]", i), policy, policyIDs) 597 } 598 } 599 600 func validateBugManagementPolicy(ctx *validation.Context, name string, p *configpb.BugManagementPolicy, seenIDs map[string]struct{}) { 601 ctx.Enter(name) 602 defer ctx.Exit() 603 604 if p == nil { 605 ctx.Errorf(unspecifiedMessage) 606 return 607 } 608 609 validateBugManagementPolicyID(ctx, p.Id, seenIDs) 610 validateBugManagementPolicyOwners(ctx, p.Owners) 611 validateStringConfig(ctx, "human_readable_name", p.HumanReadableName, policyHumanReadableNameRE, policyHumanReadableMaxLengthBytes) 612 validateBuganizerPriority(ctx, p.Priority) 613 validateBugManagementPolicyMetrics(ctx, p.Metrics) 614 validateBugManagementPolicyExplanation(ctx, p.Explanation) 615 validateBugManagementPolicyBugTemplate(ctx, p.BugTemplate) 616 } 617 618 func validateBugManagementPolicyOwners(ctx *validation.Context, owners []string) { 619 ctx.Enter("owners") 620 defer ctx.Exit() 621 622 if len(owners) == 0 { 623 ctx.Errorf("at least one owner must be specified") 624 return 625 } 626 if len(owners) > 10 { 627 ctx.Errorf("exceeds maximum of 10 owners") 628 return 629 } 630 for i, owner := range owners { 631 validateStringConfig(ctx, fmt.Sprintf("[%v]", i), owner, ownerEmailRE, ownerEmailMaxLengthBytes) 632 } 633 } 634 635 func validateBugManagementPolicyMetrics(ctx *validation.Context, metrics []*configpb.BugManagementPolicy_Metric) { 636 ctx.Enter("metrics") 637 defer ctx.Exit() 638 639 if len(metrics) == 0 { 640 ctx.Errorf("at least one metric must be specified") 641 return 642 } 643 if len(metrics) > 10 { 644 ctx.Errorf("exceeds maximum of 10 metrics") 645 return 646 } 647 metricIDs := make(map[string]struct{}) 648 for i, metric := range metrics { 649 validateBugManagementPolicyMetric(ctx, fmt.Sprintf("[%v]", i), metric, metricIDs) 650 } 651 } 652 653 func validateBugManagementPolicyID(ctx *validation.Context, id string, seenIDs map[string]struct{}) { 654 ctx.Enter("id") 655 defer ctx.Exit() 656 657 if len(id) > policyIDMaxLengthBytes { 658 ctx.Errorf("exceeds maximum allowed length of %v bytes", policyIDMaxLengthBytes) 659 return 660 } 661 switch err := pbutil.ValidateWithRe(policyIDRE, id); err { 662 case pbutil.Unspecified: 663 ctx.Errorf(unspecifiedMessage) 664 return 665 case pbutil.DoesNotMatch: 666 ctx.Errorf("does not match pattern %q", policyIDRE) 667 return 668 } 669 if _, ok := seenIDs[id]; ok { 670 ctx.Errorf("policy with ID %q appears in the collection more than once", id) 671 } 672 seenIDs[id] = struct{}{} 673 } 674 675 func validateBugManagementPolicyMetric(ctx *validation.Context, name string, m *configpb.BugManagementPolicy_Metric, seenIDs map[string]struct{}) { 676 ctx.Enter(name) 677 defer ctx.Exit() 678 679 validateMetricID(ctx, m.MetricId, seenIDs) 680 681 // It is permissible for policies to not have an activation threshold. In 682 // this case the policy will not activate on new rules, but existing activations 683 // can de-activate. 684 mustBeSatifiable := false 685 validateMetricThreshold(ctx, "activation_threshold", m.ActivationThreshold, mustBeSatifiable) 686 687 // All policies must have a de-activation threshold, to allow bugs to 688 // eventually close. 689 mustBeSatifiable = true 690 validateMetricThreshold(ctx, "deactivation_threshold", m.DeactivationThreshold, mustBeSatifiable) 691 692 // Verify that if the activation_threshold is met, the deactivation_threshold is also met. 693 opts := validateImplicationOptions{ 694 lhsDescription: "the activation threshold", 695 implicationDescription: "this ensures policies which activate do not immediately de-activate", 696 } 697 validateMetricThresholdImpliedBy(ctx, "deactivation_threshold", m.DeactivationThreshold, m.ActivationThreshold, opts) 698 } 699 700 func validateBugManagementPolicyExplanation(ctx *validation.Context, e *configpb.BugManagementPolicy_Explanation) { 701 ctx.Enter("explanation") 702 defer ctx.Exit() 703 704 if e == nil { 705 ctx.Errorf(unspecifiedMessage) 706 return 707 } 708 709 if err := ValidateRunesGraphicOrNewline(e.ProblemHtml, longMaxLengthBytes); err != nil { 710 ctx.Enter("problem_html") 711 ctx.Error(err) 712 ctx.Exit() 713 } 714 if err := ValidateRunesGraphicOrNewline(e.ActionHtml, longMaxLengthBytes); err != nil { 715 ctx.Enter("action_html") 716 ctx.Error(err) 717 ctx.Exit() 718 } 719 } 720 721 func validateBugManagementPolicyBugTemplate(ctx *validation.Context, t *configpb.BugManagementPolicy_BugTemplate) { 722 ctx.Enter("bug_template") 723 defer ctx.Exit() 724 725 if t == nil { 726 ctx.Errorf(unspecifiedMessage) 727 return 728 } 729 730 validateCommentTemplate(ctx, t.CommentTemplate) 731 validateBugManagementPolicyBugTemplateBuganizer(ctx, t.Buganizer) 732 validateBugManagementPolicyBugTemplateMonorail(ctx, t.Monorail) 733 } 734 735 func validateCommentTemplate(ctx *validation.Context, t string) { 736 ctx.Enter("comment_template") 737 defer ctx.Exit() 738 739 if t == "" { 740 // It is acceptable for a policy not to comment on a bug. 741 return 742 } 743 if err := ValidateRunesGraphicOrNewline(t, longMaxLengthBytes); err != nil { 744 ctx.Error(err) 745 return 746 } 747 748 tmpl, err := bugs.ParseTemplate(t) 749 if err != nil { 750 ctx.Errorf("parsing template: %s", err) 751 return 752 } 753 if err := tmpl.Validate(); err != nil { 754 ctx.Errorf("validate template: %s", err) 755 } 756 } 757 758 // ValidateRunesGraphicOrNewline validates a value: 759 // - is non-empty 760 // - is a valid UTF-8 string 761 // - contains only runes matching unicode.IsGraphic or '\n', and 762 // - has a specified maximum length. 763 func ValidateRunesGraphicOrNewline(value string, maxLengthInBytes int) error { 764 if value == "" { 765 return errors.Reason(unspecifiedMessage).Err() 766 } 767 if len(value) > maxLengthInBytes { 768 return errors.Reason("exceeds maximum allowed length of %v bytes", maxLengthInBytes).Err() 769 } 770 if !utf8.ValidString(value) { 771 return errors.Reason("not a valid UTF-8 string").Err() 772 } 773 for i, r := range value { 774 if !unicode.IsGraphic(r) && r != rune('\n') { 775 return errors.Reason("unicode rune %q at index %v is not graphic or newline character", r, i).Err() 776 } 777 } 778 return nil 779 } 780 781 func validateBugManagementPolicyBugTemplateBuganizer(ctx *validation.Context, b *configpb.BugManagementPolicy_BugTemplate_Buganizer) { 782 ctx.Enter("buganizer") 783 defer ctx.Exit() 784 785 if b == nil { 786 // It is valid not specify buganizer-specific template options. 787 return 788 } 789 790 validateBuganizerHotlists(ctx, b.Hotlists) 791 } 792 793 func validateBuganizerHotlists(ctx *validation.Context, hotlists []int64) { 794 ctx.Enter("hotlists") 795 defer ctx.Exit() 796 if len(hotlists) > 5 { 797 ctx.Errorf("exceeds maximum of 5 hotlists") 798 } 799 seenIDs := map[int64]struct{}{} 800 for i, hotlist := range hotlists { 801 ctx.Enter("[%v]", i) 802 if hotlist <= 0 { 803 ctx.Errorf("ID must be positive") 804 } else { 805 if _, ok := seenIDs[hotlist]; ok { 806 ctx.Errorf("ID %v appears in collection more than once", hotlist) 807 } 808 seenIDs[hotlist] = struct{}{} 809 } 810 ctx.Exit() 811 } 812 } 813 814 func validateBugManagementPolicyBugTemplateMonorail(ctx *validation.Context, m *configpb.BugManagementPolicy_BugTemplate_Monorail) { 815 ctx.Enter("monorail") 816 defer ctx.Exit() 817 818 if m == nil { 819 // It is valid not specify monorail-specific template options. 820 return 821 } 822 823 validateMonorailLabels(ctx, m.Labels) 824 } 825 826 func validateMonorailLabels(ctx *validation.Context, labels []string) { 827 ctx.Enter("labels") 828 defer ctx.Exit() 829 if len(labels) > 5 { 830 ctx.Errorf("exceeds maximum of 5 labels") 831 } 832 seenLabels := map[string]struct{}{} 833 for i, label := range labels { 834 validateStringConfig(ctx, fmt.Sprintf("[%v]", i), label, monorailLabelRE, monorailLabelMaxLengthBytes) 835 836 // Check for duplicates, case-insensitively (as monorail handles 837 // labels in a case-insensitive way). 838 if _, ok := seenLabels[strings.ToLower(label)]; ok { 839 ctx.Enter("[%v]", i) 840 ctx.Errorf("label %q appears in collection more than once", strings.ToLower(label)) 841 ctx.Exit() 842 } 843 seenLabels[label] = struct{}{} 844 } 845 } 846 847 func validateTestStabilityCriteria(ctx *validation.Context, t *configpb.TestStabilityCriteria) { 848 ctx.Enter("test_stability_criteria") 849 defer ctx.Exit() 850 851 if t == nil { 852 // It is valid not to specify test stability criteria. 853 return 854 } 855 856 validateFailureRateCriteria(ctx, t.FailureRate) 857 validateFlakeRateCriteria(ctx, t.FlakeRate) 858 } 859 860 func validateFailureRateCriteria(ctx *validation.Context, f *configpb.TestStabilityCriteria_FailureRateCriteria) { 861 ctx.Enter("failure_rate") 862 defer ctx.Exit() 863 864 if f == nil { 865 ctx.Errorf(unspecifiedMessage) 866 return 867 } 868 validateIntegerConfig(ctx, "failure_threshold", int64(f.FailureThreshold), 1, 10) 869 validateIntegerConfig(ctx, "consecutive_failure_threshold", int64(f.ConsecutiveFailureThreshold), 1, 10) 870 } 871 872 func validateFlakeRateCriteria(ctx *validation.Context, f *configpb.TestStabilityCriteria_FlakeRateCriteria) { 873 ctx.Enter("flake_rate") 874 defer ctx.Exit() 875 876 if f == nil { 877 ctx.Errorf(unspecifiedMessage) 878 return 879 } 880 881 // Window sizes on the order of 100-10,000 source verdicts are expected in normal operation. 882 // 1,000,000 should be far larger than anything we ever need to use. 883 const largeValue = 1_000_000 884 validateIntegerConfig(ctx, "min_window", int64(f.MinWindow), 0, largeValue) 885 validateIntegerConfig(ctx, "flake_threshold", int64(f.FlakeThreshold), 1, largeValue) 886 validateFloat64Config(ctx, "flake_rate_threshold", f.FlakeRateThreshold, 0.0, 1.0) 887 }