go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/gce/appengine/config/config.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 config knows how import configs from LUCI-config. 16 package config 17 18 import ( 19 "context" 20 "fmt" 21 "net/http" 22 "strings" 23 24 "github.com/golang/protobuf/proto" 25 26 "go.chromium.org/luci/common/errors" 27 "go.chromium.org/luci/common/logging" 28 "go.chromium.org/luci/config" 29 "go.chromium.org/luci/config/cfgclient" 30 "go.chromium.org/luci/config/validation" 31 "go.chromium.org/luci/server/router" 32 33 gce "go.chromium.org/luci/gce/api/config/v1" 34 "go.chromium.org/luci/gce/api/projects/v1" 35 "go.chromium.org/luci/gce/appengine/rpc" 36 ) 37 38 // projectsFile is the name of the projects config file. 39 const projectsFile = "projects.cfg" 40 41 // vmsFile is the name of the VMs config file. 42 const vmsFile = "vms.cfg" 43 44 // Config encapsulates the service config. 45 type Config struct { 46 revision string 47 Projects *projects.Configs 48 VMs *gce.Configs 49 } 50 51 // prjKey is the key to a projects.ProjectsServer in the context. 52 var prjKey = "prj" 53 54 // withProjServer returns a new context with the given projects.ProjectsServer 55 // installed. 56 func withProjServer(c context.Context, srv projects.ProjectsServer) context.Context { 57 return context.WithValue(c, &prjKey, srv) 58 } 59 60 // getProjServer returns the projects.ProjectsServer installed in the current 61 // context. 62 func getProjServer(c context.Context) projects.ProjectsServer { 63 return c.Value(&prjKey).(projects.ProjectsServer) 64 } 65 66 // vmsKey is the key to a gce.ConfigurationServer in the context. 67 var vmsKey = "vms" 68 69 // withVMsServer returns a new context with the given gce.ConfigurationServer 70 // installed. 71 func withVMsServer(c context.Context, srv gce.ConfigurationServer) context.Context { 72 return context.WithValue(c, &vmsKey, srv) 73 } 74 75 // getVMsServer returns the gce.ConfigurationServer installed in the current 76 // context. 77 func getVMsServer(c context.Context) gce.ConfigurationServer { 78 return c.Value(&vmsKey).(gce.ConfigurationServer) 79 } 80 81 // fetch fetches configs from the config service. 82 func fetch(c context.Context) (*Config, error) { 83 cli := cfgclient.Client(c) 84 rev := "" 85 vms := &gce.Configs{} 86 switch vmsCfg, err := cli.GetConfig(c, "services/${appid}", vmsFile, false); { 87 case err == config.ErrNoConfig: 88 logging.Debugf(c, "%q not found", vmsFile) 89 case err != nil: 90 return nil, errors.Annotate(err, "failed to fetch %q", vmsFile).Err() 91 default: 92 rev = vmsCfg.Revision 93 logging.Debugf(c, "found %q revision %s", vmsFile, vmsCfg.Revision) 94 if err := proto.UnmarshalText(vmsCfg.Content, vms); err != nil { 95 return nil, errors.Annotate(err, "failed to load %q", vmsFile).Err() 96 } 97 } 98 prjs := &projects.Configs{} 99 switch prjsCfg, err := cli.GetConfig(c, "services/${appid}", projectsFile, false); { 100 case err == config.ErrNoConfig: 101 logging.Debugf(c, "%q not found", projectsFile) 102 case err != nil: 103 return nil, errors.Annotate(err, "failed to fetch %q", projectsFile).Err() 104 default: 105 logging.Debugf(c, "found %q revision %s", projectsFile, prjsCfg.Revision) 106 if rev != "" && prjsCfg.Revision != rev { 107 return nil, errors.Reason("config revision mismatch").Err() 108 } 109 if err := proto.UnmarshalText(prjsCfg.Content, prjs); err != nil { 110 return nil, errors.Annotate(err, "failed to load %q", projectsFile).Err() 111 } 112 } 113 return &Config{ 114 revision: rev, 115 Projects: prjs, 116 VMs: vms, 117 }, nil 118 } 119 120 // validate validates configs. 121 func validate(c context.Context, cfg *Config) error { 122 v := &validation.Context{Context: c} 123 v.SetFile(projectsFile) 124 cfg.Projects.Validate(v) 125 v.SetFile(vmsFile) 126 cfg.VMs.Validate(v) 127 return v.Finalize() 128 } 129 130 // deref dereferences VMs metadata by fetching referenced files. 131 func deref(c context.Context, cfg *Config) error { 132 // Cache fetched files. 133 fileMap := make(map[string]string) 134 cli := cfgclient.Client(c) 135 for _, v := range cfg.VMs.GetVms() { 136 for i, m := range v.GetAttributes().Metadata { 137 if m.GetFromFile() != "" { 138 parts := strings.SplitN(m.GetFromFile(), ":", 2) 139 if len(parts) < 2 { 140 return errors.Reason("metadata from file must be in key:value form").Err() 141 } 142 file := parts[1] 143 if _, ok := fileMap[file]; !ok { 144 fileCfg, err := cli.GetConfig(c, "services/${appid}", file, false) 145 if err != nil { 146 return errors.Annotate(err, "failed to fetch %q", file).Err() 147 } 148 logging.Debugf(c, "found %q revision %s", file, fileCfg.Revision) 149 if fileCfg.Revision != cfg.revision { 150 return errors.Reason("config revision mismatch %q", fileCfg.Revision).Err() 151 } 152 fileMap[file] = fileCfg.Content 153 } 154 // fileMap[file] definitely exists. 155 key := parts[0] 156 val := fileMap[file] 157 v.Attributes.Metadata[i].Metadata = &gce.Metadata_FromText{ 158 FromText: fmt.Sprintf("%s:%s", key, val), 159 } 160 } 161 } 162 } 163 return nil 164 } 165 166 // normalize normalizes VMs durations by converting them to seconds, and sets 167 // output-only properties. 168 func normalize(c context.Context, cfg *Config) error { 169 for _, p := range cfg.Projects.GetProject() { 170 p.Revision = cfg.revision 171 } 172 for _, v := range cfg.VMs.GetVms() { 173 for _, ch := range v.Amount.GetChange() { 174 if err := ch.Length.Normalize(); err != nil { 175 return errors.Annotate(err, "failed to normalize %q", v.Prefix).Err() 176 } 177 } 178 if err := v.Lifetime.Normalize(); err != nil { 179 return errors.Annotate(err, "failed to normalize %q", v.Prefix).Err() 180 } 181 v.Revision = cfg.revision 182 if err := v.Timeout.Normalize(); err != nil { 183 return errors.Annotate(err, "failed to normalize %q", v.Prefix).Err() 184 } 185 } 186 return nil 187 } 188 189 // syncVMs synchronizes the given validated VM configs. 190 func syncVMs(c context.Context, vms []*gce.Config) error { 191 // Fetch existing configs. 192 srv := getVMsServer(c) 193 rsp, err := srv.List(c, &gce.ListRequest{}) 194 if err != nil { 195 return errors.Annotate(err, "failed to fetch VMs configs").Err() 196 } 197 // Track the revision of each config. 198 revs := make(map[string]string, len(rsp.Configs)) 199 for _, v := range rsp.Configs { 200 revs[v.Prefix] = v.Revision 201 } 202 logging.Debugf(c, "fetched %d VMs configs", len(rsp.Configs)) 203 204 // Update configs to new revisions. 205 ens := &gce.EnsureRequest{} 206 for _, v := range vms { 207 rev, ok := revs[v.Prefix] 208 delete(revs, v.Prefix) 209 if ok && rev == v.Revision { 210 continue 211 } 212 ens.Id = v.Prefix 213 ens.Config = v 214 if _, err := srv.Ensure(c, ens); err != nil { 215 return errors.Annotate(err, "failed to ensure VMs config %q", ens.Id).Err() 216 } 217 } 218 219 // Delete unreferenced configs. 220 del := &gce.DeleteRequest{} 221 for id := range revs { 222 del.Id = id 223 if _, err := srv.Delete(c, del); err != nil { 224 return errors.Annotate(err, "failed to delete VMs config %q", del.Id).Err() 225 } 226 logging.Debugf(c, "deleted VMs config %q", del.Id) 227 } 228 return nil 229 } 230 231 // syncPrjs synchronizes the given validated project configs. 232 func syncPrjs(c context.Context, prjs []*projects.Config) error { 233 // Fetch existing configs. 234 srv := getProjServer(c) 235 rsp, err := srv.List(c, &projects.ListRequest{}) 236 if err != nil { 237 return errors.Annotate(err, "failed to fetch project configs").Err() 238 } 239 // Track the revision of each config. 240 revs := make(map[string]string, len(rsp.Projects)) 241 for _, p := range rsp.Projects { 242 revs[p.Project] = p.Revision 243 } 244 logging.Debugf(c, "fetched %d project configs", len(rsp.Projects)) 245 246 // Update configs to new revisions. 247 ens := &projects.EnsureRequest{} 248 for _, p := range prjs { 249 rev, ok := revs[p.Project] 250 delete(revs, p.Project) 251 if ok && rev == p.Revision { 252 continue 253 } 254 ens.Id = p.Project 255 ens.Project = p 256 if _, err := srv.Ensure(c, ens); err != nil { 257 return errors.Annotate(err, "failed to ensure project config %q", ens.Id).Err() 258 } 259 } 260 261 // Delete unreferenced configs. 262 del := &projects.DeleteRequest{} 263 for id := range revs { 264 del.Id = id 265 if _, err := srv.Delete(c, del); err != nil { 266 return errors.Annotate(err, "failed to delete project config %q", del.Id).Err() 267 } 268 logging.Debugf(c, "deleted project config %q", del.Id) 269 } 270 return nil 271 } 272 273 // sync synchronizes the given validated configs. 274 func sync(c context.Context, cfg *Config) error { 275 if err := syncVMs(c, cfg.VMs.GetVms()); err != nil { 276 return errors.Annotate(err, "failed to sync VMs configs").Err() 277 } 278 if err := syncPrjs(c, cfg.Projects.GetProject()); err != nil { 279 return errors.Annotate(err, "failed to sync project configs").Err() 280 } 281 return nil 282 } 283 284 // doImport fetches and validates configs from the config service. 285 // doImport could have been named "import" but it's a reserved keyword. 286 func doImport(c context.Context) error { 287 cfg, err := fetch(c) 288 if err != nil { 289 return errors.Annotate(err, "failed to fetch configs").Err() 290 } 291 292 // Deref before validating. VMs may be invalid until metadata from file is imported. 293 if err := deref(c, cfg); err != nil { 294 return errors.Annotate(err, "failed to dereference files").Err() 295 } 296 297 if err := validate(c, cfg); err != nil { 298 return errors.Annotate(err, "invalid configs").Err() 299 } 300 301 if err := normalize(c, cfg); err != nil { 302 return errors.Annotate(err, "failed to normalize configs").Err() 303 } 304 305 if err := sync(c, cfg); err != nil { 306 return errors.Annotate(err, "failed to synchronize configs").Err() 307 } 308 return nil 309 } 310 311 // importHandler imports the config from the config service. 312 func importHandler(c *router.Context) { 313 c.Writer.Header().Set("Content-Type", "text/plain") 314 315 if err := doImport(c.Request.Context()); err != nil { 316 errors.Log(c.Request.Context(), err) 317 c.Writer.WriteHeader(http.StatusInternalServerError) 318 return 319 } 320 321 c.Writer.WriteHeader(http.StatusOK) 322 } 323 324 // InstallHandlers installs HTTP request handlers into the given router. 325 func InstallHandlers(r *router.Router, mw router.MiddlewareChain) { 326 mw = mw.Extend(func(c *router.Context, next router.Handler) { 327 // Install the services. 328 c.Request = c.Request.WithContext(withProjServer(c.Request.Context(), &rpc.Projects{})) 329 c.Request = c.Request.WithContext(withVMsServer(c.Request.Context(), &rpc.Config{})) 330 next(c) 331 }) 332 r.GET("/internal/cron/import-config", mw, importHandler) 333 }