stoobly-agent 1.4.1__py3-none-any.whl → 1.5.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/cli/helpers/handle_mock_service.py +6 -2
- stoobly_agent/app/cli/helpers/request_facade.py +5 -1
- stoobly_agent/app/cli/scaffold/constants.py +1 -1
- stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +3 -2
- stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py +2 -2
- stoobly_agent/app/cli/scaffold/service_config.py +16 -2
- stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py +19 -19
- stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +1 -1
- stoobly_agent/app/cli/scaffold/templates/app/.Makefile +12 -5
- stoobly_agent/app/cli/scaffold/templates/constants.py +3 -3
- stoobly_agent/app/cli/scaffold/templates/factory.py +5 -5
- stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/configure +1 -8
- stoobly_agent/app/cli/scaffold/templates/workflow/mock/fixtures.yml +1 -1
- stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/configure +1 -8
- stoobly_agent/app/cli/scaffold/templates/workflow/test/fixtures.yml +1 -1
- stoobly_agent/app/cli/scaffold/workflow_command.py +3 -3
- stoobly_agent/app/cli/scaffold/workflow_create_command.py +2 -2
- stoobly_agent/app/cli/scaffold_cli.py +65 -50
- stoobly_agent/app/proxy/context.py +4 -0
- stoobly_agent/app/proxy/handle_mock_service.py +81 -54
- stoobly_agent/app/proxy/handle_record_service.py +15 -3
- stoobly_agent/app/proxy/handle_replay_service.py +44 -18
- stoobly_agent/app/proxy/handle_test_service.py +75 -16
- stoobly_agent/app/proxy/intercept_handler.py +11 -16
- stoobly_agent/app/proxy/intercept_settings.py +17 -4
- stoobly_agent/app/proxy/mitmproxy/request_facade.py +5 -2
- stoobly_agent/app/proxy/mitmproxy/response_facade.py +5 -4
- stoobly_agent/app/proxy/mock/eval_fixtures_service.py +78 -14
- stoobly_agent/app/proxy/mock/eval_request_service.py +2 -2
- stoobly_agent/app/proxy/record/join_request_service.py +7 -8
- stoobly_agent/app/proxy/record/upload_request_service.py +2 -2
- stoobly_agent/app/proxy/replay/replay_request_service.py +4 -4
- stoobly_agent/app/proxy/test/helpers/upload_test_service.py +2 -2
- stoobly_agent/app/proxy/utils/allowed_request_service.py +3 -3
- stoobly_agent/app/proxy/utils/response_handler.py +0 -2
- stoobly_agent/app/proxy/utils/rewrite.py +72 -0
- stoobly_agent/app/settings/constants/request_component.py +4 -1
- stoobly_agent/cli.py +35 -28
- stoobly_agent/config/constants/intercept_policy.py +2 -0
- stoobly_agent/config/constants/mock_policy.py +4 -2
- stoobly_agent/config/constants/record_policy.py +4 -2
- stoobly_agent/config/constants/replay_policy.py +4 -2
- stoobly_agent/public/{18-es2015.583f191cc7ad512ee262.js → 18-es2015.503207073756a9c8211a.js} +1 -1
- stoobly_agent/public/{18-es5.583f191cc7ad512ee262.js → 18-es5.503207073756a9c8211a.js} +1 -1
- stoobly_agent/public/index.html +1 -1
- stoobly_agent/public/{main-es2015.2cc16523aa3fcaba51e5.js → main-es2015.d682619f3d6d53d64c6a.js} +1 -1
- stoobly_agent/public/{main-es5.2cc16523aa3fcaba51e5.js → main-es5.d682619f3d6d53d64c6a.js} +1 -1
- stoobly_agent/public/{runtime-es2015.b914470164e4d6e75d96.js → runtime-es2015.8c1efed946fc02c923fc.js} +1 -1
- stoobly_agent/public/{runtime-es5.b914470164e4d6e75d96.js → runtime-es5.8c1efed946fc02c923fc.js} +1 -1
- stoobly_agent/test/app/cli/helpers/openapi_endpoint_adapter_test.py +2 -1
- stoobly_agent/test/app/cli/scaffold/e2e_test.py +2 -2
- stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
- stoobly_agent/test/app/proxy/mock/eval_fixtures_service_test.py +140 -71
- stoobly_agent/test/cli/lifecycle_hooks_test.py +66 -0
- stoobly_agent/test/cli/mock_test.py +53 -29
- stoobly_agent/test/cli/record_test.py +67 -0
- stoobly_agent/test/mock_data/lifecycle_hooks.py +35 -0
- {stoobly_agent-1.4.1.dist-info → stoobly_agent-1.5.0.dist-info}/LICENSE +1 -1
- {stoobly_agent-1.4.1.dist-info → stoobly_agent-1.5.0.dist-info}/METADATA +7 -12
- {stoobly_agent-1.4.1.dist-info → stoobly_agent-1.5.0.dist-info}/RECORD +65 -61
- /stoobly_agent/app/cli/scaffold/templates/workflow/mock/{fixtures/.keep → public/.gitignore} +0 -0
- /stoobly_agent/app/cli/scaffold/templates/workflow/test/{fixtures/.keep → public/.gitignore} +0 -0
- {stoobly_agent-1.4.1.dist-info → stoobly_agent-1.5.0.dist-info}/WHEEL +0 -0
- {stoobly_agent-1.4.1.dist-info → stoobly_agent-1.5.0.dist-info}/entry_points.txt +0 -0
stoobly_agent/__init__.py
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
COMMAND = 'stoobly-agent'
|
2
|
-
VERSION = '1.
|
2
|
+
VERSION = '1.5.0'
|
@@ -6,9 +6,13 @@ from stoobly_agent.app.proxy.record.response_string import ResponseString
|
|
6
6
|
|
7
7
|
RAW_FORMAT = 'raw'
|
8
8
|
|
9
|
-
def print_raw_response(response: requests.Response):
|
9
|
+
def print_raw_response(response: requests.Response, file_path = None):
|
10
10
|
mitmproxy_response = PythonResponseAdapterFactory(response).mitmproxy_response()
|
11
11
|
facade = MitmproxyResponseFacade(mitmproxy_response)
|
12
12
|
response_string = ResponseString(facade, None)
|
13
13
|
|
14
|
-
|
14
|
+
if not file_path:
|
15
|
+
print(response_string.get().decode(), end="")
|
16
|
+
else:
|
17
|
+
with open(file_path, 'w') as fp:
|
18
|
+
fp.write(response_string.get().decode())
|
@@ -53,10 +53,14 @@ class RequestFacade(ReplayFacade):
|
|
53
53
|
def replay(self, request_key: str, cli_options: ReplayCliOptions):
|
54
54
|
replay_context = self.__build_replay_context(request_key)
|
55
55
|
replay_options = {
|
56
|
-
'mode': mode.
|
56
|
+
'mode': mode.REPLAY,
|
57
57
|
**self.__common_replay_options(request_key),
|
58
58
|
**self.common_replay_cli_options(cli_options)
|
59
59
|
}
|
60
|
+
|
61
|
+
if cli_options.get('record'):
|
62
|
+
replay_options['response_mode'] = mode.RECORD
|
63
|
+
|
60
64
|
trace_context = replay_options.get('trace_context')
|
61
65
|
|
62
66
|
return self.__replay(replay_context, trace_context, replay_options)
|
@@ -14,8 +14,8 @@ CONFIG_FILE = '.config.yml'
|
|
14
14
|
CONTEXT_DIR_ENV = 'CONTEXT_DIR'
|
15
15
|
DOCKER_NAMESPACE = 'docker'
|
16
16
|
ENV_FILE = '.env'
|
17
|
-
FIXTURES_FOLDER_NAME = 'fixtures'
|
18
17
|
NAMESERVERS_FILE = '.nameservers'
|
18
|
+
PUBLIC_FOLDER_NAME = 'public'
|
19
19
|
SERVICE_DETACHED = '${SERVICE_DETACHED}'
|
20
20
|
SERVICE_DETACHED_ENV = 'SERVICE_DETACHED'
|
21
21
|
SERVICE_HOSTNAME = '${SERVICE_HOSTNAME}'
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import os
|
2
2
|
import pdb
|
3
3
|
|
4
|
-
from ...constants import SERVICE_HOSTNAME, SERVICE_PORT, STOOBLY_CERTS_DIR
|
4
|
+
from ...constants import SERVICE_HOSTNAME, SERVICE_PORT, SERVICE_PROXY_MODE, STOOBLY_CERTS_DIR
|
5
5
|
from .builder import WorkflowBuilder
|
6
6
|
|
7
7
|
class MockDecorator():
|
@@ -24,8 +24,9 @@ class MockDecorator():
|
|
24
24
|
'--headless',
|
25
25
|
'--intercept',
|
26
26
|
'--lifecycle-hooks-path', 'lifecycle_hooks.py',
|
27
|
-
'--proxy-mode',
|
27
|
+
'--proxy-mode', SERVICE_PROXY_MODE,
|
28
28
|
'--proxy-port', f"{SERVICE_PORT}",
|
29
|
+
'--public-directory-path', 'public',
|
29
30
|
'--response-fixtures-path', 'fixtures.yml',
|
30
31
|
'--ssl-insecure'
|
31
32
|
]
|
@@ -3,7 +3,7 @@ import pdb
|
|
3
3
|
|
4
4
|
from urllib.parse import urlparse
|
5
5
|
|
6
|
-
from ...constants import SERVICE_HOSTNAME, SERVICE_PORT, STOOBLY_CERTS_DIR
|
6
|
+
from ...constants import SERVICE_HOSTNAME, SERVICE_PORT, SERVICE_PROXY_MODE, STOOBLY_CERTS_DIR
|
7
7
|
from .builder import WorkflowBuilder
|
8
8
|
|
9
9
|
class ReverseProxyDecorator():
|
@@ -25,7 +25,7 @@ class ReverseProxyDecorator():
|
|
25
25
|
command = [
|
26
26
|
'--headless',
|
27
27
|
'--lifecycle-hooks-path', 'lifecycle_hooks.py',
|
28
|
-
'--proxy-mode',
|
28
|
+
'--proxy-mode', SERVICE_PROXY_MODE,
|
29
29
|
'--proxy-port', f"{SERVICE_PORT}",
|
30
30
|
'--ssl-insecure'
|
31
31
|
]
|
@@ -92,7 +92,21 @@ class ServiceConfig(Config):
|
|
92
92
|
if self.__proxy_mode:
|
93
93
|
return (self.__proxy_mode or '').strip()
|
94
94
|
|
95
|
-
|
95
|
+
if not self.hostname:
|
96
|
+
return ''
|
97
|
+
|
98
|
+
_proxy_mode = f"reverse:{self.scheme}://{self.hostname}"
|
99
|
+
|
100
|
+
if not self.port:
|
101
|
+
return _proxy_mode
|
102
|
+
|
103
|
+
if self.scheme == 'http' and self.port == '80':
|
104
|
+
return _proxy_mode
|
105
|
+
|
106
|
+
if self.scheme == 'https' and self.port == '443':
|
107
|
+
return _proxy_mode
|
108
|
+
|
109
|
+
return f"{_proxy_mode}:{self.port}"
|
96
110
|
|
97
111
|
@proxy_mode.setter
|
98
112
|
def proxy_mode(self, v):
|
@@ -123,7 +137,7 @@ class ServiceConfig(Config):
|
|
123
137
|
'port': self.port,
|
124
138
|
'priority': self.priority,
|
125
139
|
'proxy_mode': self.proxy_mode,
|
126
|
-
'scheme': self.scheme,
|
140
|
+
'scheme': self.scheme if self.hostname else '',
|
127
141
|
}
|
128
142
|
|
129
143
|
def write(self):
|
@@ -10,7 +10,7 @@ import yaml
|
|
10
10
|
from docker.models.containers import Container
|
11
11
|
|
12
12
|
from stoobly_agent.app.cli.scaffold.constants import (
|
13
|
-
|
13
|
+
PUBLIC_FOLDER_NAME,
|
14
14
|
STOOBLY_DATA_DIR,
|
15
15
|
VIRTUAL_HOST_ENV,
|
16
16
|
VIRTUAL_PORT_ENV,
|
@@ -40,8 +40,8 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
|
|
40
40
|
)
|
41
41
|
|
42
42
|
@property
|
43
|
-
def
|
44
|
-
return os.path.join(self.workflow_path,
|
43
|
+
def public_dir_path(self):
|
44
|
+
return os.path.join(self.workflow_path, PUBLIC_FOLDER_NAME)
|
45
45
|
|
46
46
|
@property
|
47
47
|
def workflow_path(self):
|
@@ -124,14 +124,14 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
|
|
124
124
|
if ('200 OK' not in logs) and ('499' not in logs):
|
125
125
|
raise ScaffoldValidateException(f"Error reaching {url} from inside Docker network")
|
126
126
|
|
127
|
-
# Check
|
128
|
-
def
|
127
|
+
# Check public folder exists in container
|
128
|
+
def validate_public_folder(self, container: Container):
|
129
129
|
|
130
130
|
if self.workflow_name == WORKFLOW_RECORD_TYPE:
|
131
|
-
print(f"Skipping validating
|
131
|
+
print(f"Skipping validating public folder in workflow: {self.workflow_name}, container: {container.name}")
|
132
132
|
return
|
133
133
|
|
134
|
-
print(f"Validating
|
134
|
+
print(f"Validating public folder in container: {container.name}")
|
135
135
|
|
136
136
|
data_dir_mounted = False
|
137
137
|
volume_mounts = container.attrs['Mounts']
|
@@ -145,21 +145,21 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
|
|
145
145
|
|
146
146
|
# Only the running proxy containers will be checkable
|
147
147
|
if container.status == 'exited':
|
148
|
-
print(f"Skipping validating
|
148
|
+
print(f"Skipping validating public folder contents because container is exited: {container.name}")
|
149
149
|
return
|
150
150
|
|
151
|
-
# Check contents of
|
152
|
-
|
153
|
-
exec_result = container.exec_run(f"ls -A {
|
151
|
+
# Check contents of public folder to confirm it's shared
|
152
|
+
public_folder_path = f"{PUBLIC_FOLDER_NAME}"
|
153
|
+
exec_result = container.exec_run(f"ls -A {public_folder_path}")
|
154
154
|
output = exec_result.output
|
155
155
|
|
156
|
-
|
157
|
-
if
|
158
|
-
|
159
|
-
|
156
|
+
public_folder_contents_container = output.decode('ascii').split('\n')
|
157
|
+
if public_folder_contents_container[-1] == '':
|
158
|
+
public_folder_contents_container.pop()
|
159
|
+
public_folder_contents_scaffold = os.listdir(self.public_dir_path)
|
160
160
|
|
161
|
-
if Counter(
|
162
|
-
raise ScaffoldValidateException(f"
|
161
|
+
if Counter(public_folder_contents_container) != Counter(public_folder_contents_scaffold):
|
162
|
+
raise ScaffoldValidateException(f"public folder was not mounted properly, expected {self.public_dir_path} to exist in container path {public_folder_path}")
|
163
163
|
|
164
164
|
# Note: might not need this if the hostname is reachable and working
|
165
165
|
def proxy_environment_variables_exist(self, container: Container) -> None:
|
@@ -191,7 +191,7 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
|
|
191
191
|
raise ScaffoldValidateException(f"Container attributes are missing for: {container.name}")
|
192
192
|
|
193
193
|
if not self.service_config.detached:
|
194
|
-
self.
|
194
|
+
self.validate_public_folder(service_proxy_container)
|
195
195
|
|
196
196
|
self.proxy_environment_variables_exist(service_proxy_container)
|
197
197
|
|
@@ -215,7 +215,7 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
|
|
215
215
|
|
216
216
|
# Service init containers have a mounted dist folder unlike the core init container
|
217
217
|
init_container = self.docker_client.containers.get(self.service_docker_compose.init_container_name)
|
218
|
-
self.
|
218
|
+
self.validate_public_folder(init_container)
|
219
219
|
|
220
220
|
if self.service_config.hostname:
|
221
221
|
service_proxy_container = self.docker_client.containers.get(self.service_docker_compose.proxy_container_name)
|
@@ -61,8 +61,15 @@ stoobly_exec_run_env=$(source_env) && $(exec_env) && export CONTEXT_DIR="$(app_d
|
|
61
61
|
workflow_run=$(source_env) && bash "$(workflow_run_script)"
|
62
62
|
|
63
63
|
ca-cert/install: stoobly/install
|
64
|
-
@
|
65
|
-
|
64
|
+
@if [ ! -d "$$HOME/.mitmproxy" ]; then \
|
65
|
+
read -p "Installing CA certificate is required for $(WORKFLOW)ing requests, continue? (y/N) " confirm && \
|
66
|
+
if [ "$$confirm" = "y" ] || [ "$$confirm" = "Y" ]; then \
|
67
|
+
echo "Running stoobly-agent ca-cert install..."; \
|
68
|
+
stoobly-agent ca-cert install; \
|
69
|
+
else \
|
70
|
+
echo "You can install the CA certificate later by running: stoobly-agent ca-cert install"; \
|
71
|
+
fi \
|
72
|
+
fi
|
66
73
|
certs:
|
67
74
|
@export EXEC_COMMAND=bin/.mkcert && \
|
68
75
|
$(stoobly_exec)
|
@@ -89,7 +96,7 @@ intercept/enable:
|
|
89
96
|
@export EXEC_COMMAND=bin/.enable && \
|
90
97
|
export EXEC_ARGS=$(scenario_key) && \
|
91
98
|
$(stoobly_exec)
|
92
|
-
mock: workflow/mock workflow/hostname/install nameservers workflow/up
|
99
|
+
mock: workflow/mock ca-cert/install workflow/hostname/install nameservers workflow/up
|
93
100
|
mock/services: workflow/mock workflow/services
|
94
101
|
mock/logs: workflow/mock workflow/logs
|
95
102
|
mock/down: workflow/mock workflow/down workflow/hostname/uninstall
|
@@ -103,7 +110,7 @@ python/validate:
|
|
103
110
|
echo "Error: Python 3.10, 3.11, or 3.12 is required."; \
|
104
111
|
exit 1; \
|
105
112
|
fi
|
106
|
-
record: workflow/record workflow/hostname/install nameservers workflow/up
|
113
|
+
record: workflow/record ca-cert/install workflow/hostname/install nameservers workflow/up
|
107
114
|
record/down: workflow/record workflow/down workflow/hostname/uninstall
|
108
115
|
record/services: workflow/record workflow/services
|
109
116
|
record/logs: workflow/record workflow/logs
|
@@ -139,7 +146,7 @@ scenario/snapshot:
|
|
139
146
|
stoobly/install: python/validate pipx/install
|
140
147
|
@if ! pipx list 2> /dev/null | grep -q 'stoobly-agent'; then \
|
141
148
|
echo "stoobly-agent not found. Installing..."; \
|
142
|
-
pipx install stoobly-agent; \
|
149
|
+
pipx install stoobly-agent || { echo "Failed to install stoobly-agent"; exit 1; }; \
|
143
150
|
fi
|
144
151
|
test: workflow/test workflow/up
|
145
152
|
test/services: workflow/test workflow/services
|
@@ -18,12 +18,12 @@ CUSTOM_FIXTURES = 'fixtures.yml'
|
|
18
18
|
CUSTOM_LIFECYCLE_HOOKS = os.path.join('lifecycle_hooks.py')
|
19
19
|
MAINTAINED_CONFIGURE = os.path.join('bin', '.configure')
|
20
20
|
MAINTAINED_INIT = os.path.join('bin', '.init')
|
21
|
-
|
21
|
+
MAINTAINED_PUBLIC = os.path.join('public', '.gitignore')
|
22
22
|
|
23
23
|
MOCK_WORKFLOW_MAINTAINED_FILES = [
|
24
24
|
MAINTAINED_CONFIGURE,
|
25
25
|
MAINTAINED_INIT,
|
26
|
-
|
26
|
+
MAINTAINED_PUBLIC
|
27
27
|
]
|
28
28
|
|
29
29
|
MOCK_WORKFLOW_CUSTOM_FILES = [
|
@@ -49,7 +49,7 @@ RECORD_WORKFLOW_CUSTOM_FILES = [
|
|
49
49
|
TEST_WORKFLOW_MAINTAINED_FILES = [
|
50
50
|
MAINTAINED_CONFIGURE,
|
51
51
|
MAINTAINED_INIT,
|
52
|
-
|
52
|
+
MAINTAINED_PUBLIC
|
53
53
|
]
|
54
54
|
|
55
55
|
TEST_WORKFLOW_CUSTOM_FILES = [
|
@@ -1,7 +1,7 @@
|
|
1
1
|
from ..constants import WORKFLOW_MOCK_TYPE, WORKFLOW_RECORD_TYPE, WORKFLOW_TEST_TYPE
|
2
2
|
from ..docker.workflow.builder import WorkflowBuilder
|
3
3
|
from .constants import (
|
4
|
-
CUSTOM_CONFIGURE, CUSTOM_INIT, MAINTAINED_CONFIGURE,
|
4
|
+
CUSTOM_CONFIGURE, CUSTOM_INIT, MAINTAINED_CONFIGURE, MAINTAINED_PUBLIC, MOCK_WORKFLOW_CUSTOM_FILES, MOCK_WORKFLOW_MAINTAINED_FILES, RECORD_WORKFLOW_CUSTOM_FILES, RECORD_WORKFLOW_MAINTAINED_FILES, TEST_WORKFLOW_CUSTOM_FILES, TEST_WORKFLOW_MAINTAINED_FILES
|
5
5
|
)
|
6
6
|
|
7
7
|
def custom_files(workflow: str, workflow_builder: WorkflowBuilder):
|
@@ -21,8 +21,8 @@ def custom_files(workflow: str, workflow_builder: WorkflowBuilder):
|
|
21
21
|
|
22
22
|
# Fixtures are only relevant if the workflow is mock/test and if the service has a hostname
|
23
23
|
if not workflow_builder.config.hostname:
|
24
|
-
if
|
25
|
-
files.remove(
|
24
|
+
if MAINTAINED_PUBLIC in files:
|
25
|
+
files.remove(MAINTAINED_PUBLIC)
|
26
26
|
|
27
27
|
return files
|
28
28
|
|
@@ -40,7 +40,7 @@ def maintained_files(workflow: str, workflow_builder: WorkflowBuilder):
|
|
40
40
|
files.append(MAINTAINED_CONFIGURE)
|
41
41
|
|
42
42
|
if not workflow_builder.config.hostname:
|
43
|
-
if
|
44
|
-
files.remove(
|
43
|
+
if MAINTAINED_PUBLIC in files:
|
44
|
+
files.remove(MAINTAINED_PUBLIC)
|
45
45
|
|
46
46
|
return files
|
@@ -10,11 +10,4 @@ url="$scheme://$hostname"
|
|
10
10
|
|
11
11
|
if [ "$scheme" = 'http' -a "$port" != '80' ] || [ "$scheme" = 'https' -a "$port" != '443' ]; then
|
12
12
|
url="$url:$port"
|
13
|
-
fi
|
14
|
-
|
15
|
-
# Match Rules
|
16
|
-
echo "Configuring match rules"
|
17
|
-
stoobly-agent config match set \
|
18
|
-
--method GET --method POST --method OPTIONS --method PUT --method DELETE \
|
19
|
-
--mode mock \
|
20
|
-
--pattern ".*?"
|
13
|
+
fi
|
@@ -1,5 +1,5 @@
|
|
1
1
|
# The following example matches requests for GET /users/d+ (e.g. /users/1) with the contents of user-1.json
|
2
|
-
# Assumes that 'user-1.json' is created in
|
2
|
+
# Assumes that 'user-1.json' is created in a 'fixtures' folder in the same directory as this file
|
3
3
|
#GET:
|
4
4
|
# /users/d+:
|
5
5
|
# path: ./fixtures/user-1.json
|
@@ -10,11 +10,4 @@ url="$scheme://$hostname"
|
|
10
10
|
|
11
11
|
if [ "$scheme" = 'http' -a "$port" != '80' ] || [ "$scheme" = 'https' -a "$port" != '443' ]; then
|
12
12
|
url="$url:$port"
|
13
|
-
fi
|
14
|
-
|
15
|
-
# Match
|
16
|
-
echo "Configuring match rules"
|
17
|
-
stoobly-agent config match set \
|
18
|
-
--method GET --method POST --method OPTIONS --method PUT --method DELETE \
|
19
|
-
--mode mock \
|
20
|
-
--pattern ".*?"
|
13
|
+
fi
|
@@ -1,5 +1,5 @@
|
|
1
1
|
# The following example matches requests for GET /users/d+ (e.g. /users/1) with the contents of user-1.json
|
2
|
-
# Assumes that 'user-1.json' is created in
|
2
|
+
# Assumes that 'user-1.json' is created in a 'fixtures' folder in the same directory as this file
|
3
3
|
#GET:
|
4
4
|
# /users/d+:
|
5
5
|
# path: ./fixtures/user-1.json
|
@@ -6,7 +6,7 @@ from stoobly_agent.lib.logger import Logger
|
|
6
6
|
|
7
7
|
from .app import App
|
8
8
|
from .config import Config
|
9
|
-
from .constants import BIN_FOLDER_NAME, COMPOSE_TEMPLATE, CONFIG_FILE, ENV_FILE,
|
9
|
+
from .constants import BIN_FOLDER_NAME, COMPOSE_TEMPLATE, CONFIG_FILE, ENV_FILE, PUBLIC_FOLDER_NAME
|
10
10
|
from .docker.constants import DOCKER_COMPOSE_CUSTOM
|
11
11
|
from .service_command import ServiceCommand
|
12
12
|
|
@@ -88,8 +88,8 @@ class WorkflowCommand(ServiceCommand):
|
|
88
88
|
return services
|
89
89
|
|
90
90
|
@property
|
91
|
-
def
|
92
|
-
return os.path.join(self.workflow_path,
|
91
|
+
def public_dir_path(self):
|
92
|
+
return os.path.join(self.workflow_path, PUBLIC_FOLDER_NAME)
|
93
93
|
|
94
94
|
@property
|
95
95
|
def workflow_config(self):
|
@@ -67,11 +67,11 @@ class WorkflowCreateCommand(WorkflowCommand):
|
|
67
67
|
return
|
68
68
|
|
69
69
|
# Maintained files are files that will always be overwritten
|
70
|
-
maintained_workflow_files = maintained_files(self.workflow_name, workflow_builder)
|
70
|
+
maintained_workflow_files = maintained_files(template or self.workflow_name, workflow_builder)
|
71
71
|
self.copy_files(templates_path, maintained_workflow_files, self.workflow_path)
|
72
72
|
|
73
73
|
# Custom files are files that may be modified by the user
|
74
|
-
custom_workflow_files = custom_files(self.workflow_name, workflow_builder)
|
74
|
+
custom_workflow_files = custom_files(template or self.workflow_name, workflow_builder)
|
75
75
|
self.copy_files_no_replace(templates_path, custom_workflow_files, self.workflow_path)
|
76
76
|
|
77
77
|
def __write_docker_compose_file(self, **kwargs: BuildOptions):
|
@@ -108,14 +108,19 @@ def create(**kwargs):
|
|
108
108
|
@click.option('--certs-dir-path', help='Path to certs directory. Defaults to the certs dir of the context.')
|
109
109
|
@click.option('--context-dir-path', default=data_dir.context_dir_path, help='Path to Stoobly data directory.')
|
110
110
|
@click.option('--service', multiple=True, help='Select which services to run. Defaults to all.')
|
111
|
+
@click.option('--workflow', multiple=True, help='Specify services by workflow(s). Defaults to all.')
|
111
112
|
def mkcert(**kwargs):
|
112
113
|
app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, **kwargs)
|
113
114
|
__validate_app(app)
|
114
115
|
|
115
|
-
services = __get_services(
|
116
|
+
services = __get_services(
|
117
|
+
app, service=kwargs['service'], without_core=True, workflow=kwargs['workflow']
|
118
|
+
)
|
116
119
|
|
117
120
|
for service_name in services:
|
118
121
|
service = Service(service_name, app)
|
122
|
+
__validate_service_dir(service.dir_path)
|
123
|
+
|
119
124
|
service_config = ServiceConfig(service.dir_path)
|
120
125
|
|
121
126
|
if service_config.scheme != 'https':
|
@@ -135,19 +140,19 @@ def mkcert(**kwargs):
|
|
135
140
|
help="Scaffold a service",
|
136
141
|
)
|
137
142
|
@click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
|
138
|
-
@click.option('--detached', is_flag=True)
|
143
|
+
@click.option('--detached', is_flag=True, help='Use isolated and non-persistent context directory.')
|
139
144
|
@click.option('--env', multiple=True, help='Specify an environment variable.')
|
140
145
|
@click.option('--force', is_flag=True, help='Overwrite maintained scaffolded service files.')
|
141
|
-
@click.option('--hostname')
|
142
|
-
@click.option('--port')
|
143
|
-
@click.option('--priority', default=
|
146
|
+
@click.option('--hostname', help='Service hostname.')
|
147
|
+
@click.option('--port', help='Service port.')
|
148
|
+
@click.option('--priority', default=5, type=click.FloatRange(1.0, 9.0), help='Determines the service run order. Lower values run first.')
|
144
149
|
@click.option('--proxy-mode', help='''
|
145
150
|
Proxy mode can be "regular", "transparent", "socks5",
|
146
151
|
"reverse:SPEC", or "upstream:SPEC". For reverse and
|
147
152
|
upstream proxy modes, SPEC is host specification in
|
148
153
|
the form of "http[s]://host[:port]".
|
149
154
|
''')
|
150
|
-
@click.option('--scheme', type=click.Choice(['http', 'https']))
|
155
|
+
@click.option('--scheme', type=click.Choice(['http', 'https']), help='Defaults to https if hostname is set.')
|
151
156
|
@click.option('--workflow', multiple=True, type=click.Choice([WORKFLOW_MOCK_TYPE, WORKFLOW_RECORD_TYPE, WORKFLOW_TEST_TYPE]), help='Include pre-defined workflows.')
|
152
157
|
@click.argument('service_name')
|
153
158
|
def create(**kwargs):
|
@@ -170,19 +175,23 @@ def create(**kwargs):
|
|
170
175
|
@click.option('--select', multiple=True, help='Select column(s) to display.')
|
171
176
|
@click.option('--service', multiple=True, help='Select specific services.')
|
172
177
|
@click.option('--without-headers', is_flag=True, default=False, help='Disable printing column headers.')
|
173
|
-
@click.option('--workflow', multiple=True, help='Specify workflow(s) to filter the services by.')
|
178
|
+
@click.option('--workflow', multiple=True, help='Specify workflow(s) to filter the services by. Defaults to all.')
|
174
179
|
def _list(**kwargs):
|
175
|
-
__validate_app_dir(kwargs['app_dir_path'])
|
176
|
-
|
177
180
|
app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
|
178
181
|
__validate_app(app)
|
179
182
|
|
183
|
+
services = __get_services(app, service=kwargs['service'], workflow=kwargs['workflow'])
|
184
|
+
|
180
185
|
rows = []
|
181
|
-
for service_name in
|
186
|
+
for service_name in services:
|
182
187
|
service = Service(service_name, app)
|
183
188
|
__validate_service_dir(service.dir_path)
|
189
|
+
|
184
190
|
service_config = ServiceConfig(service.dir_path)
|
185
|
-
rows.append(
|
191
|
+
rows.append({
|
192
|
+
'name': service_name,
|
193
|
+
**service_config.to_dict()
|
194
|
+
})
|
186
195
|
|
187
196
|
print_services(rows, **select_print_options(kwargs))
|
188
197
|
|
@@ -192,9 +201,9 @@ def _list(**kwargs):
|
|
192
201
|
@click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
|
193
202
|
@click.argument('service_name')
|
194
203
|
def delete(**kwargs):
|
195
|
-
__validate_app_dir(kwargs['app_dir_path'])
|
196
|
-
|
197
204
|
app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
|
205
|
+
__validate_app(app)
|
206
|
+
|
198
207
|
service = Service(kwargs['service_name'], app)
|
199
208
|
|
200
209
|
if not os.path.exists(service.dir_path):
|
@@ -211,9 +220,9 @@ def delete(**kwargs):
|
|
211
220
|
@click.option('--priority', help='Determines the service run order.')
|
212
221
|
@click.argument('service_name')
|
213
222
|
def update(**kwargs):
|
214
|
-
__validate_app_dir(kwargs['app_dir_path'])
|
215
|
-
|
216
223
|
app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
|
224
|
+
__validate_app(app)
|
225
|
+
|
217
226
|
service = Service(kwargs['service_name'], app)
|
218
227
|
|
219
228
|
__validate_service_dir(service.dir_path)
|
@@ -304,8 +313,9 @@ def down(**kwargs):
|
|
304
313
|
if kwargs['namespace'] and not kwargs['network']:
|
305
314
|
kwargs['network'] = kwargs['namespace']
|
306
315
|
|
307
|
-
|
308
|
-
|
316
|
+
services = __get_services(
|
317
|
+
app, service=kwargs['service'], workflow=[kwargs['workflow_name']]
|
318
|
+
)
|
309
319
|
|
310
320
|
commands: List[WorkflowRunCommand] = []
|
311
321
|
for service in services:
|
@@ -379,8 +389,9 @@ def logs(**kwargs):
|
|
379
389
|
if len(kwargs['container']) == 0:
|
380
390
|
kwargs['container'] = [WORKFLOW_CONTAINER_PROXY]
|
381
391
|
|
382
|
-
|
383
|
-
|
392
|
+
services = __get_services(
|
393
|
+
app, service=kwargs['service'], without_core=True, workflow=[kwargs['workflow_name']]
|
394
|
+
)
|
384
395
|
|
385
396
|
commands: List[WorkflowLogCommand] = []
|
386
397
|
for service in services:
|
@@ -445,10 +456,12 @@ def up(**kwargs):
|
|
445
456
|
if kwargs['namespace'] and not kwargs['network']:
|
446
457
|
kwargs['network'] = kwargs['namespace']
|
447
458
|
|
448
|
-
|
449
|
-
|
459
|
+
services = __get_services(
|
460
|
+
app, service=kwargs['service'], workflow=[kwargs['workflow_name']]
|
461
|
+
)
|
450
462
|
|
451
463
|
# Gateway ports are dynamically set depending on the workflow run
|
464
|
+
workflow = Workflow(kwargs['workflow_name'], app)
|
452
465
|
set_gateway_ports(workflow.service_paths_from_services(services))
|
453
466
|
|
454
467
|
commands: List[WorkflowRunCommand] = []
|
@@ -541,13 +554,15 @@ def validate(**kwargs):
|
|
541
554
|
help="Update the system hosts file for all scaffold service hostnames"
|
542
555
|
)
|
543
556
|
@click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
|
544
|
-
@click.option('--service', multiple=True, help='Select specific services.')
|
545
|
-
@click.option('--workflow', multiple=True, help='Specify services by workflow(s).')
|
557
|
+
@click.option('--service', multiple=True, help='Select specific services. Defaults to all.')
|
558
|
+
@click.option('--workflow', multiple=True, help='Specify services by workflow(s). Defaults to all.')
|
546
559
|
def install(**kwargs):
|
547
560
|
app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
|
548
561
|
__validate_app(app)
|
549
562
|
|
550
|
-
services =
|
563
|
+
services = __get_services(
|
564
|
+
app, service=kwargs['service'], without_core=True, workflow=kwargs['workflow']
|
565
|
+
)
|
551
566
|
|
552
567
|
hostnames = []
|
553
568
|
for service_name in services:
|
@@ -571,13 +586,15 @@ def install(**kwargs):
|
|
571
586
|
help="Delete from the system hosts file all scaffold service hostnames"
|
572
587
|
)
|
573
588
|
@click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
|
574
|
-
@click.option('--service', multiple=True, help='Select specific services.')
|
575
|
-
@click.option('--workflow', multiple=True, help='Specify services by workflow(s).')
|
589
|
+
@click.option('--service', multiple=True, help='Select specific services. Defaults to all.')
|
590
|
+
@click.option('--workflow', multiple=True, help='Specify services by workflow(s). Defaults to all.')
|
576
591
|
def uninstall(**kwargs):
|
577
592
|
app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
|
578
593
|
__validate_app(app)
|
579
594
|
|
580
|
-
services =
|
595
|
+
services = __get_services(
|
596
|
+
app, service=kwargs['service'], without_core=True, workflow=kwargs['workflow']
|
597
|
+
)
|
581
598
|
|
582
599
|
hostnames = []
|
583
600
|
for service_name in services:
|
@@ -609,39 +626,37 @@ def __elevate_sudo():
|
|
609
626
|
subprocess.run(["sudo", sys.executable] + sys.argv)
|
610
627
|
sys.exit(0)
|
611
628
|
|
612
|
-
def __get_services(
|
629
|
+
def __get_services(app: App, **kwargs):
|
613
630
|
selected_services = list(kwargs['service'])
|
614
631
|
|
615
|
-
|
616
|
-
|
617
|
-
|
632
|
+
if not selected_services:
|
633
|
+
selected_services = app.services
|
634
|
+
else:
|
635
|
+
selected_services += CORE_SERVICES
|
636
|
+
missing_services = [service for service in selected_services if service not in app.services]
|
618
637
|
|
619
|
-
# Remove services that don't exist
|
620
638
|
if missing_services:
|
639
|
+
# Warn if an invalid service is provided
|
621
640
|
Logger.instance(LOG_ID).warn(f"Service(s) {','.join(missing_services)} are not found")
|
622
|
-
selected_services = list(set(selected_services) - set(missing_services))
|
623
641
|
|
624
|
-
services
|
642
|
+
# Remove services that don't exist
|
643
|
+
selected_services = list(set(selected_services) - set(missing_services))
|
625
644
|
|
626
|
-
|
645
|
+
# If without_score is set, filter out CORE_SERVICES
|
646
|
+
if kwargs.get('without_core'):
|
647
|
+
selected_services = list(set(selected_services) - set(CORE_SERVICES))
|
627
648
|
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
continue
|
632
|
-
services_index[service] = True
|
633
|
-
|
634
|
-
return services_index.keys()
|
635
|
-
|
636
|
-
def __get_workflow_services(app: App, **kwargs):
|
637
|
-
selected_services = []
|
638
|
-
if not kwargs['workflow']:
|
639
|
-
selected_services += __get_services(app.services, service=kwargs['service'], without_core=True)
|
640
|
-
else:
|
649
|
+
# If workflow is set, keep only services in the workflow
|
650
|
+
if kwargs.get('workflow'):
|
651
|
+
workflow_services = []
|
641
652
|
for workflow_name in kwargs['workflow']:
|
642
653
|
workflow = Workflow(workflow_name, app)
|
643
|
-
|
644
|
-
|
654
|
+
workflow_services += workflow.services
|
655
|
+
|
656
|
+
# Intersection
|
657
|
+
selected_services = list(filter(lambda x: x in workflow_services, selected_services))
|
658
|
+
|
659
|
+
return list(set(selected_services))
|
645
660
|
|
646
661
|
def __print_header(text: str):
|
647
662
|
Logger.instance(LOG_ID).info(f"{bcolors.OKBLUE}{text}{bcolors.ENDC}")
|