go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/internal/config/config.go (about) 1 // Copyright 2021 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 "context" 19 "regexp" 20 "strings" 21 22 "google.golang.org/protobuf/encoding/prototext" 23 24 "go.chromium.org/luci/config" 25 "go.chromium.org/luci/config/server/cfgcache" 26 "go.chromium.org/luci/config/validation" 27 28 pb "go.chromium.org/luci/buildbucket/proto" 29 ) 30 31 const settingsCfgFilename = "settings.cfg" 32 33 // Cached settings config. 34 var cachedSettingsCfg = cfgcache.Register(&cfgcache.Entry{ 35 Path: settingsCfgFilename, 36 Type: (*pb.SettingsCfg)(nil), 37 }) 38 39 // init registers validation rules. 40 func init() { 41 validation.Rules.Add("services/${appid}", settingsCfgFilename, validateSettingsCfg) 42 validation.Rules.Add("regex:projects/.*", "${appid}.cfg", validateProjectCfg) 43 } 44 45 // validateSettingsCfg implements validation.Func and validates the content of 46 // the settings file. 47 // 48 // Validation errors are returned via validation.Context. An error directly 49 // returned by this function means a bug in the code. 50 func validateSettingsCfg(ctx *validation.Context, configSet, path string, content []byte) error { 51 cfg := pb.SettingsCfg{} 52 if err := prototext.Unmarshal(content, &cfg); err != nil { 53 ctx.Errorf("invalid SettingsCfg proto message: %s", err) 54 return nil 55 } 56 if s := cfg.Swarming; s != nil { 57 ctx.Enter("swarming") 58 validateSwarmingSettings(ctx, s) 59 ctx.Exit() 60 } 61 62 for i, exp := range cfg.Experiment.GetExperiments() { 63 ctx.Enter("experiment.experiments #%d", i) 64 validateExperiment(ctx, exp) 65 ctx.Exit() 66 } 67 68 for i, backend := range cfg.GetBackends() { 69 ctx.Enter("Backends.BackendSetting #%d", i) 70 validateHostname(ctx, "BackendSetting.hostname", backend.GetHostname()) 71 switch backend.Mode.(type) { 72 case *pb.BackendSetting_FullMode_: 73 validateBackendFullMode(ctx, backend.GetFullMode()) 74 case *pb.BackendSetting_LiteMode_: 75 default: 76 ctx.Errorf("mode field is not set or its type is unsupported") 77 } 78 ctx.Exit() 79 } 80 81 validateHostname(ctx, "logdog.hostname", cfg.Logdog.GetHostname()) 82 validateHostname(ctx, "resultdb.hostname", cfg.Resultdb.GetHostname()) 83 return nil 84 } 85 86 func validateBackendFullMode(ctx *validation.Context, m *pb.BackendSetting_FullMode) { 87 if m.PubsubId == "" { 88 ctx.Errorf("pubsub_id for UpdateBuildTask must be specified") 89 } 90 validateBuildSyncSetting(ctx, m.GetBuildSyncSetting()) 91 } 92 93 func validateSwarmingSettings(ctx *validation.Context, s *pb.SwarmingSettings) { 94 validateHostname(ctx, "milo_hostname", s.MiloHostname) 95 for i, pkg := range s.UserPackages { 96 ctx.Enter("user_packages #%d", i) 97 validatePackage(ctx, pkg) 98 ctx.Exit() 99 } 100 101 for i, pkg := range s.AlternativeAgentPackages { 102 ctx.Enter("alternative_agent_packages #%d", i) 103 validatePackage(ctx, pkg) 104 if len(pkg.OmitOnExperiment) == 0 && len(pkg.IncludeOnExperiment) == 0 { 105 ctx.Errorf("alternative_agent_package must set constraints on either omit_on_experiment or include_on_experiment") 106 } 107 ctx.Exit() 108 } 109 110 if bbPkg := s.BbagentPackage; bbPkg != nil { 111 ctx.Enter("bbagent_package") 112 validatePackage(ctx, bbPkg) 113 if !strings.HasSuffix(bbPkg.PackageName, "/${platform}") { 114 ctx.Errorf("package_name must end with '/${platform}'") 115 } 116 ctx.Exit() 117 } 118 119 if kitchen := s.KitchenPackage; kitchen != nil { 120 ctx.Enter("kitchen_package") 121 validatePackage(ctx, kitchen) 122 ctx.Exit() 123 } 124 } 125 126 func validateHostname(ctx *validation.Context, field string, host string) { 127 if host == "" { 128 ctx.Errorf("%s unspecified", field) 129 } 130 if strings.Contains(host, "://") { 131 ctx.Errorf("%s must not contain '://'", field) 132 } 133 } 134 135 func validateBuildSyncSetting(ctx *validation.Context, setting *pb.BackendSetting_BuildSyncSetting) { 136 if setting.GetShards() < 0 { 137 ctx.Errorf("shards must be greater than or equal to 0") 138 } 139 140 if setting.GetSyncIntervalSeconds() != 0 && setting.GetSyncIntervalSeconds() < 60 { 141 ctx.Errorf("sync_interval_seconds must be greater than or equal to 60") 142 } 143 } 144 145 func validatePackage(ctx *validation.Context, pkg *pb.SwarmingSettings_Package) { 146 if pkg.PackageName == "" { 147 ctx.Errorf("package_name is required") 148 } 149 if pkg.Version == "" { 150 ctx.Errorf("version is required") 151 } 152 if pkg.Builders != nil { 153 validateRegex(ctx, "builders.regex", pkg.Builders.Regex) 154 validateRegex(ctx, "builders.regex_exclude", pkg.Builders.RegexExclude) 155 } 156 } 157 158 func validateExperiment(ctx *validation.Context, exp *pb.ExperimentSettings_Experiment) { 159 if exp.Name == "" { 160 ctx.Errorf("name is required") 161 } 162 if exp.MinimumValue < 0 || exp.MinimumValue > 100 { 163 ctx.Errorf("minimum_value must be in [0,100]") 164 } 165 if exp.DefaultValue < exp.MinimumValue || exp.DefaultValue > 100 { 166 ctx.Errorf("default_value must be in [${minimum_value},100]") 167 } 168 if exp.Inactive && (exp.DefaultValue != 0 || exp.MinimumValue != 0) { 169 ctx.Errorf("default_value and minimum_value must both be 0 when inactive is true") 170 } 171 if exp.Builders != nil { 172 validateRegex(ctx, "builders.regex", exp.Builders.Regex) 173 validateRegex(ctx, "builders.regex_exclude", exp.Builders.RegexExclude) 174 } 175 } 176 177 func validateRegex(ctx *validation.Context, field string, patterns []string) { 178 for _, p := range patterns { 179 if _, err := regexp.Compile(p); err != nil { 180 ctx.Errorf("%s %q: invalid regex", field, p) 181 } 182 } 183 } 184 185 // UpdateSettingsCfg is called from a cron periodically to import settings.cfg into datastore. 186 func UpdateSettingsCfg(ctx context.Context) error { 187 _, err := cachedSettingsCfg.Update(ctx, nil) 188 return err 189 } 190 191 // GetSettingsCfg fetches the settings.cfg from luci-config. 192 func GetSettingsCfg(ctx context.Context) (*pb.SettingsCfg, error) { 193 cfg, err := cachedSettingsCfg.Get(ctx, nil) 194 if err != nil { 195 return nil, err 196 } 197 return cfg.(*pb.SettingsCfg), nil 198 } 199 200 // SetTestSettingsCfg is used in tests only. 201 func SetTestSettingsCfg(ctx context.Context, cfg *pb.SettingsCfg) error { 202 return cachedSettingsCfg.Set(ctx, cfg, &config.Meta{Path: "settings.cfg"}) 203 }