stoobly-agent 1.9.12__py3-none-any.whl → 1.10.1__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 (98) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/app/api/__init__.py +4 -20
  3. stoobly_agent/app/api/application_http_request_handler.py +5 -2
  4. stoobly_agent/app/api/configs_controller.py +3 -3
  5. stoobly_agent/app/cli/decorators/exec.py +1 -1
  6. stoobly_agent/app/cli/helpers/handle_config_update_service.py +4 -0
  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 -3
  12. stoobly_agent/app/cli/scaffold/docker/constants.py +4 -6
  13. stoobly_agent/app/cli/scaffold/docker/service/build_decorator.py +2 -2
  14. stoobly_agent/app/cli/scaffold/docker/service/builder.py +36 -10
  15. stoobly_agent/app/cli/scaffold/docker/workflow/builder.py +0 -27
  16. stoobly_agent/app/cli/scaffold/docker/workflow/command_decorator.py +25 -0
  17. stoobly_agent/app/cli/scaffold/docker/workflow/decorators_factory.py +7 -2
  18. stoobly_agent/app/cli/scaffold/docker/workflow/detached_decorator.py +42 -0
  19. stoobly_agent/app/cli/scaffold/docker/workflow/local_decorator.py +26 -0
  20. stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +9 -10
  21. stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py +5 -8
  22. stoobly_agent/app/cli/scaffold/service_config.py +133 -34
  23. stoobly_agent/app/cli/scaffold/service_create_command.py +11 -2
  24. stoobly_agent/app/cli/scaffold/service_dependency.py +51 -0
  25. stoobly_agent/app/cli/scaffold/service_docker_compose.py +3 -3
  26. stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py +10 -7
  27. stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +1 -1
  28. stoobly_agent/app/cli/scaffold/templates/app/build/.docker-compose.base.yml +2 -2
  29. stoobly_agent/app/cli/scaffold/templates/app/build/mock/bin/configure +1 -1
  30. stoobly_agent/app/cli/scaffold/templates/app/build/mock/docker-compose.yml +16 -6
  31. stoobly_agent/app/cli/scaffold/templates/app/build/record/bin/configure +26 -1
  32. stoobly_agent/app/cli/scaffold/templates/app/build/record/docker-compose.yml +16 -6
  33. stoobly_agent/app/cli/scaffold/templates/app/build/test/bin/configure +1 -1
  34. stoobly_agent/app/cli/scaffold/templates/app/build/test/docker-compose.yml +16 -6
  35. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/.docker-compose.base.yml +2 -2
  36. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/bin/configure +1 -1
  37. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/docker-compose.yml +16 -10
  38. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/bin/configure +1 -1
  39. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/docker-compose.yml +16 -10
  40. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/bin/configure +1 -1
  41. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/docker-compose.yml +16 -10
  42. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/.docker-compose.base.yml +2 -1
  43. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/mock/.docker-compose.mock.yml +6 -3
  44. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/record/.docker-compose.record.yml +6 -4
  45. stoobly_agent/app/cli/scaffold/templates/build/workflows/record/.configure +21 -1
  46. stoobly_agent/app/cli/scaffold/templates/constants.py +4 -0
  47. stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.Dockerfile.cypress +22 -0
  48. stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.docker-compose.test.yml +19 -0
  49. stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.Dockerfile.playwright +33 -0
  50. stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.docker-compose.test.yml +18 -0
  51. stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.entrypoint.sh +11 -0
  52. stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/configure +2 -10
  53. stoobly_agent/app/cli/scaffold/templates/workflow/mock/docker-compose.yml +17 -0
  54. stoobly_agent/app/cli/scaffold/templates/workflow/record/bin/configure +19 -45
  55. stoobly_agent/app/cli/scaffold/templates/workflow/record/docker-compose.yml +17 -0
  56. stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/configure +2 -10
  57. stoobly_agent/app/cli/scaffold/templates/workflow/test/docker-compose.yml +17 -0
  58. stoobly_agent/app/cli/scaffold/workflow_create_command.py +0 -1
  59. stoobly_agent/app/cli/scaffold/workflow_run_command.py +1 -1
  60. stoobly_agent/app/cli/scaffold_cli.py +85 -96
  61. stoobly_agent/app/proxy/handle_record_service.py +12 -3
  62. stoobly_agent/app/proxy/handle_replay_service.py +14 -2
  63. stoobly_agent/app/proxy/intercept_settings.py +12 -8
  64. stoobly_agent/app/proxy/record/upload_request_service.py +5 -8
  65. stoobly_agent/app/proxy/replay/replay_request_service.py +3 -0
  66. stoobly_agent/app/proxy/run.py +3 -28
  67. stoobly_agent/app/proxy/utils/allowed_request_service.py +3 -2
  68. stoobly_agent/app/proxy/utils/minimize_headers.py +47 -0
  69. stoobly_agent/app/proxy/utils/publish_change_service.py +22 -24
  70. stoobly_agent/app/proxy/utils/strategy.py +16 -0
  71. stoobly_agent/app/settings/__init__.py +15 -6
  72. stoobly_agent/app/settings/data_rules.py +25 -1
  73. stoobly_agent/app/settings/intercept_settings.py +5 -2
  74. stoobly_agent/app/settings/types/__init__.py +0 -1
  75. stoobly_agent/app/settings/ui_settings.py +5 -5
  76. stoobly_agent/cli.py +41 -16
  77. stoobly_agent/config/constants/custom_headers.py +1 -0
  78. stoobly_agent/config/constants/env_vars.py +4 -3
  79. stoobly_agent/config/constants/record_strategy.py +6 -0
  80. stoobly_agent/config/data_dir.py +1 -0
  81. stoobly_agent/config/settings.yml.sample +2 -3
  82. stoobly_agent/lib/logger.py +15 -5
  83. stoobly_agent/public/index.html +1 -1
  84. stoobly_agent/public/main-es2015.5a9aa16433404c3f423a.js +1 -0
  85. stoobly_agent/public/main-es5.5a9aa16433404c3f423a.js +1 -0
  86. stoobly_agent/test/app/cli/intercept/intercept_configure_test.py +231 -1
  87. stoobly_agent/test/app/cli/scaffold/cli_invoker.py +3 -2
  88. stoobly_agent/test/app/cli/scaffold/cli_test.py +3 -3
  89. stoobly_agent/test/app/cli/scaffold/e2e_test.py +11 -11
  90. stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
  91. stoobly_agent/test/app/proxy/utils/minimize_headers_test.py +342 -0
  92. {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.1.dist-info}/METADATA +2 -1
  93. {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.1.dist-info}/RECORD +96 -80
  94. stoobly_agent/public/main-es2015.089b46f303768fbe864f.js +0 -1
  95. stoobly_agent/public/main-es5.089b46f303768fbe864f.js +0 -1
  96. {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.1.dist-info}/LICENSE +0 -0
  97. {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.1.dist-info}/WHEEL +0 -0
  98. {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.1.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.12'
2
+ VERSION = '1.10.1'
@@ -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()
@@ -104,11 +104,14 @@ class ApplicationHTTPRequestHandler(SimpleHTTPRequestHandler):
104
104
  headers.CLIENT.title(),
105
105
  custom_headers.DO_PROXY.title(),
106
106
  headers.EXPIRY.title(),
107
+ custom_headers.REQUEST_ORIGIN.title(),
108
+ headers.TOKEN_TYPE.title(),
109
+ headers.UID.title(),
110
+
111
+ # ProxyController headers
107
112
  headers.PROXY_HEADERS.title(),
108
113
  headers.REQUEST_PATH.title(),
109
114
  custom_headers.SERVICE_URL.title(),
110
- headers.TOKEN_TYPE.title(),
111
- headers.UID.title(),
112
115
  ])
