stoobly-agent 1.3.0__py3-none-any.whl → 1.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/app/api/application_http_request_handler.py +3 -3
  3. stoobly_agent/app/api/proxy_controller.py +8 -7
  4. stoobly_agent/app/cli/config_cli.py +1 -1
  5. stoobly_agent/app/cli/helpers/certificate_authority.py +6 -1
  6. stoobly_agent/app/cli/helpers/print_service.py +17 -0
  7. stoobly_agent/app/cli/scaffold/app.py +2 -2
  8. stoobly_agent/app/cli/scaffold/constants.py +0 -2
  9. stoobly_agent/app/cli/scaffold/hosts_file_manager.py +112 -0
  10. stoobly_agent/app/cli/scaffold/service.py +0 -1
  11. stoobly_agent/app/cli/scaffold/service_config.py +10 -14
  12. stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py +3 -3
  13. stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +1 -1
  14. stoobly_agent/app/cli/scaffold/templates/app/.Makefile +77 -53
  15. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/bin/.services +9 -0
  16. stoobly_agent/app/cli/scaffold/workflow_create_command.py +0 -1
  17. stoobly_agent/app/cli/scaffold/workflow_run_command.py +6 -9
  18. stoobly_agent/app/cli/scaffold_cli.py +200 -69
  19. stoobly_agent/app/cli/snapshot_cli.py +1 -1
  20. stoobly_agent/app/proxy/handle_mock_service.py +2 -0
  21. stoobly_agent/app/proxy/handle_replay_service.py +2 -0
  22. stoobly_agent/app/proxy/mitmproxy/request_facade.py +1 -1
  23. stoobly_agent/app/proxy/mitmproxy/response_body_facade.py +19 -0
  24. stoobly_agent/app/proxy/mitmproxy/response_facade.py +90 -18
  25. stoobly_agent/app/proxy/record/join_request_service.py +1 -1
  26. stoobly_agent/app/settings/constants/request_component.py +2 -1
  27. stoobly_agent/config/constants/custom_headers.py +13 -13
  28. stoobly_agent/config/constants/headers.py +0 -2
  29. stoobly_agent/public/18-es2015.583f191cc7ad512ee262.js +1 -0
  30. stoobly_agent/public/18-es5.583f191cc7ad512ee262.js +1 -0
  31. stoobly_agent/public/35-es2015.8f79ff8748d4ff06ab03.js +1 -0
  32. stoobly_agent/public/35-es5.8f79ff8748d4ff06ab03.js +1 -0
  33. stoobly_agent/public/index.html +1 -1
  34. stoobly_agent/public/main-es2015.2cc16523aa3fcaba51e5.js +1 -0
  35. stoobly_agent/public/main-es5.2cc16523aa3fcaba51e5.js +1 -0
  36. stoobly_agent/public/{runtime-es2015.9addf49b79aca951b7e2.js → runtime-es2015.b914470164e4d6e75d96.js} +1 -1
  37. stoobly_agent/public/{runtime-es5.9addf49b79aca951b7e2.js → runtime-es5.b914470164e4d6e75d96.js} +1 -1
  38. stoobly_agent/test/app/cli/scaffold/{hosts_file_reader_test.py → hosts_file_manager_test.py} +20 -20
  39. stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
  40. {stoobly_agent-1.3.0.dist-info → stoobly_agent-1.4.0.dist-info}/METADATA +1 -1
  41. {stoobly_agent-1.3.0.dist-info → stoobly_agent-1.4.0.dist-info}/RECORD +44 -42
  42. stoobly_agent/app/cli/scaffold/hosts_file_reader.py +0 -65
  43. stoobly_agent/public/18-es2015.d3b430636a4d6f544d92.js +0 -1
  44. stoobly_agent/public/18-es5.d3b430636a4d6f544d92.js +0 -1
  45. stoobly_agent/public/35-es2015.f741ebce0bfc25f0ec99.js +0 -1
  46. stoobly_agent/public/35-es5.f741ebce0bfc25f0ec99.js +0 -1
  47. stoobly_agent/public/main-es2015.ccd46ac1b6638ddf2066.js +0 -1
  48. stoobly_agent/public/main-es5.ccd46ac1b6638ddf2066.js +0 -1
  49. {stoobly_agent-1.3.0.dist-info → stoobly_agent-1.4.0.dist-info}/LICENSE +0 -0
  50. {stoobly_agent-1.3.0.dist-info → stoobly_agent-1.4.0.dist-info}/WHEEL +0 -0
  51. {stoobly_agent-1.3.0.dist-info → stoobly_agent-1.4.0.dist-info}/entry_points.txt +0 -0
