stoobly-agent 1.9.12__py3-none-any.whl → 1.10.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 (74) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/app/api/__init__.py +4 -20
  3. stoobly_agent/app/api/configs_controller.py +3 -3
  4. stoobly_agent/app/cli/decorators/exec.py +1 -1
  5. stoobly_agent/app/cli/helpers/handle_config_update_service.py +4 -0
  6. stoobly_agent/app/cli/intercept_cli.py +40 -7
  7. stoobly_agent/app/cli/scaffold/app_command.py +4 -0
  8. stoobly_agent/app/cli/scaffold/app_config.py +21 -3
  9. stoobly_agent/app/cli/scaffold/app_create_command.py +109 -2
  10. stoobly_agent/app/cli/scaffold/constants.py +13 -0
  11. stoobly_agent/app/cli/scaffold/docker/constants.py +4 -6
  12. stoobly_agent/app/cli/scaffold/docker/service/builder.py +19 -4
  13. stoobly_agent/app/cli/scaffold/docker/workflow/builder.py +0 -18
  14. stoobly_agent/app/cli/scaffold/docker/workflow/command_decorator.py +24 -0
  15. stoobly_agent/app/cli/scaffold/docker/workflow/decorators_factory.py +7 -2
  16. stoobly_agent/app/cli/scaffold/docker/workflow/detached_decorator.py +42 -0
  17. stoobly_agent/app/cli/scaffold/docker/workflow/local_decorator.py +26 -0
  18. stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +9 -10
  19. stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py +5 -8
  20. stoobly_agent/app/cli/scaffold/service_config.py +144 -21
  21. stoobly_agent/app/cli/scaffold/service_create_command.py +11 -2
  22. stoobly_agent/app/cli/scaffold/service_dependency.py +51 -0
  23. stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +1 -1
  24. stoobly_agent/app/cli/scaffold/templates/app/build/mock/docker-compose.yml +16 -6
  25. stoobly_agent/app/cli/scaffold/templates/app/build/record/docker-compose.yml +16 -6
  26. stoobly_agent/app/cli/scaffold/templates/app/build/test/docker-compose.yml +16 -6
  27. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/docker-compose.yml +16 -10
  28. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/docker-compose.yml +16 -10
  29. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/docker-compose.yml +16 -10
  30. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/.docker-compose.base.yml +2 -1
  31. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/mock/.docker-compose.mock.yml +6 -3
  32. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/record/.docker-compose.record.yml +6 -4
  33. stoobly_agent/app/cli/scaffold/templates/constants.py +4 -0
  34. stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.Dockerfile.cypress +22 -0
  35. stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.docker-compose.test.yml +19 -0
  36. stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.Dockerfile.playwright +33 -0
  37. stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.docker-compose.test.yml +18 -0
  38. stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.entrypoint.sh +11 -0
  39. stoobly_agent/app/cli/scaffold/templates/workflow/mock/docker-compose.yml +17 -0
  40. stoobly_agent/app/cli/scaffold/templates/workflow/record/docker-compose.yml +17 -0
  41. stoobly_agent/app/cli/scaffold/templates/workflow/test/docker-compose.yml +17 -0
  42. stoobly_agent/app/cli/scaffold/workflow_create_command.py +0 -1
  43. stoobly_agent/app/cli/scaffold/workflow_run_command.py +1 -1
  44. stoobly_agent/app/cli/scaffold_cli.py +68 -77
  45. stoobly_agent/app/proxy/handle_record_service.py +12 -3
  46. stoobly_agent/app/proxy/handle_replay_service.py +14 -2
  47. stoobly_agent/app/proxy/intercept_settings.py +11 -7
  48. stoobly_agent/app/proxy/record/upload_request_service.py +2 -2
  49. stoobly_agent/app/proxy/replay/replay_request_service.py +3 -0
  50. stoobly_agent/app/proxy/run.py +3 -28
  51. stoobly_agent/app/proxy/utils/allowed_request_service.py +3 -2
  52. stoobly_agent/app/proxy/utils/minimize_headers.py +47 -0
  53. stoobly_agent/app/proxy/utils/publish_change_service.py +5 -4
  54. stoobly_agent/app/proxy/utils/strategy.py +16 -0
  55. stoobly_agent/app/settings/__init__.py +9 -3
  56. stoobly_agent/app/settings/data_rules.py +25 -1
  57. stoobly_agent/app/settings/intercept_settings.py +5 -2
  58. stoobly_agent/app/settings/types/__init__.py +0 -1
  59. stoobly_agent/app/settings/ui_settings.py +5 -5
  60. stoobly_agent/cli.py +41 -16
  61. stoobly_agent/config/constants/custom_headers.py +1 -0
  62. stoobly_agent/config/constants/env_vars.py +4 -3
  63. stoobly_agent/config/constants/record_strategy.py +6 -0
  64. stoobly_agent/config/settings.yml.sample +2 -3
  65. stoobly_agent/lib/logger.py +15 -5
  66. stoobly_agent/test/app/cli/intercept/intercept_configure_test.py +231 -1
  67. stoobly_agent/test/app/cli/scaffold/cli_invoker.py +3 -2
  68. stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
  69. stoobly_agent/test/app/proxy/utils/minimize_headers_test.py +342 -0
  70. {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.0.dist-info}/METADATA +2 -1
  71. {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.0.dist-info}/RECORD +74 -58
  72. {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.0.dist-info}/LICENSE +0 -0
  73. {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.0.dist-info}/WHEEL +0 -0
  74. {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.0.dist-info}/entry_points.txt +0 -0
