go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/rpc/names.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 rpc
    16  
    17  import (
    18  	"fmt"
    19  	"net/url"
    20  	"regexp"
    21  
    22  	"go.chromium.org/luci/common/errors"
    23  	rdbpbutil "go.chromium.org/luci/resultdb/pbutil"
    24  
    25  	"go.chromium.org/luci/analysis/internal/analysis/metrics"
    26  	"go.chromium.org/luci/analysis/internal/clustering"
    27  	"go.chromium.org/luci/analysis/internal/clustering/rules"
    28  	"go.chromium.org/luci/analysis/internal/config"
    29  	"go.chromium.org/luci/analysis/pbutil"
    30  )
    31  
    32  // Regular expressions for matching resource names used in APIs.
    33  var (
    34  	GenericKeyPattern = "[a-z0-9\\-]+"
    35  	// ClusterNameRe performs partial validation of a cluster resource name.
    36  	// Cluster algorithm and ID must be further validated by
    37  	// ClusterID.Validate().
    38  	ClusterNameRe = regexp.MustCompile(`^projects/(` + pbutil.ProjectRePattern + `)/clusters/(` + GenericKeyPattern + `)/(` + GenericKeyPattern + `)$`)
    39  	// ClusterFailuresNameRe performs a partial validation of the resource
    40  	// name for a cluster's failures.
    41  	// Cluster algorithm and ID must be further validated by
    42  	// ClusterID.Validate().
    43  	ClusterFailuresNameRe = regexp.MustCompile(`^projects/(` + pbutil.ProjectRePattern + `)/clusters/(` + GenericKeyPattern + `)/(` + GenericKeyPattern + `)/failures$`)
    44  	// ClusterExoneratedTestVariantsNameRe and
    45  	// ClusterExoneratedTestVariantBranchesNameRe performs a partial
    46  	// validation of the resource name for a cluster's exonerated
    47  	// test variant (branches).
    48  	// Cluster algorithm and ID must be further validated by
    49  	// ClusterID.Validate().
    50  	ClusterExoneratedTestVariantsNameRe        = regexp.MustCompile(`^projects/(` + pbutil.ProjectRePattern + `)/clusters/(` + GenericKeyPattern + `)/(` + GenericKeyPattern + `)/exoneratedTestVariants$`)
    51  	ClusterExoneratedTestVariantBranchesNameRe = regexp.MustCompile(`^projects/(` + pbutil.ProjectRePattern + `)/clusters/(` + GenericKeyPattern + `)/(` + GenericKeyPattern + `)/exoneratedTestVariantBranches$`)
    52  	ProjectNameRe                              = regexp.MustCompile(`^projects/(` + pbutil.ProjectRePattern + `)$`)
    53  	ProjectConfigNameRe                        = regexp.MustCompile(`^projects/(` + pbutil.ProjectRePattern + `)/config$`)
    54  	ProjectMetricNameRe                        = regexp.MustCompile(`^projects/(` + pbutil.ProjectRePattern + `)/metrics/(` + metrics.MetricIDPattern + `)$`)
    55  	ReclusteringProgressNameRe                 = regexp.MustCompile(`^projects/(` + pbutil.ProjectRePattern + `)/reclusteringProgress$`)
    56  	RuleNameRe                                 = regexp.MustCompile(`^projects/(` + pbutil.ProjectRePattern + `)/rules/(` + rules.RuleIDRePattern + `)$`)
    57  	// TestVariantBranchNameRe performs a partial validation of a TestVariantBranch name.
    58  	// Test ID must be further validated with ValidateTestID.
    59  	TestVariantBranchNameRe = regexp.MustCompile(`^projects/(` + pbutil.ProjectRePattern + `)/tests/([^/]+)/variants/(` + config.VariantHashRePattern + `)/refs/(` + config.RefHashRePattern + `)$`)
    60  )
    61  
    62  // parseProjectName parses a project resource name into a project ID.
    63  func parseProjectName(name string) (project string, err error) {
    64  	match := ProjectNameRe.FindStringSubmatch(name)
    65  	if match == nil {
    66  		return "", errors.New("invalid project name, expected format: projects/{project}")
    67  	}
    68  	return match[1], nil
    69  }
    70  
    71  // parseProjectConfigName parses a project config resource name into a project ID.
    72  func parseProjectConfigName(name string) (project string, err error) {
    73  	match := ProjectConfigNameRe.FindStringSubmatch(name)
    74  	if match == nil {
    75  		return "", errors.New("invalid project config name, expected format: projects/{project}/config")
    76  	}
    77  	return match[1], nil
    78  }
    79  
    80  // parseProjectMetricName parses a project metric name into its constituent
    81  // project and metric ID parts.
    82  func parseProjectMetricName(name string) (project string, metricID metrics.ID, err error) {
    83  	match := ProjectMetricNameRe.FindStringSubmatch(name)
    84  	if match == nil {
    85  		return "", "", errors.New("invalid project metric name, expected format: projects/{project}/metrics/{metric_id}")
    86  	}
    87  	return match[1], metrics.ID(match[2]), nil
    88  }
    89  
    90  // parseReclusteringProgressName parses a reclustering progress resource name
    91  // into its constituent project ID part.
    92  func parseReclusteringProgressName(name string) (project string, err error) {
    93  	match := ReclusteringProgressNameRe.FindStringSubmatch(name)
    94  	if match == nil {
    95  		return "", errors.New("invalid reclustering progress name, expected format: projects/{project}/reclusteringProgress")
    96  	}
    97  	return match[1], nil
    98  }
    99  
   100  // parseRuleName parses a rule resource name into its constituent ID parts.
   101  func parseRuleName(name string) (project, ruleID string, err error) {
   102  	match := RuleNameRe.FindStringSubmatch(name)
   103  	if match == nil {
   104  		return "", "", errors.New("invalid rule name, expected format: projects/{project}/rules/{rule_id}")
   105  	}
   106  	return match[1], match[2], nil
   107  }
   108  
   109  // parseClusterName parses a cluster resource name into its constituent ID
   110  // parts. Algorithm aliases are resolved to concrete algorithm names.
   111  func parseClusterName(name string) (project string, clusterID clustering.ClusterID, err error) {
   112  	if name == "" {
   113  		return "", clustering.ClusterID{}, errors.Reason("must be specified").Err()
   114  	}
   115  	match := ClusterNameRe.FindStringSubmatch(name)
   116  	if match == nil {
   117  		return "", clustering.ClusterID{}, errors.New("invalid cluster name, expected format: projects/{project}/clusters/{cluster_alg}/{cluster_id}")
   118  	}
   119  	algorithm := resolveAlgorithm(match[2])
   120  	id := match[3]
   121  	cID := clustering.ClusterID{Algorithm: algorithm, ID: id}
   122  	if err := cID.Validate(); err != nil {
   123  		return "", clustering.ClusterID{}, errors.Annotate(err, "invalid cluster identity").Err()
   124  	}
   125  	return match[1], cID, nil
   126  }
   127  
   128  // parseClusterFailuresName parses the resource name for a cluster's failures
   129  // into its constituent ID parts. Algorithm aliases are resolved to
   130  // concrete algorithm names.
   131  func parseClusterFailuresName(name string) (project string, clusterID clustering.ClusterID, err error) {
   132  	match := ClusterFailuresNameRe.FindStringSubmatch(name)
   133  	if match == nil {
   134  		return "", clustering.ClusterID{}, errors.New("invalid cluster failures name, expected format: projects/{project}/clusters/{cluster_alg}/{cluster_id}/failures")
   135  	}
   136  	algorithm := resolveAlgorithm(match[2])
   137  	id := match[3]
   138  	cID := clustering.ClusterID{Algorithm: algorithm, ID: id}
   139  	if err := cID.Validate(); err != nil {
   140  		return "", clustering.ClusterID{}, errors.Annotate(err, "cluster id").Err()
   141  	}
   142  	return match[1], cID, nil
   143  }
   144  
   145  // parseClusterExoneratedTestVariantsName parses the resource name for a cluster's
   146  // exonerated test variants into its constituent ID parts. Algorithm aliases are
   147  // resolved to concrete algorithm names.
   148  func parseClusterExoneratedTestVariantsName(name string) (project string, clusterID clustering.ClusterID, err error) {
   149  	match := ClusterExoneratedTestVariantsNameRe.FindStringSubmatch(name)
   150  	if match == nil {
   151  		return "", clustering.ClusterID{}, errors.New("invalid resource name, expected format: projects/{project}/clusters/{cluster_alg}/{cluster_id}/exoneratedTestVariants")
   152  	}
   153  	algorithm := resolveAlgorithm(match[2])
   154  	id := match[3]
   155  	cID := clustering.ClusterID{Algorithm: algorithm, ID: id}
   156  	if err := cID.Validate(); err != nil {
   157  		return "", clustering.ClusterID{}, errors.Annotate(err, "cluster id").Err()
   158  	}
   159  	return match[1], cID, nil
   160  }
   161  
   162  // parseClusterExoneratedTestVariantBranchesName parses the resource name for a
   163  // cluster's exonerated test variant branches into its constituent ID parts.
   164  // Algorithm aliases are resolved to concrete algorithm names.
   165  func parseClusterExoneratedTestVariantBranchesName(name string) (project string, clusterID clustering.ClusterID, err error) {
   166  	match := ClusterExoneratedTestVariantBranchesNameRe.FindStringSubmatch(name)
   167  	if match == nil {
   168  		return "", clustering.ClusterID{}, errors.New("invalid resource name, expected format: projects/{project}/clusters/{cluster_alg}/{cluster_id}/exoneratedTestVariantBranches")
   169  	}
   170  	algorithm := resolveAlgorithm(match[2])
   171  	id := match[3]
   172  	cID := clustering.ClusterID{Algorithm: algorithm, ID: id}
   173  	if err := cID.Validate(); err != nil {
   174  		return "", clustering.ClusterID{}, errors.Annotate(err, "cluster id").Err()
   175  	}
   176  	return match[1], cID, nil
   177  }
   178  
   179  // parseTestVariantBranchName parses the resource name into project, test_id,
   180  // variant hash and ref hash.
   181  func parseTestVariantBranchName(name string) (project, testID, variantHash, refHash string, err error) {
   182  	matches := TestVariantBranchNameRe.FindStringSubmatch(name)
   183  	if matches == nil || len(matches) != 5 {
   184  		return "", "", "", "", errors.Reason("name must be of format projects/{PROJECT}/tests/{URL_ESCAPED_TEST_ID}/variants/{VARIANT_HASH}/refs/{REF_HASH}").Err()
   185  	}
   186  	// Unescape test_id.
   187  	testID, err = url.PathUnescape(matches[2])
   188  	if err != nil {
   189  		return "", "", "", "", errors.Annotate(err, "malformed test id").Err()
   190  	}
   191  
   192  	if err := rdbpbutil.ValidateTestID(testID); err != nil {
   193  		return "", "", "", "", errors.Annotate(err, "test id %q", testID).Err()
   194  	}
   195  
   196  	return matches[1], testID, matches[3], matches[4], nil
   197  }
   198  
   199  // ruleName constructs a rule resource name from its components.
   200  func ruleName(project, ruleID string) string {
   201  	return fmt.Sprintf("projects/%s/rules/%s", project, ruleID)
   202  }
   203  
   204  // testVariantBranchName constructs a test variant branch resource name
   205  // from its components.
   206  func testVariantBranchName(project, testID, variantHash, refHash string) string {
   207  	encodedTestID := url.PathEscape(testID)
   208  	return fmt.Sprintf("projects/%s/tests/%s/variants/%s/refs/%s", project, encodedTestID, variantHash, refHash)
   209  }