@@ -1,4 +1,5 @@
1
1
  import click
2
+ import errno
2
3
  import os
3
4
  import pdb
4
5
  import sys
@@ -14,6 +15,7 @@ from stoobly_agent.app.cli.scaffold.constants import (
14
15
  )
15
16
  from stoobly_agent.app.cli.scaffold.docker.service.set_gateway_ports import set_gateway_ports
16
17
  from stoobly_agent.app.cli.scaffold.docker.workflow.decorators_factory import get_workflow_decorators
18
+ from stoobly_agent.app.cli.scaffold.hosts_file_manager import HostsFileManager
17
19
  from stoobly_agent.app.cli.scaffold.service import Service
18
20
  from stoobly_agent.app.cli.scaffold.service_config import ServiceConfig
19
21
  from stoobly_agent.app.cli.scaffold.service_create_command import ServiceCreateCommand
@@ -31,8 +33,13 @@ from stoobly_agent.config.constants import env_vars
31
33
  from stoobly_agent.config.data_dir import DataDir
32
34
  from stoobly_agent.lib.logger import bcolors, DEBUG, ERROR, INFO, Logger, WARNING
33
35
 
36
+ from .helpers.print_service import FORMATS, print_services, select_print_options
37
+
34
38
  LOG_ID = 'Scaffold'
35
39
 
40
+ current_working_dir = os.getcwd()
41
+ data_dir: DataDir = DataDir.instance()
42
+
36
43
  @click.group(
37
44
  epilog="Run 'stoobly-agent project COMMAND --help' for more information on a command.",
38
45
  help="Manage scaffold"
@@ -57,10 +64,26 @@ def app(ctx):
57
64
  def service(ctx):
58
65
  pass
59
66
 
67
+ @click.group(
68
+ epilog="Run 'stoobly-agent request response COMMAND --help' for more information on a command.",
69
+ help="Manage workflow scaffold"
70
+ )
71
+ @click.pass_context
72
+ def workflow(ctx):
73
+ pass
74
+
75
+ @click.group(
76
+ epilog="Run 'stoobly-agent scaffold hostname COMMAND --help' for more information on a command.",
77
+ help="Manage scaffold service hostnames"
78
+ )
79
+ @click.pass_context
80
+ def hostname(ctx):
81
+ pass
82
+
60
83
  @app.command(
61
84
  help="Scaffold application"
62
85
  )
63
- @click.option('--app-dir-path', default=os.getcwd(), help='Path to create the app scaffold.')
86
+ @click.option('--app-dir-path', default=current_working_dir, help='Path to create the app scaffold.')
64
87
  @click.option('--force', is_flag=True, help='Overwrite maintained scaffolded app files.')
65
88
  @click.option('--network', help='App default network name. Defaults to app name.')
66
89
  @click.argument('app_name')
@@ -80,17 +103,14 @@ def create(**kwargs):
80
103
  @app.command(
81
104
  help="Scaffold app service certs"
82
105
  )
83
- @click.option('--app-dir-path', default=os.getcwd(), help='Path to application directory.')
84
- @click.option('--ca-certs-dir-path', default=DataDir.instance().mitmproxy_conf_dir_path, help='Path to ca certs directory used to sign SSL certs. Defaults to ~/.mitmproxy')
106
+ @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
107
+ @click.option('--ca-certs-dir-path', default=data_dir.mitmproxy_conf_dir_path, help='Path to ca certs directory used to sign SSL certs. Defaults to ~/.mitmproxy')
85
108
  @click.option('--certs-dir-path', help='Path to certs directory. Defaults to the certs dir of the context.')
86
- @click.option('--context-dir-path', default=DataDir.instance().context_dir_path, help='Path to Stoobly data directory.')
109
+ @click.option('--context-dir-path', default=data_dir.context_dir_path, help='Path to Stoobly data directory.')
87
110
  @click.option('--service', multiple=True, help='Select which services to run. Defaults to all.')
88
111
  def mkcert(**kwargs):
89
112
  app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, **kwargs)
90
-
91
- if not app.exists:
92
- print(f"Error: {app.dir_path} does not exist", file=sys.stderr)
93
- sys.exit(1)
113
+ __validate_app(app)
94
114
 
95
115
  services = __get_services(app.services, service=kwargs['service'])
96
116
 
@@ -114,7 +134,7 @@ def mkcert(**kwargs):
114
134
  @service.command(
115
135
  help="Scaffold a service",
116
136
  )
117
- @click.option('--app-dir-path', default=os.getcwd(), help='Path to application directory.')
137
+ @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
118
138
  @click.option('--detached', is_flag=True)
119
139
  @click.option('--env', multiple=True, help='Specify an environment variable.')
120
140
  @click.option('--force', is_flag=True, help='Overwrite maintained scaffolded service files.')
@@ -142,9 +162,34 @@ def create(**kwargs):
142
162
  print(f"{service.dir_path} already exists, use option '--force' to continue")
143
163
 
144
164
  @service.command(
145
- help="Delete a service",
165
+ help="List services",
166
+ name="list"
146
167
  )
147
168
  @click.option('--app-dir-path', default=os.getcwd(), help='Path to application directory.')
169
+ @click.option('--format', type=click.Choice(FORMATS), help='Format output.')
170
+ @click.option('--select', multiple=True, help='Select column(s) to display.')
171
+ @click.option('--service', multiple=True, help='Select specific services.')
172
+ @click.option('--without-headers', is_flag=True, default=False, help='Disable printing column headers.')
173
+ @click.option('--workflow', multiple=True, help='Specify workflow(s) to filter the services by.')
174
+ def _list(**kwargs):
175
+ __validate_app_dir(kwargs['app_dir_path'])
176
+
177
+ app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
178
+ __validate_app(app)
179
+
180
+ rows = []
181
+ for service_name in __get_workflow_services(app, **kwargs):
182
+ service = Service(service_name, app)
183
+ __validate_service_dir(service.dir_path)
184
+ service_config = ServiceConfig(service.dir_path)
185
+ rows.append(service_config.to_dict())
186
+
187
+ print_services(rows, **select_print_options(kwargs))
188
+
189
+ @service.command(
190
+ help="Delete a service",
191
+ )
192
+ @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
148
193
  @click.argument('service_name')
149
194
  def delete(**kwargs):
150
195
  __validate_app_dir(kwargs['app_dir_path'])
@@ -162,7 +207,7 @@ def delete(**kwargs):
162
207
  @service.command(
163
208
  help="Update a service config"
164
209
  )
165
- @click.option('--app-dir-path', default=os.getcwd(), help='Path to application directory.')
210
+ @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
166
211
  @click.option('--priority', help='Determines the service run order.')
167
212
  @click.argument('service_name')
168
213
  def update(**kwargs):
@@ -180,18 +225,11 @@ def update(**kwargs):
180
225
 
181
226
  service_config.write()
182
227
 
183
- @click.group(
184
- epilog="Run 'stoobly-agent request response COMMAND --help' for more information on a command.",
185
- help="Manage service scaffold"
186
- )
187
- @click.pass_context
188
- def workflow(ctx):
189
- pass
190
-
191
228
  @workflow.command(
192
229
  help="Create workflow for service(s)"
193
230
  )
194
- @click.option('--app-dir-path', default=os.getcwd(), help='Path to application directory.')
231
+ @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
232
+ @click.option('--context-dir-path', default=data_dir.context_dir_path, help='Path to Stoobly data directory.')
195
233
  @click.option('--env', multiple=True, help='Specify an environment variable.')
196
234
  @click.option('--force', is_flag=True, help='Overwrite maintained scaffolded workflow files.')
197
235
  @click.option('--service', multiple=True, help='Specify the service(s) to create the workflow for.')
@@ -200,7 +238,7 @@ def workflow(ctx):
200
238
  def create(**kwargs):
201
239
  __validate_app_dir(kwargs['app_dir_path'])
202
240
 
203
- app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
241
+ app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, **kwargs)
204
242
 
205
243
  for service_name in kwargs['service']:
206
244
  config = { **kwargs }
@@ -212,19 +250,20 @@ def create(**kwargs):
212
250
 
213
251
  workflow_dir_path = service.workflow_dir_path(kwargs['workflow_name'])
214
252
  if kwargs['force'] or not os.path.exists(workflow_dir_path):
215
- __workflow_build(app, **config)
253
+ __workflow_create(app, **config)
216
254
  else:
217
255
  print(f"{workflow_dir_path} already exists, use option '--force' to continue")
218
256
 
219
257
  @workflow.command(
220
258
  help="Copy a workflow for service(s)",
221
259
  )
222
- @click.option('--app-dir-path', default=os.getcwd(), help='Path to application directory.')
260
+ @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
261
+ @click.option('--context-dir-path', default=data_dir.context_dir_path, help='Path to Stoobly data directory.')
223
262
  @click.option('--service', multiple=True, help='Specify service(s) to add the workflow to.')
224
263
  @click.argument('workflow_name')
225
264
  @click.argument('destination_workflow_name')
226
265
  def copy(**kwargs):
227
- app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
266
+ app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, **kwargs)
228
267
 
229
268
  for service_name in kwargs['service']:
230
269
  config = { **kwargs }
@@ -240,9 +279,10 @@ def copy(**kwargs):
240
279
  command.copy(kwargs['destination_workflow_name'])
241
280
 
242
281
  @workflow.command()
243
- @click.option('--app-dir-path', default=os.getcwd(), help='Path to application directory.')
244
- @click.option('--context-dir-path', default=DataDir.instance().context_dir_path, help='Path to Stoobly data directory.')
282
+ @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
283
+ @click.option('--context-dir-path', default=data_dir.context_dir_path, help='Path to Stoobly data directory.')
245
284
  @click.option('--dry-run', default=False, is_flag=True)
285
+ @click.option('--extra-entrypoint-compose-path', help='Path to extra entrypoint compose file.')
246
286
  @click.option('--log-level', default=INFO, type=click.Choice([DEBUG, INFO, WARNING, ERROR]), help='''
247
287
  Log levels can be "debug", "info", "warning", or "error"
248
288
  ''')
@@ -258,15 +298,12 @@ def down(**kwargs):
258
298
  os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
259
299
 
260
300
  app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, **kwargs)
301
+ __validate_app(app)
261
302
 
262
303
  # If namespace is set, default network to namespace
263
304
  if kwargs['namespace'] and not kwargs['network']:
264
305
  kwargs['network'] = kwargs['namespace']
265
306
 
266
- if not app.exists:
267
- print(f"Error: {app.dir_path} does not exist", file=sys.stderr)
268
- sys.exit(1)
269
-
270
307
  workflow = Workflow(kwargs['workflow_name'], app)
271
308
  services = __get_services(workflow.services, service=kwargs['service'])
272
309
 
@@ -279,10 +316,22 @@ def down(**kwargs):
279
316
  commands.append(command)
