stoobly-agent 1.9.11__py3-none-any.whl → 1.10.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/app/api/__init__.py +4 -20
  3. stoobly_agent/app/api/configs_controller.py +3 -3
  4. stoobly_agent/app/cli/decorators/exec.py +1 -1
  5. stoobly_agent/app/cli/helpers/handle_config_update_service.py +4 -0
  6. stoobly_agent/app/cli/helpers/shell.py +0 -10
  7. stoobly_agent/app/cli/intercept_cli.py +40 -7
  8. stoobly_agent/app/cli/scaffold/app_command.py +4 -0
  9. stoobly_agent/app/cli/scaffold/app_config.py +21 -3
  10. stoobly_agent/app/cli/scaffold/app_create_command.py +109 -2
  11. stoobly_agent/app/cli/scaffold/constants.py +14 -0
  12. stoobly_agent/app/cli/scaffold/docker/constants.py +4 -6
  13. stoobly_agent/app/cli/scaffold/docker/service/builder.py +19 -4
  14. stoobly_agent/app/cli/scaffold/docker/workflow/builder.py +0 -18
  15. stoobly_agent/app/cli/scaffold/docker/workflow/command_decorator.py +24 -0
  16. stoobly_agent/app/cli/scaffold/docker/workflow/decorators_factory.py +7 -2
  17. stoobly_agent/app/cli/scaffold/docker/workflow/detached_decorator.py +42 -0
  18. stoobly_agent/app/cli/scaffold/docker/workflow/local_decorator.py +26 -0
  19. stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +9 -10
  20. stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py +5 -8
  21. stoobly_agent/app/cli/scaffold/service_config.py +144 -21
  22. stoobly_agent/app/cli/scaffold/service_create_command.py +11 -2
  23. stoobly_agent/app/cli/scaffold/service_dependency.py +51 -0
  24. stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +1 -1
  25. stoobly_agent/app/cli/scaffold/templates/app/build/mock/docker-compose.yml +16 -6
  26. stoobly_agent/app/cli/scaffold/templates/app/build/record/docker-compose.yml +16 -6
  27. stoobly_agent/app/cli/scaffold/templates/app/build/test/docker-compose.yml +16 -6
  28. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/docker-compose.yml +16 -10
  29. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/docker-compose.yml +16 -10
  30. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/docker-compose.yml +16 -10
  31. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/.docker-compose.base.yml +2 -1
  32. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/mock/.docker-compose.mock.yml +6 -3
  33. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/record/.docker-compose.record.yml +6 -4
  34. stoobly_agent/app/cli/scaffold/templates/constants.py +4 -0
  35. stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.Dockerfile.cypress +22 -0
  36. stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.docker-compose.test.yml +19 -0
  37. stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.Dockerfile.playwright +33 -0
  38. stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.docker-compose.test.yml +18 -0
  39. stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.entrypoint.sh +11 -0
  40. stoobly_agent/app/cli/scaffold/templates/workflow/mock/docker-compose.yml +17 -0
  41. stoobly_agent/app/cli/scaffold/templates/workflow/record/docker-compose.yml +17 -0
  42. stoobly_agent/app/cli/scaffold/templates/workflow/test/docker-compose.yml +17 -0
  43. stoobly_agent/app/cli/scaffold/workflow_create_command.py +0 -1
  44. stoobly_agent/app/cli/scaffold/workflow_namesapce.py +8 -2
  45. stoobly_agent/app/cli/scaffold/workflow_run_command.py +1 -1
  46. stoobly_agent/app/cli/scaffold_cli.py +77 -83
  47. stoobly_agent/app/proxy/handle_record_service.py +12 -3
  48. stoobly_agent/app/proxy/handle_replay_service.py +14 -2
  49. stoobly_agent/app/proxy/intercept_settings.py +11 -7
  50. stoobly_agent/app/proxy/mock/eval_fixtures_service.py +33 -2
  51. stoobly_agent/app/proxy/record/upload_request_service.py +2 -2
  52. stoobly_agent/app/proxy/replay/replay_request_service.py +3 -0
  53. stoobly_agent/app/proxy/run.py +3 -28
  54. stoobly_agent/app/proxy/utils/allowed_request_service.py +3 -2
  55. stoobly_agent/app/proxy/utils/minimize_headers.py +47 -0
  56. stoobly_agent/app/proxy/utils/publish_change_service.py +5 -4
  57. stoobly_agent/app/proxy/utils/strategy.py +16 -0
  58. stoobly_agent/app/settings/__init__.py +9 -3
  59. stoobly_agent/app/settings/data_rules.py +25 -1
  60. stoobly_agent/app/settings/intercept_settings.py +5 -2
  61. stoobly_agent/app/settings/types/__init__.py +0 -1
  62. stoobly_agent/app/settings/ui_settings.py +5 -5
  63. stoobly_agent/cli.py +41 -16
  64. stoobly_agent/config/constants/custom_headers.py +1 -0
  65. stoobly_agent/config/constants/env_vars.py +4 -3
  66. stoobly_agent/config/constants/record_strategy.py +6 -0
  67. stoobly_agent/config/settings.yml.sample +2 -3
  68. stoobly_agent/lib/logger.py +15 -5
  69. stoobly_agent/test/app/cli/intercept/intercept_configure_test.py +231 -1
  70. stoobly_agent/test/app/cli/scaffold/cli_invoker.py +3 -2
  71. stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
  72. stoobly_agent/test/app/proxy/mock/eval_fixtures_service_test.py +14 -2
  73. stoobly_agent/test/app/proxy/utils/minimize_headers_test.py +342 -0
  74. {stoobly_agent-1.9.11.dist-info → stoobly_agent-1.10.0.dist-info}/METADATA +2 -1
  75. {stoobly_agent-1.9.11.dist-info → stoobly_agent-1.10.0.dist-info}/RECORD +78 -62
  76. {stoobly_agent-1.9.11.dist-info → stoobly_agent-1.10.0.dist-info}/LICENSE +0 -0
  77. {stoobly_agent-1.9.11.dist-info → stoobly_agent-1.10.0.dist-info}/WHEEL +0 -0
  78. {stoobly_agent-1.9.11.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.9.11'
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 initialize(**kwargs):
21
- url = f"http://{kwargs['ui_host']}:{kwargs['ui_port']}"
22
-
23
- os.environ[env_vars.AGENT_URL] = url
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
 
@@ -19,7 +19,7 @@ class ExecDecorator():
19
19
  shell = kwargs['shell']
20
20
  file_path = kwargs['file_path']
21
21
 
22
- settings = Settings.instance()
22
+ settings: Settings = Settings.instance()
23
23
  proxy_url = settings.proxy.url
24
24
 
25
25
  if not proxy_url:
@@ -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']
@@ -1,19 +1,9 @@
1
- import os
2
1
  import subprocess
3
2
  import sys
4
3
 
5
- from dotenv import load_dotenv
6
-
7
4
  DOTENV_PATH = 'STOOBLY_DOTENV_PATH'
8
5
 
9
6
  def exec_stream(command):
10
- dotenv_path = os.environ.get(DOTENV_PATH)
11
-
12
- if dotenv_path and os.path.exists(dotenv_path):
13
- load_dotenv(dotenv_path=dotenv_path)
14
- else:
15
- load_dotenv()
16
-
17
7
  # Start the process
18
8
  process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
19
9
 
@@ -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(['gateway/.docker-compose.base.yml', '**/.env']))
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
- self.app_config.write()
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'
@@ -18,7 +20,11 @@ CONFIG_FILE = '.config.yml'
18
20
  CONTEXT_DIR_ENV = 'CONTEXT_DIR'
