stoobly-agent 1.9.12__py3-none-any.whl → 1.10.1__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 (98) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/app/api/__init__.py +4 -20
  3. stoobly_agent/app/api/application_http_request_handler.py +5 -2
  4. stoobly_agent/app/api/configs_controller.py +3 -3
  5. stoobly_agent/app/cli/decorators/exec.py +1 -1
  6. stoobly_agent/app/cli/helpers/handle_config_update_service.py +4 -0
  7. stoobly_agent/app/cli/intercept_cli.py +40 -7
  8. stoobly_agent/app/cli/scaffold/app_command.py +4 -0
  9. stoobly_agent/app/cli/scaffold/app_config.py +21 -3
  10. stoobly_agent/app/cli/scaffold/app_create_command.py +109 -2
  11. stoobly_agent/app/cli/scaffold/constants.py +14 -3
  12. stoobly_agent/app/cli/scaffold/docker/constants.py +4 -6
  13. stoobly_agent/app/cli/scaffold/docker/service/build_decorator.py +2 -2
  14. stoobly_agent/app/cli/scaffold/docker/service/builder.py +36 -10
  15. stoobly_agent/app/cli/scaffold/docker/workflow/builder.py +0 -27
  16. stoobly_agent/app/cli/scaffold/docker/workflow/command_decorator.py +25 -0
  17. stoobly_agent/app/cli/scaffold/docker/workflow/decorators_factory.py +7 -2
  18. stoobly_agent/app/cli/scaffold/docker/workflow/detached_decorator.py +42 -0
  19. stoobly_agent/app/cli/scaffold/docker/workflow/local_decorator.py +26 -0
  20. stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +9 -10
  21. stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py +5 -8
  22. stoobly_agent/app/cli/scaffold/service_config.py +133 -34
  23. stoobly_agent/app/cli/scaffold/service_create_command.py +11 -2
  24. stoobly_agent/app/cli/scaffold/service_dependency.py +51 -0
  25. stoobly_agent/app/cli/scaffold/service_docker_compose.py +3 -3
  26. stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py +10 -7
  27. stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +1 -1
  28. stoobly_agent/app/cli/scaffold/templates/app/build/.docker-compose.base.yml +2 -2
  29. stoobly_agent/app/cli/scaffold/templates/app/build/mock/bin/configure +1 -1
  30. stoobly_agent/app/cli/scaffold/templates/app/build/mock/docker-compose.yml +16 -6
  31. stoobly_agent/app/cli/scaffold/templates/app/build/record/bin/configure +26 -1
  32. stoobly_agent/app/cli/scaffold/templates/app/build/record/docker-compose.yml +16 -6
  33. stoobly_agent/app/cli/scaffold/templates/app/build/test/bin/configure +1 -1
  34. stoobly_agent/app/cli/scaffold/templates/app/build/test/docker-compose.yml +16 -6
  35. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/.docker-compose.base.yml +2 -2
  36. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/bin/configure +1 -1
  37. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/docker-compose.yml +16 -10
  38. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/bin/configure +1 -1
  39. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/docker-compose.yml +16 -10
  40. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/bin/configure +1 -1
  41. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/docker-compose.yml +16 -10
  42. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/.docker-compose.base.yml +2 -1
  43. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/mock/.docker-compose.mock.yml +6 -3
  44. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/record/.docker-compose.record.yml +6 -4
  45. stoobly_agent/app/cli/scaffold/templates/build/workflows/record/.configure +21 -1
  46. stoobly_agent/app/cli/scaffold/templates/constants.py +4 -0
  47. stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.Dockerfile.cypress +22 -0
  48. stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.docker-compose.test.yml +19 -0
  49. stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.Dockerfile.playwright +33 -0
  50. stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.docker-compose.test.yml +18 -0
  51. stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.entrypoint.sh +11 -0
  52. stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/configure +2 -10
  53. stoobly_agent/app/cli/scaffold/templates/workflow/mock/docker-compose.yml +17 -0
  54. stoobly_agent/app/cli/scaffold/templates/workflow/record/bin/configure +19 -45
  55. stoobly_agent/app/cli/scaffold/templates/workflow/record/docker-compose.yml +17 -0
  56. stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/configure +2 -10
  57. stoobly_agent/app/cli/scaffold/templates/workflow/test/docker-compose.yml +17 -0
  58. stoobly_agent/app/cli/scaffold/workflow_create_command.py +0 -1
  59. stoobly_agent/app/cli/scaffold/workflow_run_command.py +1 -1
  60. stoobly_agent/app/cli/scaffold_cli.py +85 -96
  61. stoobly_agent/app/proxy/handle_record_service.py +12 -3
  62. stoobly_agent/app/proxy/handle_replay_service.py +14 -2
  63. stoobly_agent/app/proxy/intercept_settings.py +12 -8
  64. stoobly_agent/app/proxy/record/upload_request_service.py +5 -8
  65. stoobly_agent/app/proxy/replay/replay_request_service.py +3 -0
  66. stoobly_agent/app/proxy/run.py +3 -28
  67. stoobly_agent/app/proxy/utils/allowed_request_service.py +3 -2
  68. stoobly_agent/app/proxy/utils/minimize_headers.py +47 -0
  69. stoobly_agent/app/proxy/utils/publish_change_service.py +22 -24
  70. stoobly_agent/app/proxy/utils/strategy.py +16 -0
  71. stoobly_agent/app/settings/__init__.py +15 -6
  72. stoobly_agent/app/settings/data_rules.py +25 -1
  73. stoobly_agent/app/settings/intercept_settings.py +5 -2
  74. stoobly_agent/app/settings/types/__init__.py +0 -1
  75. stoobly_agent/app/settings/ui_settings.py +5 -5
  76. stoobly_agent/cli.py +41 -16
  77. stoobly_agent/config/constants/custom_headers.py +1 -0
  78. stoobly_agent/config/constants/env_vars.py +4 -3
  79. stoobly_agent/config/constants/record_strategy.py +6 -0
  80. stoobly_agent/config/data_dir.py +1 -0
  81. stoobly_agent/config/settings.yml.sample +2 -3
  82. stoobly_agent/lib/logger.py +15 -5
  83. stoobly_agent/public/index.html +1 -1
  84. stoobly_agent/public/main-es2015.5a9aa16433404c3f423a.js +1 -0
  85. stoobly_agent/public/main-es5.5a9aa16433404c3f423a.js +1 -0
  86. stoobly_agent/test/app/cli/intercept/intercept_configure_test.py +231 -1
  87. stoobly_agent/test/app/cli/scaffold/cli_invoker.py +3 -2
  88. stoobly_agent/test/app/cli/scaffold/cli_test.py +3 -3
  89. stoobly_agent/test/app/cli/scaffold/e2e_test.py +11 -11
  90. stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
  91. stoobly_agent/test/app/proxy/utils/minimize_headers_test.py +342 -0
  92. {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.1.dist-info}/METADATA +2 -1
  93. {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.1.dist-info}/RECORD +96 -80
  94. stoobly_agent/public/main-es2015.089b46f303768fbe864f.js +0 -1
  95. stoobly_agent/public/main-es5.089b46f303768fbe864f.js +0 -1
  96. {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.1.dist-info}/LICENSE +0 -0
  97. {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.1.dist-info}/WHEEL +0 -0
  98. {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.1.dist-info}/entry_points.txt +0 -0
