Chromium Code Reviews| OLD | NEW |
|---|---|
| 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 { | |
|
iannucci
2017/07/19 22:54:10
The Project only has the ID... why bother putting
Ryan Tseng
2017/07/20 00:15:15
Removed.
| |
| 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 | |
|
iannucci
2017/07/19 22:54:10
capital Project (since its a type)
Ryan Tseng
2017/07/20 00:15:15
Done.
| |
| 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 Loading... | |
| 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 updateProjectConsoles(c context.Context, projectName string, cfg *configInt erface.Config) error { |
| 182 // update updates Milo's configuration based off luci config. This includes | 258 » // Ensure that the project exists as an entity. |
|
iannucci
2017/07/19 22:54:10
but y?
Ryan Tseng
2017/07/20 00:15:16
Removed
| |
| 183 // scanning through all project and extract all console configs. | 259 » if err := maybePutProject(c, projectName); err != nil { |
| 184 func UpdateProjectConfigs(c context.Context) error { | 260 » » return errors.Annotate(err, "adding project entity").Err() |
| 261 » } | |
| 262 » proj := config.Project{} | |
| 263 » if err := proto.UnmarshalText(cfg.Content, &proj); err != nil { | |
| 264 » » return errors.Annotate(err, "unmarshalling proto").Err() | |
| 265 » } | |
| 266 | |
| 267 » // Keep a list of known consoles so we can prune unknown ones later. | |
| 268 » knownConsoles := make(map[string]bool, len(proj.Consoles)) | |
|
iannucci
2017/07/19 22:54:09
stringset.New is handy (in luci-go/common/data/str
Ryan Tseng
2017/07/20 00:15:15
Done.
| |
| 269 » // Iterate through all the proto consoles, adding and replacing the | |
| 270 » // known ones if needed. If any consoles errors out, then the whole pro ject fails. | |
|
iannucci
2017/07/19 22:54:09
'whole project fails' but not really; this isn't i
Ryan Tseng
2017/07/20 00:15:15
Done.
| |
| 271 » parentKey := datastore.MakeKey(c, "Project", projectName) | |
| 272 » for _, pc := range proj.Consoles { | |
| 273 » » knownConsoles[pc.ID] = true | |
| 274 » » con, err := GetConsole(c, projectName, pc.ID) | |
| 275 » » switch err { | |
| 276 » » case datastore.ErrNoSuchEntity: | |
| 277 » » » // continue | |
| 278 » » case nil: | |
| 279 » » » // Check if revisions match, if so just skip it. | |
| 280 » » » if con.Revision == cfg.Revision { | |
| 281 » » » » continue | |
| 282 » » » } | |
| 283 » » default: | |
| 284 » » » return errors.Annotate(err, "checking %s", pc.ID).Err() | |
| 285 » » } | |
| 286 » » URL := LuciConfigURL(c, cfg.ConfigSet, cfg.Path, cfg.Revision) | |
| 287 » » con = NewConsole(parentKey, URL, cfg.Revision, pc) | |
| 288 » » if err = datastore.Put(c, con); err != nil { | |
| 289 » » » return errors.Annotate(err, "saving %s", pc.ID).Err() | |
| 290 » » } else { | |
| 291 » » » logging.Infof(c, "saved a new %s / %s (revision %s)", pr ojectName, con.ID, cfg.Revision) | |
| 292 » » } | |
| 293 » } | |
| 294 | |
| 295 » // Now delete all of the extra consoles. | |
| 296 » q := datastore.NewQuery("Console") | |
| 297 » q = q.Ancestor(parentKey) | |
| 298 » q = q.KeysOnly(true) | |
|
iannucci
2017/07/19 22:54:10
you can string these together btw
q := datastore.
Ryan Tseng
2017/07/20 00:15:16
Yeah, I like doing them line by line tho, i though
| |
| 299 » existingConsoles := []*Console{} | |
|
iannucci
2017/07/19 22:54:10
make this []*datastore.Key{}
Ryan Tseng
2017/07/20 00:15:15
Done.
| |
| 300 » if err := datastore.GetAll(c, q, &existingConsoles); err != nil { | |
| 301 » » return err | |
| 302 » } | |
| 303 » for _, ec := range existingConsoles { | |
| 304 » » if _, ok := knownConsoles[ec.ID]; !ok { | |
|
iannucci
2017/07/19 22:54:10
make a new []*datastore.Key{}, and append to-be-de
Ryan Tseng
2017/07/20 00:15:15
Done.
| |
| 305 » » » // This console no longer exists, delete it. | |
| 306 » » » logging.Infof(c, "deleting console %s / %s", projectName , ec.ID) | |
| 307 » » » if err := datastore.Delete(c, ec); err != nil { | |
| 308 » » » » return err | |
| 309 » » » } | |
| 310 » » } | |
| 311 » } | |
| 312 » return nil | |
|
iannucci
2017/07/19 22:54:10
do a single `datastore.Delete(c, keysToDelete)`
Ryan Tseng
2017/07/20 00:15:15
Done.
| |
| 313 } | |
| 314 | |
| 315 // UpdateConsoles updates internal console definitions entities based off luci-c onfig. | |
| 316 func UpdateConsoles(c context.Context) error { | |
| 185 cfgName := info.AppID(c) + ".cfg" | 317 cfgName := info.AppID(c) + ".cfg" |
| 186 | 318 |
| 187 » var ( | 319 » logging.Debugf(c, "fetching configs for %s", cfgName) |
| 188 » » configs []*config.Project | 320 » // Acquire the raw config client. |
| 189 » » metas []*cfgclient.Meta | 321 » lucicfg := backend.Get(c).GetConfigInterface(c, backend.AsService) |
| 190 » » merr errors.MultiError | 322 » // Project configs for Milo contains console definitions. |
| 191 » ) | 323 » configs, err := lucicfg.GetProjectConfigs(c, cfgName, false) |
| 324 » if err != nil { | |
| 325 » » return errors.Annotate(err, "while fetching project configs").Er r() | |
| 326 » } | |
| 327 » logging.Infof(c, "got %d project configs", len(configs)) | |
| 192 | 328 |
| 193 » logging.Debugf(c, "fetching configs for %s", cfgName) | 329 » merr := errors.MultiError{} |
| 194 » if err := cfgclient.Projects(c, cfgclient.AsService, cfgName, textproto. Slice(&configs), &metas); err != nil { | 330 » knownProjects := map[string]bool{} |
| 195 » » merr = err.(errors.MultiError) | 331 » // 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. | 332 » for _, cfg := range configs { |
| 333 » » // This looks like "projects/<project name>" | |
| 334 » » splitPath := strings.SplitN(cfg.ConfigSet, "/", 2) | |
| 335 » » if len(splitPath) != 2 { | |
| 336 » » » return fmt.Errorf("Invalid config set path %s", cfg.Conf igSet) | |
| 337 » » } | |
| 338 » » projectName := splitPath[1] | |
| 339 » » knownProjects[projectName] = true | |
| 340 » » if err := updateProjectConsoles(c, projectName, &cfg); err != ni l { | |
| 341 » » » err = errors.Annotate(err, "processing project %s", cfg. ConfigSet).Err() | |
| 342 » » » merr = append(merr, err) | |
| 343 » » } | |
| 197 } | 344 } |
| 198 | 345 |
| 199 » // A map of project ID to project. | 346 » // Delete all the projects that no longer exist. |
| 200 » projects := map[string]*Project{} | 347 » projects := []*Project{} |
| 201 » for i, proj := range configs { | 348 » q := datastore.NewQuery("Project") |
|
iannucci
2017/07/19 22:54:09
projectKey := datastore.MakeKey("Project", projNam
Ryan Tseng
2017/07/20 00:15:16
Project removed.
| |
| 202 » » meta := metas[i] | 349 » if err := datastore.GetAll(c, q, &projects); err != nil { |
| 203 » » projectName, _, _ := meta.ConfigSet.SplitProject() | 350 » » 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 } | 351 } |
| 228 | |
| 229 // Now load all the data into the datastore. | |
| 230 projs := make([]*Project, 0, len(projects)) | |
| 231 for _, proj := range projects { | 352 for _, proj := range projects { |
| 232 » » if proj != nil { | 353 » » if _, ok := knownProjects[proj.ID]; !ok { |
| 233 » » » projs = append(projs, proj) | 354 » » » // Delete all of the project's consoles, then the projec t itself. |
| 355 » » » parentKey := datastore.KeyForObj(c, &proj) | |
| 356 » » » q := datastore.NewQuery("Console") | |
| 357 » » » q = q.Ancestor(parentKey) | |
| 358 » » » q = q.KeysOnly(true) | |
| 359 » » » projectConsoles := []*Console{} | |
| 360 » » » if err := datastore.GetAll(c, q, &projectConsoles); err != nil { | |
| 361 » » » » return append(merr, err) | |
| 362 » » » } | |
| 363 » » » logging.Infof(c, "deleting project %s and %s", proj.ID, &projectConsoles) | |
| 364 » » » if err := datastore.Delete(c, &projectConsoles); err != nil { | |
| 365 » » » » return append(merr, err) | |
| 366 » » » } | |
| 367 » » » if err := datastore.Delete(c, &proj); err != nil { | |
| 368 » » » » return append(merr, err) | |
| 369 » » » } | |
| 234 } | 370 } |
| 235 } | 371 } |
| 236 » if err := datastore.Put(c, projs); err != nil { | 372 » if len(merr) == 0 { |
| 237 » » return err | 373 » » return nil |
| 238 } | 374 } |
| 239 | 375 » 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 } | 376 } |
| 252 | 377 |
| 253 // GetAllProjects returns all registered projects. | 378 // GetAllConsoles returns all registered projects. |
| 254 func GetAllProjects(c context.Context) ([]*config.Project, error) { | 379 func GetAllConsoles(c context.Context) ([]*Console, error) { |
| 255 » q := datastore.NewQuery("Project") | 380 » q := datastore.NewQuery("Console") |
| 256 q.Order("ID") | 381 q.Order("ID") |
| 257 | 382 » con := []*Console{} |
| 258 » ps := []*Project{} | 383 » err := datastore.GetAll(c, q, &con) |
| 259 » err := datastore.GetAll(c, q, &ps) | 384 » 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 } | 385 } |
| 272 | 386 |
| 273 // GetProject returns the requested project. | 387 // GetConsole returns the requested console. |
| 274 func GetProject(c context.Context, projName string) (*config.Project, error) { | 388 func GetConsole(c context.Context, proj, id string) (*Console, error) { |
| 275 » // Next, Try datastore | 389 » // TODO(hinoka): Memcache this. |
| 276 » p := Project{ID: projName} | 390 » con := Console{ |
| 277 » if err := datastore.Get(c, &p); err != nil { | 391 » » Parent: datastore.MakeKey(c, "Project", proj), |
| 278 » » return nil, err | 392 » » ID: id, |
| 279 } | 393 } |
| 280 » mp := config.Project{} | 394 » err := datastore.Get(c, &con) |
| 281 » if err := proto.Unmarshal(p.Data, &mp); err != nil { | 395 » 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 } | 396 } |
| 301 | 397 |
| 302 // ProjectConsole is a simple tuple type for GetConsolesForBuilder. | 398 // ProjectConsole is a simple tuple type for GetConsolesForBuilder. |
| 303 type ProjectConsole struct { | 399 type ProjectConsole struct { |
| 304 ProjectID string | 400 ProjectID string |
| 305 Console *config.Console | 401 Console *config.Console |
| 306 } | 402 } |
| 307 | 403 |
| 308 // GetConsolesForBuilder retrieves all the console definitions that this builder | 404 // GetConsolesForBuilder retrieves all the console definitions that this builder |
| 309 // belongs to. | 405 // belongs to. |
| 310 func GetConsolesForBuilder(c context.Context, builderName string) ([]*ProjectCon sole, error) { | 406 func GetConsolesForBuilder(c context.Context, builderName string) ([]*Console, e rror) { |
| 311 » projs, err := GetAllProjects(c) | 407 » cons, err := GetAllConsoles(c) |
|
Ryan Tseng
2017/07/20 00:15:15
do the thing
Done
| |
| 312 if err != nil { | 408 if err != nil { |
| 313 return nil, err | 409 return nil, err |
| 314 } | 410 } |
| 315 » ret := []*ProjectConsole{} | 411 » ret := []*Console{} |
| 316 » for _, p := range projs { | 412 » for _, con := range cons { |
| 317 » » for _, con := range p.Consoles { | 413 » » for _, b := range con.Builders { |
| 318 » » » for _, b := range con.Builders { | 414 » » » if b == builderName { |
| 319 » » » » if b.Name == builderName { | 415 » » » » ret = append(ret, con) |
| 320 » » » » » ret = append(ret, &ProjectConsole{p.ID, con}) | |
| 321 » » » » } | |
| 322 } | 416 } |
| 323 } | 417 } |
| 324 } | 418 } |
| 325 return ret, nil | 419 return ret, nil |
| 326 } | 420 } |
| OLD | NEW |