@@ -13,6 +13,7 @@ CORE_RECORD_WORKFLOW = 'record'
13
13
 
14
14
  CUSTOM_BUILD = os.path.join('bin', 'build')
15
15
  CUSTOM_CONFIGURE = os.path.join('bin', 'configure')
16
+ CUSTOM_DOCKER_COMPOSE = 'docker-compose.yml'
16
17
  CUSTOM_INIT = os.path.join('bin', 'init')
17
18
  CUSTOM_FIXTURES = 'fixtures.yml'
18
19
  CUSTOM_LIFECYCLE_HOOKS = os.path.join('lifecycle_hooks.py')
@@ -28,6 +29,7 @@ MOCK_WORKFLOW_MAINTAINED_FILES = [
28
29
  MOCK_WORKFLOW_CUSTOM_FILES = [
29
30
  CUSTOM_BUILD,
30
31
  CUSTOM_CONFIGURE,
32
+ CUSTOM_DOCKER_COMPOSE,
31
33
  CUSTOM_FIXTURES,
32
34
  CUSTOM_INIT,
33
35
  CUSTOM_LIFECYCLE_HOOKS,
@@ -42,6 +44,7 @@ RECORD_WORKFLOW_MAINTAINED_FILES = [
42
44
  RECORD_WORKFLOW_CUSTOM_FILES = [
43
45
  CUSTOM_BUILD,
44
46
  CUSTOM_CONFIGURE,
47
+ CUSTOM_DOCKER_COMPOSE,
45
48
  CUSTOM_INIT,
46
49
  CUSTOM_LIFECYCLE_HOOKS,
47
50
  ]
@@ -54,6 +57,7 @@ TEST_WORKFLOW_MAINTAINED_FILES = [
54
57
  TEST_WORKFLOW_CUSTOM_FILES = [
55
58
  CUSTOM_BUILD,
56
59
  CUSTOM_CONFIGURE,
60
+ CUSTOM_DOCKER_COMPOSE,
57
61
  CUSTOM_FIXTURES,
58
62
  CUSTOM_INIT,
59
63
  CUSTOM_LIFECYCLE_HOOKS,
@@ -0,0 +1,22 @@
1
+ ARG CYPRESS_IMAGE
2
+ FROM ${CYPRESS_IMAGE}
3
+
4
+ # Change user id of stoobly user to that of host's user id
5
+ ARG USER_ID
6
+ ARG USER_NAME=stoobly
7
+ RUN set -eux; \
8
+ # Check if a user with this UID exists
9
+ if getent passwd "${USER_ID}" > /dev/null; then \
10
+ EXISTING_USER="$(getent passwd "${USER_ID}" | cut -d: -f1)"; \
11
+ if [ "$EXISTING_USER" != "${USER_NAME}" ]; then \
12
+ usermod -l "${USER_NAME}" "$EXISTING_USER"; \
13
+ usermod -d "/home/${USER_NAME}" -m "${USER_NAME}"; \
14
+ fi; \
15
+ else \
16
+ useradd -u "${USER_ID}" -m "${USER_NAME}"; \
17
+ fi
18
+
19
+ USER stoobly
20
+ WORKDIR /home/stoobly
21
+
22
+ RUN npx cypress install # Install Cypress binary into image
@@ -0,0 +1,19 @@
1
+ services:
2
+ entrypoint.cypress:
3
+ build:
4
+ args:
5
+ CYPRESS_IMAGE: ${CYPRESS_IMAGE:-cypress/browsers:22.11.0}
6
+ USER_ID: ${USER_ID}
7
+ context: ./
8
+ dockerfile: ./.Dockerfile.cypress
9
+ command: npx cypress run --project .
10
+ depends_on:
11
+ entrypoint.configure:
12
+ condition: service_completed_successfully
13
+ networks:
14
+ - "app.ingress"
15
+ profiles:
16
+ - ${WORKFLOW_NAME}
17
+ user: stoobly
18
+ volumes:
19
+ - ${CONTEXT_DIR}:/home/stoobly
@@ -0,0 +1,33 @@
1
+ ARG PLAYWRIGHT_IMAGE
2
+ FROM ${PLAYWRIGHT_IMAGE}
3
+
4
+ RUN set -eux; \
5
+ apt-get update; \
6
+ apt-get install -y --no-install-recommends ca-certificates wget; \
7
+ dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
8
+ wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.16/gosu-$dpkgArch"; \
9
+ chmod +x /usr/local/bin/gosu; \
10
+ gosu --version;
11
+ RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
12
+ RUN npx playwright install --with-deps
13
+
14
+ # Change user id of stoobly user to that of host's user id
15
+ ARG USER_ID
16
+ ARG USER_NAME=stoobly
17
+ RUN set -eux; \
18
+ # Check if a user with this UID exists
19
+ if getent passwd "${USER_ID}" > /dev/null; then \
20
+ EXISTING_USER="$(getent passwd "${USER_ID}" | cut -d: -f1)"; \
21
+ if [ "$EXISTING_USER" != "${USER_NAME}" ]; then \
22
+ usermod -l "${USER_NAME}" "$EXISTING_USER"; \
23
+ usermod -d "/home/${USER_NAME}" -m "${USER_NAME}"; \
24
+ fi; \
25
+ else \
26
+ useradd -u "${USER_ID}" -m "${USER_NAME}"; \
27
+ fi
28
+
29
+ WORKDIR /home/stoobly
30
+
31
+ COPY .entrypoint.sh /usr/local/bin/entrypoint.sh
32
+ RUN chmod +x /usr/local/bin/entrypoint.sh
33
+ ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
@@ -0,0 +1,18 @@
1
+ services:
2
+ entrypoint.playwright:
3
+ build:
4
+ args:
5
+ PLAYWRIGHT_IMAGE: ${PLAYWRIGHT_IMAGE:-mcr.microsoft.com/playwright:v1.46.1-jammy}
6
+ USER_ID: ${USER_ID}
7
+ context: ./
8
+ dockerfile: ./.Dockerfile.playwright
9
+ command: npx playwright test --reporter dot
10
+ depends_on:
11
+ entrypoint.configure:
12
+ condition: service_completed_successfully
13
+ networks:
14
+ - "app.ingress"
15
+ profiles:
16
+ - ${WORKFLOW_NAME}
17
+ volumes:
18
+ - ${CONTEXT_DIR}:/home/stoobly
@@ -0,0 +1,11 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ # Copy certs and update trust store
5
+ if [ -d "/home/stoobly/.stoobly/certs" ]; then
6
+ cp /home/stoobly/.stoobly/certs/*.crt /usr/local/share/ca-certificates/ || true
7
+ update-ca-certificates
8
+ fi
9
+
10
+ # Execute the CMD from Dockerfile or passed command
11
+ exec gosu stoobly "$@"
@@ -0,0 +1,17 @@
1
+ # Define custom services here
2
+ #
3
+ # 1. Uncomment the following
4
+ # 2. Replace <SERVICE-NAME> with the name of the service, this can be found in ../config.yml
5
+ # 3. Extend as needed
6
+ #
7
+ # To learn more, see https://docs.stoobly.com/core-concepts/scaffold
8
+
9
+ #services:
10
+ # <SERVICE-NAME>.<CUSTOM-SERVICE-NAME>:
11
+ # depends_on:
12
+ # <SERVICE-NAME>.configure:
13
+ # condition: service_completed_successfully
14
+ # networks:
15
+ # app.ingress: {}
16
+ # profiles:
17
+ # - ${WORKFLOW_NAME}
@@ -0,0 +1,17 @@
1
+ # Define custom services here
2
+ #
3
+ # 1. Uncomment the following
4
+ # 2. Replace <SERVICE-NAME> with the name of the service, this can be found in ../config.yml
5
+ # 3. Extend as needed
6
+ #
7
+ # To learn more, see https://docs.stoobly.com/core-concepts/scaffold
8
+
9
+ #services:
10
+ # <SERVICE-NAME>.<CUSTOM-SERVICE-NAME>:
11
+ # depends_on:
12
+ # <SERVICE-NAME>.configure:
13
+ # condition: service_completed_successfully
14
+ # networks:
15
+ # app.egress: {}
16
+ # profiles:
17
+ # - ${WORKFLOW_NAME}
@@ -0,0 +1,17 @@
1
+ # Define custom services here
2
+ #
3
+ # 1. Uncomment the following
4
+ # 2. Replace <SERVICE-NAME> with the name of the service, this can be found in ../config.yml
5
+ # 3. Extend as needed
6
+ #
7
+ # To learn more, see https://docs.stoobly.com/core-concepts/scaffold
8
+
9
+ #services:
10
+ # <SERVICE-NAME>.<CUSTOM-SERVICE-NAME>:
11
+ # depends_on:
12
+ # <SERVICE-NAME>.configure:
13
+ # condition: service_completed_successfully
14
+ # networks:
15
+ # app.ingress: {}
16
+ # profiles:
17
+ # - ${WORKFLOW_NAME}
@@ -92,6 +92,5 @@ class WorkflowCreateCommand(WorkflowCommand):
92
92
  workflow_decorator(workflow_builder).decorate()
93
93
 
94
94
  workflow_builder.write()
95
- workflow_builder.initialize_custom_file()
96
95
 
97
96
  return workflow_builder
@@ -10,7 +10,7 @@ from stoobly_agent.lib.logger import Logger
10
10
 
11
11
  from .app import App
12
12
  from .constants import (
13
- APP_DIR_ENV, APP_NETWORK_ENV, CA_CERTS_DIR_ENV, CERTS_DIR_ENV, CONTEXT_DIR_ENV,
13
+ APP_DIR_ENV, APP_NETWORK_ENV, APP_PLUGINS_ENV, CA_CERTS_DIR_ENV, CERTS_DIR_ENV, CONTEXT_DIR_ENV,
14
14
  SERVICE_DNS_ENV, SERVICE_NAME_ENV, SERVICE_SCRIPTS_DIR, SERVICE_SCRIPTS_ENV, USER_ID_ENV,
15
15
  WORKFLOW_NAME_ENV, WORKFLOW_NAMESPACE_ENV, WORKFLOW_SCRIPTS_DIR, WORKFLOW_SCRIPTS_ENV, WORKFLOW_TEMPLATE_ENV
16
16
  )
@@ -14,6 +14,7 @@ from stoobly_agent.app.cli.scaffold.app_create_command import AppCreateCommand
14
14
  from stoobly_agent.app.cli.scaffold.constants import (
15
15
  DOCKER_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,6 +91,7 @@ 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)
@@ -101,7 +103,10 @@ def create(**kwargs):
101
103
  if not kwargs['quiet'] and os.path.exists(app.scaffold_namespace_path):
102
104
  print(f"{kwargs['app_dir_path']} already exists, updating scaffold maintained files...")
103
105
 
104
- AppCreateCommand(app, **kwargs).build()
106
+ res = AppCreateCommand(app, **kwargs).build()
107
+
108
+ for warning in res['warnings']:
109
+ print(f"{bcolors.WARNING}WARNING{bcolors.ENDC}: {warning}")
105
110
 
106
111
  @app.command(
107
112
  help="Scaffold app service certs"
@@ -129,24 +134,20 @@ def mkcert(**kwargs):
129
134
  @click.option('--detached', is_flag=True, help='Use isolated and non-persistent context directory.')
130
135
  @click.option('--env', multiple=True, help='Specify an environment variable.')
131
136
  @click.option('--hostname', callback=validate_hostname, help='Service hostname.')
137
+ @click.option('--local', is_flag=True, help='Specifies upstream service is local. Overrides `--upstream-hostname` option.')
132
138
  @click.option('--port', type=click.IntRange(1, 65535), help='Service port.')
133
139
  @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
+ @click.option('--proxy-mode', type=click.Choice(['regular', 'reverse']), help='Proxy mode can be regular or reverse.')
140
141
  @click.option('--quiet', is_flag=True, help='Disable log output.')
141
142
  @click.option('--scheme', type=click.Choice(['http', 'https']), help='Defaults to https if hostname is set.')
143
+ @click.option('--upstream-hostname', callback=validate_hostname, help='Upstream service hostname.')
144
+ @click.option('--upstream-port', type=click.IntRange(1, 65535), help='Upstream service port.')
145
+ @click.option('--upstream-scheme', type=click.Choice(['http', 'https']), help='Upstream service scheme.')
142
146
  @click.option('--workflow', multiple=True, type=click.Choice([WORKFLOW_MOCK_TYPE, WORKFLOW_RECORD_TYPE, WORKFLOW_TEST_TYPE]), help='Include pre-defined workflows.')
143
147
  @click.argument('service_name', callback=validate_service_name)
144
148
  def create(**kwargs):
145
149
  __validate_app_dir(kwargs['app_dir_path'])
146
150
 
147
- if kwargs.get("proxy_mode"):
148
- __validate_proxy_mode(kwargs.get("proxy_mode"))
149
-
150
151
  app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
151
152
  service = Service(kwargs['service_name'], app)
152
153
 
@@ -164,15 +165,17 @@ def create(**kwargs):
164
165
  @click.option('--select', multiple=True, help='Select column(s) to display.')
165
166
  @click.option('--service', multiple=True, help='Select specific services.')
166
167
  @click.option('--without-headers', is_flag=True, default=False, help='Disable printing column headers.')
168
+ @click.option('--all', is_flag=True, default=False, help='Display all services including core and user defined services')
167
169
  @click.option('--workflow', multiple=True, help='Specify workflow(s) to filter the services by. Defaults to all.')
168
170
  def _list(**kwargs):
169
171
  app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
170
172
  __validate_app(app)
171
173
 
172
- services = __get_services(app, service=kwargs['service'], workflow=kwargs['workflow'])
174
+ without_core = not kwargs['all']
175
+ services = __get_services(app, service=kwargs['service'], without_core=without_core, workflow=kwargs['workflow'])
173
176
 
174
177
  rows = []
175
- for service_name in services:
178
+ for service_name in services:
176
179
  service = Service(service_name, app)
177
180
  __validate_service_dir(service.dir_path)
178
181
 
@@ -184,6 +187,22 @@ def _list(**kwargs):
184
187
 
185
188
  print_services(rows, **select_print_options(kwargs))
186
189
 
190
+ @service.command(
191
+ help="Show information about a service",
192
+ )
193
+ @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
194
+ @click.option('--format', type=click.Choice(FORMATS), help='Format output.')
195
+ @click.option('--without-headers', is_flag=True, default=False, help='Disable printing column headers.')
196
+ @click.argument('service_name')
197
+ @click.pass_context
198
+ def show(ctx, **kwargs):
199
+ service_name = kwargs['service_name']
200
+ del kwargs['service_name']
201
+ kwargs['service'] = [service_name]
202
+
203
+ # Invoke list with 1 service
204
+ ctx.invoke(_list, **kwargs)
205
+
187
206
  @service.command(
188
207
  help="Delete a service",
189
208
  )
@@ -207,16 +226,15 @@ def delete(**kwargs):
207
226
  )
208
227
  @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
209
228
  @click.option('--hostname', callback=validate_hostname, help='Service hostname.')
229
+ @click.option('--local', is_flag=True, help='Specifies upstream service is local. Overrides `--upstream-hostname` option.')
230
+ @click.option('--name', callback=validate_service_name, type=click.STRING, help='New name of the service to update to.')
210
231
  @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.')
232
+ @click.option('--priority', type=click.FloatRange(1.0, 9.0), help='Determines the service run order. Lower values run first.')
233
+ @click.option('--proxy-mode', type=click.Choice(['regular', 'reverse']), help='Proxy mode can be regular or reverse.')
212
234
  @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
- ''')
235
+ @click.option('--upstream-hostname', callback=validate_hostname, help='Upstream service hostname.')
236
+ @click.option('--upstream-port', type=click.IntRange(1, 65535), help='Upstream service port.')
237
+ @click.option('--upstream-scheme', type=click.Choice(['http', 'https']), help='Upstream service scheme.')
220
238
  @click.argument('service_name')
221
239
  def update(**kwargs):
222
240
  app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
@@ -228,29 +246,32 @@ def update(**kwargs):
228
246
 
229
247
  service_config = ServiceConfig(service.dir_path)
230
248
 
231
- if kwargs['hostname']:
232
- old_hostname = service_config.hostname
233
-
234
- if old_hostname != kwargs['hostname']:
235
- service_config.hostname = kwargs['hostname']
249
+ service_config.local = kwargs['local']
236
250
 
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)
251
+ if kwargs['hostname']:
252
+ service_config.hostname = kwargs['hostname']
241
253
 
242
- if old_hostname == parsed_origin_url.hostname:
243
- service_config.proxy_mode = service_config.proxy_mode.replace(old_hostname, service_config.hostname)
254
+ if kwargs['port']:
255
+ service_config.port = kwargs['port']
244
256
 
245
257
  if kwargs['priority']:
246
258
  service_config.priority = kwargs['priority']
247
259
 
248
- if kwargs['port']:
249
- service_config.port = kwargs['port']
260
+ if kwargs['proxy_mode']:
261
+ service_config.proxy_mode = kwargs['proxy_mode']
250
262
 
251
263
  if kwargs['scheme']:
252
264
  service_config.scheme = kwargs['scheme']
253
265
 
266
+ if kwargs['upstream_hostname']:
267
+ service_config.upstream_hostname = kwargs['upstream_hostname']
268
+
269
+ if kwargs['upstream_port']:
270
+ service_config.upstream_port = kwargs['upstream_port']
271
+
272
+ if kwargs['upstream_scheme']:
273
+ service_config.upstream_scheme = kwargs['upstream_scheme']
274
+
254
275
  if kwargs['name']:
255
276
  old_service_name = service.service_name
256
277
  new_service_name = kwargs['name']
@@ -264,10 +285,6 @@ def update(**kwargs):
264
285
 
265
286
  print(f"Successfully renamed service to: {new_service_name}")
266
287
 
267
- if kwargs['proxy_mode']:
268
- __validate_proxy_mode(kwargs['proxy_mode'])
269
- service_config.proxy_mode = kwargs['proxy_mode']
270
-
271
288
  service_config.write()
272
289
 
273
290
  @workflow.command(
@@ -314,7 +331,7 @@ def copy(**kwargs):
314
331
  config = { **kwargs }
315
332
  del config['service']
316
333
  config['service_name'] = service_name
317
-
334
+
318
335
  command = WorkflowCopyCommand(app, **config)
319
336
 
320
337
  if not command.app_dir_exists:
@@ -384,7 +401,7 @@ def down(**kwargs):
384
401
  )
385
402
  if not exec_command:
386
403
  continue
387
-
404
+
388
405
  print(exec_command, file=script)
389
406
 
390
407
  # After services are stopped, their network needs to be removed
@@ -448,7 +465,7 @@ def logs(**kwargs):
448
465
  continue
449
466
 
450
467
  config = { **kwargs }
451
- config['service_name'] = service
468
+ config['service_name'] = service
452
469
  command = WorkflowLogCommand(app, **config)
453
470
  commands.append(command)
454
471
 
@@ -585,7 +602,7 @@ def validate(**kwargs):
585
602
  __validate_app(app)
586
603
 
587
604
  workflow = Workflow(kwargs['workflow_name'], app)
588
-
605
+
589
606
  config = { **kwargs }
590
607
  config['service_name'] = 'build'
591
608
 
@@ -625,7 +642,7 @@ def install(**kwargs):
625
642
  )
626
643
 
627
644
  hostnames = []
628
- for service_name in services:
645
+ for service_name in services:
629
646
  service = Service(service_name, app)
630
647
  __validate_service_dir(service.dir_path)
631
648
 
@@ -657,7 +674,7 @@ def uninstall(**kwargs):
657
674
  )
658
675
 
659
676
  hostnames = []
660
- for service_name in services:
677
+ for service_name in services:
661
678
  service = Service(service_name, app)
662
679
  __validate_service_dir(service.dir_path)
663
680
 
@@ -734,7 +751,7 @@ def __get_services(app: App, **kwargs):
734
751
 
735
752
  services = list(set(selected_services))
736
753
  services.sort()
737
-
754
+
738
755
  return services
739
756
 
740
757
  def __print_header(text: str):
@@ -771,7 +788,7 @@ def __services_mkcert(app: App, services):
771
788
  continue
772
789
 
773
790
  hostname = service_config.hostname
774
-
791
+
775
792
  if not hostname:
776
793
  continue
777
794
 
@@ -786,8 +803,11 @@ def __validate_app(app: App):
786
803
  sys.exit(1)
787
804
 
788
805
  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)
806
+ __validate_dir(app_dir_path)
807
+
808
+ def __validate_dir(dir_path):
809
+ if not os.path.exists(dir_path):
810
+ print(f"Error: {dir_path} does not exist", file=sys.stderr)
791
811
  sys.exit(1)
792
812
 
793
813
  def __validate_service_dir(service_dir_path):
@@ -795,35 +815,6 @@ def __validate_service_dir(service_dir_path):
795
815
  print(f"Error: '{service_dir_path}' does not exist, please scaffold this service", file=sys.stderr)
796
816
  sys.exit(1)
797
817
 
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
818
  def __with_namespace_defaults(kwargs):
828
819
  if not kwargs.get('namespace'):
829
820
  kwargs['namespace'] = kwargs.get('workflow_name')
@@ -842,4 +833,4 @@ def __workflow_create(app, **kwargs):
842
833
  def __with_workflow_namespace(app: App, namespace: str):
843
834
  workflow_namespace = WorkflowNamespace(app, namespace)
844
835
  workflow_namespace.copy_dotenv()
845
- return workflow_namespace
836
+ 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
@@ -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)
@@ -80,7 +80,7 @@ def upload_request(
80
80
  def upload_staged_request(
81
81
  request: Request, request_model: RequestModel, project_key: str, scenario_key: str = None
82
82
  ):
83
- Logger.instance(LOG_ID).info(f"{bcolors.OKCYAN}Recording{bcolors.ENDC} {request.url}")
83
+ Logger.instance(LOG_ID).info(f"{bcolors.OKBLUE}Recording{bcolors.ENDC} {request.url}")
84
84
 
85
85
  response = request.response
86
86
 
@@ -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