@@ -12,8 +12,9 @@ from stoobly_agent.app.cli.helpers.shell import exec_stream
12
12
  from stoobly_agent.app.cli.scaffold.app import App
13
13
  from stoobly_agent.app.cli.scaffold.app_create_command import AppCreateCommand
14
14
  from stoobly_agent.app.cli.scaffold.constants import (
15
- DOCKER_NAMESPACE, WORKFLOW_CONTAINER_PROXY, WORKFLOW_MOCK_TYPE, WORKFLOW_RECORD_TYPE, WORKFLOW_TEST_TYPE
15
+ SERVICES_NAMESPACE, WORKFLOW_CONTAINER_PROXY, WORKFLOW_MOCK_TYPE, WORKFLOW_RECORD_TYPE, WORKFLOW_TEST_TYPE
16
16
  )
17
+ from stoobly_agent.app.cli.scaffold.constants import PLUGIN_CYPRESS, PLUGIN_PLAYWRIGHT
17
18
  from stoobly_agent.app.cli.scaffold.containerized_app import ContainerizedApp
18
19
  from stoobly_agent.app.cli.scaffold.docker.service.configure_gateway import configure_gateway
19
20
  from stoobly_agent.app.cli.scaffold.docker.workflow.decorators_factory import get_workflow_decorators
@@ -90,18 +91,25 @@ def hostname(ctx):
90
91
  )
91
92
  @click.option('--app-dir-path', default=current_working_dir, help='Path to create the app scaffold.')
92
93
  @click.option('--docker-socket-path', default='/var/run/docker.sock', type=click.Path(exists=True, file_okay=True, dir_okay=False), help='Path to Docker socket.')
94
+ @click.option('--plugin', multiple=True, type=click.Choice([PLUGIN_CYPRESS, PLUGIN_PLAYWRIGHT]), help='Scaffold integrations.')
93
95
  @click.option('--quiet', is_flag=True, help='Disable log output.')
94
96
  @click.option('--ui-port', default=4200, type=click.IntRange(1, 65535), help='UI service port.')
95
97
  @click.argument('app_name', callback=validate_app_name)
96
98
  def create(**kwargs):
97
99
  __validate_app_dir(kwargs['app_dir_path'])
98
100
 
99
- app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
101
+ app = App(kwargs['app_dir_path'], SERVICES_NAMESPACE)
100
102
 
101
- if not kwargs['quiet'] and os.path.exists(app.scaffold_namespace_path):
102
- print(f"{kwargs['app_dir_path']} already exists, updating scaffold maintained files...")
103
+ if not kwargs['quiet']:
104
+ if os.path.exists(app.scaffold_namespace_path):
105
+ print(f"{kwargs['app_dir_path']} already exists, updating scaffold maintained files...")
106
+ else:
107
+ print(f"Creating scaffold in {kwargs['app_dir_path']}")
108
+
109
+ res = AppCreateCommand(app, **kwargs).build()
103
110
 
104
- AppCreateCommand(app, **kwargs).build()
111
+ for warning in res['warnings']:
112
+ print(f"{bcolors.WARNING}WARNING{bcolors.ENDC}: {warning}")
105
113
 