113
116
  self.send_header(header, allowed_headers)
114
117
  rendered_headers.append(header)
@@ -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']
@@ -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'
@@ -16,10 +18,12 @@ CERTS_DIR_ENV = 'CERTS_DIR'
16
18
  COMPOSE_TEMPLATE = '.docker-compose.{workflow}.yml'
17
19
  CONFIG_FILE = '.config.yml'
18
20
  CONTEXT_DIR_ENV = 'CONTEXT_DIR'
19
- DOCKER_NAMESPACE = 'docker'
20
21
  DOTENV_FILE = '.env'
21
22
  DOTENV_PATH_ENV = 'STOOBLY_DOTENV_PATH'
22
23
  NAMESERVERS_FILE = '.nameservers'
24
+ PLUGIN_CYPRESS = 'cypress'
25
+ PLUGIN_PLAYWRIGHT = 'playwright'
26
+ PLUGINS_FOLDER = 'plugins'
23
27
  PUBLIC_FOLDER_NAME = 'public'
24
28
  SERVICE_DETACHED = '${SERVICE_DETACHED}'
25
29
  SERVICE_DETACHED_ENV = 'SERVICE_DETACHED'
@@ -29,10 +33,9 @@ SERVICE_HOSTNAME = '${SERVICE_HOSTNAME}'
29
33
  SERVICE_HOSTNAME_ENV = 'SERVICE_HOSTNAME'
