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.
- stoobly_agent/__init__.py +1 -1
- stoobly_agent/app/api/__init__.py +4 -20
- 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 +13 -0
- stoobly_agent/app/cli/scaffold/docker/constants.py +4 -6
- stoobly_agent/app/cli/scaffold/docker/service/builder.py +19 -4
- stoobly_agent/app/cli/scaffold/docker/workflow/builder.py +0 -18
- stoobly_agent/app/cli/scaffold/docker/workflow/command_decorator.py +24 -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 +144 -21
- 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/templates/app/.Dockerfile.context +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/docker-compose.yml +16 -6
- stoobly_agent/app/cli/scaffold/templates/app/build/test/docker-compose.yml +16 -6
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/docker-compose.yml +16 -10
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/docker-compose.yml +16 -10
- 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/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/docker-compose.yml +17 -0
- stoobly_agent/app/cli/scaffold/templates/workflow/record/docker-compose.yml +17 -0
- 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 +68 -77
- 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 +11 -7
- stoobly_agent/app/proxy/record/upload_request_service.py +2 -2
- 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 +5 -4
- stoobly_agent/app/proxy/utils/strategy.py +16 -0
- stoobly_agent/app/settings/__init__.py +9 -3
- 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/settings.yml.sample +2 -3
- stoobly_agent/lib/logger.py +15 -5
- 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/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.0.dist-info}/METADATA +2 -1
- {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.0.dist-info}/RECORD +74 -58
- {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.0.dist-info}/LICENSE +0 -0
- {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.0.dist-info}/WHEEL +0 -0
- {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}
|
@@ -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
|
-
|
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',
|
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('--
|
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
|
-
''')
|
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
|
-
|
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
|
-
|
238
|
-
|
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
|
-
|
243
|
-
|
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['
|
249
|
-
service_config.
|
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
|
-
|
790
|
-
|
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
|
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
|
@@ -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)
|
@@ -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.
|
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
|
|