280
317
 
281
318
  commands = sorted(commands, key=lambda command: command.service_config.priority)
282
- for command in commands:
319
+ for index, command in enumerate(commands):
283
320
  __print_header(f"SERVICE {command.service_name}")
284
321
 
285
- exec_command = command.down(namespace=kwargs['namespace'], rmi=kwargs['rmi'], user_id=kwargs['user_id'])
322
+ extra_compose_path = None
323
+
324
+ # By default, the entrypoint service should be last
325
+ # However, this can change if the user has configured a service's priority to be higher
326
+ if index == len(commands) - 1:
327
+ extra_compose_path = kwargs['extra_entrypoint_compose_path']
328
+
329
+ exec_command = command.down(
330
+ extra_compose_path=extra_compose_path,
331
+ namespace=kwargs['namespace'],
332
+ rmi=kwargs['rmi'],
333
+ user_id=kwargs['user_id']
334
+ )
286
335
  if not exec_command:
287
336
  continue
288
337
 
@@ -309,7 +358,7 @@ def down(**kwargs):
309
358
  print(remove_network_command)
310
359
 
311
360
  @workflow.command()
312
- @click.option('--app-dir-path', default=os.getcwd(), help='Path to application directory.')
361
+ @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
313
362
  @click.option(
314
363
  '--container', multiple=True, help=f"Select which containers to log. Defaults to '{WORKFLOW_CONTAINER_PROXY}'"
315
364
  )
@@ -324,14 +373,11 @@ def down(**kwargs):
324
373
  def logs(**kwargs):
325
374
  os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
326
375
 
327
- if len(kwargs['container']) == 0:
328
- kwargs['container'] = [WORKFLOW_CONTAINER_PROXY]
329
-
330
376
  app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
377
+ __validate_app(app)
331
378
 
332
- if not app.exists:
333
- print(f"Error: {app.dir_path} does not exist", file=sys.stderr)
334
- sys.exit(1)
379
+ if len(kwargs['container']) == 0:
380
+ kwargs['container'] = [WORKFLOW_CONTAINER_PROXY]
335
381
 
336
382
  workflow = Workflow(kwargs['workflow_name'], app)
337
383
  services = __get_services(workflow.services, service=kwargs['service'], without_core=True)
@@ -366,16 +412,16 @@ def logs(**kwargs):
366
412
 
367
413
  if not kwargs['dry_run']:
368
414
  exec_stream(shell_command)
369
-
415
+
370
416
  @workflow.command()
371
- @click.option('--app-dir-path', default=os.getcwd(), help='Path to application directory.')
417
+ @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
372
418
  @click.option('--build', is_flag=True, help='Build images before starting containers.')
373
- @click.option('--ca-certs-dir-path', default=DataDir.instance().mitmproxy_conf_dir_path, help='Path to ca certs directory used to sign SSL certs. Defaults to ~/.mitmproxy')
419
+ @click.option('--ca-certs-dir-path', default=data_dir.mitmproxy_conf_dir_path, help='Path to ca certs directory used to sign SSL certs. Defaults to ~/.mitmproxy')
374
420
  @click.option('--certs-dir-path', help='Path to certs directory. Defaults to the certs dir of the context.')
375
- @click.option('--context-dir-path', default=DataDir.instance().context_dir_path, help='Path to Stoobly data directory.')
421
+ @click.option('--context-dir-path', default=data_dir.context_dir_path, help='Path to Stoobly data directory.')
376
422
  @click.option('--detached', is_flag=True, help='If set, will not run the highest priority service in the foreground.')
377
423
  @click.option('--dry-run', default=False, is_flag=True, help='If set, prints commands.')
378
- @click.option('--extra-compose-path', help='Path to extra compose configuration files.')
424
+ @click.option('--extra-entrypoint-compose-path', help='Path to extra entrypoint compose file.')
379
425
  @click.option('--from-make', is_flag=True, help='Set if run from scaffolded Makefile.')
