gecko-dev/taskcluster/actions/registry.py

306 строки
13 KiB
Python

import json
import os
import inspect
from types import FunctionType
from collections import namedtuple
from taskgraph.util.docker import docker_image
from taskgraph.parameters import Parameters
GECKO = os.path.realpath(os.path.join(__file__, '..', '..', '..'))
actions = []
callbacks = {}
Action = namedtuple('Action', [
'title', 'description', 'order', 'context', 'schema', 'task_template_builder',
])
def is_json(data):
""" Return ``True``, if ``data`` is a JSON serializable data structure. """
try:
json.dumps(data)
except ValueError:
return False
return True
def register_task_action(title, description, order, context, schema):
"""
Register an action task that can be triggered from supporting
user interfaces, such as Treeherder.
Most actions will create intermediate action tasks that call back into
in-tree python code. To write such an action please use
:func:`register_callback_action`.
This function is to be used a decorator for a function that returns a task
template, see :doc:`specification <action-spec>` for details on the
templating features. The decorated function will be given decision task
parameters, which can be embedded in the task template that is returned.
Parameters
----------
title : str
A human readable title for the action to be used as label on a button
or text on a link for triggering the action.
description : str
A human readable description of the action in **markdown**.
This will be display as tooltip and in dialog window when the action
is triggered. This is a good place to describe how to use the action.
order : int
Order of the action in menus, this is relative to the ``order`` of
other actions declared.
context : list of dict
List of tag-sets specifying which tasks the action is can take as input.
If no tag-sets is specified as input the action is related to the
entire task-group, and won't be triggered with a given task.
Otherwise, if ``context = [{'k': 'b', 'p': 'l'}, {'k': 't'}]`` will only
be displayed in the context menu for tasks that has
``task.tags.k == 'b' && task.tags.p = 'l'`` or ``task.tags.k = 't'``.
Esentially, this allows filtering on ``task.tags``.
schema : dict
JSON schema specifying input accepted by the action.
This is optional and can be left ``null`` if no input is taken.
Returns
-------
function
To be used as decorator for the function that builds the task template.
The decorated function will be given decision parameters and may return
``None`` instead of a task template, if the action is disabled.
"""
assert isinstance(title, basestring), 'title must be a string'
assert isinstance(description, basestring), 'description must be a string'
assert isinstance(order, int), 'order must be an integer'
assert is_json(schema), 'schema must be a JSON compatible object'
mem = {"registered": False} # workaround nonlocal missing in 2.x
def register_task_template_builder(task_template_builder):
assert not mem['registered'], 'register_task_action must be used as decorator'
actions.append(Action(
title.strip(), description.strip(), order, context, schema, task_template_builder,
))
mem['registered'] = True
return register_task_template_builder
def register_callback_action(title, symbol, description, order=10000, context=[],
available=lambda parameters: True, schema=None):
"""
Register an action callback that can be triggered from supporting
user interfaces, such as Treeherder.
This function is to be used as a decorator for a callback that takes
parameters as follows:
``parameters``:
Decision task parameters, see ``taskgraph.parameters.Parameters``.
``input``:
Input matching specified JSON schema, ``None`` if no ``schema``
parameter is given to ``register_callback_action``.
``task_group_id``:
The id of the task-group this was triggered for.
``task_id`` and `task``:
task identifier and task definition for task the action was triggered
for, ``None`` if no ``context`` parameters was given to
``register_callback_action``.
Parameters
----------
title : str
A human readable title for the action to be used as label on a button
or text on a link for triggering the action.
symbol : str
Treeherder symbol for the action callback, this is the symbol that the
task calling your callback will be displayed as. This is usually 1-3
letters abbreviating the action title.
description : str
A human readable description of the action in **markdown**.
This will be display as tooltip and in dialog window when the action
is triggered. This is a good place to describe how to use the action.
order : int
Order of the action in menus, this is relative to the ``order`` of
other actions declared.
context : list of dict
List of tag-sets specifying which tasks the action is can take as input.
If no tag-sets is specified as input the action is related to the
entire task-group, and won't be triggered with a given task.
Otherwise, if ``context = [{'k': 'b', 'p': 'l'}, {'k': 't'}]`` will only
be displayed in the context menu for tasks that has
``task.tags.k == 'b' && task.tags.p = 'l'`` or ``task.tags.k = 't'``.
Esentially, this allows filtering on ``task.tags``.
available : function
An optional function that given decision parameters decides if the
action is available. Defaults to a function that always returns ``True``.
schema : dict
JSON schema specifying input accepted by the action.
This is optional and can be left ``null`` if no input is taken.
Returns
-------
function
To be used as decorator for the callback function.
"""
mem = {"registered": False} # workaround nonlocal missing in 2.x
def register_callback(cb):
assert isinstance(cb, FunctionType), 'callback must be a function'
assert isinstance(symbol, basestring), 'symbol must be a string'
assert 1 <= len(symbol) <= 25, 'symbol must be between 1 and 25 characters'
assert not mem['registered'], 'register_callback_action must be used as decorator'
assert cb.__name__ not in callbacks, 'callback name {} is not unique'.format(cb.__name__)
source_path = os.path.relpath(inspect.stack()[1][1], GECKO)
@register_task_action(title, description, order, context, schema)
def build_callback_action_task(parameters):
if not available(parameters):
return None
return {
'created': {'$fromNow': ''},
'deadline': {'$fromNow': '12 hours'},
'expires': {'$fromNow': '14 days'},
'metadata': {
'owner': 'mozilla-taskcluster-maintenance@mozilla.com',
'source': '{}raw-file/{}/{}'.format(
parameters['head_repository'], parameters['head_rev'], source_path,
),
'name': 'Action: {}'.format(title),
'description': 'Task executing callback for action.\n\n---\n' + description,
},
'workerType': 'gecko-decision',
'provisionerId': 'aws-provisioner-v1',
'scopes': [
'assume:repo:hg.mozilla.org/projects/{}:*'.format(parameters['project']),
],
'tags': {
'createdForUser': parameters['owner'],
'kind': 'action-callback',
},
'routes': [
'tc-treeherder.v2.{}.{}.{}'.format(
parameters['project'], parameters['head_rev'], parameters['pushlog_id']),
'tc-treeherder-stage.v2.{}.{}.{}'.format(
parameters['project'], parameters['head_rev'], parameters['pushlog_id']),
],
'payload': {
'env': {
'GECKO_BASE_REPOSITORY': 'https://hg.mozilla.org/mozilla-unified',
'GECKO_HEAD_REPOSITORY': parameters['head_repository'],
'GECKO_HEAD_REF': parameters['head_ref'],
'GECKO_HEAD_REV': parameters['head_rev'],
'HG_STORE_PATH': '/home/worker/checkouts/hg-store',
'ACTION_TASK_GROUP_ID': {'$eval': 'taskGroupId'},
'ACTION_TASK_ID': {'$dumps': {'$eval': 'taskId'}},
'ACTION_TASK': {'$dumps': {'$eval': 'task'}},
'ACTION_INPUT': {'$dumps': {'$eval': 'input'}},
'ACTION_CALLBACK': cb.__name__,
'ACTION_PARAMETERS': {'$dumps': {'$eval': 'parameters'}},
},
'cache': {
'level-{}-checkouts'.format(parameters['level']):
'/home/worker/checkouts',
},
'features': {
'taskclusterProxy': True,
'chainOfTrust': True,
},
'image': docker_image('decision'),
'maxRunTime': 1800,
'command': [
'/home/worker/bin/run-task', '--vcs-checkout=/home/worker/checkouts/gecko',
'--', 'bash', '-cx',
"""\
cd /home/worker/checkouts/gecko &&
ln -s /home/worker/artifacts artifacts &&
./mach --log-no-times taskgraph action-callback """ + ' '.join([
"--pushlog-id='{}'".format(parameters['pushlog_id']),
"--pushdate='{}'".format(parameters['pushdate']),
"--project='{}'".format(parameters['project']),
"--message='{}'".format(parameters['message'].replace("'", "'\\''")),
"--owner='{}'".format(parameters['owner']),
"--level='{}'".format(parameters['level']),
"--base-repository='https://hg.mozilla.org/mozilla-central'",
"--head-repository='{}'".format(parameters['head_repository']),
"--head-ref='{}'".format(parameters['head_ref']),
"--head-rev='{}'".format(parameters['head_rev']),
"--revision-hash='{}'\n".format(parameters['head_rev']),
]),
],
},
'extra': {
'treeherder': {
'groupName': 'action-callback',
'groupSymbol': 'AC',
'symbol': symbol,
},
},
}
mem['registered'] = True
callbacks[cb.__name__] = cb
return register_callback
def render_actions_json(parameters):
"""
Render JSON object for the ``public/actions.json`` artifact.
Parameters
----------
parameters : taskgraph.parameters.Parameters
Decision task parameters.
Returns
-------
dict
JSON object representation of the ``public/actions.json`` artifact.
"""
global actions
assert isinstance(parameters, Parameters), 'requires instance of Parameters'
result = []
for action in sorted(actions, key=lambda action: action.order):
task = action.task_template_builder(parameters)
if task:
assert is_json(task), 'task must be a JSON compatible object'
result.append({
'title': action.title,
'description': action.description,
'context': action.context,
'schema': action.schema,
'task': task,
})
return {
'version': 1,
'variables': {
'parameters': dict(**parameters),
},
'actions': result,
}
def trigger_action_callback():
"""
Trigger action callback using arguments from environment variables.
"""
global callbacks
task_group_id = os.environ.get('ACTION_TASK_GROUP_ID', None)
task_id = json.loads(os.environ.get('ACTION_TASK_ID', 'null'))
task = json.loads(os.environ.get('ACTION_TASK', 'null'))
input = json.loads(os.environ.get('ACTION_INPUT', 'null'))
callback = os.environ.get('ACTION_CALLBACK', None)
parameters = json.loads(os.environ.get('ACTION_PARAMETERS', 'null'))
cb = callbacks.get(callback, None)
if not cb:
raise Exception('Unknown callback: {}'.format(callback))
cb(Parameters(**parameters), input, task_group_id, task_id, task)
# Load all modules from this folder, relying on the side-effects of register_
# functions to populate the action registry.
for f in os.listdir(os.path.dirname(__file__)):
if f.endswith('.py') and f not in ('__init__.py', 'registry.py'):
__import__('actions.' + f[:-3])