blob: ba8a5317640252b062521a2360de7ac2a092fbef [file] [log] [blame]
# Copyright 2017 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.
"""A simplistic method for running steps generated by an external script.
This module was created before there was a way to put recipes directly into
another repo. It is not recommended to use this, and it will be removed in the
near future.
"""
from recipe_engine import recipe_api
class GeneratorScriptApi(recipe_api.RecipeApi):
ALLOWED_KEYS = frozenset([
# supported by step module
'name', 'cmd', 'env', 'cwd', 'allow_subannotations',
# implemented by GeneratorScriptApi
'always_run', 'outputs_presentation_json',
])
class UnknownKey(recipe_api.StepFailure):
def __init__(self, step_name, bad_keys):
reason = 'Step(%r) generated step with bad keys %r' % (
step_name, bad_keys)
super(GeneratorScriptApi.UnknownKey, self).__init__(
reason)
self.bad_keys = frozenset(bad_keys)
class MalformedStepList(recipe_api.StepFailure):
def __init__(self, step_name):
super(GeneratorScriptApi.MalformedStepList, self).__init__(
'Step(%r) generated non-list JSON output' % (step_name,))
class MalformedCmd(recipe_api.StepFailure):
def __init__(self, step_name):
super(GeneratorScriptApi.MalformedCmd, self).__init__(
'Step(%r) generated step with "cmd" containing non-strings' % (
step_name,))
def __call__(self, path_to_script, *args):
"""Run a script and generate the steps emitted by that script.
The script will be invoked with --output-json /path/to/file.json. The script
is expected to exit 0 and write steps into that file. Once the script
outputs all of the steps to that file, the recipe will read the steps from
that file and execute them in order. Any *args specified will be
additionally passed to the script.
The step data is formatted as a list of JSON objects. Each object
corresponds to one step, and contains the following keys:
* name: the name of this step.
* cmd: a list of strings that indicate the command to run (e.g. argv)
* env: a {key:value} dictionary of the environment variables to override.
every value is formatted with the current environment with the python
% operator, so a value of "%(PATH)s:/some/other/path" would resolve to
the current PATH value, concatenated with ":/some/other/path"
* cwd: an absolute path to the current working directory for this script.
* always_run: a bool which indicates that this step should run, even if
some previous step failed.
* outputs_presentation_json: a bool which indicates that this step will
emit a presentation json file. If this is True, the cmd will be extended
with a `--presentation-json /path/to/file.json`. This file will be used
to update the step's presentation on the build status page. The file
will be expected to contain a single json object, with any of the
following keys:
* logs: {logname: [lines]} specifies one or more auxillary logs.
* links: {link_name: link_content} to add extra links to the step.
* step_summary_text: A string to set as the step summary.
* step_text: A string to set as the step text.
* properties: {prop: value} build_properties to add to the build
status page. Note that these are write-only: The only way to read
them is via the status page. There is intentionally no mechanism to
read them back from inside of the recipes.
* allow_subannotations: allow this step to emit legacy buildbot
subannotations. If you don't know what this is, you shouldn't use it. If
you know what it is, you also shouldn't use it.
"""
f = '--output-json'
step_name = 'gen step(%s)' % self.m.path.basename(path_to_script)
with self.m.context(cwd=self.m.path['checkout']):
if str(path_to_script).endswith('.py'):
step_result = self.m.python(
step_name,
path_to_script, list(args) + [f, self.m.json.output()])
else:
step_result = self.m.step(
step_name,
[path_to_script,] + list(args) + [f, self.m.json.output()])
new_steps = step_result.json.output
if not isinstance(new_steps, list):
# pylint: disable=nonstandard-exception
raise self.MalformedStepList(step_name)
failed_steps = []
for step in new_steps:
diff = set(step) - self.ALLOWED_KEYS
if diff:
# pylint: disable=nonstandard-exception
raise self.UnknownKey(step_name, diff)
cmd = step['cmd']
if not all(isinstance(arg, basestring) for arg in cmd):
# pylint: disable=nonstandard-exception
raise self.MalformedCmd(step_name)
outputs_json = step.pop('outputs_presentation_json', False)
if outputs_json:
# This step has requested a JSON file which the binary that
# it invokes can write to, so provide it with one.
cmd.extend(['--presentation-json', self.m.json.output(False)])
if not failed_steps or step.get('always_run'):
try:
cwd = self.m.path.abs_to_path(step['cwd']) if 'cwd' in step else None
with self.m.context(env=step.get('env'), cwd=cwd):
self.m.step(
step['name'], cmd,
allow_subannotations=bool(step.get(
'allow_subannotations', False)),
)
except self.m.step.StepFailure:
failed_steps.append(step['name'])
finally:
step_result = self.m.step.active_result
if outputs_json:
p = step_result.presentation
j = step_result.json.output
if j:
p.logs.update(j.get('logs', {}))
p.links.update(j.get('links', {}))
p.step_summary_text = j.get('step_summary_text', '')
p.step_text = j.get('step_text', '')
p.properties.update(j.get('properties', {}))
if failed_steps:
raise self.m.step.StepFailure(
"the following steps in %s failed: %s" %
(step_name, failed_steps))