Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(71)

Side by Side Diff: milo/common/config.go

Issue 2982183002: Milo: Store console defs as their own entities (Closed)
Patch Set: tests Created 3 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2016 The LUCI Authors. 1 // Copyright 2016 The LUCI Authors.
2 // 2 //
3 // Licensed under the Apache License, Version 2.0 (the "License"); 3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with 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 5 // You may obtain a copy of the License at
6 // 6 //
7 // http://www.apache.org/licenses/LICENSE-2.0 7 // http://www.apache.org/licenses/LICENSE-2.0
8 // 8 //
9 // Unless required by applicable law or agreed to in writing, software 9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS, 10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and 12 // See the License for the specific language governing permissions and
13 // limitations under the License. 13 // limitations under the License.
14 14
15 package common 15 package common
16 16
17 import ( 17 import (
18 "fmt" 18 "fmt"
19 "strings"
19 "time" 20 "time"
20 21
21 "github.com/golang/protobuf/proto" 22 "github.com/golang/protobuf/proto"
22 "golang.org/x/net/context" 23 "golang.org/x/net/context"
23 24
24 "github.com/luci/gae/service/datastore" 25 "github.com/luci/gae/service/datastore"
25 "github.com/luci/gae/service/info" 26 "github.com/luci/gae/service/info"
27 configInterface "github.com/luci/luci-go/common/config"
26 "github.com/luci/luci-go/common/data/caching/proccache" 28 "github.com/luci/luci-go/common/data/caching/proccache"
27 "github.com/luci/luci-go/common/errors" 29 "github.com/luci/luci-go/common/errors"
28 "github.com/luci/luci-go/common/logging" 30 "github.com/luci/luci-go/common/logging"
29 "github.com/luci/luci-go/luci_config/server/cfgclient" 31 "github.com/luci/luci-go/luci_config/server/cfgclient"
30 "github.com/luci/luci-go/luci_config/server/cfgclient/backend" 32 "github.com/luci/luci-go/luci_config/server/cfgclient/backend"
31 "github.com/luci/luci-go/luci_config/server/cfgclient/textproto"
32 33
33 "github.com/luci/luci-go/milo/api/config" 34 "github.com/luci/luci-go/milo/api/config"
34 ) 35 )
35 36
36 // Project is a LUCI project. 37 // Project is a single luci-config project name. This is only used as a way
38 // to index Console definitions, as it is the parent entity.
37 type Project struct { 39 type Project struct {
38 » // ID of the project, as per self defined. This is the luci-config name . 40 » // ID is the name of the project
39 ID string `gae:"$id"` 41 ID string `gae:"$id"`
40 » // Data is the Project data in protobuf binary format. 42 }
41 » Data []byte `gae:",noindex"` 43
42 » // Revision is the latest revision we have of this project's config. 44 // maybePutProject makes the project if it doesn't exist.
45 func maybePutProject(c context.Context, id string) error {
46 » return datastore.RunInTransaction(c, func(context.Context) error {
47 » » p := Project{ID: id}
48 » » if err := datastore.Get(c, &p); err == datastore.ErrNoSuchEntity {
49 » » » return datastore.Put(c, &p)
50 » » } else {
51 » » » return err
52 » » }
53 » }, nil)
54 }
55
56 // Console is af datastore entity representing a single console.
57 type Console struct {
58 » // Parent is a key to the parent project entity where this console was
59 » // defined in.
60 » Parent *datastore.Key `gae:"$parent"`
61 » // ID is the ID of the console.
62 » ID string `gae:"$id"`
63 » // RepoURL and Ref combined defines the commits the show up on the left
64 » // hand side of a Console.
65 » RepoURL string
66 » // RepoURL and Ref combined defines the commits the show up on the left
67 » // hand side of a Console.
68 » Ref string
69 » // ManifestName is the name of the manifest to look for when querying fo r
70 » // builds under this console.
71 » ManifestName string
72 » // URL is the URL to the luci-config definition of this console.
73 » URL string
74 » // Revision is the luci-config reivision from when this Console was retr ieved.
43 Revision string 75 Revision string
76 // Builders is a list of universal builder IDs.
77 Builders []string
78 }
79
80 // GetProjectName retrieves the project name of the console out of the Console's
81 // parent key.
82 func (con *Console) GetProjectName() string {
83 return con.Parent.StringID()
84 }
85
86 // NewConsole creates a fully populated console out of the luci-config proto
87 // definition of a console.
88 func NewConsole(project *datastore.Key, URL, revision string, con *config.Consol e) *Console {
89 return &Console{
90 Parent: project,
91 ID: con.ID,
92 RepoURL: con.RepoURL,
93 Ref: con.Ref,
94 ManifestName: con.ManifestName,
95 Revision: revision,
96 URL: URL,
97 Builders: BuilderFromProto(con.Builders),
98 }
99 }
100
101 // BuilderFromProto tranforms a luci-config proto builder format into the datast ore
102 // format.
103 func BuilderFromProto(cb []*config.Builder) []string {
104 builders := make([]string, len(cb))
105 for i, b := range cb {
106 builders[i] = b.Name
107 }
108 return builders
109 }
110
111 // LuciConfigURL returns a user friendly URL that specifies where to view
112 // this console definition.
113 func LuciConfigURL(c context.Context, configSet, path, revision string) string {
114 // TODO(hinoka): This shouldn't be hardcoded, instead we should get the
115 // luci-config instance from the context. But we only use this instance at
116 // the moment so it is okay for now.
117 // TODO(hinoka): The UI doesn't allow specifying paths and revision yet. Add
118 // that in when it is supported.
119 return fmt.Sprintf("https://luci-config.appspot.com/newui#/%s", configSe t)
44 } 120 }
45 121
46 // The key for the service config entity in datastore. 122 // The key for the service config entity in datastore.
47 const ServiceConfigID = "service_config" 123 const ServiceConfigID = "service_config"
48 124
49 // ServiceConfig is a container for the instance's service config. 125 // ServiceConfig is a container for the instance's service config.
50 type ServiceConfig struct { 126 type ServiceConfig struct {
51 // ID is the datastore key. This should be static, as there should only be 127 // ID is the datastore key. This should be static, as there should only be
52 // one service config. 128 // one service config.
53 ID string `gae:"$id"` 129 ID string `gae:"$id"`
(...skipping 117 matching lines...) Expand 10 before | Expand all | Expand 10 after
171 }, nil) 247 }, nil)
172 248
173 if err != nil { 249 if err != nil {
174 return nil, fmt.Errorf("failed to update config entry in transac tion", err) 250 return nil, fmt.Errorf("failed to update config entry in transac tion", err)
175 } 251 }
176 logging.Infof(c, "successfully updated to new config") 252 logging.Infof(c, "successfully updated to new config")
177 253
178 return settings, nil 254 return settings, nil
179 } 255 }
180 256
181 // UpdateProjectConfigs internal project configuration based off luci-config. 257 func validateProject(c context.Context, p *Project) {
182 // update updates Milo's configuration based off luci config. This includes 258
183 // scanning through all project and extract all console configs. 259 }
184 func UpdateProjectConfigs(c context.Context) error { 260
261 func updateProjectConsoles(c context.Context, projectName string, cfg *configInt erface.Config) error {
262 » // Ensure that the project exists as an entity.
263 » if err := maybePutProject(c, projectName); err != nil {
264 » » return errors.Annotate(err, "adding project entity").Err()
265 » }
266 » proj := config.Project{}
267 » if err := proto.UnmarshalText(cfg.Content, &proj); err != nil {
268 » » return errors.Annotate(err, "unmarshalling proto").Err()
269 » }
270
271 » // Keep a list of known consoles so we can prune unknown ones later.
272 » knownConsoles := make(map[string]bool, len(proj.Consoles))
273 » // Iterate through all the proto consoles, adding and replacing the
274 » // known ones if needed. If any consoles errors out, then the whole pro ject fails.
275 » parentKey := datastore.MakeKey(c, "Project", projectName)
276 » for _, pc := range proj.Consoles {
277 » » knownConsoles[pc.ID] = true
278 » » con, err := GetConsole(c, projectName, pc.ID)
279 » » switch err {
280 » » case datastore.ErrNoSuchEntity:
281 » » » // continue
282 » » case nil:
283 » » » // Check if revisions match, if so just skip it.
284 » » » if con.Revision == cfg.Revision {
285 » » » » continue
286 » » » }
287 » » default:
288 » » » return errors.Annotate(err, "checking %s", pc.ID).Err()
289 » » }
290 » » URL := LuciConfigURL(c, cfg.ConfigSet, cfg.Path, cfg.Revision)
291 » » con = NewConsole(parentKey, URL, cfg.Revision, pc)
292 » » if err = datastore.Put(c, con); err != nil {
293 » » » return errors.Annotate(err, "saving %s", pc.ID).Err()
294 » » } else {
295 » » » logging.Infof(c, "saved a new %s / %s (revision %s)", pr ojectName, con.ID, cfg.Revision)
296 » » }
297 » }
298
299 » // Now delete all of the extra consoles.
300 » q := datastore.NewQuery("Console")
301 » q = q.Ancestor(parentKey)
302 » q = q.KeysOnly(true)
303 » existingConsoles := []*Console{}
304 » if err := datastore.GetAll(c, q, &existingConsoles); err != nil {
305 » » return err
306 » }
307 » for _, ec := range existingConsoles {
308 » » if _, ok := knownConsoles[ec.ID]; !ok {
309 » » » // This console no longer exists, delete it.
310 » » » logging.Infof(c, "deleting console %s / %s", projectName , ec.ID)
311 » » » if err := datastore.Delete(c, ec); err != nil {
312 » » » » return err
313 » » » }
314 » » }
315 » }
316 » return nil
317 }
318
319 // UpdateConsoles updates internal console definitions entities based off luci-c onfig.
320 func UpdateConsoles(c context.Context) error {
185 cfgName := info.AppID(c) + ".cfg" 321 cfgName := info.AppID(c) + ".cfg"
186 322
187 » var ( 323 » logging.Debugf(c, "fetching configs for %s", cfgName)
188 » » configs []*config.Project 324 » // Acquire the raw config client.
189 » » metas []*cfgclient.Meta 325 » lucicfg := backend.Get(c).GetConfigInterface(c, backend.AsService)
190 » » merr errors.MultiError 326 » // Project configs for Milo contains console definitions.
191 » ) 327 » configs, err := lucicfg.GetProjectConfigs(c, cfgName, false)
328 » if err != nil {
329 » » return errors.Annotate(err, "while fetching project configs").Er r()
330 » }
331 » logging.Infof(c, "got %d project configs", len(configs))
192 332
193 » logging.Debugf(c, "fetching configs for %s", cfgName) 333 » merr := errors.MultiError{}
194 » if err := cfgclient.Projects(c, cfgclient.AsService, cfgName, textproto. Slice(&configs), &metas); err != nil { 334 » knownProjects := map[string]bool{}
195 » » merr = err.(errors.MultiError) 335 » // Iterate through each project config, extracting the console definitio n.
196 » » // Some configs errored out, but we can continue with the ones t hat work still. 336 » for _, cfg := range configs {
337 » » // This looks like "projects/<project name>"
338 » » splitPath := strings.SplitN(cfg.ConfigSet, "/", 2)
339 » » if len(splitPath) != 2 {
340 » » » return fmt.Errorf("Invalid config set path %s", cfg.Conf igSet)
341 » » }
342 » » projectName := splitPath[1]
343 » » knownProjects[projectName] = true
344 » » if err := updateProjectConsoles(c, projectName, &cfg); err != ni l {
345 » » » err = errors.Annotate(err, "processing project %s", cfg. ConfigSet).Err()
346 » » » merr = append(merr, err)
347 » » }
197 } 348 }
198 349
199 » // A map of project ID to project. 350 » // Delete all the projects that no longer exist.
200 » projects := map[string]*Project{} 351 » projects := []*Project{}
201 » for i, proj := range configs { 352 » q := datastore.NewQuery("Project")
202 » » meta := metas[i] 353 » if err := datastore.GetAll(c, q, &projects); err != nil {
203 » » projectName, _, _ := meta.ConfigSet.SplitProject() 354 » » return append(merr, err)
204 » » name := string(projectName)
205 » » projects[name] = nil
206 » » if merr != nil && merr[i] != nil {
207 » » » logging.WithError(merr[i]).Warningf(c, "skipping %s due to error", name)
208 » » » continue
209 » » }
210
211 » » p := &Project{
212 » » » ID: name,
213 » » » Revision: meta.Revision,
214 » » }
215
216 » » logging.Infof(c, "prossing %s", name)
217
218 » » var err error
219 » » p.Data, err = proto.Marshal(proj)
220 » » if err != nil {
221 » » » logging.WithError(err).Errorf(c, "Encountered error whil e processing project %s", name)
222 » » » // Set this to nil to signal this project exists but we don't want to update it.
223 » » » projects[name] = nil
224 » » » continue
225 » » }
226 » » projects[name] = p
227 } 355 }
228
229 // Now load all the data into the datastore.
230 projs := make([]*Project, 0, len(projects))
231 for _, proj := range projects { 356 for _, proj := range projects {
232 » » if proj != nil { 357 » » if _, ok := knownProjects[proj.ID]; !ok {
233 » » » projs = append(projs, proj) 358 » » » // Delete all of the project's consoles, then the projec t itself.
359 » » » parentKey := datastore.KeyForObj(c, &proj)
360 » » » q := datastore.NewQuery("Console")
361 » » » q = q.Ancestor(parentKey)
362 » » » q = q.KeysOnly(true)
363 » » » projectConsoles := []*Console{}
364 » » » if err := datastore.GetAll(c, q, &projectConsoles); err != nil {
365 » » » » return append(merr, err)
366 » » » }
367 » » » logging.Infof(c, "deleting project %s and %s", proj.ID, &projectConsoles)
368 » » » if err := datastore.Delete(c, &projectConsoles); err != nil {
369 » » » » return append(merr, err)
370 » » » }
371 » » » if err := datastore.Delete(c, &proj); err != nil {
372 » » » » return append(merr, err)
373 » » » }
234 } 374 }
235 } 375 }
236 » if err := datastore.Put(c, projs); err != nil { 376 » if len(merr) == 0 {
237 » » return err 377 » » return nil
238 } 378 }
239 379 » return merr
240 » // Delete entries that no longer exist.
241 » q := datastore.NewQuery("Project").KeysOnly(true)
242 » allProjs := []Project{}
243 » datastore.GetAll(c, q, &allProjs)
244 » toDelete := []Project{}
245 » for _, proj := range allProjs {
246 » » if _, ok := projects[proj.ID]; !ok {
247 » » » toDelete = append(toDelete, proj)
248 » » }
249 » }
250 » return datastore.Delete(c, toDelete)
251 } 380 }
252 381
253 // GetAllProjects returns all registered projects. 382 // GetAllConsoles returns all registered projects.
254 func GetAllProjects(c context.Context) ([]*config.Project, error) { 383 func GetAllConsoles(c context.Context) ([]*Console, error) {
255 » q := datastore.NewQuery("Project") 384 » q := datastore.NewQuery("Console")
256 q.Order("ID") 385 q.Order("ID")
257 386 » con := []*Console{}
258 » ps := []*Project{} 387 » err := datastore.GetAll(c, q, &con)
259 » err := datastore.GetAll(c, q, &ps) 388 » return con, err
260 » if err != nil {
261 » » return nil, err
262 » }
263 » results := make([]*config.Project, len(ps))
264 » for i, p := range ps {
265 » » results[i] = &config.Project{}
266 » » if err := proto.Unmarshal(p.Data, results[i]); err != nil {
267 » » » return nil, err
268 » » }
269 » }
270 » return results, nil
271 } 389 }
272 390
273 // GetProject returns the requested project. 391 // GetConsole returns the requested console.
274 func GetProject(c context.Context, projName string) (*config.Project, error) { 392 func GetConsole(c context.Context, proj, id string) (*Console, error) {
275 » // Next, Try datastore 393 » // TODO(hinoka): Memcache this.
276 » p := Project{ID: projName} 394 » con := Console{
277 » if err := datastore.Get(c, &p); err != nil { 395 » » Parent: datastore.MakeKey(c, "Project", proj),
278 » » return nil, err 396 » » ID: id,
279 } 397 }
280 » mp := config.Project{} 398 » err := datastore.Get(c, &con)
281 » if err := proto.Unmarshal(p.Data, &mp); err != nil { 399 » return &con, err
282 » » return nil, err
283 » }
284
285 » return &mp, nil
286 }
287
288 // GetConsole returns the requested console instance.
289 func GetConsole(c context.Context, projName, consoleName string) (*config.Consol e, error) {
290 » p, err := GetProject(c, projName)
291 » if err != nil {
292 » » return nil, err
293 » }
294 » for _, cs := range p.Consoles {
295 » » if cs.Name == consoleName {
296 » » » return cs, nil
297 » » }
298 » }
299 » return nil, fmt.Errorf("Console %s not found in project %s", consoleName , projName)
300 } 400 }
301 401
302 // ProjectConsole is a simple tuple type for GetConsolesForBuilder. 402 // ProjectConsole is a simple tuple type for GetConsolesForBuilder.
303 type ProjectConsole struct { 403 type ProjectConsole struct {
304 ProjectID string 404 ProjectID string
305 Console *config.Console 405 Console *config.Console
306 } 406 }
307 407
308 // GetConsolesForBuilder retrieves all the console definitions that this builder 408 // GetConsolesForBuilder retrieves all the console definitions that this builder
309 // belongs to. 409 // belongs to.
310 func GetConsolesForBuilder(c context.Context, builderName string) ([]*ProjectCon sole, error) { 410 func GetConsolesForBuilder(c context.Context, builderName string) ([]*Console, e rror) {
311 » projs, err := GetAllProjects(c) 411 » cons, err := GetAllConsoles(c)
312 if err != nil { 412 if err != nil {
313 return nil, err 413 return nil, err
314 } 414 }
315 » ret := []*ProjectConsole{} 415 » ret := []*Console{}
316 » for _, p := range projs { 416 » for _, con := range cons {
317 » » for _, con := range p.Consoles { 417 » » for _, b := range con.Builders {
318 » » » for _, b := range con.Builders { 418 » » » if b == builderName {
319 » » » » if b.Name == builderName { 419 » » » » ret = append(ret, con)
320 » » » » » ret = append(ret, &ProjectConsole{p.ID, con})
321 » » » » }
322 } 420 }
323 } 421 }
324 } 422 }
325 return ret, nil 423 return ret, nil
326 } 424 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698