30
34
  SERVICE_ID = '${SERVICE_ID}'
31
35
  SERVICE_ID_ENV = 'SERVICE_ID'
36
+ SERVICE_LOCAL_ENV = 'SERVICE_LOCAL'
32
37
  SERVICE_NAME = '${SERVICE_NAME}'
33
38
  SERVICE_NAME_ENV = 'SERVICE_NAME'
34
- SERVICE_PROXY_MODE = '${SERVICE_PROXY_MODE}'
35
- SERVICE_PROXY_MODE_ENV = 'SERVICE_PROXY_MODE'
36
39
  SERVICE_SCHEME = '${SERVICE_SCHEME}'
37
40
  SERVICE_SCHEME_ENV = 'SERVICE_SCHEME'
38
41
  SERVICE_PORT = '${SERVICE_PORT}'
@@ -41,10 +44,18 @@ SERVICE_PRIORITY_ENV = 'SERVICE_PRIORITY'
41
44
  SERVICE_SCRIPTS = '${SERVICE_SCRIPTS}'
42
45
  SERVICE_SCRIPTS_DIR = '/usr/local/bin/services'
43
46
  SERVICE_SCRIPTS_ENV = 'SERVICE_SCRIPTS'
47
+ SERVICE_UPSTREAM_HOSTNAME = '${SERVICE_UPSTREAM_HOSTNAME}'
48
+ SERVICE_UPSTREAM_HOSTNAME_ENV = 'SERVICE_UPSTREAM_HOSTNAME'
49
+ SERVICE_UPSTREAM_PORT = '${SERVICE_UPSTREAM_PORT}'
50
+ SERVICE_UPSTREAM_PORT_ENV = 'SERVICE_UPSTREAM_PORT'
51
+ SERVICE_UPSTREAM_SCHEME = '${SERVICE_UPSTREAM_SCHEME}'
52
+ SERVICE_UPSTREAM_SCHEME_ENV = 'SERVICE_UPSTREAM_SCHEME'
53
+ SERVICES_NAMESPACE = 'services'
44
54
  STOOBLY_HOME_DIR = '/home/stoobly'
45
55
  STOOBLY_DATA_DIR = os.path.join(STOOBLY_HOME_DIR, DATA_DIR_NAME)
46
56
  STOOBLY_CERTS_DIR = os.path.join(STOOBLY_DATA_DIR, CERTS_DIR_NAME)
47
57
  USER_ID_ENV = 'USER_ID'
58
+ WORKFLOW_CONTAINER_BRIDGE = 'bridge'
48
59
  WORKFLOW_CONTAINER_CONFIGURE = 'configure'
