forked from mirrors/gecko-dev
Bug 1398277: special-case retriggering of tasks not in the taskgraph; r=bstack
This will apply to cron tasks, action tasks, and decision tasks. It is a distinct retrigger implementation because (a) we do not want to follow dependencies, and (b) it takes a lot of scopes to create a decision task, so we need to limit access to this action. MozReview-Commit-ID: 21DVSiagcrO --HG-- extra : rebase_source : 6f027e349e245e4aa4dbed81145a0a5d75218cb1 extra : histedit_source : eff99aee5a0e7496b0734748b29739480eb0e3fb
This commit is contained in:
parent
5ab4495828
commit
91f4fe8c61
9 changed files with 181 additions and 8 deletions
|
|
@ -48,12 +48,18 @@ tasks:
|
||||||
|
|
||||||
tags:
|
tags:
|
||||||
$if: 'tasks_for == "hg-push"'
|
$if: 'tasks_for == "hg-push"'
|
||||||
then: {createdForUser: "${ownerEmail}"}
|
then:
|
||||||
|
createdForUser: "${ownerEmail}"
|
||||||
|
kind: decision-task
|
||||||
else:
|
else:
|
||||||
$if: 'tasks_for == "action"'
|
$if: 'tasks_for == "action"'
|
||||||
then:
|
then:
|
||||||
createdForUser: '${ownerEmail}'
|
createdForUser: '${ownerEmail}'
|
||||||
kind: 'action-callback'
|
kind: 'action-callback'
|
||||||
|
else:
|
||||||
|
$if: 'tasks_for == "cron"'
|
||||||
|
then:
|
||||||
|
kind: cron-task
|
||||||
|
|
||||||
routes:
|
routes:
|
||||||
$flatten:
|
$flatten:
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,15 @@ from __future__ import absolute_import, print_function, unicode_literals
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import textwrap
|
||||||
|
|
||||||
from slugid import nice as slugid
|
from slugid import nice as slugid
|
||||||
from .util import (
|
from .util import (
|
||||||
combine_task_graph_files,
|
combine_task_graph_files,
|
||||||
create_tasks,
|
create_tasks,
|
||||||
|
fetch_graph_and_labels,
|
||||||
|
relativize_datestamps,
|
||||||
create_task_from_def,
|
create_task_from_def,
|
||||||
fetch_graph_and_labels
|
|
||||||
)
|
)
|
||||||
from ..util.parameterization import resolve_task_references
|
from ..util.parameterization import resolve_task_references
|
||||||
from .registry import register_callback_action
|
from .registry import register_callback_action
|
||||||
|
|
@ -143,6 +145,35 @@ def mochitest_retrigger_action(parameters, graph_config, input, task_group_id, t
|
||||||
create_task_from_def(new_task_id, new_task_definition, parameters['level'])
|
create_task_from_def(new_task_id, new_task_definition, parameters['level'])
|
||||||
|
|
||||||
|
|
||||||
|
@register_callback_action(
|
||||||
|
title='Retrigger',
|
||||||
|
name='retrigger',
|
||||||
|
symbol='rt',
|
||||||
|
kind='hook',
|
||||||
|
cb_name='retrigger-decision',
|
||||||
|
description=textwrap.dedent('''\
|
||||||
|
Create a clone of the task (retriggering decision, action, and cron tasks requires
|
||||||
|
special scopes).'''),
|
||||||
|
order=11,
|
||||||
|
context=[
|
||||||
|
{'kind': 'decision-task'},
|
||||||
|
{'kind': 'action-callback'},
|
||||||
|
{'kind': 'cron-task'},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def retrigger_decision_action(parameters, graph_config, input, task_group_id, task_id, task):
|
||||||
|
decision_task_id, full_task_graph, label_to_taskid = fetch_graph_and_labels(
|
||||||
|
parameters, graph_config)
|
||||||
|
"""For a single task, we try to just run exactly the same task once more.
|
||||||
|
It's quite possible that we don't have the scopes to do so (especially for
|
||||||
|
an action), but this is best-effort."""
|
||||||
|
|
||||||
|
# make all of the timestamps relative; they will then be turned back into
|
||||||
|
# absolute timestamps relative to the current time.
|
||||||
|
task = relativize_datestamps(task)
|
||||||
|
create_task_from_def(slugid(), task, parameters['level'])
|
||||||
|
|
||||||
|
|
||||||
@register_callback_action(
|
@register_callback_action(
|
||||||
title='Retrigger',
|
title='Retrigger',
|
||||||
name='retrigger',
|
name='retrigger',
|
||||||
|
|
@ -150,9 +181,9 @@ def mochitest_retrigger_action(parameters, graph_config, input, task_group_id, t
|
||||||
kind='hook',
|
kind='hook',
|
||||||
generic=True,
|
generic=True,
|
||||||
description=(
|
description=(
|
||||||
'Create a clone of the task.\n\n'
|
'Create a clone of the task.'
|
||||||
),
|
),
|
||||||
order=11, # must be greater than other orders in this file, as this is the fallback version
|
order=19, # must be greater than other orders in this file, as this is the fallback version
|
||||||
context=[{}],
|
context=[{}],
|
||||||
schema={
|
schema={
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
|
|
@ -181,6 +212,7 @@ def retrigger_action(parameters, graph_config, input, task_group_id, task_id, ta
|
||||||
parameters, graph_config)
|
parameters, graph_config)
|
||||||
|
|
||||||
label = task['metadata']['name']
|
label = task['metadata']['name']
|
||||||
|
|
||||||
with_downstream = ' '
|
with_downstream = ' '
|
||||||
to_run = [label]
|
to_run = [label]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import copy
|
||||||
import logging
|
import logging
|
||||||
import requests
|
import requests
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
from requests.exceptions import HTTPError
|
from requests.exceptions import HTTPError
|
||||||
|
|
||||||
|
|
@ -17,7 +18,13 @@ from taskgraph import create
|
||||||
from taskgraph.decision import read_artifact, write_artifact
|
from taskgraph.decision import read_artifact, write_artifact
|
||||||
from taskgraph.taskgraph import TaskGraph
|
from taskgraph.taskgraph import TaskGraph
|
||||||
from taskgraph.optimize import optimize_task_graph
|
from taskgraph.optimize import optimize_task_graph
|
||||||
from taskgraph.util.taskcluster import get_session, find_task_id, get_artifact, list_tasks
|
from taskgraph.util.taskcluster import (
|
||||||
|
get_session,
|
||||||
|
find_task_id,
|
||||||
|
get_artifact,
|
||||||
|
list_tasks,
|
||||||
|
parse_time,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -164,3 +171,30 @@ def combine_task_graph_files(suffixes):
|
||||||
for suffix in suffixes:
|
for suffix in suffixes:
|
||||||
all.update(read_artifact('task-graph-{}.json'.format(suffix)))
|
all.update(read_artifact('task-graph-{}.json'.format(suffix)))
|
||||||
write_artifact('task-graph.json', all)
|
write_artifact('task-graph.json', all)
|
||||||
|
|
||||||
|
|
||||||
|
def relativize_datestamps(task_def):
|
||||||
|
"""
|
||||||
|
Given a task definition as received from the queue, convert all datestamps
|
||||||
|
to {relative_datestamp: ..} format, with the task creation time as "now".
|
||||||
|
The result is useful for handing to ``create_task``.
|
||||||
|
"""
|
||||||
|
base = parse_time(task_def['created'])
|
||||||
|
# borrowed from https://github.com/epoberezkin/ajv/blob/master/lib/compile/formats.js
|
||||||
|
ts_pattern = re.compile(
|
||||||
|
r'^\d\d\d\d-[0-1]\d-[0-3]\d[t\s]'
|
||||||
|
r'(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?'
|
||||||
|
r'(?:z|[+-]\d\d:\d\d)$', re.I)
|
||||||
|
|
||||||
|
def recurse(value):
|
||||||
|
if isinstance(value, basestring):
|
||||||
|
if ts_pattern.match(value):
|
||||||
|
value = parse_time(value)
|
||||||
|
diff = value - base
|
||||||
|
return {'relative-datestamp': '{} seconds'.format(int(diff.total_seconds()))}
|
||||||
|
if isinstance(value, list):
|
||||||
|
return [recurse(e) for e in value]
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return {k: recurse(v) for k, v in value.items()}
|
||||||
|
return value
|
||||||
|
return recurse(task_def)
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,9 @@ class TaskGraph(object):
|
||||||
"Get a task by label"
|
"Get a task by label"
|
||||||
return self.tasks[label]
|
return self.tasks[label]
|
||||||
|
|
||||||
|
def __contains__(self, label):
|
||||||
|
return label in self.tasks
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
"Iterate over tasks in undefined order"
|
"Iterate over tasks in undefined order"
|
||||||
return self.tasks.itervalues()
|
return self.tasks.itervalues()
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
subsuite = taskgraph
|
subsuite = taskgraph
|
||||||
skip-if = python == 3
|
skip-if = python == 3
|
||||||
|
|
||||||
|
[test_actions_util.py]
|
||||||
[test_create.py]
|
[test_create.py]
|
||||||
[test_cron_util.py]
|
[test_cron_util.py]
|
||||||
[test_decision.py]
|
[test_decision.py]
|
||||||
|
|
@ -21,6 +22,7 @@ skip-if = python == 3
|
||||||
[test_util_python_path.py]
|
[test_util_python_path.py]
|
||||||
[test_util_runnable_jobs.py]
|
[test_util_runnable_jobs.py]
|
||||||
[test_util_schema.py]
|
[test_util_schema.py]
|
||||||
|
[test_util_taskcluster.py]
|
||||||
[test_util_templates.py]
|
[test_util_templates.py]
|
||||||
[test_util_time.py]
|
[test_util_time.py]
|
||||||
[test_util_treeherder.py]
|
[test_util_treeherder.py]
|
||||||
|
|
|
||||||
46
taskcluster/taskgraph/test/test_actions_util.py
Normal file
46
taskcluster/taskgraph/test/test_actions_util.py
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
from __future__ import absolute_import, print_function, unicode_literals
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from mozunit import main
|
||||||
|
from taskgraph.actions.util import (
|
||||||
|
relativize_datestamps
|
||||||
|
)
|
||||||
|
|
||||||
|
TASK_DEF = {
|
||||||
|
'created': '2017-10-10T18:33:03.460Z',
|
||||||
|
# note that this is not an even number of seconds off!
|
||||||
|
'deadline': '2017-10-11T18:33:03.461Z',
|
||||||
|
'dependencies': [],
|
||||||
|
'expires': '2018-10-10T18:33:04.461Z',
|
||||||
|
'payload': {
|
||||||
|
'artifacts': {
|
||||||
|
'public': {
|
||||||
|
'expires': '2018-10-10T18:33:03.463Z',
|
||||||
|
'path': '/builds/worker/artifacts',
|
||||||
|
'type': 'directory',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'maxRunTime': 1800,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestRelativize(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_relativize(self):
|
||||||
|
rel = relativize_datestamps(TASK_DEF)
|
||||||
|
import pprint
|
||||||
|
pprint.pprint(rel)
|
||||||
|
assert rel['created'] == {'relative-datestamp': '0 seconds'}
|
||||||
|
assert rel['deadline'] == {'relative-datestamp': '86400 seconds'}
|
||||||
|
assert rel['expires'] == {'relative-datestamp': '31536001 seconds'}
|
||||||
|
assert rel['payload']['artifacts']['public']['expires'] == \
|
||||||
|
{'relative-datestamp': '31536000 seconds'}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
@ -74,6 +74,27 @@ class TestTaskGraph(unittest.TestCase):
|
||||||
tasks, new_graph = TaskGraph.from_json(graph.to_json())
|
tasks, new_graph = TaskGraph.from_json(graph.to_json())
|
||||||
self.assertEqual(graph, new_graph)
|
self.assertEqual(graph, new_graph)
|
||||||
|
|
||||||
|
simple_graph = TaskGraph(tasks={
|
||||||
|
'a': Task(
|
||||||
|
kind='fancy',
|
||||||
|
label='a',
|
||||||
|
attributes={},
|
||||||
|
dependencies={'prereq': 'b'}, # must match edges, below
|
||||||
|
optimization={'seta': None},
|
||||||
|
task={'task': 'def'}),
|
||||||
|
'b': Task(
|
||||||
|
kind='pre',
|
||||||
|
label='b',
|
||||||
|
attributes={},
|
||||||
|
dependencies={},
|
||||||
|
optimization={'seta': None},
|
||||||
|
task={'task': 'def2'}),
|
||||||
|
}, graph=Graph(nodes={'a', 'b'}, edges={('a', 'b', 'prereq')}))
|
||||||
|
|
||||||
|
def test_contains(self):
|
||||||
|
assert 'a' in self.simple_graph
|
||||||
|
assert 'c' not in self.simple_graph
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|
|
||||||
24
taskcluster/taskgraph/test/test_util_taskcluster.py
Normal file
24
taskcluster/taskgraph/test/test_util_taskcluster.py
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
from __future__ import absolute_import, print_function, unicode_literals
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import mozunit
|
||||||
|
from taskgraph.util.taskcluster import (
|
||||||
|
parse_time
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTCUtils(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_parse_time(self):
|
||||||
|
exp = datetime.datetime(2018, 10, 10, 18, 33, 3, 463000)
|
||||||
|
assert parse_time('2018-10-10T18:33:03.463Z') == exp
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
mozunit.main()
|
||||||
|
|
@ -17,10 +17,10 @@ from requests.adapters import HTTPAdapter
|
||||||
from taskgraph.task import Task
|
from taskgraph.task import Task
|
||||||
|
|
||||||
_PUBLIC_TC_ARTIFACT_LOCATION = \
|
_PUBLIC_TC_ARTIFACT_LOCATION = \
|
||||||
'https://queue.taskcluster.net/v1/task/{task_id}/artifacts/{artifact_prefix}/{postfix}'
|
'https://queue.taskcluster.net/v1/task/{task_id}/artifacts/{artifact_prefix}/{postfix}'
|
||||||
|
|
||||||
_PRIVATE_TC_ARTIFACT_LOCATION = \
|
_PRIVATE_TC_ARTIFACT_LOCATION = \
|
||||||
'http://taskcluster/queue/v1/task/{task_id}/artifacts/{artifact_prefix}/{postfix}'
|
'http://taskcluster/queue/v1/task/{task_id}/artifacts/{artifact_prefix}/{postfix}'
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -151,10 +151,15 @@ def list_tasks(index_path, use_proxy=False):
|
||||||
# all of these tasks should be created with the same expires time so they end up in
|
# all of these tasks should be created with the same expires time so they end up in
|
||||||
# order from earliest to latest action. If more correctness is needed, consider
|
# order from earliest to latest action. If more correctness is needed, consider
|
||||||
# fetching each task and sorting on the created date.
|
# fetching each task and sorting on the created date.
|
||||||
results.sort(key=lambda t: datetime.datetime.strptime(t['expires'], '%Y-%m-%dT%H:%M:%S.%fZ'))
|
results.sort(key=lambda t: parse_time(t['expires']))
|
||||||
return [t['taskId'] for t in results]
|
return [t['taskId'] for t in results]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_time(timestamp):
|
||||||
|
"""Turn a "JSON timestamp" as used in TC APIs into a datetime"""
|
||||||
|
return datetime.datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S.%fZ')
|
||||||
|
|
||||||
|
|
||||||
def get_task_url(task_id, use_proxy=False):
|
def get_task_url(task_id, use_proxy=False):
|
||||||
if use_proxy:
|
if use_proxy:
|
||||||
TASK_URL = 'http://taskcluster/queue/v1/task/{}'
|
TASK_URL = 'http://taskcluster/queue/v1/task/{}'
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue