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 }