49
60
  WORKFLOW_CONTAINER_INIT = 'init'
50
61
  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}'
@@ -2,7 +2,7 @@ import pdb
2
2
 
3
3
  from typing import TypedDict
4
4
 
5
- from ...constants import DOCKER_NAMESPACE
5
+ from ...constants import SERVICES_NAMESPACE
6
6
  from ..constants import DOCKERFILE_SERVICE
7
7
  from .builder import ServiceBuilder
8
8
  from .types import BuildDecoratorOptions
@@ -16,7 +16,7 @@ class BuildDecorator():
16
16
  service_builder = self.__service_builder
17
17
  build = {
18
18
  'context': '../..', # Assumes app root is 2 levels up
19
- 'dockerfile': f"./{DOCKER_NAMESPACE}/{service_builder.service_name}/{DOCKERFILE_SERVICE}"
19
+ 'dockerfile': f"./{SERVICES_NAMESPACE}/{service_builder.service_name}/{DOCKERFILE_SERVICE}"
20
20
  }
21
21
 
22
22
  if 'build_args' in kwargs:
@@ -7,12 +7,13 @@ from stoobly_agent.config.data_dir import DATA_DIR_NAME
7
7
 
8
8
  from ...app_config import AppConfig
9
9
  from ...constants import (
10
- APP_DIR, DOCKER_NAMESPACE,
10
+ APP_DIR, SERVICES_NAMESPACE,
11
11
  SERVICE_HOSTNAME, SERVICE_HOSTNAME_ENV,
12
12
  SERVICE_NAME, SERVICE_NAME_ENV,
13
13
  SERVICE_ID,
14
14
  SERVICE_PORT, SERVICE_PORT_ENV,
15
- SERVICE_SCHEME, SERVICE_SCHEME_ENV,
15
+ SERVICE_SCHEME, SERVICE_SCHEME_ENV,
16
+ SERVICE_UPSTREAM_HOSTNAME, SERVICE_UPSTREAM_HOSTNAME_ENV, SERVICE_UPSTREAM_PORT, SERVICE_UPSTREAM_PORT_ENV, SERVICE_UPSTREAM_SCHEME, SERVICE_UPSTREAM_SCHEME_ENV,
16
17
  STOOBLY_HOME_DIR, STOOBLY_HOME_DIR,
17
18
  WORKFLOW_NAME, WORKFLOW_NAME_ENV, WORKFLOW_SCRIPTS, WORKFLOW_TEMPLATE
18
19
  )
@@ -33,10 +34,11 @@ class ServiceBuilder(Builder):
33
34
  self.app_builder = app_builder
34
35
 
35
36
  self.__config = config
37
+ self.__upstream_port = None
36
38
  self.__env = [SERVICE_NAME_ENV, WORKFLOW_NAME_ENV]
37
39
  self.__service_name = os.path.basename(service_path)
38
40
  self.__working_dir = os.path.join(
39
- STOOBLY_HOME_DIR, DATA_DIR_NAME, DOCKER_NAMESPACE, SERVICE_NAME, WORKFLOW_NAME
41
+ STOOBLY_HOME_DIR, DATA_DIR_NAME, SERVICES_NAMESPACE, SERVICE_NAME, WORKFLOW_NAME
40
42
  )
41
43
 
42
44
  @property
@@ -55,6 +57,10 @@ class ServiceBuilder(Builder):
55
57
  def configure_base_service(self):
56
58
  return self.services.get(self.configure_base)
57
59
 
60
+ @property
61
+ def upstream_port(self) -> int:
62
+ return self.__upstream_port
63
+
58
64
  @property
59
65
  def extends_service(self):
60
66
  if self.config.detached:
@@ -99,9 +105,6 @@ class ServiceBuilder(Builder):
99
105
  environment = { **self.env_dict() }
100
106
  labels = [
101
107
  '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
108
  ]
106
109
  volumes = []
107
110
 
