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
stoobly_agent/__init__.py
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
COMMAND = 'stoobly-agent'
|
2
|
-
VERSION = '1.
|
2
|
+
VERSION = '1.10.0'
|
@@ -5,7 +5,6 @@ import threading
|
|
5
5
|
from http.server import HTTPServer
|
6
6
|
from urllib.parse import urlparse
|
7
7
|
|
8
|
-
from stoobly_agent.app.settings import Settings
|
9
8
|
from stoobly_agent.config.constants import env_vars
|
10
9
|
from stoobly_agent.lib.logger import Logger
|
11
10
|
|
@@ -17,22 +16,7 @@ def start_server(host, port):
|
|
17
16
|
httpd = HTTPServer((host, port), ApplicationHTTPRequestHandler)
|
18
17
|
httpd.serve_forever()
|
19
18
|
|
20
|
-
def
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
# TODO: debug why the following is needed or else logger won't print
|
26
|
-
settings = Settings.instance()
|
27
|
-
settings.ui.url = url
|
28
|
-
settings.commit()
|
29
|
-
|
30
|
-
return url
|
31
|
-
|
32
|
-
def run(url):
|
33
|
-
parsed_url = urlparse(url)
|
34
|
-
|
35
|
-
Logger.instance(LOG_ID).info(f"Server listening at {url}\n")
|
36
|
-
|
37
|
-
thread = threading.Thread(target=start_server, args=(parsed_url.hostname, parsed_url.port))
|
38
|
-
thread.start()
|
19
|
+
def run(**kwargs):
|
20
|
+
Logger.instance(LOG_ID).info(f"starting and listening at {kwargs['ui_host']}:{kwargs['ui_port']}")
|
21
|
+
thread = threading.Thread(target=start_server, args=(kwargs['ui_host'], kwargs['ui_port']))
|
22
|
+
return thread.start()
|
@@ -4,7 +4,7 @@ from mergedeep import merge
|
|
4
4
|
|
5
5
|
from stoobly_agent.app.api.simple_http_request_handler import SimpleHTTPRequestHandler
|
6
6
|
from stoobly_agent.app.cli.helpers.handle_config_update_service import (
|
7
|
-
context as handle_context, handle_intercept_active_update, handle_order_update, handle_project_update, handle_scenario_update
|
7
|
+
context as handle_context, handle_intercept_active_update, handle_order_update, handle_project_update, handle_scenario_update, handle_strategy_update
|
8
8
|
)
|
9
9
|
from stoobly_agent.app.models.scenario_model import ScenarioModel
|
10
10
|
from stoobly_agent.app.proxy.intercept_settings import InterceptSettings
|
@@ -61,7 +61,7 @@ class ConfigsController:
|
|
61
61
|
|
62
62
|
# GET /configs/summary
|
63
63
|
def summary(self, context):
|
64
|
-
settings = Settings.instance()
|
64
|
+
settings: Settings = Settings.instance()
|
65
65
|
proxy = settings.proxy
|
66
66
|
intercept_settings = InterceptSettings(settings)
|
67
67
|
|
@@ -94,7 +94,6 @@ class ConfigsController:
|
|
94
94
|
'mode': intercept_settings.mode,
|
95
95
|
'modes': modes,
|
96
96
|
'project_id': int(project_id) if project_id != None else None,
|
97
|
-
'proxy_url': proxy.url,
|
98
97
|
'remote_enabled': settings.cli.features.remote,
|
99
98
|
'remote_project_id': remote_project_id,
|
100
99
|
'scenario_id': int(scenario_id) if scenario_id != None else None,
|
@@ -113,6 +112,7 @@ class ConfigsController:
|
|
113
112
|
|
114
113
|
handle_intercept_active_update(settings, _handle_context)
|
115
114
|
handle_order_update(settings, _handle_context)
|
115
|
+
handle_strategy_update(settings, _handle_context)
|
116
116
|
handle_project_update(settings, _handle_context)
|
117
117
|
handle_scenario_update(settings, _handle_context)
|
118
118
|
|
@@ -127,6 +127,10 @@ def handle_order_update(new_settings: Settings, context: Context = None):
|
|
127
127
|
scenario_model = ScenarioModel(new_settings)
|
128
128
|
scenario_model.update(_scenario_key.id, **{ 'overwritable': False })[1]
|
129
129
|
|
130
|
+
def handle_strategy_update(new_settings: Settings, context: Context = None):
|
131
|
+
# Handle side effects here
|
132
|
+
pass
|
133
|
+
|
130
134
|
def __current_proxy_settings(context: Context = None):
|
131
135
|
if context and context.get('current_proxy_settings'):
|
132
136
|
return context['current_proxy_settings']
|
@@ -3,27 +3,27 @@ import pdb
|
|
3
3
|
import sys
|
4
4
|
|
5
5
|
from stoobly_agent.app.settings import Settings
|
6
|
-
from stoobly_agent.config.constants import mode, mock_policy, record_order, record_policy, replay_policy
|
6
|
+
from stoobly_agent.config.constants import mode, mock_policy, record_order, record_policy, record_strategy, replay_policy, test_strategy
|
7
7
|
from stoobly_agent.lib.api.keys.project_key import ProjectKey
|
8
8
|
|
9
|
-
from .helpers.handle_config_update_service import handle_intercept_active_update, handle_order_update
|
9
|
+
from .helpers.handle_config_update_service import handle_intercept_active_update, handle_order_update, handle_strategy_update
|
10
10
|
|
11
11
|
settings: Settings = Settings.instance()
|
12
12
|
|
13
|
-
mode_options = [mode.MOCK, mode.RECORD, mode.REPLAY]
|
13
|
+
mode_options = [mode.MOCK, mode.RECORD, mode.REPLAY, mode.TEST]
|
14
14
|
|
15
15
|
if settings.cli.features.remote:
|
16
16
|
mode_options.append(mode.TEST)
|
17
17
|
|
18
18
|
active_mode = settings.proxy.intercept.mode
|
19
19
|
|
20
|
-
def __get_order_options(active_mode):
|
20
|
+
def __get_order_options(active_mode: str) -> list[str]:
|
21
21
|
if active_mode == mode.RECORD:
|
22
22
|
return [record_order.APPEND, record_order.OVERWRITE]
|
23
23
|
else:
|
24
24
|
return []
|
25
25
|
|
26
|
-
def __get_policy_options(active_mode):
|
26
|
+
def __get_policy_options(active_mode: str) -> list[str]:
|
27
27
|
if active_mode == mode.MOCK:
|
28
28
|
return [mock_policy.ALL, mock_policy.FOUND]
|
29
29
|
elif active_mode == mode.RECORD:
|
@@ -35,8 +35,17 @@ def __get_policy_options(active_mode):
|
|
35
35
|
else:
|
36
36
|
return []
|
37
37
|
|
38
|
+
def __get_strategy_options(active_mode: str) -> list[str]:
|
39
|
+
if active_mode == mode.RECORD:
|
40
|
+
return [record_strategy.FULL, record_strategy.MINIMAL]
|
41
|
+
elif active_mode == mode.TEST:
|
42
|
+
return [test_strategy.CONTRACT, test_strategy.CUSTOM, test_strategy.DIFF, test_strategy.FUZZY]
|
43
|
+
else:
|
44
|
+
return []
|
45
|
+
|
38
46
|
order_options = __get_order_options(active_mode)
|
39
47
|
policy_options = __get_policy_options(active_mode)
|
48
|
+
strategy_options = __get_strategy_options(active_mode)
|
40
49
|
|
41
50
|
@click.group(
|
42
51
|
epilog="Run 'stoobly-agent intercept COMMAND --help' for more information on a command.",
|
@@ -80,10 +89,11 @@ def disable(**kwargs):
|
|
80
89
|
@click.option('--mode', type=click.Choice(mode_options))
|
81
90
|
@click.option('--order', type=click.Choice(order_options))
|
82
91
|
@click.option('--policy', type=click.Choice(policy_options))
|
92
|
+
@click.option('--strategy', type=click.Choice(strategy_options))
|
83
93
|
def configure(**kwargs):
|
84
94
|
settings: Settings = Settings.instance()
|
85
95
|
|
86
|
-
if not kwargs['mode'] and not kwargs['order'] and not kwargs['policy']:
|
96
|
+
if not kwargs['mode'] and not kwargs['order'] and not kwargs['policy'] and not kwargs['strategy']:
|
87
97
|
print("Error: Missing an option")
|
88
98
|
sys.exit(1)
|
89
99
|
|
@@ -136,6 +146,26 @@ def configure(**kwargs):
|
|
136
146
|
|
137
147
|
print(f"Updating {_mode} policy to {kwargs['policy']}")
|
138
148
|
|
149
|
+
if kwargs['strategy']:
|
150
|
+
active_mode = settings.proxy.intercept.mode
|
151
|
+
|
152
|
+
if active_mode == mode.RECORD or active_mode == mode.TEST:
|
153
|
+
project_key = ProjectKey(settings.proxy.intercept.project_key)
|
154
|
+
data_rule = settings.proxy.data.data_rules(project_key.id)
|
155
|
+
|
156
|
+
if active_mode == mode.RECORD:
|
157
|
+
data_rule.record_strategy = kwargs['strategy']
|
158
|
+
elif active_mode == mode.TEST:
|
159
|
+
data_rule.test_strategy = kwargs['strategy']
|
160
|
+
else:
|
161
|
+
print("Error: set --strategy to a intercept mode that supports the strategy option", file=sys.stderr)
|
162
|
+
sys.exit(1)
|
163
|
+
|
164
|
+
handle_strategy_update(settings)
|
165
|
+
|
166
|
+
print(f"Updating {_mode} policy to {kwargs['strategy']}")
|
167
|
+
|
168
|
+
|
139
169
|
settings.commit()
|
140
170
|
|
141
171
|
@intercept.command(
|
@@ -148,17 +178,20 @@ def show(**kwargs):
|
|
148
178
|
project_key = ProjectKey(settings.proxy.intercept.project_key)
|
149
179
|
data_rule = settings.proxy.data.data_rules(project_key.id)
|
150
180
|
policy = None
|
181
|
+
strategy = None
|
151
182
|
|
152
183
|
if active_mode == mode.MOCK:
|
153
184
|
policy = data_rule.mock_policy
|
154
185
|
elif active_mode == mode.RECORD:
|
155
186
|
policy = data_rule.record_policy
|
187
|
+
strategy = data_rule.record_strategy
|
156
188
|
elif active_mode == mode.REPLAY:
|
157
189
|
policy = data_rule.replay_policy
|
158
190
|
elif active_mode == mode.TEST:
|
159
191
|
policy = data_rule.test_policy
|
192
|
+
strategy = data_rule.test_strategy
|
160
193
|
|
161
194
|
if not _mode:
|
162
195
|
print('No intercept mode set')
|
163
196
|
else:
|
164
|
-
print(f"{_mode.capitalize()} with policy {policy} {'enabled' if settings.proxy.intercept.active else 'disabled'}")
|
197
|
+
print(f"{_mode.capitalize()} with policy: '{policy}', strategy: '{strategy}', {'enabled' if settings.proxy.intercept.active else 'disabled'}")
|
@@ -56,6 +56,10 @@ class AppCommand(Command):
|
|
56
56
|
def data_dir_path(self):
|
57
57
|
return self.app.data_dir_path
|
58
58
|
|
59
|
+
@property
|
60
|
+
def plugins_root_dir(self):
|
61
|
+
return os.path.join(self.templates_root_dir, 'plugins')
|
62
|
+
|
59
63
|
@property
|
60
64
|
def scaffold_namespace_path(self):
|
61
65
|
return self.app.scaffold_namespace_path
|
@@ -1,5 +1,7 @@
|
|
1
|
+
import os
|
2
|
+
|
1
3
|
from .config import Config
|
2
|
-
from .constants import APP_DOCKER_SOCKET_PATH_ENV, APP_NAME_ENV, APP_UI_PORT_ENV
|
4
|
+
from .constants import APP_DOCKER_SOCKET_PATH_ENV, APP_NAME_ENV, APP_NETWORK_ENV, APP_PLUGINS_DELMITTER, APP_PLUGINS_ENV, APP_UI_PORT_ENV
|
3
5
|
|
4
6
|
class AppConfig(Config):
|
5
7
|
|
@@ -8,6 +10,7 @@ class AppConfig(Config):
|
|
8
10
|
|
9
11
|
self.__docker_socket_path = '/var/run/docker.sock'
|
10
12
|
self.__name = None
|
13
|
+
self.__plugins = None
|
11
14
|
self.__ui_port = None
|
12
15
|
|
13
16
|
self.load()
|
@@ -28,6 +31,14 @@ class AppConfig(Config):
|
|
28
31
|
def name(self, v):
|
29
32
|
self.__name = v
|
30
33
|
|
34
|
+
@property
|
35
|
+
def plugins(self):
|
36
|
+
return self.__plugins or []
|
37
|
+
|
38
|
+
@plugins.setter
|
39
|
+
def plugins(self, v: list):
|
40
|
+
self.__plugins = v
|
41
|
+
|
31
42
|
@property
|
32
43
|
def ui_port(self):
|
33
44
|
return self.__ui_port
|
@@ -41,7 +52,11 @@ class AppConfig(Config):
|
|
41
52
|
|
42
53
|
self.name = config.get(APP_NAME_ENV)
|
43
54
|
self.ui_port = config.get(APP_UI_PORT_ENV)
|
44
|
-
|
55
|
+
|
56
|
+
if config.get(APP_PLUGINS_ENV):
|
57
|
+
plugins: str = config.get(APP_PLUGINS_ENV)
|
58
|
+
self.plugins = plugins.split(APP_PLUGINS_DELMITTER)
|
59
|
+
|
45
60
|
def write(self):
|
46
61
|
config = {}
|
47
62
|
|
@@ -51,7 +66,10 @@ class AppConfig(Config):
|
|
51
66
|
if self.name:
|
52
67
|
config[APP_NAME_ENV] = self.name
|
53
68
|
|
69
|
+
if self.plugins:
|
70
|
+
config[APP_PLUGINS_ENV] = APP_PLUGINS_DELMITTER.join(self.plugins)
|
71
|
+
|
54
72
|
if self.ui_port:
|
55
73
|
config[APP_UI_PORT_ENV] = self.ui_port
|
56
74
|
|
57
|
-
super().write(config)
|
75
|
+
super().write(config)
|
@@ -1,10 +1,16 @@
|
|
1
1
|
import os
|
2
2
|
import pdb
|
3
|
+
import shutil
|
4
|
+
import yaml
|
3
5
|
|
6
|
+
from mergedeep import merge
|
4
7
|
from typing import TypedDict
|
5
8
|
|
6
9
|
from .app import App
|
7
10
|
from .app_command import AppCommand
|
11
|
+
from .constants import PLUGIN_CYPRESS, PLUGIN_PLAYWRIGHT, PLUGINS_FOLDER, WORKFLOW_TEST_TYPE
|
12
|
+
from .docker.constants import DOCKER_COMPOSE_WORKFLOW_TEMPLATE, PLUGIN_CONTAINER_SERVICE_TEMPLATE, PLUGIN_DOCKER_ENTRYPOINT, PLUGIN_DOCKERFILE_TEMPLATE
|
13
|
+
from .templates.constants import CORE_ENTRYPOINT_SERVICE_NAME, CORE_GATEWAY_SERVICE_NAME
|
8
14
|
|
9
15
|
class AppCreateOptions(TypedDict):
|
10
16
|
docker_socket_path: str
|
@@ -22,6 +28,9 @@ class AppCreateCommand(AppCommand):
|
|
22
28
|
if kwargs.get('docker_socket_path'):
|
23
29
|
self.app_config.docker_socket_path = kwargs['docker_socket_path']
|
24
30
|
|
31
|
+
if kwargs.get('plugin'):
|
32
|
+
self.app_config.plugins = kwargs['plugin']
|
33
|
+
|
25
34
|
if kwargs.get('ui_port'):
|
26
35
|
self.app_config.ui_port = kwargs['ui_port']
|
27
36
|
|
@@ -34,6 +43,9 @@ class AppCreateCommand(AppCommand):
|
|
34
43
|
return self.app_config.name
|
35
44
|
|
36
45
|
@property
|
46
|
+
def app_plugins(self):
|
47
|
+
return self.app_config.plugins
|
48
|
+
|
37
49
|
def app_ui_port(self):
|
38
50
|
return self.app_config.ui_port
|
39
51
|
|
@@ -43,6 +55,101 @@ class AppCreateCommand(AppCommand):
|
|
43
55
|
self.app.copy_folders_and_hidden_files(self.app_templates_root_dir, dest)
|
44
56
|
|
45
57
|
with open(os.path.join(dest, '.gitignore'), 'w') as fp:
|
46
|
-
fp.write("\n".join(
|
58
|
+
fp.write("\n".join(
|
59
|
+
[os.path.join(CORE_GATEWAY_SERVICE_NAME, '.docker-compose.base.yml'), '**/.env']
|
60
|
+
))
|
61
|
+
|
62
|
+
self.app_config.write()
|
63
|
+
|
64
|
+
# Provide plugins
|
65
|
+
warnings = []
|
66
|
+
if PLUGIN_CYPRESS in self.app_plugins:
|
67
|
+
self.__plugin_cypress(dest, PLUGIN_CYPRESS)
|
68
|
+
|
69
|
+
if not self.__cypress_initialized(self.app):
|
70
|
+
warnings.append(f"missing cypress.config.(js|ts), please run `npx cypress open` in {self.app.context_dir_path}")
|
71
|
+
|
72
|
+
|
73
|
+
if PLUGIN_PLAYWRIGHT in self.app_plugins:
|
74
|
+
self.__plugin_playwright(dest, PLUGIN_PLAYWRIGHT)
|
75
|
+
|
76
|
+
if not self.__playwright_initialized(self.app):
|
77
|
+
warnings.append(f"missing playwright.config.(js|ts), please run `npm init playwright@latest` in {self.app.context_dir_path}")
|
78
|
+
|
79
|
+
return {
|
80
|
+
'warnings': warnings
|
81
|
+
}
|
82
|
+
|
83
|
+
def __cypress_initialized(self, app: App):
|
84
|
+
if os.path.exists(os.path.join(app.context_dir_path, 'cypress.config.js')):
|
85
|
+
return True
|
86
|
+
|
87
|
+
if os.path.exists(os.path.join(app.context_dir_path, 'cypress.config.ts')):
|
88
|
+
return True
|
89
|
+
|
90
|
+
return False
|
91
|
+
|
92
|
+
def __merge_compose_plugin(self, dest_path: str, template_path: str, plugin: str):
|
93
|
+
if not os.path.exists(dest_path):
|
94
|
+
open(dest_path, 'a').close()
|
95
|
+
|
96
|
+
def load_yaml(path):
|
97
|
+
with open(path, 'r') as f:
|
98
|
+
return yaml.safe_load(f) or {}
|
99
|
+
|
100
|
+
data1 = load_yaml(dest_path)
|
101
|
+
data2 = load_yaml(template_path)
|
102
|
+
|
103
|
+
services = data1.get('services') or {}
|
104
|
+
if services.get(PLUGIN_CONTAINER_SERVICE_TEMPLATE.format(plugin=plugin, service=CORE_ENTRYPOINT_SERVICE_NAME)):
|
105
|
+
return
|
106
|
+
|
107
|
+
with open(dest_path, 'w') as out:
|
108
|
+
merged = merge(data1, data2)
|
109
|
+
yaml.dump(merged, out, default_flow_style=False)
|
110
|
+
|
111
|
+
def __playwright_initialized(self, app: App):
|
112
|
+
if os.path.exists(os.path.join(app.context_dir_path, 'playwright.config.js')):
|
113
|
+
return True
|
114
|
+
|
115
|
+
if os.path.exists(os.path.join(app.context_dir_path, 'playwright.config.ts')):
|
116
|
+
return True
|
117
|
+
|
118
|
+
return False
|
119
|
+
|
120
|
+
def __plugin_cypress(self, dest: str, plugin: str):
|
121
|
+
dockerfile_name = PLUGIN_DOCKERFILE_TEMPLATE.format(plugin=plugin)
|
122
|
+
dockerfile_dest_path = os.path.join(dest, CORE_ENTRYPOINT_SERVICE_NAME, WORKFLOW_TEST_TYPE, dockerfile_name)
|
123
|
+
|
124
|
+
# Copy Dockerfile to workflow
|
125
|
+
dockerfile_src_path = os.path.join(self.templates_root_dir, PLUGINS_FOLDER, plugin, WORKFLOW_TEST_TYPE, dockerfile_name)
|
126
|
+
shutil.copyfile(dockerfile_src_path, dockerfile_dest_path)
|
127
|
+
|
128
|
+
# Merge template into dest compose yml
|
129
|
+
compose_dest_path = os.path.join(
|
130
|
+
dest, CORE_ENTRYPOINT_SERVICE_NAME, WORKFLOW_TEST_TYPE, DOCKER_COMPOSE_WORKFLOW_TEMPLATE.format(workflow=WORKFLOW_TEST_TYPE)
|
131
|
+
)
|
132
|
+
template_path = os.path.join(
|
133
|
+
self.templates_root_dir, PLUGINS_FOLDER, plugin, WORKFLOW_TEST_TYPE, DOCKER_COMPOSE_WORKFLOW_TEMPLATE.format(workflow=WORKFLOW_TEST_TYPE)
|
134
|
+
)
|
135
|
+
self.__merge_compose_plugin(compose_dest_path, template_path, plugin)
|
136
|
+
|
137
|
+
def __plugin_playwright(self, dest: str, plugin: str):
|
138
|
+
# Copy Dockerfile to workflow
|
139
|
+
dockerfile_name = PLUGIN_DOCKERFILE_TEMPLATE.format(plugin=plugin)
|
140
|
+
dockerfile_src_path = os.path.join(self.templates_root_dir, PLUGINS_FOLDER, plugin, WORKFLOW_TEST_TYPE, dockerfile_name)
|
141
|
+
dockerfile_dest_path = os.path.join(dest, CORE_ENTRYPOINT_SERVICE_NAME, WORKFLOW_TEST_TYPE, dockerfile_name)
|
142
|
+
shutil.copyfile(dockerfile_src_path, dockerfile_dest_path)
|
143
|
+
|
144
|
+
entrypoint_src_path = os.path.join(self.templates_root_dir, PLUGINS_FOLDER, plugin, WORKFLOW_TEST_TYPE, PLUGIN_DOCKER_ENTRYPOINT)
|
145
|
+
entrypoint_dest_path = os.path.join(dest, CORE_ENTRYPOINT_SERVICE_NAME, WORKFLOW_TEST_TYPE, PLUGIN_DOCKER_ENTRYPOINT)
|
146
|
+
shutil.copyfile(entrypoint_src_path, entrypoint_dest_path)
|
47
147
|
|
48
|
-
|
148
|
+
# Merge template into dest compose yml
|
149
|
+
compose_dest_path = os.path.join(
|
150
|
+
dest, CORE_ENTRYPOINT_SERVICE_NAME, WORKFLOW_TEST_TYPE, DOCKER_COMPOSE_WORKFLOW_TEMPLATE.format(workflow=WORKFLOW_TEST_TYPE)
|
151
|
+
)
|
152
|
+
template_path = os.path.join(
|
153
|
+
self.templates_root_dir, PLUGINS_FOLDER, plugin, WORKFLOW_TEST_TYPE, DOCKER_COMPOSE_WORKFLOW_TEMPLATE.format(workflow=WORKFLOW_TEST_TYPE)
|
154
|
+
)
|
155
|
+
self.__merge_compose_plugin(compose_dest_path, template_path, plugin)
|
@@ -9,6 +9,8 @@ APP_DIR_ENV = 'APP_DIR'
|
|
9
9
|
APP_DOCKER_SOCKET_PATH_ENV = 'APP_DOCKER_SOCKET_PATH'
|
10
10
|
APP_NETWORK_ENV = 'APP_NETWORK'
|
11
11
|
APP_NAME_ENV = 'APP_NAME'
|
12
|
+
APP_PLUGINS_ENV = 'APP_PLUGINS'
|
13
|
+
APP_PLUGINS_DELMITTER = ','
|
12
14
|
APP_UI_PORT_ENV = 'APP_UI_PORT'
|
13
15
|
BIN_FOLDER_NAME = 'bin'
|
14
16
|
CA_CERTS_DIR_ENV = 'CA_CERTS_DIR'
|
@@ -20,6 +22,9 @@ DOCKER_NAMESPACE = 'docker'
|
|
20
22
|
DOTENV_FILE = '.env'
|
21
23
|
DOTENV_PATH_ENV = 'STOOBLY_DOTENV_PATH'
|
22
24
|
NAMESERVERS_FILE = '.nameservers'
|
25
|
+
PLUGIN_CYPRESS = 'cypress'
|
26
|
+
PLUGIN_PLAYWRIGHT = 'playwright'
|
27
|
+
PLUGINS_FOLDER = 'plugins'
|
23
28
|
PUBLIC_FOLDER_NAME = 'public'
|
24
29
|
SERVICE_DETACHED = '${SERVICE_DETACHED}'
|
25
30
|
SERVICE_DETACHED_ENV = 'SERVICE_DETACHED'
|
@@ -29,6 +34,7 @@ SERVICE_HOSTNAME = '${SERVICE_HOSTNAME}'
|
|
29
34
|
SERVICE_HOSTNAME_ENV = 'SERVICE_HOSTNAME'
|
30
35
|
SERVICE_ID = '${SERVICE_ID}'
|
31
36
|
SERVICE_ID_ENV = 'SERVICE_ID'
|
37
|
+
SERVICE_LOCAL_ENV = 'SERVICE_LOCAL'
|
32
38
|
SERVICE_NAME = '${SERVICE_NAME}'
|
33
39
|
SERVICE_NAME_ENV = 'SERVICE_NAME'
|
34
40
|
SERVICE_PROXY_MODE = '${SERVICE_PROXY_MODE}'
|
@@ -41,10 +47,17 @@ SERVICE_PRIORITY_ENV = 'SERVICE_PRIORITY'
|
|
41
47
|
SERVICE_SCRIPTS = '${SERVICE_SCRIPTS}'
|
42
48
|
SERVICE_SCRIPTS_DIR = '/usr/local/bin/services'
|
43
49
|
SERVICE_SCRIPTS_ENV = 'SERVICE_SCRIPTS'
|
50
|
+
SERVICE_UPSTREAM_HOSTNAME = '${SERVICE_UPSTREAM_HOSTNAME}'
|
51
|
+
SERVICE_UPSTREAM_HOSTNAME_ENV = 'SERVICE_UPSTREAM_HOSTNAME'
|
52
|
+
SERVICE_UPSTREAM_PORT = '${SERVICE_UPSTREAM_PORT}'
|
53
|
+
SERVICE_UPSTREAM_PORT_ENV = 'SERVICE_UPSTREAM_PORT'
|
54
|
+
SERVICE_UPSTREAM_SCHEME = '${SERVICE_UPSTREAM_SCHEME}'
|
55
|
+
SERVICE_UPSTREAM_SCHEME_ENV = 'SERVICE_UPSTREAM_SCHEME'
|
44
56
|
STOOBLY_HOME_DIR = '/home/stoobly'
|
45
57
|
STOOBLY_DATA_DIR = os.path.join(STOOBLY_HOME_DIR, DATA_DIR_NAME)
|
46
58
|
STOOBLY_CERTS_DIR = os.path.join(STOOBLY_DATA_DIR, CERTS_DIR_NAME)
|
47
59
|
USER_ID_ENV = 'USER_ID'
|
60
|
+
WORKFLOW_CONTAINER_BRIDGE = 'bridge'
|
48
61
|
WORKFLOW_CONTAINER_CONFIGURE = 'configure'
|
49
62
|
WORKFLOW_CONTAINER_INIT = 'init'
|
50
63
|
WORKFLOW_CONTAINER_PROXY = 'proxy'
|
@@ -8,11 +8,9 @@ DOCKER_COMPOSE_BASE = '.docker-compose.base.yml'
|
|
8
8
|
DOCKER_COMPOSE_BASE_TEMPLATE = '.docker-compose.base.template.yml'
|
9
9
|
DOCKER_COMPOSE_CUSTOM = 'docker-compose.yml'
|
10
10
|
DOCKER_COMPOSE_NETWORKS = '.docker-compose.networks.yml'
|
11
|
+
DOCKER_COMPOSE_WORKFLOW_TEMPLATE = '.docker-compose.{workflow}.yml'
|
11
12
|
DOCKERFILE_CONTEXT = '.Dockerfile.context'
|
12
13
|
DOCKERFILE_SERVICE = 'Dockerfile.source'
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
# Example:
|
17
|
-
# COMPOSE_TEMPLATE = 'docker-compose.{workflow}.yml'
|
18
|
-
|
14
|
+
PLUGIN_CONTAINER_SERVICE_TEMPLATE = '{service}.{plugin}'
|
15
|
+
PLUGIN_DOCKER_ENTRYPOINT = '.entrypoint.sh'
|
16
|
+
PLUGIN_DOCKERFILE_TEMPLATE = '.Dockerfile.{plugin}'
|
@@ -33,6 +33,7 @@ class ServiceBuilder(Builder):
|
|
33
33
|
self.app_builder = app_builder
|
34
34
|
|
35
35
|
self.__config = config
|
36
|
+
self.__upstream_port = None
|
36
37
|
self.__env = [SERVICE_NAME_ENV, WORKFLOW_NAME_ENV]
|
37
38
|
self.__service_name = os.path.basename(service_path)
|
38
39
|
self.__working_dir = os.path.join(
|
@@ -55,6 +56,10 @@ class ServiceBuilder(Builder):
|
|
55
56
|
def configure_base_service(self):
|
56
57
|
return self.services.get(self.configure_base)
|
57
58
|
|
59
|
+
@property
|
60
|
+
def upstream_port(self) -> int:
|
61
|
+
return self.__upstream_port
|
62
|
+
|
58
63
|
@property
|
59
64
|
def extends_service(self):
|
60
65
|
if self.config.detached:
|
@@ -99,9 +104,6 @@ class ServiceBuilder(Builder):
|
|
99
104
|
environment = { **self.env_dict() }
|
100
105
|
labels = [
|
101
106
|
'traefik.enable=true',
|
102
|
-
f"traefik.http.routers.{service_id}.rule=Host(`{SERVICE_HOSTNAME}`)",
|
103
|
-
f"traefik.http.routers.{service_id}.entrypoints={SERVICE_PORT}",
|
104
|
-
f"traefik.http.services.{service_id}.loadbalancer.server.port={SERVICE_PORT}"
|
105
107
|
]
|
106
108
|
volumes = []
|
107
109
|
|
@@ -109,7 +111,14 @@ class ServiceBuilder(Builder):
|
|
109
111
|
self.__with_detached_volumes(volumes)
|
110
112
|
|
111
113
|
if self.config.tls:
|
112
|
-
labels.append(f"traefik.
|
114
|
+
labels.append(f"traefik.tcp.routers.{service_id}.entrypoints={SERVICE_PORT}")
|
115
|
+
labels.append(f"traefik.tcp.routers.{service_id}.rule=HostSNI(`{SERVICE_HOSTNAME}`)")
|
116
|
+
labels.append(f"traefik.tcp.routers.{service_id}.tls.passthrough=true")
|
117
|
+
labels.append(f"traefik.tcp.services.{service_id}.loadbalancer.server.port={SERVICE_PORT}")
|
118
|
+
else:
|
119
|
+
labels.append(f"traefik.http.routers.{service_id}.entrypoints={SERVICE_PORT}")
|
120
|
+
labels.append(f"traefik.http.routers.{service_id}.rule=Host(`{SERVICE_HOSTNAME}`)")
|
121
|
+
labels.append(f"traefik.http.services.{service_id}.loadbalancer.server.port={SERVICE_PORT}")
|
113
122
|
|
114
123
|
base = {
|
115
124
|
'environment': environment,
|
@@ -178,6 +187,12 @@ class ServiceBuilder(Builder):
|
|
178
187
|
env[e] = '${' + e + '}'
|
179
188
|
return env
|
180
189
|
|
190
|
+
def with_upstream_port(self, v: int):
|
191
|
+
if not isinstance(v, int):
|
192
|
+
return self
|
193
|
+
self.__upstream_port = v
|
194
|
+
return self
|
195
|
+
|
181
196
|
def with_env(self, v: List[str]):
|
182
197
|
if not isinstance(v, list):
|
183
198
|
return self
|
@@ -94,7 +94,6 @@ class WorkflowBuilder(Builder):
|
|
94
94
|
if not self.service_builder.init_base_service:
|
95
95
|
return
|
96
96
|
|
97
|
-
return
|
98
97
|
service = {
|
99
98
|
'extends': self.service_builder.build_extends_init_base(self.dir_path),
|
100
99
|
'profiles': self.profiles,
|
@@ -138,10 +137,6 @@ class WorkflowBuilder(Builder):
|
|
138
137
|
}
|
139
138
|
|
140
139
|
if self.configure in self.services:
|
141
|
-
depends_on[self.init] = {
|
142
|
-
'condition': 'service_completed_successfully',
|
143
|
-
}
|
144
|
-
|
145
140
|
depends_on[self.configure] = {
|
146
141
|
'condition': 'service_completed_successfully',
|
147
142
|
}
|
@@ -158,19 +153,6 @@ class WorkflowBuilder(Builder):
|
|
158
153
|
env[e] = '${' + e + '}'
|
159
154
|
return env
|
160
155
|
|
161
|
-
def initialize_custom_file(self):
|
162
|
-
dest = self.custom_compose_file_path
|
163
|
-
|
164
|
-
if not os.path.exists(dest):
|
165
|
-
compose = {
|
166
|
-
'services': {}
|
167
|
-
}
|
168
|
-
|
169
|
-
if self.networks:
|
170
|
-
compose['networks'] = self.networks
|
171
|
-
|
172
|
-
super().write(compose, dest)
|
173
|
-
|
174
156
|
def write(self):
|
175
157
|
compose = {
|
176
158
|
'services': self.services,
|
@@ -0,0 +1,24 @@
|
|
1
|
+
from ...constants import (
|
2
|
+
SERVICE_PROXY_MODE, SERVICE_UPSTREAM_HOSTNAME, SERVICE_UPSTREAM_PORT, SERVICE_UPSTREAM_SCHEME,
|
3
|
+
)
|
4
|
+
from .builder import WorkflowBuilder
|
5
|
+
|
6
|
+
class CommandDecorator():
|
7
|
+
|
8
|
+
def __init__(self, workflow_builder: WorkflowBuilder):
|
9
|
+
self.__workflow_builder = workflow_builder
|
10
|
+
|
11
|
+
@property
|
12
|
+
def workflow_builder(self):
|
13
|
+
return self.__workflow_builder
|
14
|
+
|
15
|
+
@property
|
16
|
+
def proxy_mode(self):
|
17
|
+
config = self.workflow_builder.config
|
18
|
+
if config.upstream_hostname == 'host.docker.internal':
|
19
|
+
return f"upstream:{SERVICE_UPSTREAM_SCHEME}://{SERVICE_UPSTREAM_HOSTNAME}:{SERVICE_UPSTREAM_PORT}"
|
20
|
+
|
21
|
+
if config.hostname != config.upstream_hostname or config.scheme != config.upstream_scheme or config.port != config.upstream_port:
|
22
|
+
return f"reverse:{SERVICE_UPSTREAM_SCHEME}://{SERVICE_UPSTREAM_HOSTNAME}:{SERVICE_UPSTREAM_PORT}"
|
23
|
+
|
24
|
+
return 'regular'
|
@@ -1,7 +1,9 @@
|
|
1
1
|
from stoobly_agent.app.cli.scaffold.service_config import ServiceConfig
|
2
2
|
|
3
3
|
from ...constants import WORKFLOW_MOCK_TYPE, WORKFLOW_RECORD_TYPE, WORKFLOW_TEST_TYPE
|
4
|
+
from .detached_decorator import DetachedDecorator
|
4
5
|
from .dns_decorator import DnsDecorator
|
6
|
+
from .local_decorator import LocalDecorator
|
5
7
|
from .mock_decorator import MockDecorator
|
6
8
|
from .reverse_proxy_decorator import ReverseProxyDecorator
|
7
9
|
|
@@ -12,12 +14,15 @@ def get_workflow_decorators(workflow: str, service_config: ServiceConfig):
|
|
12
14
|
if service_config.hostname:
|
13
15
|
workflow_decorators.append(ReverseProxyDecorator)
|
14
16
|
workflow_decorators.append(DnsDecorator)
|
17
|
+
|
18
|
+
if service_config.local:
|
19
|
+
workflow_decorators.append(LocalDecorator)
|
15
20
|
elif workflow == WORKFLOW_MOCK_TYPE or workflow == WORKFLOW_TEST_TYPE:
|
16
21
|
if service_config.hostname:
|
17
|
-
workflow_decorators.append(
|
22
|
+
workflow_decorators.append(DetachedDecorator if service_config.detached else MockDecorator)
|
18
23
|
workflow_decorators.append(DnsDecorator)
|
19
24
|
else:
|
20
25
|
if service_config.hostname:
|
21
|
-
workflow_decorators.append(
|
26
|
+
workflow_decorators.append(DetachedDecorator if service_config.detached else MockDecorator)
|
22
27
|
|
23
28
|
return workflow_decorators
|
@@ -0,0 +1,42 @@
|
|
1
|
+
import os
|
2
|
+
import pdb
|
3
|
+
|
4
|
+
from ...constants import SERVICE_HOSTNAME, SERVICE_PORT, STOOBLY_CERTS_DIR
|
5
|
+
from .builder import WorkflowBuilder
|
6
|
+
from .command_decorator import CommandDecorator
|
7
|
+
|
8
|
+
class DetachedDecorator(CommandDecorator):
|
9
|
+
|
10
|
+
def __init__(self, workflow_builder: WorkflowBuilder):
|
11
|
+
super().__init__(workflow_builder)
|
12
|
+
|
13
|
+
@property
|
14
|
+
def service_builder(self):
|
15
|
+
return self.workflow_builder.service_builder
|
16
|
+
|
17
|
+
def decorate(self):
|
18
|
+
config = self.service_builder.config
|
19
|
+
|
20
|
+
command = [
|
21
|
+
'--headless',
|
22
|
+
'--lifecycle-hooks-path', 'lifecycle_hooks.py',
|
23
|
+
'--proxy-mode', self.proxy_mode,
|
24
|
+
'--proxy-port', f"{SERVICE_PORT}",
|
25
|
+
'--public-directory-path', 'public',
|
26
|
+
'--response-fixtures-path', 'fixtures.yml',
|
27
|
+
'--ssl-insecure'
|
28
|
+
]
|
29
|
+
|
30
|
+
if config.scheme == 'https':
|
31
|
+
command.append('--certs')
|
32
|
+
command.append(os.path.join(STOOBLY_CERTS_DIR, f"{SERVICE_HOSTNAME}-joined.pem"))
|
33
|
+
|
34
|
+
services = self.workflow_builder.services
|
35
|
+
proxy_name = self.workflow_builder.proxy
|
36
|
+
proxy_service = services.get(proxy_name) or {}
|
37
|
+
|
38
|
+
services[proxy_name] = {
|
39
|
+
**proxy_service,
|
40
|
+
**{ 'command': command },
|
41
|
+
}
|
42
|
+
|