106
114
  @app.command(
107
115
  help="Scaffold app service certs"
@@ -113,7 +121,7 @@ def create(**kwargs):
113
121
  @click.option('--service', multiple=True, help='Select which services to run. Defaults to all.')
114
122
  @click.option('--workflow', multiple=True, help='Specify services by workflow(s). Defaults to all.')
115
123
  def mkcert(**kwargs):
116
- app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, **kwargs)
124
+ app = App(kwargs['app_dir_path'], SERVICES_NAMESPACE, **kwargs)
117
125
  __validate_app(app)
118
126
 
119
127
  services = __get_services(
@@ -129,25 +137,20 @@ def mkcert(**kwargs):
129
137
  @click.option('--detached', is_flag=True, help='Use isolated and non-persistent context directory.')
130
138
  @click.option('--env', multiple=True, help='Specify an environment variable.')
131
139
  @click.option('--hostname', callback=validate_hostname, help='Service hostname.')
140
+ @click.option('--local', is_flag=True, help='Specifies upstream service is local. Overrides `--upstream-hostname` option.')
132
141
  @click.option('--port', type=click.IntRange(1, 65535), help='Service port.')
133
142
  @click.option('--priority', default=5, type=click.FloatRange(1.0, 9.0), help='Determines the service run order. Lower values run first.')
134
- @click.option('--proxy-mode', help='''
135
- Proxy mode can be "regular", "transparent", "socks5",
136
- "reverse:SPEC", or "upstream:SPEC". For reverse and
137
- upstream proxy modes, SPEC is host specification in
138
- the form of "http[s]://host[:port]".
139
- ''')
140
143
  @click.option('--quiet', is_flag=True, help='Disable log output.')
141
144
  @click.option('--scheme', type=click.Choice(['http', 'https']), help='Defaults to https if hostname is set.')
145
+ @click.option('--upstream-hostname', callback=validate_hostname, help='Upstream service hostname.')
146
+ @click.option('--upstream-port', type=click.IntRange(1, 65535), help='Upstream service port.')
147
+ @click.option('--upstream-scheme', type=click.Choice(['http', 'https']), help='Upstream service scheme.')
142
148
  @click.option('--workflow', multiple=True, type=click.Choice([WORKFLOW_MOCK_TYPE, WORKFLOW_RECORD_TYPE, WORKFLOW_TEST_TYPE]), help='Include pre-defined workflows.')
143
149
  @click.argument('service_name', callback=validate_service_name)
144
150
  def create(**kwargs):
145
151
  __validate_app_dir(kwargs['app_dir_path'])
146
152
 
147
- if kwargs.get("proxy_mode"):
148
- __validate_proxy_mode(kwargs.get("proxy_mode"))
149
-
150
- app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
153
+ app = App(kwargs['app_dir_path'], SERVICES_NAMESPACE)
151
154
  service = Service(kwargs['service_name'], app)
152
155
 
153
156
  if not kwargs['quiet'] and os.path.exists(service.dir_path):
@@ -164,15 +167,17 @@ def create(**kwargs):
164
167
  @click.option('--select', multiple=True, help='Select column(s) to display.')
165
168
  @click.option('--service', multiple=True, help='Select specific services.')
166
169
  @click.option('--without-headers', is_flag=True, default=False, help='Disable printing column headers.')
170
+ @click.option('--all', is_flag=True, default=False, help='Display all services including core and user defined services')
167
171
  @click.option('--workflow', multiple=True, help='Specify workflow(s) to filter the services by. Defaults to all.')
168
172
  def _list(**kwargs):
169
- app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
173
+ app = App(kwargs['app_dir_path'], SERVICES_NAMESPACE)
170
174
  __validate_app(app)
171
175
 
172
- services = __get_services(app, service=kwargs['service'], workflow=kwargs['workflow'])
176
+ without_core = not kwargs['all']
177
+ services = __get_services(app, service=kwargs['service'], without_core=without_core, workflow=kwargs['workflow'])
173
178
 
174
179
  rows = []
175
- for service_name in services:
180
+ for service_name in services:
176
181
  service = Service(service_name, app)
177
182
  __validate_service_dir(service.dir_path)
178
183
 
@@ -184,13 +189,29 @@ def _list(**kwargs):
184
189
 
185
190
  print_services(rows, **select_print_options(kwargs))
186
191
 
192
+ @service.command(
193
+ help="Show information about a service",
194
+ )
195
+ @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
196
+ @click.option('--format', type=click.Choice(FORMATS), help='Format output.')
197
+ @click.option('--without-headers', is_flag=True, default=False, help='Disable printing column headers.')
198
+ @click.argument('service_name')
199
+ @click.pass_context
200
+ def show(ctx, **kwargs):
201
+ service_name = kwargs['service_name']
202
+ del kwargs['service_name']
203
+ kwargs['service'] = [service_name]
204
+
205
+ # Invoke list with 1 service
206
+ ctx.invoke(_list, **kwargs)
207
+
187
208
  @service.command(
188
209
  help="Delete a service",
189
210
  )
190
211
  @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
191
212
  @click.argument('service_name')
192
213
  def delete(**kwargs):
193
- app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
214
+ app = App(kwargs['app_dir_path'], SERVICES_NAMESPACE)
194
215
  __validate_app(app)
195
216
 
196
217
  service = Service(kwargs['service_name'], app)
@@ -207,19 +228,17 @@ def delete(**kwargs):
207
228
  )