380
426
  @click.option('--log-level', default=INFO, type=click.Choice([DEBUG, INFO, WARNING, ERROR]), help='''
381
427
  Log levels can be "debug", "info", "warning", or "error"
@@ -393,15 +439,12 @@ def up(**kwargs):
393
439
  os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
394
440
 
395
441
  app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, **kwargs)
442
+ __validate_app(app)
396
443
 
397
444
  # If namespace is set, default network to namespace
398
445
  if kwargs['namespace'] and not kwargs['network']:
399
446
  kwargs['network'] = kwargs['namespace']
400
447
 
401
- if not app.exists:
402
- print(f"Error: {app.dir_path} does not exist", file=sys.stderr)
403
- sys.exit(1)
404
-
405
448
  workflow = Workflow(kwargs['workflow_name'], app)
406
449
  services = __get_services(workflow.services, service=kwargs['service'])
407
450
 
@@ -427,9 +470,9 @@ def up(**kwargs):
427
470
 
428
471
  init_commands.append(command.create_network())
429
472
  joined_command = ' && '.join(init_commands)
430
- command.write_nameservers()
431
473
 
432
474
  if not kwargs['dry_run']:
475
+ command.write_nameservers()
433
476
  exec_stream(joined_command)
434
477
  else:
435
478
  print(joined_command)
@@ -438,11 +481,22 @@ def up(**kwargs):
438
481
  for index, command in enumerate(commands):
439
482
  __print_header(f"SERVICE {command.service_name}")
440
483
 
484
+ attached = False
485
+ extra_compose_path = None
486
+
441
487
  # By default, the entrypoint service should be last
442
488
  # However, this can change if the user has configured a service's priority to be higher
443
- attached = not kwargs['detached'] and index == len(commands) - 1
489
+ if index == len(commands) - 1:
490
+ attached = not kwargs['detached']
491
+ extra_compose_path = kwargs['extra_entrypoint_compose_path']
492
+
444
493
  exec_command = command.up(
445
- attached=attached, build=kwargs['build'], namespace=kwargs['namespace'], pull=kwargs['pull'], user_id=kwargs['user_id']
494
+ attached=attached,
495
+ build=kwargs['build'],
496
+ extra_compose_path=extra_compose_path,
497
+ namespace=kwargs['namespace'],
498
+ pull=kwargs['pull'],
499
+ user_id=kwargs['user_id']
446
500
  )
447
501
  if not exec_command:
448
502
  continue
@@ -455,10 +509,12 @@ def up(**kwargs):
455
509
  @workflow.command(
456
510
  help="Validate a scaffold workflow"
457
511
  )
458
- @click.option('--app-dir-path', default=os.getcwd(), help='Path to validate the app scaffold.')
512
+ @click.option('--app-dir-path', default=current_working_dir, help='Path to validate the app scaffold.')
459
513
  @click.argument('workflow_name')
460
514
  def validate(**kwargs):
461
515
  app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
516
+ __validate_app(app)
517
+
462
518
  workflow = Workflow(kwargs['workflow_name'], app)
463
519
 
464
520
  config = { **kwargs }
@@ -481,28 +537,99 @@ def validate(**kwargs):
481
537
  print(f"\nFatal Scaffold Validation Exception: {sve}", file=sys.stderr)
482
538
  sys.exit(1)
483
539
 
540
+ @hostname.command(
541
+ help="Update the system hosts file for all scaffold service hostnames"
542
+ )
543
+ @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
544
+ @click.option('--service', multiple=True, help='Select specific services.')
545
+ @click.option('--workflow', multiple=True, help='Specify services by workflow(s).')
546
+ def install(**kwargs):
547
+ app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
548
+ __validate_app(app)
549
+
550
+ services = __get_workflow_services(app, **kwargs)
551
+
552
+ hostnames = []
553
+ for service_name in services:
554
+ service = Service(service_name, app)
555
+ __validate_service_dir(service.dir_path)
556
+
557
+ service_config = ServiceConfig(service.dir_path)
558
+ if service_config.hostname:
559
+ hostnames.append(service_config.hostname)
560
+
561
+ __elevate_sudo()
562
+
563
+ try:
564
+ hosts_file_manager = HostsFileManager()
565
+ hosts_file_manager.install_hostnames(hostnames)
566
+ except PermissionError:
567
+ print("Permission denied. Please run this command with sudo.", file=sys.stderr)
568
+
569
+ @hostname.command(
570
+ help="Delete from the system hosts file all scaffold service hostnames"
571
+ )
572
+ @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
573
+ def uninstall(**kwargs):
574
+ app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
575
+ __validate_app(app)
576
+
577
+ __elevate_sudo()
578
+
579
+ try:
580
+ hosts_file_manager = HostsFileManager()
581
+ hosts_file_manager.uninstall_hostnames()
582
+ except OSError as e:
583
+ if e.errno == errno.EACCES or e.errno == errno.EPERM:
584
+ print("Permission denied. Please run this command with sudo.", file=sys.stderr)
585
+ else:
586
+ print(f"An unexpected error occurred: {e}", file=sys.stderr)
587
+
484
588
  scaffold.add_command(app)
485
589
  scaffold.add_command(service)
486
590
  scaffold.add_command(workflow)
591
+ scaffold.add_command(hostname)
487
592
 
488
- def __get_services(services: List[str], **kwargs):
489
- # Log services that don't exist
490
- missing_services = [service for service in kwargs['service'] if service not in services]
491
- if missing_services:
492
- Logger.instance(LOG_ID).warn(f"Service(s) {','.join(missing_services)} are not found")
593
+ def __elevate_sudo():
594
+ import subprocess
493
595
 
494
- if kwargs['service']:
495
- # If service is specified, run only those services
496
- services = list(kwargs['service'])
596
+ if os.geteuid() != 0:
597
+ subprocess.run(["sudo", sys.executable] + sys.argv)
598
+ sys.exit(0)
497
599
 
498
- if not kwargs.get('without_core'):
499
- services += CORE_SERVICES
600
+ def __get_services(services: List[str], **kwargs) -> List[str]:
601
+ selected_services = list(kwargs['service'])
602
+
603
+ # If service is specified, run only those services
604
+ if selected_services:
605
+ missing_services = [service for service in selected_services if service not in services]
606
+
607
+ # Remove services that don't exist
608
+ if missing_services:
609
+ Logger.instance(LOG_ID).warn(f"Service(s) {','.join(missing_services)} are not found")
610
+ selected_services = list(set(selected_services) - set(missing_services))
611
+
612
+ services = selected_services
613
+
614
+ services += CORE_SERVICES
615
+
616
+ services_index = {}
617
+ for service in services:
618
+ if kwargs.get('without_core') and service in CORE_SERVICES:
619
+ continue
620
+ services_index[service] = True
621
+
622
+ return services_index.keys()
623
+
624
+ def __get_workflow_services(app: App, **kwargs):
625
+ selected_services = []
626
+ if not kwargs['workflow']:
627
+ selected_services += __get_services(app.services, service=kwargs['service'], without_core=True)
500
628
  else:
501
- # If set, filter out core services
502
- if kwargs.get('without_core'):
503
- services = list(filter(lambda service: service not in CORE_SERVICES, services))
504
-
505
- return list(set(services))
629
+ for workflow_name in kwargs['workflow']:
630
+ workflow = Workflow(workflow_name, app)
631
+ selected_services += __get_services(workflow.services, service=kwargs['service'], without_core=True)
632
+ return set(selected_services)
506
633
 
507
634
  def __print_header(text: str):
508
635
  Logger.instance(LOG_ID).info(f"{bcolors.OKBLUE}{text}{bcolors.ENDC}")
@@ -517,6 +644,11 @@ def __scaffold_delete(app, **kwargs):
517
644
 
518
645
  command.delete()
519
646
 
647
+ def __validate_app(app: App):
648
+ if not app.valid:
649
+ print(f"Error: {app.dir_path} is not a valid scaffold app", file=sys.stderr)
650
+ sys.exit(1)
651
+
520
652
  def __validate_app_dir(app_dir_path):
521
653
  if not os.path.exists(app_dir_path):
522
654
  print(f"Error: {app_dir_path} does not exist", file=sys.stderr)
@@ -527,13 +659,12 @@ def __validate_service_dir(service_dir_path):
527
659
  print(f"Error: {service_dir_path} does not exist, please scaffold this service", file=sys.stderr)
528
660
  sys.exit(1)
529
661
 
530
- def __workflow_build(app, **kwargs):
662
+ def __workflow_create(app, **kwargs):
531
663
  command = WorkflowCreateCommand(app, **kwargs)
532
664
 
533
665
  service_config = command.service_config
534
666
  workflow_decorators = get_workflow_decorators(kwargs['template'], service_config)
535
667
  command.build(
536
- headless=kwargs['headless'],
537
668
  template=kwargs['template'],
538
669
  workflow_decorators=workflow_decorators
539
670
  )
@@ -100,7 +100,7 @@ def prune(**kwargs):
100
100
  log.prune(kwargs['dry_run'])
101
101
 
102
102
  @snapshot.command(
103
- help="Update snapshot.",
103
+ help="Update a snapshot.",
104
104
  )
105
105
  @click.option('--format', type=click.Choice(FORMATS), help='Format output.')
106
106
  @click.option('--select', multiple=True, help='Select column(s) to display.')
@@ -71,6 +71,7 @@ def handle_request_mock_generic(context: MockContext, **options: MockOptions):
71
71
  context.with_response(res)
72
72
 
73
73
  if handle_success:
74
+ # TODO: rewrite response, see #332
74
75
  res = handle_success(context) or res
75
76
  elif policy == mock_policy.FOUND:
76
77
  res = eval_request_with_retry(context, eval_request, **options)
@@ -86,6 +87,7 @@ def handle_request_mock_generic(context: MockContext, **options: MockOptions):
86
87
  pass
87
88
  else:
88
89
  if handle_success:
90
+ # TODO: rewrite response, see #332
89
91
  res = handle_success(context) or res
90
92
  else:
91
93
  return bad_request(
@@ -24,6 +24,8 @@ def handle_request_replay(replay_context: ReplayContext):
24
24
  def handle_response_replay(replay_context: ReplayContext):
25
25
  __replay_hook(lifecycle_hooks.AFTER_REPLAY, replay_context)
26
26
 
27
+ # TODO: rewrite response, see #332
28
+
27
29
  def __replay_request(replay_context: ReplayContext):
28
30
  """
29
31
  Before replaying a request, see if the request needs to be rewritten
@@ -70,7 +70,7 @@ class MitmproxyRequestFacade(Request):
70
70
 
71
71
  @property
72
72
  def body(self):
73
- content = self.request.raw_content or ''
73
+ content = self.request.raw_content or b''
74
74
 
75
75
  return decode(content)
76
76
 
@@ -0,0 +1,19 @@
1
+ import pdb
2
+
3
+ from mitmproxy.http import Response as MitmproxyResponse
4
+ from typing import Union
5
+
6
+ from ..replay.body_parser_service import decode_response, encode_response
7
+
8
+ class MitmproxyResponseBodyFacade:
9
+ def __init__(self, response: MitmproxyResponse):
10
+ self.__response = response
11
+
12
+ def get(self, content_type: Union[bytes, str]):
13
+ return decode_response(self.__response.content, content_type)
14
+
15
+ def set(self, content, content_type: Union[bytes, str]):
16
+ """
17
+ Adjusting Content-Length header should be done by MitmproxyResponse
18
+ """
19
+ self.__response.content = encode_response(content, content_type).encode()