Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(251)

Side by Side Diff: recipes.py

Issue 2237593002: Add support for operational arguments protobuf. (Closed) Base URL: https://github.com/luci/recipes-py@proto3-release
Patch Set: Comments, add list type, small changes. Created 4 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « recipe_engine/arguments_pb2.py ('k') | unittests/recipes_py_test.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # Copyright 2015 The LUCI Authors. All rights reserved. 2 # Copyright 2015 The LUCI Authors. All rights reserved.
3 # Use of this source code is governed under the Apache License, Version 2.0 3 # Use of this source code is governed under the Apache License, Version 2.0
4 # that can be found in the LICENSE file. 4 # that can be found in the LICENSE file.
5 5
6 """Tool to interact with recipe repositories. 6 """Tool to interact with recipe repositories.
7 7
8 This tool operates on the nearest ancestor directory containing an 8 This tool operates on the nearest ancestor directory containing an
9 infra/config/recipes.cfg. 9 infra/config/recipes.cfg.
10 """ 10 """
11 11
12 import argparse 12 import argparse
13 import json 13 import json
14 import logging 14 import logging
15 import os 15 import os
16 import subprocess 16 import subprocess
17 import sys 17 import sys
18 18
19 ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) 19 ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
20 sys.path.insert(0, ROOT_DIR) 20 sys.path.insert(0, ROOT_DIR)
21 21
22 from recipe_engine import env 22 from recipe_engine import env
23 from recipe_engine import arguments_pb2
24 from google.protobuf import json_format as jsonpb
23 25
24 def get_package_config(args): 26 def get_package_config(args):
25 from recipe_engine import package 27 from recipe_engine import package
26 28
27 assert args.package, 'No recipe config (--package) given.' 29 assert args.package, 'No recipe config (--package) given.'
28 assert os.path.exists(args.package), ( 30 assert os.path.exists(args.package), (
29 'Given recipes config file %s does not exist.' % args.package) 31 'Given recipes config file %s does not exist.' % args.package)
30 return ( 32 return (
31 package.InfraRepoConfig().from_recipes_cfg(args.package), 33 package.InfraRepoConfig().from_recipes_cfg(args.package),
32 package.ProtoFile(args.package) 34 package.ProtoFile(args.package)
(...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after
77 with s.new_log_stream('reason') as l: 79 with s.new_log_stream('reason') as l:
78 for line in recipe_result.result['reason'].splitlines(): 80 for line in recipe_result.result['reason'].splitlines():
79 l.write_line(line) 81 l.write_line(line)
80 82
81 if 'status_code' in recipe_result.result: 83 if 'status_code' in recipe_result.result:
82 return recipe_result.result['status_code'] 84 return recipe_result.result['status_code']
83 else: 85 else:
84 return 0 86 return 0
85 87
86 88
87 def run(package_deps, args): 89 def run(package_deps, args, op_args):
88 from recipe_engine import run as recipe_run 90 from recipe_engine import run as recipe_run
89 from recipe_engine import loader 91 from recipe_engine import loader
90 from recipe_engine import step_runner 92 from recipe_engine import step_runner
91 from recipe_engine import stream 93 from recipe_engine import stream
92 94
93 def get_properties_from_args(args): 95 def get_properties_from_args(args):
94 properties = dict(x.split('=', 1) for x in args) 96 properties = dict(x.split('=', 1) for x in args)
95 for key, val in properties.iteritems(): 97 for key, val in properties.iteritems():
96 try: 98 try:
97 properties[key] = json.loads(val) 99 properties[key] = json.loads(val)
98 except (ValueError, SyntaxError): 100 except (ValueError, SyntaxError):
99 pass # If a value couldn't be evaluated, keep the string version 101 pass # If a value couldn't be evaluated, keep the string version
100 return properties 102 return properties
101 103
102 def get_properties_from_file(filename): 104 def get_properties_from_file(filename):
103 properties_file = sys.stdin if filename == '-' else open(filename) 105 properties_file = sys.stdin if filename == '-' else open(filename)
104 properties = json.load(properties_file) 106 properties = json.load(properties_file)
105 assert isinstance(properties, dict) 107 assert isinstance(properties, dict)
106 return properties 108 return properties
107 109
108 def get_properties_from_json(props): 110 def get_properties_from_json(props):
109 return json.loads(props) 111 return json.loads(props)
110 112
113 def get_properties_from_operational_args(op_args):
114 if not op_args.properties.property:
115 return None
116 return _op_properties_to_dict(op_args.properties.property)
117
118
111 arg_properties = get_properties_from_args(args.props) 119 arg_properties = get_properties_from_args(args.props)
120 op_properties = get_properties_from_operational_args(op_args)
112 assert len(filter(bool, 121 assert len(filter(bool,
113 [arg_properties, args.properties_file, args.properties])) <= 1, ( 122 (arg_properties, args.properties_file, args.properties,
123 op_properties))) <= 1, (
114 'Only one source of properties is allowed') 124 'Only one source of properties is allowed')
115 if args.properties: 125 if args.properties:
116 properties = get_properties_from_json(args.properties) 126 properties = get_properties_from_json(args.properties)
117 elif args.properties_file: 127 elif args.properties_file:
118 properties = get_properties_from_file(args.properties_file) 128 properties = get_properties_from_file(args.properties_file)
129 elif op_properties is not None:
130 properties = op_properties
119 else: 131 else:
120 properties = arg_properties 132 properties = arg_properties
121 133
122 properties['recipe'] = args.recipe 134 properties['recipe'] = args.recipe
123 135
124 os.environ['PYTHONUNBUFFERED'] = '1' 136 os.environ['PYTHONUNBUFFERED'] = '1'
125 os.environ['PYTHONIOENCODING'] = 'UTF-8' 137 os.environ['PYTHONIOENCODING'] = 'UTF-8'
126 138
127 _, config_file = get_package_config(args) 139 _, config_file = get_package_config(args)
128 universe = loader.UniverseView( 140 universe = loader.UniverseView(
129 loader.RecipeUniverse( 141 loader.RecipeUniverse(
130 package_deps, config_file), package_deps.root_package) 142 package_deps, config_file), package_deps.root_package)
131 143
132 workdir = (args.workdir or 144 workdir = (args.workdir or
133 os.path.join(os.path.dirname(os.path.realpath(__file__)), 'workdir')) 145 os.path.join(os.path.dirname(os.path.realpath(__file__)), 'workdir'))
134 logging.info('Using %s as work directory' % workdir) 146 logging.info('Using %s as work directory' % workdir)
135 if not os.path.exists(workdir): 147 if not os.path.exists(workdir):
136 os.makedirs(workdir) 148 os.makedirs(workdir)
137 149
138 old_cwd = os.getcwd() 150 old_cwd = os.getcwd()
139 os.chdir(workdir) 151 os.chdir(workdir)
140 stream_engine = stream.ProductStreamEngine( 152 stream_engine = stream.ProductStreamEngine(
141 stream.StreamEngineInvariants(), 153 stream.StreamEngineInvariants(),
142 stream.AnnotatorStreamEngine(sys.stdout, emit_timestamps=args.timestamps)) 154 stream.AnnotatorStreamEngine(
155 sys.stdout, emit_timestamps=(args.timestamps or
156 op_args.annotation_flags.emit_timestamp)))
143 with stream_engine: 157 with stream_engine:
144 try: 158 try:
145 ret = recipe_run.run_steps( 159 ret = recipe_run.run_steps(
146 properties, stream_engine, 160 properties, stream_engine,
147 step_runner.SubprocessStepRunner(stream_engine), 161 step_runner.SubprocessStepRunner(stream_engine),
148 universe=universe) 162 universe=universe)
149 finally: 163 finally:
150 os.chdir(old_cwd) 164 os.chdir(old_cwd)
151 165
152 return handle_recipe_return(ret, args.output_result_json, stream_engine) 166 return handle_recipe_return(ret, args.output_result_json, stream_engine)
(...skipping 68 matching lines...) Expand 10 before | Expand all | Expand 10 after
221 235
222 def info(args): 236 def info(args):
223 from recipe_engine import package 237 from recipe_engine import package
224 repo_root, config_file = get_package_config(args) 238 repo_root, config_file = get_package_config(args)
225 package_spec = package.PackageSpec.load_proto(config_file) 239 package_spec = package.PackageSpec.load_proto(config_file)
226 240
227 if args.recipes_dir: 241 if args.recipes_dir:
228 print package_spec.recipes_path 242 print package_spec.recipes_path
229 243
230 244
245 # Map of arguments_pb2.Property "value" oneof conversion functions.
martiniss 2016/08/10 22:54:45 Add comment saying "Keep in sync with recipe_engin
dnj 2016/08/11 01:21:31 Done.
246 _OP_PROPERTY_CONV = {
247 's': lambda prop: prop.s,
248 'int': lambda prop: prop.int,
249 'uint': lambda prop: prop.uint,
250 'd': lambda prop: prop.d,
251 'b': lambda prop: prop.b,
252 'data': lambda prop: prop.data,
253 'map': lambda prop: _op_properties_to_dict(prop.map.property),
254 'list': lambda prop: [_op_property_value(v) for v in prop.list.property],
255 }
256
257 def _op_property_value(prop):
258 """Returns the Python-converted value of an arguments_pb2.Property.
259
260 Args:
261 prop (arguments_pb2.Property): property to convert.
262 Returns: The converted value.
263 Raises:
264 ValueError: If "prop" is incomplete or invalid.
265 """
266 typ = prop.WhichOneof('value')
267 conv = _OP_PROPERTY_CONV.get(typ)
268 if not conv:
269 raise ValueError('Unknown property field [%s]' % (typ,))
270 return conv(prop)
271
272
273 def _op_properties_to_dict(pmap):
274 """Creates a properties dictionary from an arguments_pb2.PropertyMap entry.
275
276 Args:
277 pmap (arguments_pb2.PropertyMap): Map to convert to dictionary form.
278 Returns (dict): A dictionary derived from the properties in "pmap".
279 """
280 return dict((k, _op_property_value(pmap[k])) for k in pmap)
281
282
231 def main(): 283 def main():
232 from recipe_engine import package 284 from recipe_engine import package
233 285
234 # Super-annoyingly, we need to manually parse for simulation_test since 286 # Super-annoyingly, we need to manually parse for simulation_test since
235 # argparse is bonkers and doesn't allow us to forward --help to subcommands. 287 # argparse is bonkers and doesn't allow us to forward --help to subcommands.
236 # Save old_args for if we're using bootstrap 288 # Save old_args for if we're using bootstrap
237 original_sys_argv = sys.argv[:] 289 original_sys_argv = sys.argv[:]
238 if 'simulation_test' in sys.argv: 290 if 'simulation_test' in sys.argv:
239 index = sys.argv.index('simulation_test') 291 index = sys.argv.index('simulation_test')
240 sys.argv = sys.argv[:index+1] + [json.dumps(sys.argv[index+1:])] 292 sys.argv = sys.argv[:index+1] + [json.dumps(sys.argv[index+1:])]
(...skipping 18 matching lines...) Expand all
259 parser.add_argument( 311 parser.add_argument(
260 '--bootstrap-script', 312 '--bootstrap-script',
261 help='Path to the script used to bootstrap this tool (internal use only)') 313 help='Path to the script used to bootstrap this tool (internal use only)')
262 parser.add_argument('-O', '--project-override', metavar='ID=PATH', 314 parser.add_argument('-O', '--project-override', metavar='ID=PATH',
263 action=ProjectOverrideAction, 315 action=ProjectOverrideAction,
264 help='Override a project repository path with a local one.') 316 help='Override a project repository path with a local one.')
265 parser.add_argument( 317 parser.add_argument(
266 '--use-bootstrap', action='store_true', 318 '--use-bootstrap', action='store_true',
267 help='Use bootstrap/bootstrap.py to create a isolated python virtualenv' 319 help='Use bootstrap/bootstrap.py to create a isolated python virtualenv'
268 ' with required python dependencies.') 320 ' with required python dependencies.')
321 parser.add_argument(
322 '--operational-args-path', action='store',
323 help='The path to an operational Arguments file. If provided, this file '
324 'must contain a JSONPB-encoded Arguments protobuf message, and will '
325 'be integrated into the runtime parameters.')
269 326
270 subp = parser.add_subparsers() 327 subp = parser.add_subparsers()
271 328
272 fetch_p = subp.add_parser( 329 fetch_p = subp.add_parser(
273 'fetch', 330 'fetch',
274 description='Fetch and update dependencies.') 331 description='Fetch and update dependencies.')
275 fetch_p.set_defaults(command='fetch') 332 fetch_p.set_defaults(command='fetch')
276 333
277 simulation_test_p = subp.add_parser( 334 simulation_test_p = subp.add_parser(
278 'simulation_test', 335 'simulation_test',
(...skipping 121 matching lines...) Expand 10 before | Expand all | Expand 10 after
400 info_p = subp.add_parser( 457 info_p = subp.add_parser(
401 'info', 458 'info',
402 description='Query information about the current recipe package') 459 description='Query information about the current recipe package')
403 info_p.set_defaults(command='info') 460 info_p.set_defaults(command='info')
404 info_p.add_argument( 461 info_p.add_argument(
405 '--recipes-dir', action='store_true', 462 '--recipes-dir', action='store_true',
406 help='Get the subpath where the recipes live relative to repository root') 463 help='Get the subpath where the recipes live relative to repository root')
407 464
408 args = parser.parse_args() 465 args = parser.parse_args()
409 466
467 # Load/parse operational arguments.
468 op_args = arguments_pb2.Arguments()
469 if args.operational_args_path is not None:
470 with open(args.operational_args_path) as fd:
471 data = fd.read()
472 jsonpb.Parse(data, op_args)
473
410 if args.use_bootstrap and not os.environ.pop('RECIPES_RUN_BOOTSTRAP', None): 474 if args.use_bootstrap and not os.environ.pop('RECIPES_RUN_BOOTSTRAP', None):
411 subprocess.check_call( 475 subprocess.check_call(
412 [sys.executable, 'bootstrap/bootstrap.py', '--deps-file', 476 [sys.executable, 'bootstrap/bootstrap.py', '--deps-file',
413 'bootstrap/deps.pyl', 'ENV'], 477 'bootstrap/deps.pyl', 'ENV'],
414 cwd=os.path.dirname(os.path.realpath(__file__))) 478 cwd=os.path.dirname(os.path.realpath(__file__)))
415 479
416 os.environ['RECIPES_RUN_BOOTSTRAP'] = '1' 480 os.environ['RECIPES_RUN_BOOTSTRAP'] = '1'
417 args = sys.argv 481 args = sys.argv
418 return subprocess.call( 482 return subprocess.call(
419 [os.path.join( 483 [os.path.join(
(...skipping 28 matching lines...) Expand all
448 512
449 if args.command == 'fetch': 513 if args.command == 'fetch':
450 # We already did everything in the create() call above. 514 # We already did everything in the create() call above.
451 assert not args.no_fetch, 'Fetch? No-fetch? Make up your mind!' 515 assert not args.no_fetch, 'Fetch? No-fetch? Make up your mind!'
452 return 0 516 return 0
453 if args.command == 'simulation_test': 517 if args.command == 'simulation_test':
454 return simulation_test(package_deps, args) 518 return simulation_test(package_deps, args)
455 elif args.command == 'lint': 519 elif args.command == 'lint':
456 return lint(package_deps, args) 520 return lint(package_deps, args)
457 elif args.command == 'run': 521 elif args.command == 'run':
458 return run(package_deps, args) 522 return run(package_deps, args, op_args)
459 elif args.command == 'autoroll': 523 elif args.command == 'autoroll':
460 return autoroll(args) 524 return autoroll(args)
461 elif args.command == 'depgraph': 525 elif args.command == 'depgraph':
462 return depgraph(package_deps, args) 526 return depgraph(package_deps, args)
463 elif args.command == 'refs': 527 elif args.command == 'refs':
464 return refs(package_deps, args) 528 return refs(package_deps, args)
465 elif args.command == 'doc': 529 elif args.command == 'doc':
466 return doc(package_deps, args) 530 return doc(package_deps, args)
467 elif args.command == 'info': 531 elif args.command == 'info':
468 return info(args) 532 return info(args)
(...skipping 22 matching lines...) Expand all
491 ret = main() 555 ret = main()
492 if not isinstance(ret, int): 556 if not isinstance(ret, int):
493 if ret is None: 557 if ret is None:
494 ret = 0 558 ret = 0
495 else: 559 else:
496 print >> sys.stderr, ret 560 print >> sys.stderr, ret
497 ret = 1 561 ret = 1
498 sys.stdout.flush() 562 sys.stdout.flush()
499 sys.stderr.flush() 563 sys.stderr.flush()
500 os._exit(ret) 564 os._exit(ret)
OLDNEW
« no previous file with comments | « recipe_engine/arguments_pb2.py ('k') | unittests/recipes_py_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698