blob: 360df2da73c7df0872e4d50a7e79606172386fa5 [file] [log] [blame]
// 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)
}