208
229
  @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
209
230
  @click.option('--hostname', callback=validate_hostname, help='Service hostname.')
231
+ @click.option('--local', is_flag=True, help='Specifies upstream service is local. Overrides `--upstream-hostname` option.')
232
+ @click.option('--name', callback=validate_service_name, type=click.STRING, help='New name of the service to update to.')
210
233
  @click.option('--port', type=click.IntRange(1, 65535), help='Service port.')
211
- @click.option('--priority', default=5, type=click.FloatRange(1.0, 9.0), help='Determines the service run order. Lower values run first.')
234
+ @click.option('--priority', type=click.FloatRange(1.0, 9.0), help='Determines the service run order. Lower values run first.')
212
235
  @click.option('--scheme', type=click.Choice(['http', 'https']), help='Defaults to https if hostname is set.')
213
- @click.option('--name', callback=validate_service_name, type=click.STRING, help='New name of the service to update to.')
214
- @click.option('--proxy-mode', help='''
215
- Proxy mode can be "regular", "transparent", "socks5",
216
- "reverse:SPEC", or "upstream:SPEC". For reverse and
217
- upstream proxy modes, SPEC is host specification in
218
- the form of "http[s]://host[:port]".
219
- ''')
236
+ @click.option('--upstream-hostname', callback=validate_hostname, help='Upstream service hostname.')
237
+ @click.option('--upstream-port', type=click.IntRange(1, 65535), help='Upstream service port.')
238
+ @click.option('--upstream-scheme', type=click.Choice(['http', 'https']), help='Upstream service scheme.')
220
239
  @click.argument('service_name')
221
240
  def update(**kwargs):
222
- app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
241
+ app = App(kwargs['app_dir_path'], SERVICES_NAMESPACE)
223
242
  __validate_app(app)
224
243
 
225
244
  service = Service(kwargs['service_name'], app)
@@ -228,29 +247,29 @@ def update(**kwargs):
228
247
 
229
248
  service_config = ServiceConfig(service.dir_path)
230
249
 
231
- if kwargs['hostname']:
232
- old_hostname = service_config.hostname
233
-
234
- if old_hostname != kwargs['hostname']:
235
- service_config.hostname = kwargs['hostname']
250
+ service_config.local = kwargs['local']
236
251
 
237
- # If this is the default proxy_mode and the origin matches the original hostname, assume it is safe to update with the new hostname
238
- if service_config.proxy_mode.startswith("reverse:"):
239
- old_origin = service_config.proxy_mode.split("reverse:")[1]
240
- parsed_origin_url = urlparse(old_origin)
252
+ if kwargs['hostname']:
253
+ service_config.hostname = kwargs['hostname']
241
254
 
242
- if old_hostname == parsed_origin_url.hostname:
243
- service_config.proxy_mode = service_config.proxy_mode.replace(old_hostname, service_config.hostname)
255
+ if kwargs['port']:
256
+ service_config.port = kwargs['port']
244
257
 
245
258
  if kwargs['priority']:
246
259
  service_config.priority = kwargs['priority']
247
260
 
248
- if kwargs['port']:
249
- service_config.port = kwargs['port']
250
-
251
261
  if kwargs['scheme']:
252
262
  service_config.scheme = kwargs['scheme']
253
263
 
264
+ if kwargs['upstream_hostname']:
265
+ service_config.upstream_hostname = kwargs['upstream_hostname']
266
+
267
+ if kwargs['upstream_port']:
268
+ service_config.upstream_port = kwargs['upstream_port']
269
+
270
+ if kwargs['upstream_scheme']:
271
+ service_config.upstream_scheme = kwargs['upstream_scheme']
272
+
254
273
  if kwargs['name']:
255
274
  old_service_name = service.service_name
256
275
  new_service_name = kwargs['name']
@@ -264,10 +283,6 @@ def update(**kwargs):
264
283
 
265
284
  print(f"Successfully renamed service to: {new_service_name}")
266
285
 
267
- if kwargs['proxy_mode']:
268
- __validate_proxy_mode(kwargs['proxy_mode'])
269
- service_config.proxy_mode = kwargs['proxy_mode']
270
-
271
286
  service_config.write()
272
287
 
273
288
  @workflow.command(
@@ -282,7 +297,7 @@ def update(**kwargs):
282
297
  def create(**kwargs):
283
298
  __validate_app_dir(kwargs['app_dir_path'])
284
299
 
285
- app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, **kwargs)
300
+ app = App(kwargs['app_dir_path'], SERVICES_NAMESPACE, **kwargs)
286
301
 
287
302
  for service_name in kwargs['service']:
288
303
  config = { **kwargs }
@@ -308,13 +323,13 @@ def create(**kwargs):
308
323
  @click.argument('workflow_name')
309
324
  @click.argument('destination_workflow_name')
310
325
  def copy(**kwargs):
311
- app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, **kwargs)
326
+ app = App(kwargs['app_dir_path'], SERVICES_NAMESPACE, **kwargs)
312
327
 