19
21
  DOCKER_NAMESPACE = 'docker'
20
22
  DOTENV_FILE = '.env'
23
+ DOTENV_PATH_ENV = 'STOOBLY_DOTENV_PATH'
21
24
  NAMESERVERS_FILE = '.nameservers'
25
+ PLUGIN_CYPRESS = 'cypress'
26
+ PLUGIN_PLAYWRIGHT = 'playwright'
27
+ PLUGINS_FOLDER = 'plugins'
22
28
  PUBLIC_FOLDER_NAME = 'public'
23
29
  SERVICE_DETACHED = '${SERVICE_DETACHED}'
24
30
  SERVICE_DETACHED_ENV = 'SERVICE_DETACHED'
@@ -28,6 +34,7 @@ SERVICE_HOSTNAME = '${SERVICE_HOSTNAME}'
28
34
  SERVICE_HOSTNAME_ENV = 'SERVICE_HOSTNAME'
29
35
  SERVICE_ID = '${SERVICE_ID}'
30
36
  SERVICE_ID_ENV = 'SERVICE_ID'
37
+ SERVICE_LOCAL_ENV = 'SERVICE_LOCAL'
31
38
  SERVICE_NAME = '${SERVICE_NAME}'
32
39
  SERVICE_NAME_ENV = 'SERVICE_NAME'
33
40
  SERVICE_PROXY_MODE = '${SERVICE_PROXY_MODE}'
@@ -40,10 +47,17 @@ SERVICE_PRIORITY_ENV = 'SERVICE_PRIORITY'
40
47
  SERVICE_SCRIPTS = '${SERVICE_SCRIPTS}'
41
48
  SERVICE_SCRIPTS_DIR = '/usr/local/bin/services'
42
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'
43
56
  STOOBLY_HOME_DIR = '/home/stoobly'
44
57
  STOOBLY_DATA_DIR = os.path.join(STOOBLY_HOME_DIR, DATA_DIR_NAME)
45
58
  STOOBLY_CERTS_DIR = os.path.join(STOOBLY_DATA_DIR, CERTS_DIR_NAME)
46
59
  USER_ID_ENV = 'USER_ID'
60
+ WORKFLOW_CONTAINER_BRIDGE = 'bridge'
47
61
  WORKFLOW_CONTAINER_CONFIGURE = 'configure'
48
62
  WORKFLOW_CONTAINER_INIT = 'init'
49
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
- # TODO: add scaffold container name templates here
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.http.routers.{service_id}.tls=true")
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(ReverseProxyDecorator if service_config.detached else MockDecorator)
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(ReverseProxyDecorator if service_config.detached else MockDecorator)
26
+ workflow_decorators.append(DetachedDecorator if service_config.detached else MockDecorator)
22
27
 
23
28
  return workflow_decorators