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  }