@@ -109,7 +112,14 @@ class ServiceBuilder(Builder):
109
112
  self.__with_detached_volumes(volumes)
110
113
 
111
114
  if self.config.tls:
112
- labels.append(f"traefik.http.routers.{service_id}.tls=true")
115
+ labels.append(f"traefik.tcp.routers.{service_id}.entrypoints={SERVICE_PORT}")
116
+ labels.append(f"traefik.tcp.routers.{service_id}.rule=HostSNI(`{SERVICE_HOSTNAME}`)")
117
+ labels.append(f"traefik.tcp.routers.{service_id}.tls.passthrough=true")
118
+ labels.append(f"traefik.tcp.services.{service_id}.loadbalancer.server.port={SERVICE_PORT}")
119
+ else:
120
+ labels.append(f"traefik.http.routers.{service_id}.entrypoints={SERVICE_PORT}")
121
+ labels.append(f"traefik.http.routers.{service_id}.rule=Host(`{SERVICE_HOSTNAME}`)")
122
+ labels.append(f"traefik.http.services.{service_id}.loadbalancer.server.port={SERVICE_PORT}")
113
123
 
114
124
  base = {
115
125
  'environment': environment,
@@ -178,6 +188,12 @@ class ServiceBuilder(Builder):
178
188
  env[e] = '${' + e + '}'
179
189
  return env
180
190
 
191
+ def with_upstream_port(self, v: int):
192
+ if not isinstance(v, int):
193
+ return self
194
+ self.__upstream_port = v
195
+ return self
196
+
181
197
  def with_env(self, v: List[str]):
182
198
  if not isinstance(v, list):
183
199
  return self
@@ -205,13 +221,23 @@ class ServiceBuilder(Builder):
205
221
  volumes.append(f"{self.service_name}:{STOOBLY_HOME_DIR}/{DATA_DIR_NAME}")
206
222
 
207
223
  # Mount docker folder
208
- volumes.append(f"../:{STOOBLY_HOME_DIR}/{DATA_DIR_NAME}/{DOCKER_NAMESPACE}")
224
+ volumes.append(f"../:{STOOBLY_HOME_DIR}/{DATA_DIR_NAME}/{SERVICES_NAMESPACE}")
209
225
 
210
226
  def __with_url_environment(self, environment):
211
- environment[SERVICE_HOSTNAME_ENV] = SERVICE_HOSTNAME
227
+ if self.config.hostname:
228
+ environment[SERVICE_HOSTNAME_ENV] = SERVICE_HOSTNAME
212
229
 
213
230
  if self.config.scheme:
214
231
  environment[SERVICE_SCHEME_ENV] = SERVICE_SCHEME
215
232
 
216
233
  if self.config.port:
217
- environment[SERVICE_PORT_ENV] = SERVICE_PORT
234
+ environment[SERVICE_PORT_ENV] = SERVICE_PORT
235
+
236
+ if self.config.upstream_hostname:
237
+ environment[SERVICE_UPSTREAM_HOSTNAME_ENV] = SERVICE_UPSTREAM_HOSTNAME
238
+
239
+ if self.config.upstream_port:
240
+ environment[SERVICE_UPSTREAM_PORT_ENV] = SERVICE_UPSTREAM_PORT
241
+
242
+ if self.config.upstream_scheme:
243
+ environment[SERVICE_UPSTREAM_SCHEME_ENV] = SERVICE_UPSTREAM_SCHEME
@@ -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,
@@ -183,12 +165,3 @@ class WorkflowBuilder(Builder):
183
165
  compose['volumes'] = self.volumes
184
166
 
185
167
  super().write(compose)
186
-
187
- def __with_url_environment(self, environment):
188
- environment[SERVICE_HOSTNAME_ENV] = SERVICE_HOSTNAME
189
-
190
- if self.config.scheme:
191
- environment[SERVICE_SCHEME_ENV] = SERVICE_SCHEME
192
-
193
- if self.config.port:
194
- environment[SERVICE_PORT_ENV] = SERVICE_PORT