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" |
| 29 "github.com/luci/luci-go/common/data/stringset" | |
| 27 "github.com/luci/luci-go/common/errors" | 30 "github.com/luci/luci-go/common/errors" |
| 28 "github.com/luci/luci-go/common/logging" | 31 "github.com/luci/luci-go/common/logging" |
| 29 "github.com/luci/luci-go/luci_config/server/cfgclient" | 32 "github.com/luci/luci-go/luci_config/server/cfgclient" |
| 30 "github.com/luci/luci-go/luci_config/server/cfgclient/backend" | 33 "github.com/luci/luci-go/luci_config/server/cfgclient/backend" |
| 31 "github.com/luci/luci-go/luci_config/server/cfgclient/textproto" | |
| 32 | 34 |
| 33 "github.com/luci/luci-go/milo/api/config" | 35 "github.com/luci/luci-go/milo/api/config" |
| 34 ) | 36 ) |
| 35 | 37 |
| 36 // Project is a LUCI project. | 38 // Console is af datastore entity representing a single console. |
| 37 type Project struct { | 39 type Console struct { |
| 38 » // ID of the project, as per self defined. This is the luci-config name . | 40 » // Parent is a key to the parent Project entity where this console was |
| 41 » // defined in. | |
| 42 » Parent *datastore.Key `gae:"$parent"` | |
| 43 » // ID is the ID of the console. | |
| 39 ID string `gae:"$id"` | 44 ID string `gae:"$id"` |
| 40 » // Data is the Project data in protobuf binary format. | 45 » // RepoURL and Ref combined defines the commits the show up on the left |
| 41 » Data []byte `gae:",noindex"` | 46 » // hand side of a Console. |
| 42 » // Revision is the latest revision we have of this project's config. | 47 » RepoURL string |
| 48 » // RepoURL and Ref combined defines the commits the show up on the left | |
| 49 » // hand side of a Console. | |
| 50 » Ref string | |
| 51 » // ManifestName is the name of the manifest to look for when querying fo r | |
| 52 » // builds under this console. | |
| 53 » ManifestName string | |
| 54 » // URL is the URL to the luci-config definition of this console. | |
| 55 » URL string | |
| 56 » // Revision is the luci-config reivision from when this Console was retr ieved. | |
| 43 Revision string | 57 Revision string |
| 58 // Builders is a list of universal builder IDs. | |
| 59 Builders []string | |
| 60 } | |
| 61 | |
| 62 // GetProjectName retrieves the project name of the console out of the Console's | |
| 63 // parent key. | |
| 64 func (con *Console) GetProjectName() string { | |
| 65 return con.Parent.StringID() | |
| 66 } | |
| 67 | |
| 68 // NewConsole creates a fully populated console out of the luci-config proto | |
| 69 // definition of a console. | |
| 70 func NewConsole(project *datastore.Key, URL, revision string, con *config.Consol e) *Console { | |
| 71 return &Console{ | |
| 72 Parent: project, | |
| 73 ID: con.ID, | |
| 74 RepoURL: con.RepoURL, | |
| 75 Ref: con.Ref, | |
| 76 ManifestName: con.ManifestName, | |
| 77 Revision: revision, | |
| 78 URL: URL, | |
| 79 Builders: BuilderFromProto(con.Builders), | |
| 80 } | |
| 81 } | |
| 82 | |
| 83 // BuilderFromProto tranforms a luci-config proto builder format into the datast ore | |
| 84 // format. | |
| 85 func BuilderFromProto(cb []*config.Builder) []string { | |
| 86 builders := make([]string, len(cb)) | |
| 87 for i, b := range cb { | |
| 88 builders[i] = b.Name | |
| 89 } | |
| 90 return builders | |
| 91 } | |
| 92 | |
| 93 // LuciConfigURL returns a user friendly URL that specifies where to view | |
| 94 // this console definition. | |
| 95 func LuciConfigURL(c context.Context, configSet, path, revision string) string { | |
| 96 // TODO(hinoka): This shouldn't be hardcoded, instead we should get the | |
| 97 // luci-config instance from the context. But we only use this instance at | |
| 98 // the moment so it is okay for now. | |
| 99 // TODO(hinoka): The UI doesn't allow specifying paths and revision yet. Add | |
| 100 // that in when it is supported. | |
| 101 return fmt.Sprintf("https://luci-config.appspot.com/newui#/%s", configSe t) | |
| 44 } | 102 } |
| 45 | 103 |
| 46 // The key for the service config entity in datastore. | 104 // The key for the service config entity in datastore. |
| 47 const ServiceConfigID = "service_config" | 105 const ServiceConfigID = "service_config" |
| 48 | 106 |
| 49 // ServiceConfig is a container for the instance's service config. | 107 // ServiceConfig is a container for the instance's service config. |
| 50 type ServiceConfig struct { | 108 type ServiceConfig struct { |
| 51 // ID is the datastore key. This should be static, as there should only be | 109 // ID is the datastore key. This should be static, as there should only be |
| 52 // one service config. | 110 // one service config. |
| 53 ID string `gae:"$id"` | 111 ID string `gae:"$id"` |
| (...skipping 117 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 171 }, nil) | 229 }, nil) |
| 172 | 230 |
| 173 if err != nil { | 231 if err != nil { |
| 174 return nil, fmt.Errorf("failed to update config entry in transac tion", err) | 232 return nil, fmt.Errorf("failed to update config entry in transac tion", err) |
| 175 } | 233 } |
| 176 logging.Infof(c, "successfully updated to new config") | 234 logging.Infof(c, "successfully updated to new config") |
| 177 | 235 |
| 178 return settings, nil | 236 return settings, nil |
| 179 } | 237 } |
| 180 | 238 |
| 181 // UpdateProjectConfigs internal project configuration based off luci-config. | 239 // updateProjectConsoles updates all of the consoles for a given project, |
| 182 // update updates Milo's configuration based off luci config. This includes | 240 // and then returns a set of known console names. |
| 183 // scanning through all project and extract all console configs. | 241 func updateProjectConsoles(c context.Context, projectName string, cfg *configInt erface.Config) (stringset.Set, error) { |
| 184 func UpdateProjectConfigs(c context.Context) error { | 242 » proj := config.Project{} |
| 243 » if err := proto.UnmarshalText(cfg.Content, &proj); err != nil { | |
| 244 » » return nil, errors.Annotate(err, "unmarshalling proto").Err() | |
| 245 » } | |
| 246 | |
| 247 » // Keep a list of known consoles so we can prune deleted ones later. | |
| 248 » knownConsoles := stringset.New(len(proj.Consoles)) | |
| 249 » // Iterate through all the proto consoles, adding and replacing the | |
| 250 » // known ones if needed. | |
| 251 » parentKey := datastore.MakeKey(c, "Project", projectName) | |
| 252 » for _, pc := range proj.Consoles { | |
| 253 » » knownConsoles.Add(pc.ID) | |
| 254 » » con, err := GetConsole(c, projectName, pc.ID) | |
| 255 » » switch err { | |
| 256 » » case datastore.ErrNoSuchEntity: | |
| 257 » » » // continue | |
| 258 » » case nil: | |
| 259 » » » // Check if revisions match, if so just skip it. | |
| 260 » » » if con.Revision == cfg.Revision { | |
| 261 » » » » continue | |
| 262 » » » } | |
| 263 » » default: | |
| 264 » » » return nil, errors.Annotate(err, "checking %s", pc.ID).E rr() | |
| 265 » » } | |
| 266 » » URL := LuciConfigURL(c, cfg.ConfigSet, cfg.Path, cfg.Revision) | |
| 267 » » con = NewConsole(parentKey, URL, cfg.Revision, pc) | |
| 268 » » if err = datastore.Put(c, con); err != nil { | |
| 269 » » » return nil, errors.Annotate(err, "saving %s", pc.ID).Err () | |
| 270 » » } else { | |
| 271 » » » logging.Infof(c, "saved a new %s / %s (revision %s)", pr ojectName, con.ID, cfg.Revision) | |
| 272 » » } | |
| 273 » } | |
| 274 » return knownConsoles, nil | |
| 275 } | |
| 276 | |
| 277 // UpdateConsoles updates internal console definitions entities based off luci-c onfig. | |
| 278 func UpdateConsoles(c context.Context) error { | |
| 185 cfgName := info.AppID(c) + ".cfg" | 279 cfgName := info.AppID(c) + ".cfg" |
| 186 | 280 |
| 187 » var ( | 281 » logging.Debugf(c, "fetching configs for %s", cfgName) |
| 188 » » configs []*config.Project | 282 » // Acquire the raw config client. |
| 189 » » metas []*cfgclient.Meta | 283 » lucicfg := backend.Get(c).GetConfigInterface(c, backend.AsService) |
| 190 » » merr errors.MultiError | 284 » // Project configs for Milo contains console definitions. |
| 191 » ) | 285 » configs, err := lucicfg.GetProjectConfigs(c, cfgName, false) |
| 286 » if err != nil { | |
| 287 » » return errors.Annotate(err, "while fetching project configs").Er r() | |
| 288 » } | |
| 289 » logging.Infof(c, "got %d project configs", len(configs)) | |
| 192 | 290 |
| 193 » logging.Debugf(c, "fetching configs for %s", cfgName) | 291 » merr := errors.MultiError{} |
| 194 » if err := cfgclient.Projects(c, cfgclient.AsService, cfgName, textproto. Slice(&configs), &metas); err != nil { | 292 » knownProjects := map[string]stringset.Set{} |
| 195 » » merr = err.(errors.MultiError) | 293 » // 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. | 294 » for _, cfg := range configs { |
| 295 » » // This looks like "projects/<project name>" | |
| 296 » » splitPath := strings.SplitN(cfg.ConfigSet, "/", 2) | |
| 297 » » if len(splitPath) != 2 { | |
| 298 » » » return fmt.Errorf("Invalid config set path %s", cfg.Conf igSet) | |
| 299 » » } | |
| 300 » » projectName := splitPath[1] | |
| 301 » » knownProjects[projectName] = nil | |
| 302 » » if kp, err := updateProjectConsoles(c, projectName, &cfg); err ! = nil { | |
| 303 » » » err = errors.Annotate(err, "processing project %s", cfg. ConfigSet).Err() | |
| 304 » » » merr = append(merr, err) | |
| 305 » » } else { | |
| 306 » » » knownProjects[projectName] = kp | |
| 307 » » } | |
| 197 } | 308 } |
| 198 | 309 |
| 199 » // A map of project ID to project. | 310 » // Delete all the consoles that no longer exists or are part of deleted projects. |
| 200 » projects := map[string]*Project{} | 311 » toDelete := []*datastore.Key{} |
| 201 » for i, proj := range configs { | 312 » existingConsoles, err := GetAllConsoles(c, "") |
|
iannucci
2017/07/20 00:21:47
err := datastore.Run(c, datastore.NewQuery("Consol
| |
| 202 » » meta := metas[i] | 313 » for _, ec := range existingConsoles { |
| 203 » » projectName, _, _ := meta.ConfigSet.SplitProject() | 314 » » // If this console is either: |
| 204 » » name := string(projectName) | 315 » » // 1. In a project that no longer exists, or |
| 205 » » projects[name] = nil | 316 » » // 2. Not in the project, then delete it. |
| 206 » » if merr != nil && merr[i] != nil { | 317 » » proj := ec.GetProjectName() |
| 207 » » » logging.WithError(merr[i]).Warningf(c, "skipping %s due to error", name) | 318 » » ecKey := datastore.KeyForObj(c, ec) |
| 319 » » knownConsoles, ok := knownProjects[proj] | |
| 320 » » if !ok { | |
| 321 » » » logging.Infof( | |
| 322 » » » » c, "deleting %s/%s because the project no longer exists", proj, ec.ID) | |
| 323 » » » toDelete = append(toDelete, ecKey) | |
| 208 continue | 324 continue |
| 209 } | 325 } |
| 210 | 326 » » if knownConsoles == nil { |
| 211 » » p := &Project{ | 327 » » » // The project exists but we couldn't check it this time . Skip it and |
| 212 » » » ID: name, | 328 » » » // try again the next cron cycle. |
| 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 | 329 continue |
| 225 } | 330 } |
| 226 » » projects[name] = p | 331 » » if !knownConsoles.Has(ec.ID) { |
| 332 » » » logging.Infof( | |
| 333 » » » » c, "deleting %s/%s because the console no longer exists", proj, ec.ID) | |
| 334 » » » toDelete = append(toDelete, ecKey) | |
| 335 » » } | |
| 336 » } | |
| 337 » if err := datastore.Delete(c, toDelete); err != nil { | |
| 338 » » merr = append(merr, err) | |
| 227 } | 339 } |
| 228 | 340 |
| 229 » // Now load all the data into the datastore. | 341 » // Print some stats. |
| 230 » projs := make([]*Project, 0, len(projects)) | 342 » processedConsoles := 0 |
| 231 » for _, proj := range projects { | 343 » for _, cons := range knownProjects { |
| 232 » » if proj != nil { | 344 » » processedConsoles += cons.Len() |
| 233 » » » projs = append(projs, proj) | |
| 234 » » } | |
| 235 } | 345 } |
| 236 » if err := datastore.Put(c, projs); err != nil { | 346 » logging.Infof( |
| 237 » » return err | 347 » » c, "processed %d consoles over %d projects", len(knownProjects), processedConsoles) |
| 348 | |
| 349 » if len(merr) == 0 { | |
| 350 » » return nil | |
| 238 } | 351 } |
| 239 | 352 » 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 } | 353 } |
| 252 | 354 |
| 253 // GetAllProjects returns all registered projects. | 355 // GetAllConsoles returns all registered projects with the builder name. |
| 254 func GetAllProjects(c context.Context) ([]*config.Project, error) { | 356 // If builderName is empty, then this retrieves all Consoles. |
| 255 » q := datastore.NewQuery("Project") | 357 func GetAllConsoles(c context.Context, builderName string) ([]*Console, error) { |
| 358 » q := datastore.NewQuery("Console") | |
| 359 » if builderName != "" { | |
| 360 » » q = q.Eq("Builders", builderName) | |
| 361 » } | |
| 256 q.Order("ID") | 362 q.Order("ID") |
| 257 | 363 » con := []*Console{} |
| 258 » ps := []*Project{} | 364 » err := datastore.GetAll(c, q, &con) |
| 259 » err := datastore.GetAll(c, q, &ps) | 365 » 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 } | 366 } |
| 272 | 367 |
| 273 // GetProject returns the requested project. | 368 // GetConsole returns the requested console. |
| 274 func GetProject(c context.Context, projName string) (*config.Project, error) { | 369 func GetConsole(c context.Context, proj, id string) (*Console, error) { |
| 275 » // Next, Try datastore | 370 » // TODO(hinoka): Memcache this. |
| 276 » p := Project{ID: projName} | 371 » con := Console{ |
| 277 » if err := datastore.Get(c, &p); err != nil { | 372 » » Parent: datastore.MakeKey(c, "Project", proj), |
| 278 » » return nil, err | 373 » » ID: id, |
| 279 } | 374 } |
| 280 » mp := config.Project{} | 375 » err := datastore.Get(c, &con) |
| 281 » if err := proto.Unmarshal(p.Data, &mp); err != nil { | 376 » return &con, err |
| 282 » » return nil, err | |
| 283 » } | |
| 284 | |
| 285 » return &mp, nil | |
| 286 } | 377 } |
| 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 } | |
| 301 | |
| 302 // ProjectConsole is a simple tuple type for GetConsolesForBuilder. | |
| 303 type ProjectConsole struct { | |
| 304 ProjectID string | |
| 305 Console *config.Console | |
| 306 } | |
| 307 | |
| 308 // GetConsolesForBuilder retrieves all the console definitions that this builder | |
| 309 // belongs to. | |
| 310 func GetConsolesForBuilder(c context.Context, builderName string) ([]*ProjectCon sole, error) { | |
| 311 projs, err := GetAllProjects(c) | |
| 312 if err != nil { | |
| 313 return nil, err | |
| 314 } | |
| 315 ret := []*ProjectConsole{} | |
| 316 for _, p := range projs { | |
| 317 for _, con := range p.Consoles { | |
| 318 for _, b := range con.Builders { | |
| 319 if b.Name == builderName { | |
| 320 ret = append(ret, &ProjectConsole{p.ID, con}) | |
| 321 } | |
| 322 } | |
| 323 } | |
| 324 } | |
| 325 return ret, nil | |
| 326 } | |
| OLD | NEW |