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.
- stoobly_agent/__init__.py +1 -1
- stoobly_agent/app/api/__init__.py +4 -20
- stoobly_agent/app/api/application_http_request_handler.py +5 -2
- stoobly_agent/app/api/configs_controller.py +3 -3
- stoobly_agent/app/cli/decorators/exec.py +1 -1
- stoobly_agent/app/cli/helpers/handle_config_update_service.py +4 -0
- stoobly_agent/app/cli/intercept_cli.py +40 -7
- stoobly_agent/app/cli/scaffold/app_command.py +4 -0
- stoobly_agent/app/cli/scaffold/app_config.py +21 -3
- stoobly_agent/app/cli/scaffold/app_create_command.py +109 -2
- stoobly_agent/app/cli/scaffold/constants.py +14 -3
- stoobly_agent/app/cli/scaffold/docker/constants.py +4 -6
- stoobly_agent/app/cli/scaffold/docker/service/build_decorator.py +2 -2
- stoobly_agent/app/cli/scaffold/docker/service/builder.py +36 -10
- stoobly_agent/app/cli/scaffold/docker/workflow/builder.py +0 -27
- stoobly_agent/app/cli/scaffold/docker/workflow/command_decorator.py +25 -0
- stoobly_agent/app/cli/scaffold/docker/workflow/decorators_factory.py +7 -2
- stoobly_agent/app/cli/scaffold/docker/workflow/detached_decorator.py +42 -0
- stoobly_agent/app/cli/scaffold/docker/workflow/local_decorator.py +26 -0
- stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +9 -10
- stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py +5 -8
- stoobly_agent/app/cli/scaffold/service_config.py +133 -34
- stoobly_agent/app/cli/scaffold/service_create_command.py +11 -2
- stoobly_agent/app/cli/scaffold/service_dependency.py +51 -0
- stoobly_agent/app/cli/scaffold/service_docker_compose.py +3 -3
- stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py +10 -7
- stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +1 -1
- stoobly_agent/app/cli/scaffold/templates/app/build/.docker-compose.base.yml +2 -2
- stoobly_agent/app/cli/scaffold/templates/app/build/mock/bin/configure +1 -1
- stoobly_agent/app/cli/scaffold/templates/app/build/mock/docker-compose.yml +16 -6
- stoobly_agent/app/cli/scaffold/templates/app/build/record/bin/configure +26 -1
- stoobly_agent/app/cli/scaffold/templates/app/build/record/docker-compose.yml +16 -6
- stoobly_agent/app/cli/scaffold/templates/app/build/test/bin/configure +1 -1
- stoobly_agent/app/cli/scaffold/templates/app/build/test/docker-compose.yml +16 -6
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/.docker-compose.base.yml +2 -2
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/bin/configure +1 -1
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/docker-compose.yml +16 -10
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/bin/configure +1 -1
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/docker-compose.yml +16 -10
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/bin/configure +1 -1
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/docker-compose.yml +16 -10
- stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/.docker-compose.base.yml +2 -1
- stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/mock/.docker-compose.mock.yml +6 -3
- stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/record/.docker-compose.record.yml +6 -4
- stoobly_agent/app/cli/scaffold/templates/build/workflows/record/.configure +21 -1
- stoobly_agent/app/cli/scaffold/templates/constants.py +4 -0
- stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.Dockerfile.cypress +22 -0
- stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.docker-compose.test.yml +19 -0
- stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.Dockerfile.playwright +33 -0
- stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.docker-compose.test.yml +18 -0
- stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.entrypoint.sh +11 -0
- stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/configure +2 -10
- stoobly_agent/app/cli/scaffold/templates/workflow/mock/docker-compose.yml +17 -0
- stoobly_agent/app/cli/scaffold/templates/workflow/record/bin/configure +19 -45
- stoobly_agent/app/cli/scaffold/templates/workflow/record/docker-compose.yml +17 -0
- stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/configure +2 -10
- stoobly_agent/app/cli/scaffold/templates/workflow/test/docker-compose.yml +17 -0
- stoobly_agent/app/cli/scaffold/workflow_create_command.py +0 -1
- stoobly_agent/app/cli/scaffold/workflow_run_command.py +1 -1
- stoobly_agent/app/cli/scaffold_cli.py +85 -96
- stoobly_agent/app/proxy/handle_record_service.py +12 -3
- stoobly_agent/app/proxy/handle_replay_service.py +14 -2
- stoobly_agent/app/proxy/intercept_settings.py +12 -8
- stoobly_agent/app/proxy/record/upload_request_service.py +5 -8
- stoobly_agent/app/proxy/replay/replay_request_service.py +3 -0
- stoobly_agent/app/proxy/run.py +3 -28
- stoobly_agent/app/proxy/utils/allowed_request_service.py +3 -2
- stoobly_agent/app/proxy/utils/minimize_headers.py +47 -0
- stoobly_agent/app/proxy/utils/publish_change_service.py +22 -24
- stoobly_agent/app/proxy/utils/strategy.py +16 -0
- stoobly_agent/app/settings/__init__.py +15 -6
- stoobly_agent/app/settings/data_rules.py +25 -1
- stoobly_agent/app/settings/intercept_settings.py +5 -2
- stoobly_agent/app/settings/types/__init__.py +0 -1
- stoobly_agent/app/settings/ui_settings.py +5 -5
- stoobly_agent/cli.py +41 -16
- stoobly_agent/config/constants/custom_headers.py +1 -0
- stoobly_agent/config/constants/env_vars.py +4 -3
- stoobly_agent/config/constants/record_strategy.py +6 -0
- stoobly_agent/config/data_dir.py +1 -0
- stoobly_agent/config/settings.yml.sample +2 -3
- stoobly_agent/lib/logger.py +15 -5
- stoobly_agent/public/index.html +1 -1
- stoobly_agent/public/main-es2015.5a9aa16433404c3f423a.js +1 -0
- stoobly_agent/public/main-es5.5a9aa16433404c3f423a.js +1 -0
- stoobly_agent/test/app/cli/intercept/intercept_configure_test.py +231 -1
- stoobly_agent/test/app/cli/scaffold/cli_invoker.py +3 -2
- stoobly_agent/test/app/cli/scaffold/cli_test.py +3 -3
- stoobly_agent/test/app/cli/scaffold/e2e_test.py +11 -11
- stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
- stoobly_agent/test/app/proxy/utils/minimize_headers_test.py +342 -0
- {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.1.dist-info}/METADATA +2 -1
- {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.1.dist-info}/RECORD +96 -80
- stoobly_agent/public/main-es2015.089b46f303768fbe864f.js +0 -1
- stoobly_agent/public/main-es5.089b46f303768fbe864f.js +0 -1
- {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.1.dist-info}/LICENSE +0 -0
- {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.1.dist-info}/WHEEL +0 -0
- {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
|
-
|
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'],
|
101
|
+
app = App(kwargs['app_dir_path'], SERVICES_NAMESPACE)
|
100
102
|
|
101
|
-
if not kwargs['quiet']
|
102
|
-
|
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
|
-
|
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'],
|
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
|
-
|
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'],
|
173
|
+
app = App(kwargs['app_dir_path'], SERVICES_NAMESPACE)
|
170
174
|
__validate_app(app)
|
171
175
|
|
172
|
-
|
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'],
|
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',
|
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('--
|
214
|
-
@click.option('--
|
215
|
-
|
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'],
|
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
|
-
|
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
|
-
|
238
|
-
|
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
|
-
|
243
|
-
|
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'],
|
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'],
|
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,
|
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'],
|
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,
|
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,
|
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'],
|
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'],
|
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'],
|
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
|
-
|
790
|
-
|
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
|
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
|
-
|
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(
|
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.
|
61
|
-
return
|
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
|
-
|
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.
|
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.
|
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
|
-
|
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.
|
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
|
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']
|
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
|
|
stoobly_agent/app/proxy/run.py
CHANGED
@@ -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
|
|