go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/protoutil/builder.go (about) 1 // Copyright 2018 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 protoutil 16 17 import ( 18 "fmt" 19 "regexp" 20 "strings" 21 22 pb "go.chromium.org/luci/buildbucket/proto" 23 "go.chromium.org/luci/common/errors" 24 ) 25 26 var ( 27 projRegex = regexp.MustCompile(`^[a-z0-9\-_]+$`) 28 bucketRegex = regexp.MustCompile(`^[a-z0-9\-_.]{1,100}$`) 29 builderRegex = regexp.MustCompile(`^[a-zA-Z0-9\-_.\(\) ]{1,128}$`) 30 ) 31 32 // ValidateBuilderID validates the given builder ID. 33 // Bucket and Builder are optional and only validated if specified. 34 func ValidateBuilderID(b *pb.BuilderID) error { 35 switch parts := strings.Split(b.GetBucket(), "."); { 36 case !projRegex.MatchString(b.GetProject()): 37 return errors.Reason("project must match %q", projRegex).Err() 38 case b.GetBucket() != "" && !bucketRegex.MatchString(b.Bucket): 39 return errors.Reason("bucket must match %q", bucketRegex).Err() 40 case b.GetBuilder() != "" && !builderRegex.MatchString(b.Builder): 41 return errors.Reason("builder must match %q", builderRegex).Err() 42 case b.GetBucket() != "" && parts[0] == "luci" && len(parts) > 2: 43 return errors.Reason("invalid use of v1 bucket in v2 API (hint: try %q)", parts[2]).Err() 44 default: 45 return nil 46 } 47 } 48 49 // ValidateRequiredBuilderID validates the given builder ID, requiring Bucket 50 // and Builder. 51 func ValidateRequiredBuilderID(b *pb.BuilderID) error { 52 switch err := ValidateBuilderID(b); { 53 case err != nil: 54 return err 55 case b.Bucket == "": 56 return errors.Reason("bucket is required").Err() 57 case b.Builder == "": 58 return errors.Reason("builder is required").Err() 59 default: 60 return nil 61 } 62 } 63 64 // FormatBuilderID returns "{project}/{bucket}/{builder}" string. 65 func FormatBuilderID(id *pb.BuilderID) string { 66 return fmt.Sprintf("%s/%s/%s", id.Project, id.Bucket, id.Builder) 67 } 68 69 // ParseBuilderID parses a "{project}/{bucket}/{builder}" string. 70 // Opposite of FormatBuilderID. 71 func ParseBuilderID(s string) (*pb.BuilderID, error) { 72 parts := strings.Split(s, "/") 73 if len(parts) != 3 { 74 return nil, fmt.Errorf("invalid builder id; must have 2 slashes") 75 } 76 return &pb.BuilderID{ 77 Project: parts[0], 78 Bucket: parts[1], 79 Builder: parts[2], 80 }, nil 81 } 82 83 // FormatBucketID returns "{project}/{bucket}" string. 84 func FormatBucketID(project, bucket string) string { 85 return fmt.Sprintf("%s/%s", project, bucket) 86 } 87 88 // ParseBucketID parses a "{project}/{bucket}" string. 89 // Opposite of FormatBucketID. 90 func ParseBucketID(s string) (string, string, error) { 91 switch parts := strings.Split(s, "/"); { 92 case len(parts) != 2: 93 return "", "", errors.New("invalid bucket id; must have 1 slash") 94 case strings.TrimSpace(parts[0]) == "": 95 return "", "", errors.New("invalid bucket id; project is empty") 96 case strings.TrimSpace(parts[1]) == "": 97 return "", "", errors.New("invalid bucket id; bucket is empty") 98 default: 99 return parts[0], parts[1], nil 100 } 101 }