313
328
  for service_name in kwargs['service']:
314
329
  config = { **kwargs }
315
330
  del config['service']
316
331
  config['service_name'] = service_name
317
-
332
+
318
333
  command = WorkflowCopyCommand(app, **config)
319
334
 
320
335
  if not command.app_dir_exists:
@@ -345,7 +360,7 @@ def down(**kwargs):
345
360
  containerized = kwargs['containerized']
346
361
 
347
362
  app_dir_path = current_working_dir if containerized else kwargs['app_dir_path']
348
- app = App(app_dir_path, DOCKER_NAMESPACE, **kwargs)
363
+ app = App(app_dir_path, SERVICES_NAMESPACE, **kwargs)
349
364
  __validate_app(app)
350
365
 
351
366
  __with_namespace_defaults(kwargs)
@@ -384,7 +399,7 @@ def down(**kwargs):
384
399
  )
385
400
  if not exec_command:
386
401
  continue
387
-
402
+
388
403
  print(exec_command, file=script)
389
404
 
390
405
  # After services are stopped, their network needs to be removed
@@ -424,7 +439,7 @@ def down(**kwargs):
424
439
  def logs(**kwargs):
425
440
  os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
426
441
 
427
- app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
442
+ app = App(kwargs['app_dir_path'], SERVICES_NAMESPACE)
428
443
  __validate_app(app)
429
444
 
430
445
  __with_namespace_defaults(kwargs)
@@ -448,7 +463,7 @@ def logs(**kwargs):
448
463
  continue
449
464
 
450
465
  config = { **kwargs }
451
- config['service_name'] = service
466
+ config['service_name'] = service
452
467
  command = WorkflowLogCommand(app, **config)
453
468
  commands.append(command)
454
469
 
@@ -500,7 +515,7 @@ def up(**kwargs):
500
515
  # Because we are running a docker-compose command which depends on APP_DIR env var
501
516
  # when we are running this command within a container, the host's app_dir_path will likely differ
502
517
  app_dir_path = current_working_dir if containerized else kwargs['app_dir_path']
503
- app = App(app_dir_path, DOCKER_NAMESPACE, **kwargs)
518
+ app = App(app_dir_path, SERVICES_NAMESPACE, **kwargs)
504
519
  __validate_app(app)
505
520
 
506
521
  __with_namespace_defaults(kwargs)
@@ -511,7 +526,7 @@ def up(**kwargs):
511
526
  )
512
527
 
513
528
  if kwargs['mkcert']:
514
- _app = ContainerizedApp(app_dir_path, DOCKER_NAMESPACE) if containerized else app
529
+ _app = ContainerizedApp(app_dir_path, SERVICES_NAMESPACE) if containerized else app
515
530
  __services_mkcert(_app, services)
516
531
 
517
532
  # Gateway ports are dynamically set depending on the workflow run
@@ -581,11 +596,11 @@ def up(**kwargs):
581
596
  @click.option('--app-dir-path', default=current_working_dir, help='Path to validate the app scaffold.')
582
597
  @click.argument('workflow_name')
583
598
  def validate(**kwargs):
584
- app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
599
+ app = App(kwargs['app_dir_path'], SERVICES_NAMESPACE)
585
600
  __validate_app(app)
586
601
 
587
602
  workflow = Workflow(kwargs['workflow_name'], app)
588
-
603
+
589
604
  config = { **kwargs }
590
605
  config['service_name'] = 'build'
591
606
 
@@ -617,7 +632,7 @@ def validate(**kwargs):
617
632
  @click.option('--service', multiple=True, help='Select specific services. Defaults to all.')
618
633
  @click.option('--workflow', multiple=True, help='Specify services by workflow(s). Defaults to all.')
619
634
  def install(**kwargs):
620
- app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
635
+ app = App(kwargs['app_dir_path'], SERVICES_NAMESPACE)
621
636
  __validate_app(app)
622
637
 
623
638
  services = __get_services(
@@ -625,7 +640,7 @@ def install(**kwargs):
625
640
  )
626
641
 
627
642
  hostnames = []
628
- for service_name in services:
643
+ for service_name in services:
629
644
  service = Service(service_name, app)
630
645
  __validate_service_dir(service.dir_path)
631
646
 
@@ -649,7 +664,7 @@ def install(**kwargs):
649
664
  @click.option('--service', multiple=True, help='Select specific services. Defaults to all.')
650
665
  @click.option('--workflow', multiple=True, help='Specify services by workflow(s). Defaults to all.')
651
666
  def uninstall(**kwargs):
652
- app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
667
+ app = App(kwargs['app_dir_path'], SERVICES_NAMESPACE)
653
668
  __validate_app(app)
654
669
 
655
670
  services = __get_services(
@@ -657,7 +672,7 @@ def uninstall(**kwargs):
657
672
  )
658
673
 
659
674
  hostnames = []
660
- for service_name in services:
675
+ for service_name in services:
661
676
  service = Service(service_name, app)
662
677
  __validate_service_dir(service.dir_path)
663
678
 
@@ -734,7 +749,7 @@ def __get_services(app: App, **kwargs):
734
749
 
735
750
  services = list(set(selected_services))
736
751
  services.sort()
