| // Copyright 2016 The LUCI Authors. All rights reserved. |
| // Use of this source code is governed under the Apache License, Version 2.0 |
| // that can be found in the LICENSE file. |
| |
| package common |
| |
| import ( |
| "fmt" |
| "time" |
| |
| "github.com/golang/protobuf/proto" |
| "golang.org/x/net/context" |
| |
| "github.com/luci/gae/service/datastore" |
| "github.com/luci/gae/service/info" |
| "github.com/luci/luci-go/common/data/caching/proccache" |
| "github.com/luci/luci-go/common/logging" |
| "github.com/luci/luci-go/luci_config/server/cfgclient" |
| "github.com/luci/luci-go/luci_config/server/cfgclient/backend" |
| "github.com/luci/luci-go/luci_config/server/cfgclient/textproto" |
| |
| "github.com/luci/luci-go/milo/api/config" |
| ) |
| |
| // Project is a LUCI project. |
| type Project struct { |
| // The ID of the project, as per self defined. This is not the luci-config |
| // name. |
| ID string `gae:"$id"` |
| // The luci-config name of the project. |
| Name string |
| // The Project data in protobuf binary format. |
| Data []byte `gae:",noindex"` |
| } |
| |
| // The key for the service config entity in datastore. |
| const ServiceConfigID = "service_config" |
| |
| // ServiceConfig is a container for the instance's service config. |
| type ServiceConfig struct { |
| // ID is the datastore key. This should be static, as there should only be |
| // one service config. |
| ID string `gae:"$id"` |
| // Revision is the revision of the config, taken from luci-config. This is used |
| // to determine if the entry needs to be refreshed. |
| Revision string |
| // Data is the binary proto of the config. |
| Data []byte `gae:",noindex"` |
| // Text is the text format of the config. For human consumption only. |
| Text string `gae:",noindex"` |
| // LastUpdated is the time this config was last updated. |
| LastUpdated time.Time |
| } |
| |
| // GetServiceConfig returns the service (aka global) config for the current |
| // instance of Milo from the datastore. Returns an empty config and warn heavily |
| // if none is found. |
| // TODO(hinoka): Use process cache to cache configs. |
| func GetSettings(c context.Context) *config.Settings { |
| settings := config.Settings{ |
| Buildbot: &config.Settings_Buildbot{}, |
| } |
| |
| msg, err := GetCurrentServiceConfig(c) |
| if err != nil { |
| // The service config does not exist, just return an empty config |
| // and complain loudly in the logs. |
| logging.WithError(err).Errorf(c, |
| "Encountered error while loading service config, using empty config.") |
| return &settings |
| } |
| |
| err = proto.Unmarshal(msg.Data, &settings) |
| if err != nil { |
| // The service config is broken, just return an empty config |
| // and complain loudly in the logs. |
| logging.WithError(err).Errorf(c, |
| "Encountered error while unmarshalling service config, using empty config.") |
| // Zero out the message just incase something got written in. |
| settings = config.Settings{Buildbot: &config.Settings_Buildbot{}} |
| } |
| |
| return &settings |
| } |
| |
| // GetCurrentServiceConfig gets the service config for the instance from either |
| // process cache or datastore cache. |
| func GetCurrentServiceConfig(c context.Context) (*ServiceConfig, error) { |
| // This maker function is used to do the actual fetch of the ServiceConfig |
| // from datastore. It is called if the ServiceConfig is not in proc cache. |
| maker := func() (interface{}, time.Duration, error) { |
| msg := ServiceConfig{ID: ServiceConfigID} |
| err := datastore.Get(c, &msg) |
| if err != nil { |
| return nil, time.Minute, err |
| } |
| logging.Infof(c, "loaded service config from datastore") |
| return msg, time.Minute, nil |
| } |
| item, err := proccache.GetOrMake(c, ServiceConfigID, maker) |
| if err != nil { |
| return nil, fmt.Errorf("failed to get service config: %s", err.Error()) |
| } |
| if msg, ok := item.(ServiceConfig); ok { |
| logging.Infof(c, "loaded config entry from %s", msg.LastUpdated.Format(time.RFC3339)) |
| return &msg, nil |
| } |
| return nil, fmt.Errorf("could not load service config %#v", item) |
| } |
| |
| // UpdateServiceConfig fetches the service config from luci-config |
| // and then stores a snapshot of the configuration in datastore. |
| func UpdateServiceConfig(c context.Context) error { |
| // Load the settings from luci-config. |
| cs := string(cfgclient.CurrentServiceConfigSet(c)) |
| // Acquire the raw config client. |
| lucicfg := backend.Get(c).GetConfigInterface(c, backend.AsService) |
| // Our global config name is called settings.cfg. |
| cfg, err := lucicfg.GetConfig(c, cs, "settings.cfg", false) |
| if err != nil { |
| return fmt.Errorf("could not load settings.cfg from luci-config: %s", err) |
| } |
| settings := &config.Settings{} |
| err = proto.UnmarshalText(cfg.Content, settings) |
| if err != nil { |
| return fmt.Errorf("could not unmarshal proto from luci-config:\n%s", cfg.Content) |
| } |
| newConfig := ServiceConfig{ |
| ID: ServiceConfigID, |
| Text: cfg.Content, |
| Revision: cfg.Revision, |
| LastUpdated: time.Now().UTC(), |
| } |
| newConfig.Data, err = proto.Marshal(settings) |
| if err != nil { |
| return fmt.Errorf("could not marshal proto into binary\n%s", newConfig.Text) |
| } |
| |
| // Do the revision check & swap in a datastore transaction. |
| err = datastore.RunInTransaction(c, func(c context.Context) error { |
| oldConfig := ServiceConfig{ID: ServiceConfigID} |
| err := datastore.Get(c, &oldConfig) |
| switch err { |
| case datastore.ErrNoSuchEntity: |
| // Might be the first time this has run. |
| logging.WithError(err).Warningf(c, "No existing service config.") |
| case nil: |
| // Continue |
| default: |
| return fmt.Errorf("could not load existing config: %s", err) |
| } |
| // Check to see if we need to update |
| if oldConfig.Revision == newConfig.Revision { |
| logging.Infof(c, "revisions matched (%s), no need to update", oldConfig.Revision) |
| return nil |
| } |
| logging.Infof(c, "revisions differ (old %s, new %s), updating", |
| oldConfig.Revision, newConfig.Revision) |
| return datastore.Put(c, &newConfig) |
| }, nil) |
| |
| if err != nil { |
| return fmt.Errorf("failed to update config entry in transaction", err) |
| } |
| logging.Infof(c, "successfully updated to new config") |
| |
| return nil |
| } |
| |
| // UpdateProjectConfigs internal project configuration based off luci-config. |
| // update updates Milo's configuration based off luci config. This includes |
| // scanning through all project and extract all console configs. |
| func UpdateProjectConfigs(c context.Context) error { |
| cfgName := info.AppID(c) + ".cfg" |
| |
| var ( |
| configs []*config.Project |
| metas []*cfgclient.Meta |
| ) |
| logging.Debugf(c, "fetching configs for %s", cfgName) |
| if err := cfgclient.Projects(c, cfgclient.AsService, cfgName, textproto.Slice(&configs), &metas); err != nil { |
| logging.WithError(err).Errorf(c, "Encountered error while getting project config for %s", cfgName) |
| return err |
| } |
| |
| // A map of project ID to project. |
| projects := map[string]*Project{} |
| for i, proj := range configs { |
| projectName, _, _ := metas[i].ConfigSet.SplitProject() |
| |
| logging.Infof(c, "Prossing %s", projectName) |
| if dup, ok := projects[proj.ID]; ok { |
| return fmt.Errorf( |
| "Duplicate project ID: %s. (%s and %s)", proj.ID, dup.Name, projectName) |
| } |
| p := &Project{ |
| ID: proj.ID, |
| Name: string(projectName), |
| } |
| projects[proj.ID] = p |
| |
| var err error |
| p.Data, err = proto.Marshal(proj) |
| if err != nil { |
| return err |
| } |
| } |
| |
| // Now load all the data into the datastore. |
| projs := make([]*Project, 0, len(projects)) |
| for _, proj := range projects { |
| projs = append(projs, proj) |
| } |
| if err := datastore.Put(c, projs); err != nil { |
| return err |
| } |
| |
| // Delete entries that no longer exist. |
| q := datastore.NewQuery("Project").KeysOnly(true) |
| allProjs := []Project{} |
| datastore.GetAll(c, q, &allProjs) |
| toDelete := []Project{} |
| for _, proj := range allProjs { |
| if _, ok := projects[proj.ID]; !ok { |
| toDelete = append(toDelete, proj) |
| } |
| } |
| datastore.Delete(c, toDelete) |
| |
| return nil |
| } |
| |
| // GetAllProjects returns all registered projects. |
| func GetAllProjects(c context.Context) ([]*config.Project, error) { |
| q := datastore.NewQuery("Project") |
| q.Order("ID") |
| |
| ps := []*Project{} |
| err := datastore.GetAll(c, q, &ps) |
| if err != nil { |
| return nil, err |
| } |
| results := make([]*config.Project, len(ps)) |
| for i, p := range ps { |
| results[i] = &config.Project{} |
| if err := proto.Unmarshal(p.Data, results[i]); err != nil { |
| return nil, err |
| } |
| } |
| return results, nil |
| } |
| |
| // GetProject returns the requested project. |
| func GetProject(c context.Context, projName string) (*config.Project, error) { |
| // Next, Try datastore |
| p := Project{ID: projName} |
| if err := datastore.Get(c, &p); err != nil { |
| return nil, err |
| } |
| mp := config.Project{} |
| if err := proto.Unmarshal(p.Data, &mp); err != nil { |
| return nil, err |
| } |
| |
| return &mp, nil |
| } |
| |
| // GetConsole returns the requested console instance. |
| func GetConsole(c context.Context, projName, consoleName string) (*config.Console, error) { |
| p, err := GetProject(c, projName) |
| if err != nil { |
| return nil, err |
| } |
| for _, cs := range p.Consoles { |
| if cs.Name == consoleName { |
| return cs, nil |
| } |
| } |
| return nil, fmt.Errorf("Console %s not found in project %s", consoleName, projName) |
| } |