blob: 9fbbec2f81eacec38822699bbeda7a7783dc6dbb [file] [log] [blame]
// Copyright 2016 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package common
import (
"fmt"
"strings"
"time"
"github.com/golang/protobuf/proto"
"golang.org/x/net/context"
"github.com/luci/gae/service/datastore"
"github.com/luci/gae/service/info"
configInterface "github.com/luci/luci-go/common/config"
"github.com/luci/luci-go/common/data/caching/proccache"
"github.com/luci/luci-go/common/data/stringset"
"github.com/luci/luci-go/common/errors"
"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/milo/api/config"
)
// Console is af datastore entity representing a single console.
type Console struct {
// Parent is a key to the parent Project entity where this console was
// defined in.
Parent *datastore.Key `gae:"$parent"`
// ID is the ID of the console.
ID string `gae:"$id"`
// RepoURL and Ref combined defines the commits the show up on the left
// hand side of a Console.
RepoURL string
// RepoURL and Ref combined defines the commits the show up on the left
// hand side of a Console.
Ref string
// ManifestName is the name of the manifest to look for when querying for
// builds under this console.
ManifestName string
// URL is the URL to the luci-config definition of this console.
URL string
// Revision is the luci-config reivision from when this Console was retrieved.
Revision string
// Builders is a list of universal builder IDs.
Builders []string
}
// GetProjectName retrieves the project name of the console out of the Console's
// parent key.
func (con *Console) GetProjectName() string {
return con.Parent.StringID()
}
// NewConsole creates a fully populated console out of the luci-config proto
// definition of a console.
func NewConsole(project *datastore.Key, URL, revision string, con *config.Console) *Console {
return &Console{
Parent: project,
ID: con.ID,
RepoURL: con.RepoURL,
Ref: con.Ref,
ManifestName: con.ManifestName,
Revision: revision,
URL: URL,
Builders: BuilderFromProto(con.Builders),
}
}
// BuilderFromProto tranforms a luci-config proto builder format into the datastore
// format.
func BuilderFromProto(cb []*config.Builder) []string {
builders := make([]string, len(cb))
for i, b := range cb {
builders[i] = b.Name
}
return builders
}
// LuciConfigURL returns a user friendly URL that specifies where to view
// this console definition.
func LuciConfigURL(c context.Context, configSet, path, revision string) string {
// TODO(hinoka): This shouldn't be hardcoded, instead we should get the
// luci-config instance from the context. But we only use this instance at
// the moment so it is okay for now.
// TODO(hinoka): The UI doesn't allow specifying paths and revision yet. Add
// that in when it is supported.
return fmt.Sprintf("https://luci-config.appspot.com/newui#/%s", configSet)
}
// 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) (*config.Settings, 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 nil, 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 nil, 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 nil, 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 nil, fmt.Errorf("failed to update config entry in transaction", err)
}
logging.Infof(c, "successfully updated to new config")
return settings, nil
}
// updateProjectConsoles updates all of the consoles for a given project,
// and then returns a set of known console names.
func updateProjectConsoles(c context.Context, projectName string, cfg *configInterface.Config) (stringset.Set, error) {
proj := config.Project{}
if err := proto.UnmarshalText(cfg.Content, &proj); err != nil {
return nil, errors.Annotate(err, "unmarshalling proto").Err()
}
// Keep a list of known consoles so we can prune deleted ones later.
knownConsoles := stringset.New(len(proj.Consoles))
// Iterate through all the proto consoles, adding and replacing the
// known ones if needed.
parentKey := datastore.MakeKey(c, "Project", projectName)
for _, pc := range proj.Consoles {
knownConsoles.Add(pc.ID)
con, err := GetConsole(c, projectName, pc.ID)
switch err {
case datastore.ErrNoSuchEntity:
// continue
case nil:
// Check if revisions match, if so just skip it.
if con.Revision == cfg.Revision {
continue
}
default:
return nil, errors.Annotate(err, "checking %s", pc.ID).Err()
}
URL := LuciConfigURL(c, cfg.ConfigSet, cfg.Path, cfg.Revision)
con = NewConsole(parentKey, URL, cfg.Revision, pc)
if err = datastore.Put(c, con); err != nil {
return nil, errors.Annotate(err, "saving %s", pc.ID).Err()
} else {
logging.Infof(c, "saved a new %s / %s (revision %s)", projectName, con.ID, cfg.Revision)
}
}
return knownConsoles, nil
}
// UpdateConsoles updates internal console definitions entities based off luci-config.
func UpdateConsoles(c context.Context) error {
cfgName := info.AppID(c) + ".cfg"
logging.Debugf(c, "fetching configs for %s", cfgName)
// Acquire the raw config client.
lucicfg := backend.Get(c).GetConfigInterface(c, backend.AsService)
// Project configs for Milo contains console definitions.
configs, err := lucicfg.GetProjectConfigs(c, cfgName, false)
if err != nil {
return errors.Annotate(err, "while fetching project configs").Err()
}
logging.Infof(c, "got %d project configs", len(configs))
merr := errors.MultiError{}
knownProjects := map[string]stringset.Set{}
// Iterate through each project config, extracting the console definition.
for _, cfg := range configs {
// This looks like "projects/<project name>"
splitPath := strings.SplitN(cfg.ConfigSet, "/", 2)
if len(splitPath) != 2 {
return fmt.Errorf("Invalid config set path %s", cfg.ConfigSet)
}
projectName := splitPath[1]
knownProjects[projectName] = nil
if kp, err := updateProjectConsoles(c, projectName, &cfg); err != nil {
err = errors.Annotate(err, "processing project %s", cfg.ConfigSet).Err()
merr = append(merr, err)
} else {
knownProjects[projectName] = kp
}
}
// Delete all the consoles that no longer exists or are part of deleted projects.
toDelete := []*datastore.Key{}
err = datastore.Run(c, datastore.NewQuery("Console"), func(key *datastore.Key) error {
proj := key.Parent().StringID()
id := key.StringID()
// If this console is either:
// 1. In a project that no longer exists, or
// 2. Not in the project, then delete it.
knownConsoles, ok := knownProjects[proj]
if !ok {
logging.Infof(
c, "deleting %s/%s because the project no longer exists", proj, id)
toDelete = append(toDelete, key)
return nil
}
if knownConsoles == nil {
// The project exists but we couldn't check it this time. Skip it and
// try again the next cron cycle.
return nil
}
if !knownConsoles.Has(id) {
logging.Infof(
c, "deleting %s/%s because the console no longer exists", proj, id)
toDelete = append(toDelete, key)
}
return nil
})
if err != nil {
merr = append(merr, err)
} else if err := datastore.Delete(c, toDelete); err != nil {
merr = append(merr, err)
}
// Print some stats.
processedConsoles := 0
for _, cons := range knownProjects {
if cons != nil {
processedConsoles += cons.Len()
}
}
logging.Infof(
c, "processed %d consoles over %d projects", len(knownProjects), processedConsoles)
if len(merr) == 0 {
return nil
}
return merr
}
// GetAllConsoles returns all registered projects with the builder name.
// If builderName is empty, then this retrieves all Consoles.
func GetAllConsoles(c context.Context, builderName string) ([]*Console, error) {
q := datastore.NewQuery("Console")
if builderName != "" {
q = q.Eq("Builders", builderName)
}
con := []*Console{}
err := datastore.GetAll(c, q, &con)
return con, err
}
// GetConsole returns the requested console.
func GetConsole(c context.Context, proj, id string) (*Console, error) {
// TODO(hinoka): Memcache this.
con := Console{
Parent: datastore.MakeKey(c, "Project", proj),
ID: id,
}
err := datastore.Get(c, &con)
return &con, err
}