737
-
752
+
738
753
  return services
739
754
 
740
755
  def __print_header(text: str):
@@ -771,7 +786,7 @@ def __services_mkcert(app: App, services):
771
786
  continue
772
787
 
773
788
  hostname = service_config.hostname
774
-
789
+
775
790
  if not hostname:
776
791
  continue
777
792
 
@@ -786,8 +801,11 @@ def __validate_app(app: App):
786
801
  sys.exit(1)
787
802
 
788
803
  def __validate_app_dir(app_dir_path):
789
- if not os.path.exists(app_dir_path):
790
- print(f"Error: {app_dir_path} does not exist", file=sys.stderr)
804
+ __validate_dir(app_dir_path)
805
+
806
+ def __validate_dir(dir_path):
807
+ if not os.path.exists(dir_path):
808
+ print(f"Error: {dir_path} does not exist", file=sys.stderr)
791
809
  sys.exit(1)
792
810
 
793
811
  def __validate_service_dir(service_dir_path):
@@ -795,35 +813,6 @@ def __validate_service_dir(service_dir_path):
795
813
  print(f"Error: '{service_dir_path}' does not exist, please scaffold this service", file=sys.stderr)
796
814
  sys.exit(1)
797
815
 
798
- def __validate_proxy_mode(proxy_mode: str) -> None:
799
- valid_exact_matches = {
800
- "regular": None,
801
- "transparent": None,
802
- "socks5": None,
803
- }
804
-
805
- valid_prefixes = {
806
- "reverse": None,
807
- "upstream": None
808
- }
809
-
810
- if proxy_mode in valid_exact_matches:
811
- return
812
-
813
- split_str = proxy_mode.split(":", 1)
814
- if len(split_str) != 2:
815
- print(f"Error: {proxy_mode} is invalid.", file=sys.stderr)
816
- sys.exit(1)
817
-
818
- prefix = split_str[0]
819
- spec = split_str[1]
820
-
821
- if prefix not in valid_prefixes:
822
- print(f"Error: {proxy_mode} is invalid.", file=sys.stderr)
823
- sys.exit(1)
824
-
825
- # TODO: validate SPEC
826
-
827
816
  def __with_namespace_defaults(kwargs):
828
817
  if not kwargs.get('namespace'):
829
818
  kwargs['namespace'] = kwargs.get('workflow_name')
@@ -842,4 +831,4 @@ def __workflow_create(app, **kwargs):
842
831
  def __with_workflow_namespace(app: App, namespace: str):
843
832
  workflow_namespace = WorkflowNamespace(app, namespace)
844
833
  workflow_namespace.copy_dotenv()
845
- return workflow_namespace
834
+ return workflow_namespace
@@ -9,7 +9,7 @@ from stoobly_agent.app.settings.constants.mode import TEST
9
9
  from stoobly_agent.app.models.request_model import RequestModel
10
10
  from stoobly_agent.app.proxy.intercept_settings import InterceptSettings
11
11
  from stoobly_agent.config.constants.env_vars import ENV
12
- from stoobly_agent.config.constants import lifecycle_hooks, record_order, record_policy
12
+ from stoobly_agent.config.constants import lifecycle_hooks, record_order, record_policy, record_strategy
13
13
  from stoobly_agent.lib.logger import Logger
14
14
 
15
15
  from .constants import custom_response_codes
@@ -19,8 +19,11 @@ from .record.overwrite_scenario_service import overwrite_scenario
19
19
  from .record.upload_request_service import inject_upload_request
20
20
  from .replay.body_parser_service import is_json, is_xml
21
21
  from .utils.allowed_request_service import get_active_mode_policy
22
+ from .utils.minimize_headers import minimize_headers
22
23
  from .utils.response_handler import bad_request, disable_transfer_encoding
23
24
  from .utils.rewrite import rewrite_request_response
25
+ from .utils.strategy import get_active_mode_strategy
26
+
24
27
 
25
28
  LOG_ID = 'Record'
26
29
 
@@ -54,7 +57,7 @@ def handle_response_record(context: RecordContext):
54
57
  res = inject_eval_request(request_model, intercept_settings)(request, [])
55
58
 
56
59
  if res.status_code != custom_response_codes.NOT_FOUND:
57
- __record_request(context , request_model)
60
+ __record_request(context, request_model)
58
61
  elif active_record_policy == record_policy.NOT_FOUND:
59
62
  res = inject_eval_request(request_model, intercept_settings)(request, [])
60
63
 
@@ -74,7 +77,13 @@ def __record_handler(context: RecordContext, request_model: RequestModel):
74
77
  intercept_settings = context.intercept_settings
75
78
 
76
79
  context.flow = flow_copy # Deep copy flow to prevent response modifications from persisting
77
- rewrite_request_response(flow_copy, intercept_settings.record_rewrite_rules)
80
+
81
+ active_record_strategy = get_active_mode_strategy(intercept_settings)
82
+ if active_record_strategy == record_strategy.MINIMAL:
83
+ minimize_headers(flow_copy)
84
+
85
+ rewrite_request_response(flow_copy, intercept_settings.record_rewrite_rules)
86
+
78
87
  __record_hook(lifecycle_hooks.BEFORE_RECORD, context)
79
88
 
