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

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: Better protobuf comments. 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
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 def _op_properties_to_dict(pmap):
246 """Adds an arguments_pb2.PropertyList entry into a dictionary.
martiniss 2016/08/10 21:38:25 "into a dictionary"? This returns a dictionary...
dnj 2016/08/10 22:04:41 Acknowledged.
247
248 Args:
249 pmap (Map): Protobuf map of property name (str) to arguments_pb2.Property.
250 """
251 d = {}
252
253 for k in pmap:
254 prop = pmap[k]
255 typ = prop.WhichOneof('value')
256 if typ == 's':
257 v = prop.s
258 elif typ == 'int':
259 v = prop.int
260 elif typ == 'uint':
261 v = prop.uint
262 elif typ == 'd':
263 v = prop.d
264 elif typ == 'b':
265 v = prop.b
266 elif typ == 'data':
267 v = prop.data
268 elif typ == 'properties':
269 v = _op_properties_to_dict(prop.properties.property)
270 elif typ is None:
271 v = None
272 else:
273 raise ValueError('Unknown property field [%s]' % (typ,))
274 d[k] = v
275 return d
276
277
231 def main(): 278 def main():
232 from recipe_engine import package 279 from recipe_engine import package
233 280
234 # Super-annoyingly, we need to manually parse for simulation_test since 281 # 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. 282 # argparse is bonkers and doesn't allow us to forward --help to subcommands.
236 # Save old_args for if we're using bootstrap 283 # Save old_args for if we're using bootstrap
237 original_sys_argv = sys.argv[:] 284 original_sys_argv = sys.argv[:]
238 if 'simulation_test' in sys.argv: 285 if 'simulation_test' in sys.argv:
239 index = sys.argv.index('simulation_test') 286 index = sys.argv.index('simulation_test')
240 sys.argv = sys.argv[:index+1] + [json.dumps(sys.argv[index+1:])] 287 sys.argv = sys.argv[:index+1] + [json.dumps(sys.argv[index+1:])]
(...skipping 18 matching lines...) Expand all
259 parser.add_argument( 306 parser.add_argument(
260 '--bootstrap-script', 307 '--bootstrap-script',
261 help='Path to the script used to bootstrap this tool (internal use only)') 308 help='Path to the script used to bootstrap this tool (internal use only)')
262 parser.add_argument('-O', '--project-override', metavar='ID=PATH', 309 parser.add_argument('-O', '--project-override', metavar='ID=PATH',
263 action=ProjectOverrideAction, 310 action=ProjectOverrideAction,
264 help='Override a project repository path with a local one.') 311 help='Override a project repository path with a local one.')
265 parser.add_argument( 312 parser.add_argument(
266 '--use-bootstrap', action='store_true', 313 '--use-bootstrap', action='store_true',
267 help='Use bootstrap/bootstrap.py to create a isolated python virtualenv' 314 help='Use bootstrap/bootstrap.py to create a isolated python virtualenv'
268 ' with required python dependencies.') 315 ' with required python dependencies.')
316 parser.add_argument(
317 '--operational-args-path', action='store',
318 help='The path to an operational Arguments file. If provided, this file '
319 'must contain a JSONPB-encoded Arguments protobuf message, and will '
320 'be integrated into the runtime parameters.')
269 321
270 subp = parser.add_subparsers() 322 subp = parser.add_subparsers()
271 323
272 fetch_p = subp.add_parser( 324 fetch_p = subp.add_parser(
273 'fetch', 325 'fetch',
274 description='Fetch and update dependencies.') 326 description='Fetch and update dependencies.')
275 fetch_p.set_defaults(command='fetch') 327 fetch_p.set_defaults(command='fetch')
276 328
277 simulation_test_p = subp.add_parser( 329 simulation_test_p = subp.add_parser(
278 'simulation_test', 330 'simulation_test',
(...skipping 121 matching lines...) Expand 10 before | Expand all | Expand 10 after
400 info_p = subp.add_parser( 452 info_p = subp.add_parser(
401 'info', 453 'info',
402 description='Query information about the current recipe package') 454 description='Query information about the current recipe package')
403 info_p.set_defaults(command='info') 455 info_p.set_defaults(command='info')
404 info_p.add_argument( 456 info_p.add_argument(
405 '--recipes-dir', action='store_true', 457 '--recipes-dir', action='store_true',
406 help='Get the subpath where the recipes live relative to repository root') 458 help='Get the subpath where the recipes live relative to repository root')
407 459
408 args = parser.parse_args() 460 args = parser.parse_args()
409 461
462 # Load/parse operational arguments.
martiniss 2016/08/10 21:38:25 Why is this done down here, vs in the run function
dnj 2016/08/10 22:04:41 The idea is that the operational parameters may so
463 op_args = arguments_pb2.Arguments()
464 if args.operational_args_path is not None:
465 with open(args.operational_args_path) as fd:
466 data = fd.read()
467 jsonpb.Parse(data, op_args)
468
410 if args.use_bootstrap and not os.environ.pop('RECIPES_RUN_BOOTSTRAP', None): 469 if args.use_bootstrap and not os.environ.pop('RECIPES_RUN_BOOTSTRAP', None):
411 subprocess.check_call( 470 subprocess.check_call(
412 [sys.executable, 'bootstrap/bootstrap.py', '--deps-file', 471 [sys.executable, 'bootstrap/bootstrap.py', '--deps-file',
413 'bootstrap/deps.pyl', 'ENV'], 472 'bootstrap/deps.pyl', 'ENV'],
414 cwd=os.path.dirname(os.path.realpath(__file__))) 473 cwd=os.path.dirname(os.path.realpath(__file__)))
415 474
416 os.environ['RECIPES_RUN_BOOTSTRAP'] = '1' 475 os.environ['RECIPES_RUN_BOOTSTRAP'] = '1'
417 args = sys.argv 476 args = sys.argv
418 return subprocess.call( 477 return subprocess.call(
419 [os.path.join( 478 [os.path.join(
(...skipping 28 matching lines...) Expand all
448 507
449 if args.command == 'fetch': 508 if args.command == 'fetch':
450 # We already did everything in the create() call above. 509 # We already did everything in the create() call above.
451 assert not args.no_fetch, 'Fetch? No-fetch? Make up your mind!' 510 assert not args.no_fetch, 'Fetch? No-fetch? Make up your mind!'
452 return 0 511 return 0
453 if args.command == 'simulation_test': 512 if args.command == 'simulation_test':
454 return simulation_test(package_deps, args) 513 return simulation_test(package_deps, args)
455 elif args.command == 'lint': 514 elif args.command == 'lint':
456 return lint(package_deps, args) 515 return lint(package_deps, args)
457 elif args.command == 'run': 516 elif args.command == 'run':
458 return run(package_deps, args) 517 return run(package_deps, args, op_args)
459 elif args.command == 'autoroll': 518 elif args.command == 'autoroll':
460 return autoroll(args) 519 return autoroll(args)
461 elif args.command == 'depgraph': 520 elif args.command == 'depgraph':
462 return depgraph(package_deps, args) 521 return depgraph(package_deps, args)
463 elif args.command == 'refs': 522 elif args.command == 'refs':
464 return refs(package_deps, args) 523 return refs(package_deps, args)
465 elif args.command == 'doc': 524 elif args.command == 'doc':
466 return doc(package_deps, args) 525 return doc(package_deps, args)
467 elif args.command == 'info': 526 elif args.command == 'info':
468 return info(args) 527 return info(args)
(...skipping 22 matching lines...) Expand all
491 ret = main() 550 ret = main()
492 if not isinstance(ret, int): 551 if not isinstance(ret, int):
493 if ret is None: 552 if ret is None:
494 ret = 0 553 ret = 0
495 else: 554 else:
496 print >> sys.stderr, ret 555 print >> sys.stderr, ret
497 ret = 1 556 ret = 1
498 sys.stdout.flush() 557 sys.stdout.flush()
499 sys.stderr.flush() 558 sys.stderr.flush()
500 os._exit(ret) 559 os._exit(ret)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698