go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/config_service/internal/rules/project.go (about) 1 // Copyright 2023 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 rules 16 17 import ( 18 "google.golang.org/protobuf/encoding/prototext" 19 20 "go.chromium.org/luci/common/data/stringset" 21 cfgcommonpb "go.chromium.org/luci/common/proto/config" 22 "go.chromium.org/luci/config" 23 "go.chromium.org/luci/config/validation" 24 25 "go.chromium.org/luci/config_service/internal/common" 26 ) 27 28 func validateProjectsCfg(vctx *validation.Context, configSet, path string, content []byte) error { 29 vctx.SetFile(path) 30 cfg := &cfgcommonpb.ProjectsCfg{} 31 if err := prototext.Unmarshal(content, cfg); err != nil { 32 vctx.Errorf("invalid projects proto: %s", err) 33 return nil 34 } 35 seenTeams := stringset.New(len(cfg.GetTeams())) 36 for i, team := range cfg.GetTeams() { 37 vctx.Enter("teams #%d", i) 38 vctx.Enter("name") 39 validateUniqueID(vctx, team.GetName(), seenTeams, nil) 40 vctx.Exit() 41 if len(team.GetMaintenanceContact()) == 0 { 42 vctx.Errorf("maintenance_contact is required") 43 } 44 for i, contact := range team.GetMaintenanceContact() { 45 vctx.Enter("maintenance_contact #%d", i) 46 validateEmail(vctx, contact) 47 vctx.Exit() 48 } 49 if len(team.GetEscalationContact()) == 0 { 50 vctx.Warningf("escalation_contact is recommended") 51 } 52 for i, contact := range team.GetEscalationContact() { 53 vctx.Enter("escalation_contact #%d", i) 54 validateEmail(vctx, contact) 55 vctx.Exit() 56 } 57 vctx.Exit() 58 } 59 validateSorted[*cfgcommonpb.Team](vctx, cfg.GetTeams(), "teams", func(t *cfgcommonpb.Team) string { 60 return t.GetName() 61 }) 62 63 seenProjects := stringset.New(len(cfg.GetProjects())) 64 for i, project := range cfg.GetProjects() { 65 vctx.Enter("projects #%d", i) 66 vctx.Enter("id") 67 validateUniqueID(vctx, project.GetId(), seenProjects, func(vctx *validation.Context, id string) { 68 if _, err := config.ProjectSet(id); err != nil { 69 vctx.Errorf("invalid id: %s", err) 70 } 71 }) 72 vctx.Exit() 73 74 vctx.Enter("gitiles_location") 75 if err := common.ValidateGitilesLocation(project.GetGitilesLocation()); err != nil { 76 vctx.Error(err) 77 } 78 vctx.Exit() 79 80 vctx.Enter("owned_by") 81 switch ownedBy := project.GetOwnedBy(); { 82 case ownedBy == "": 83 vctx.Errorf("not specified") 84 case !seenTeams.Has(ownedBy): 85 vctx.Errorf(`unknown team %q`, ownedBy) 86 } 87 vctx.Exit() 88 89 vctx.Exit() 90 } 91 return nil 92 } 93 94 func validateProjectMetadata(vctx *validation.Context, configSet, path string, content []byte) error { 95 vctx.SetFile(path) 96 cfg := &cfgcommonpb.ProjectCfg{} 97 if err := prototext.Unmarshal(content, cfg); err != nil { 98 vctx.Errorf("invalid project proto: %s", err) 99 return nil 100 } 101 if cfg.GetName() == "" { 102 vctx.Errorf("name is not specified") 103 } 104 for i, access := range cfg.GetAccess() { 105 vctx.Enter("access #%d", i) 106 validateAccess(vctx, access) 107 vctx.Exit() 108 } 109 return nil 110 }