blob: 52623f7002a64d308ebe1ff2f8b0393b8b80e4e4 [file] [log] [blame]
# Copyright 2015 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.
"""Provides info about projects (service tenants)."""
import logging
from google.appengine.api import memcache
from google.appengine.ext import ndb
from google.appengine.ext.ndb import msgprop
from protorpc import messages
from components.config.proto import project_config_pb2
from components.config.proto import service_config_pb2
import common
import storage
class RepositoryType(messages.Enum):
GITILES = 1
class ProjectImportInfo(ndb.Model):
"""Contains info how a project was imported.
Entity key:
Id is project id from the project registry. Has no parent.
"""
created_ts = ndb.DateTimeProperty(auto_now_add=True)
repo_type = msgprop.EnumProperty(RepositoryType, required=True)
repo_url = ndb.StringProperty(required=True)
@ndb.transactional
def update_import_info(project_id, repo_type, repo_url):
"""Updates ProjectImportInfo if needed."""
info = ProjectImportInfo.get_by_id(project_id)
if info and info.repo_type == repo_type and info.repo_url == repo_url:
return
if info:
values = (
('repo_url', repo_url, info.repo_url),
('repo_type', repo_type, info.repo_type),
)
logging.warning('Changing project %s repo info:\n%s',
project_id,
'\n'.join([
'%s: %s -> %s' % (attr, old_value, new_value)
for attr, old_value, new_value in values
if old_value != new_value
]))
else:
logging.info('Creating project %s repo info: %s %s',
project_id, repo_type, repo_url)
ProjectImportInfo(id=project_id, repo_type=repo_type, repo_url=repo_url).put()
def get_projects():
"""Returns a list of projects stored in services/luci-config:projects.cfg.
Never returns None. Cached.
"""
cfg = storage.get_self_config_async(
common.PROJECT_REGISTRY_FILENAME,
service_config_pb2.ProjectsCfg).get_result()
return cfg.projects or []
def get_project(project_id):
"""Returns a project by id."""
for p in get_projects():
if p.id == project_id:
return p
return None
@ndb.tasklet
def get_repos_async(project_ids):
"""Returns a mapping {project_id: (repo_type, repo_url)}.
All projects must exist.
"""
assert isinstance(project_ids, list)
infos = yield ndb.get_multi_async(
ndb.Key(ProjectImportInfo, pid) for pid in project_ids)
raise ndb.Return({
pid: (info.repo_type, info.repo_url) if info else (None, None)
for pid, info in zip(project_ids, infos)
})
@ndb.tasklet
def get_metadata_async(project_ids):
"""Returns a mapping {project_id: metadata}.
If a project does not exist, the metadata is None.
The project metadata stored in project.cfg files in each project.
"""
PROJECT_DOES_NOT_EXIST_SENTINEL = (0,)
cache_ns = 'projects.get_metadata'
ctx = ndb.get_context()
# ctx.memcache_get is auto-batching. Internally it makes get_multi RPC.
cache_futs = {
pid: ctx.memcache_get(pid, namespace=cache_ns)
for pid in project_ids
}
yield cache_futs.values()
result = {}
missing = []
for pid in project_ids:
binary = cache_futs[pid].get_result()
if binary is not None:
# cache hit
if binary == PROJECT_DOES_NOT_EXIST_SENTINEL:
result[pid] = None
else:
cfg = project_config_pb2.ProjectCfg()
cfg.ParseFromString(binary)
result[pid] = cfg
else:
# cache miss
missing.append(pid)
if missing:
fetched = yield _get_project_configs_async(
missing, common.PROJECT_METADATA_FILENAME,
project_config_pb2.ProjectCfg)
result.update(fetched) # at this point result must have all project ids
# Cache metadata for 10 min. In practice, it never changes.
# ctx.memcache_set is auto-batching. Internally it makes set_multi RPC.
yield [
ctx.memcache_set(
pid,
cfg.SerializeToString() if cfg else PROJECT_DOES_NOT_EXIST_SENTINEL,
namespace=cache_ns,
time=60 * 10)
for pid, cfg in fetched.items()
]
raise ndb.Return(result)
def get_refs(project_ids):
"""DEPRECATED. Returns a mapping {project_id: list of refs}
The ref list is None if a project does not exist.
The list of refs stored in refs.cfg of a project.
"""
cfgs = _get_project_configs_async(
project_ids, common.REFS_FILENAME, project_config_pb2.RefsCfg
).get_result()
return {
pid: (
None if cfg is None
# TODO(crbug/924803): remove ref support from the service entirely.
else []) for pid, cfg in cfgs.items()
}
def _get_project_configs_async(project_ids, path, message_factory):
"""Returns a mapping {project_id: message}.
If a project does not exist, the message is None.
"""
assert isinstance(project_ids, list)
if not project_ids:
empty = ndb.Future()
empty.set_result({})
return empty
@ndb.tasklet
def get_async():
prefix = 'projects/'
msgs = yield storage.get_latest_messages_async(
[prefix + pid for pid in _filter_existing(project_ids)],
path, message_factory)
raise ndb.Return({
# msgs may not have a key because we filter project ids by existence
pid: msgs.get(prefix + pid)
for pid in project_ids
})
return get_async()
def _filter_existing(project_ids):
# TODO(nodir): optimize
assert isinstance(project_ids, list)
if not project_ids:
return project_ids
assert all(pid for pid in project_ids)
all_project_ids = set(p.id for p in get_projects())
return [
pid for pid in project_ids
if pid in all_project_ids
]