80
89
  inject_upload_request(request_model, intercept_settings)(flow_copy)
@@ -5,9 +5,10 @@ from typing import TypedDict
5
5
 
6
6
  from stoobly_agent.app.proxy.intercept_settings import InterceptSettings
7
7
  from stoobly_agent.app.proxy.replay.context import ReplayContext
8
- from stoobly_agent.config.constants import lifecycle_hooks, replay_policy
8
+ from stoobly_agent.config.constants import lifecycle_hooks, replay_policy, custom_headers, mode, record_strategy
9
9
 
10
10
  from .utils.allowed_request_service import get_active_mode_policy
11
+ from .utils.minimize_headers import minimize_response_headers
11
12
  from .utils.rewrite import rewrite_request, rewrite_response
12
13
 
13
14
  LOG_ID = 'HandleReplay'
@@ -66,7 +67,18 @@ def __rewrite_response(replay_context: ReplayContext):
66
67
  After replaying a request, see if the request needs to be rewritten
67
68
  """
68
69
  intercept_settings: InterceptSettings = replay_context.intercept_settings
70
+ flow = replay_context.flow
71
+ request = flow.request
72
+ response = flow.response
73
+
74
+ request_proxy_mode_header = request.headers.get(custom_headers.PROXY_MODE)
75
+ response_proxy_mode_header = response.headers.get(custom_headers.RESPONSE_PROXY_MODE)
76
+
77
+ if request_proxy_mode_header == mode.REPLAY and response_proxy_mode_header == mode.RECORD:
78
+ if intercept_settings.record_strategy == record_strategy.MINIMAL:
79
+ minimize_response_headers(flow)
80
+
69
81
  rewrite_rules = intercept_settings.replay_rewrite_rules
70
82
 
71
83
  if len(rewrite_rules) > 0:
72
- rewrite_response(replay_context.flow, rewrite_rules)
84
+ rewrite_response(flow, rewrite_rules)
@@ -57,14 +57,11 @@ class InterceptSettings:
57
57
 
58
58
  @property
59
59
  def active(self):
60
- if self.__intercept_settings.active:
61
- return True
60
+ if self.__headers and custom_headers.PROXY_MODE in self.__headers:
61
+ return not not self.__headers[custom_headers.PROXY_MODE]
62
62
 
63
- if not self.__headers:
64
- return False
63
+ return self.__intercept_settings.active
65
64
 
66
- return custom_headers.PROXY_MODE in self.__headers
67
-
68
65
  @property
69
66
  def lifecycle_hooks_path(self):
70
67
  if self.__headers and custom_headers.LIFECYCLE_HOOKS_PATH in self.__headers:
@@ -182,6 +179,13 @@ class InterceptSettings:
182
179
 
183
180
  return self.policy
184
181
 
182
+ @property
183
+ def record_strategy(self):
184
+ if self.__headers and custom_headers.RECORD_STRATEGY in self.__headers:
185
+ return self.__headers[custom_headers.RECORD_STRATEGY]
186
+
187
+ return self.__data_rules.record_strategy
188
+
185
189
  @property
186
190
  def exclude_rules(self) -> List[FirewallRule]:
187
191
  _mode = self.mode
@@ -276,7 +280,7 @@ class InterceptSettings:
276
280
  if self.__headers and custom_headers.REQUEST_ORIGIN in self.__headers:
277
281
  return self.__headers[custom_headers.REQUEST_ORIGIN]
278
282
 
279
- return request_origin.WEB
283
+ return request_origin.PROXY
280
284
 
281
285
  def for_response(self):
282
286
  self.__for_response = True
@@ -365,4 +369,4 @@ class InterceptSettings:
365
369
 
366
370
  return self.__data_rules.test_policy
367
371
  elif mode == intercept_mode.REPLAY:
368
- return self.__data_rules.replay_policy
372
+ return self.__data_rules.replay_policy
@@ -49,7 +49,7 @@ def inject_upload_request(request_model: RequestModel, intercept_settings: Inter
49
49
  def upload_request(
50
50
  request_model: RequestModel, intercept_settings: InterceptSettings, flow: MitmproxyHTTPFlow = None
51
51
  ):
52
- Logger.instance(LOG_ID).info(f"{bcolors.OKCYAN}Recording{bcolors.ENDC} {flow.request.url}")
52
+ Logger.instance(LOG_ID).info(f"{bcolors.OKBLUE}Recording{bcolors.ENDC} {flow.request.url}")
53
53
 
54
54
  flow_copy = deepcopy(flow) # When applying modifications we don't want to persist them in the response
55
55
  joined_request = join_request_from_flow(flow_copy, intercept_settings=intercept_settings)
@@ -63,10 +63,7 @@ def upload_request(
63
63
  scenario_key=scenario_key
64
64
  )
65
65
 
66
- # If request_origin is WEB, then we are in proxy
67
- # This means that we have access to Cache singleton and do not need send a request to update the status
68
- sync = intercept_settings.request_origin == request_origin.WEB
69
- res = __upload_request_with_body_params(request_model, body_params, sync)
66
+ res = __upload_request_with_body_params(request_model, body_params)
70
67
 
71
68
  if intercept_settings.settings.is_debug():
72
69
  file_path = __debug_request(flow.request, joined_request.build())
@@ -80,7 +77,7 @@ def upload_request(
80
77
  def upload_staged_request(
81
78
  request: Request, request_model: RequestModel, project_key: str, scenario_key: str = None
82
79
  ):
83
- Logger.instance(LOG_ID).info(f"{bcolors.OKCYAN}Recording{bcolors.ENDC} {request.url}")
80
+ Logger.instance(LOG_ID).info(f"{bcolors.OKBLUE}Recording{bcolors.ENDC} {request.url}")
84
81
 
85
82
  response = request.response
86
83
 
@@ -107,11 +104,11 @@ def upload_staged_request(
107
104
 
108
105
  return __upload_request_with_body_params(request_model, body_params)
109
106
 
110
- def __upload_request_with_body_params(request_model: RequestModel, body_params: dict, sync=True):
107
+ def __upload_request_with_body_params(request_model: RequestModel, body_params: dict):
111
108
  request, status = request_model.create(**body_params)
112
109
 
113
110
  if status < 400:
114
- publish_requests_modified(body_params['project_id'], sync=sync)
111
+ publish_requests_modified(body_params['project_id'])
115
112
 
116
113
  return request
117
114
 
@@ -72,6 +72,9 @@ def replay(context: ReplayContext, options: ReplayRequestOptions) -> requests.Re
72
72
  if options.get('public_directory_path'):
73
73
  __handle_path_header(custom_headers.PUBLIC_DIRECTORY_PATH, options['public_directory_path'], headers)
74
74
 
75
+ if options.get('record_strategy'):
76
+ headers[custom_headers.RECORD_STRATEGY] = options['record_strategy']
77
+
75
78
  if options.get('report_key'):
76
79
  headers[custom_headers.REPORT_KEY] = options['report_key']
77
80
 
@@ -16,8 +16,6 @@ tls.DEFAULT_OPTIONS |= 0x4
16
16
  from mitmproxy.options import Options
17
17
  from mitmproxy.tools.dump import DumpMaster
18
18
 
19
- from stoobly_agent.app.settings import Settings
20
-
21
19
  INTERCEPT_HANDLER_FILENAME = 'intercept_handler.py'
22
20
 
23
21
  def run(**kwargs):
@@ -67,7 +65,6 @@ def __with_static_options(config: MitmproxyConfig, cli_options):
67
65
  config.set(options)
68
66
 
69
67
  def __with_cli_options(config: MitmproxyConfig, cli_options: dict):
70
- __commit_options(cli_options)
71
68
  __filter_options(cli_options)
72
69
 
73
70
  options = []
@@ -86,31 +83,6 @@ def __with_cli_options(config: MitmproxyConfig, cli_options: dict):
86
83
 
87
84
  config.set(tuple(options))
88
85
 
89
- def __commit_options(options: dict):
90
- changed = False
91
- service_name = os.environ.get(SERVICE_NAME_ENV)
92
- settings = Settings.instance()
93
-
94
- if not service_name or options.get('intercept'):
95
- # In the case when service name is set,
96
- # only update if intercept is explicitly enabled
97
- intercept = not not options.get('intercept')
98
- changed = settings.proxy.intercept.active != intercept
99
- settings.proxy.intercept.active = intercept
100
-
101
- if not service_name or service_name == CORE_MOCK_UI_SERVICE_NAME:
102
- # Causes potentially unintended side effects when run as part of scaffold
103
- # Defer to ui service for configuration in this case
104
- if options.get('proxy_host') and options.get('proxy_port'):
105
- settings.proxy.url = f"http://{options.get('proxy_host')}:{options.get('proxy_port')}"
106
-
107
- settings.ui.active = not options.get('headless')
108
-
109
- changed = True
110
-
111
- if changed:
112
- settings.commit()
113
-
114
86
  def __filter_options(options):
115
87
  '''
116
88
  Filter out non-mitmproxy options
@@ -139,6 +111,9 @@ def __filter_options(options):
139
111
  if 'intercept' in options:
140
112
  del options['intercept']
141
113
 
114
+ if 'intercept_mode' in options:
115
+ del options['intercept_mode']
116
+
142
117
  if 'lifecycle_hooks_path' in options:
143
118
  del options['lifecycle_hooks_path']
144
119
 
@@ -5,13 +5,14 @@ from mitmproxy.http import Request as MitmproxyRequest
5
5
  from typing import List
6
6
 
7
7
  from stoobly_agent.app.proxy.intercept_settings import InterceptSettings
8
+ from stoobly_agent.app.settings.constants import intercept_mode
8
9
  from stoobly_agent.app.settings.firewall_rule import FirewallRule
9
- from stoobly_agent.config.constants import intercept_policy, request_origin
10
+ from stoobly_agent.config.constants import mode, intercept_policy, request_origin
10
11
  from stoobly_agent.lib.logger import bcolors, Logger
11
12
 
12
13
  LOG_ID = 'Firewall'
13
14
 
14
- def get_active_mode_policy(request: MitmproxyRequest, intercept_settings: InterceptSettings):
15
+ def get_active_mode_policy(request: MitmproxyRequest, intercept_settings: InterceptSettings) -> str:
15
16
  if intercept_settings.request_origin == request_origin.CLI:
16
17
  return intercept_settings.policy
17
18