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
@@ -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
+
@@ -0,0 +1,26 @@
1
+ from .builder import WorkflowBuilder
2
+
3
+ class LocalDecorator():
4
+
5
+ def __init__(self, workflow_builder: WorkflowBuilder):
6
+ self.__workflow_builder = workflow_builder
7
+
8
+ @property
9
+ def workflow_builder(self):
10
+ return self.__workflow_builder
11
+
12
+ @property
13
+ def service_builder(self):
14
+ return self.workflow_builder.service_builder
15
+
16
+ def decorate(self):
17
+ proxy_name = self.workflow_builder.proxy
18
+ services = self.workflow_builder.services
19
+
20
+ if not proxy_name in services:
21
+ services[proxy_name] = {}
22
+
23
+ if 'extra_hosts' not in services[proxy_name]:
24
+ services[proxy_name]['extra_hosts'] = []
25
+
26
+ services[proxy_name]['extra_hosts'].append('host.docker.internal:host-gateway')
@@ -1,17 +1,16 @@
1
1
  import os
2
2
  import pdb
3
3
 
4
- from ...constants import SERVICE_HOSTNAME, SERVICE_PORT, SERVICE_PROXY_MODE, STOOBLY_CERTS_DIR
4
+ from ...constants import (
5
+ SERVICE_HOSTNAME, SERVICE_PORT, STOOBLY_CERTS_DIR,
6
+ )
5
7
  from .builder import WorkflowBuilder
8
+ from .command_decorator import CommandDecorator
6
9
 
7
- class MockDecorator():
10
+ class MockDecorator(CommandDecorator):
8
11
 
9
12
  def __init__(self, workflow_builder: WorkflowBuilder):
10
- self.__workflow_builder = workflow_builder
11
-
12
- @property
13
- def workflow_builder(self):
14
- return self.__workflow_builder
13
+ super().__init__(workflow_builder)
15
14
 
16
15
  @property
17
16
  def service_builder(self):
@@ -24,7 +23,7 @@ class MockDecorator():
24
23
  '--headless',
25
24
  '--intercept',
26
25
  '--lifecycle-hooks-path', 'lifecycle_hooks.py',
27
- '--proxy-mode', SERVICE_PROXY_MODE,
26
+ '--proxy-mode', self.proxy_mode,
28
27
  '--proxy-port', f"{SERVICE_PORT}",
29
28
  '--public-directory-path', 'public',
30
29
  '--response-fixtures-path', 'fixtures.yml',
@@ -35,8 +34,8 @@ class MockDecorator():
35
34
  command.append('--certs')
36
35
  command.append(os.path.join(STOOBLY_CERTS_DIR, f"{SERVICE_HOSTNAME}-joined.pem"))
37
36
 
38
- services = self.__workflow_builder.services
39
- proxy_name = self.__workflow_builder.proxy
37
+ services = self.workflow_builder.services
38
+ proxy_name = self.workflow_builder.proxy
40
39
  proxy_service = services.get(proxy_name) or {}
41
40
 
42
41
  services[proxy_name] = {
@@ -3,17 +3,14 @@ import pdb
3
3
 
4
4
  from urllib.parse import urlparse
5
5
 
6
- from ...constants import SERVICE_HOSTNAME, SERVICE_PORT, SERVICE_PROXY_MODE, STOOBLY_CERTS_DIR
6
+ from ...constants import SERVICE_HOSTNAME, SERVICE_PORT, STOOBLY_CERTS_DIR
7
7
  from .builder import WorkflowBuilder
8
+ from .command_decorator import CommandDecorator
8
9
 
9
- class ReverseProxyDecorator():
10
+ class ReverseProxyDecorator(CommandDecorator):
10
11
 
11
12
  def __init__(self, workflow_builder: WorkflowBuilder):
12
- self.__workflow_builder = workflow_builder
13
-
14
- @property
15
- def workflow_builder(self):
16
- return self.__workflow_builder
13
+ super().__init__(workflow_builder)
17
14
 
18
15
  @property
19
16
  def service_builder(self):
@@ -25,7 +22,7 @@ class ReverseProxyDecorator():
25
22
  command = [
26
23
  '--headless',
27
24
  '--lifecycle-hooks-path', 'lifecycle_hooks.py',
28
- '--proxy-mode', SERVICE_PROXY_MODE,
25
+ '--proxy-mode', self.proxy_mode,
29
26
  '--proxy-port', f"{SERVICE_PORT}",
30
27
  '--ssl-insecure'
31
28
  ]
@@ -1,17 +1,22 @@
1
1
  # Wraps the .config.yml file in the service folder
2
2
  import hashlib
3
- import os
4
3
  import pdb
4
+ import re
5
5
 
6
6
  from .config import Config
7
7
  from .constants import (
8
8
  SERVICE_DETACHED_ENV,
9
9
  SERVICE_HOSTNAME_ENV,
10
10
  SERVICE_ID_ENV,
11
+ SERVICE_LOCAL_ENV,
12
+ SERVICE_NAME_ENV,
11
13
  SERVICE_PRIORITY_ENV,
12
14
  SERVICE_PORT_ENV,
13
15
  SERVICE_PROXY_MODE_ENV,
14
- SERVICE_SCHEME_ENV
16
+ SERVICE_SCHEME_ENV,
17
+ SERVICE_UPSTREAM_HOSTNAME_ENV,
18
+ SERVICE_UPSTREAM_PORT_ENV,
19
+ SERVICE_UPSTREAM_SCHEME_ENV,
15
20
  )
16
21
 
17
22
  class ServiceConfig(Config):
@@ -21,10 +26,15 @@ class ServiceConfig(Config):
21
26
 
22
27
  self.__detached = None
23
28
  self.__hostname = None
29
+ self.__local = None
30
+ self.__name = None
24
31
  self.__port = None
25
32
  self.__priority = None
26
33
  self.__proxy_mode = None
27
34
  self.__scheme = None
35
+ self.__upstream_hostname = None
36
+ self.__upstream_port = None
37
+ self.__upstream_scheme = None
28
38
 
29
39
  self.load()
30
40
 
@@ -34,6 +44,14 @@ class ServiceConfig(Config):
34
44
  if 'hostname' in kwargs:
35
45
  self.__hostname = kwargs.get('hostname')
36
46
 
47
+ if 'local' in kwargs:
48
+ self.__local = kwargs.get('local')
49
+
50
+ if 'name' in kwargs:
51
+ self.__name = kwargs.get('name')
52
+ elif 'service_name' in kwargs:
53
+ self.__name = kwargs.get('service_name')
54
+
37
55
  if 'port' in kwargs:
38
56
  self.__port = kwargs.get('port')
39
57
 
@@ -46,6 +64,15 @@ class ServiceConfig(Config):
46
64
  if 'scheme' in kwargs:
47
65
  self.__scheme = kwargs.get('scheme')
48
66
 
67
+ if 'upstream_hostname' in kwargs:
68
+ self.__upstream_hostname = kwargs.get('upstream_hostname')
69
+
70
+ if 'upstream_port' in kwargs:
71
+ self.__upstream_port = kwargs.get('upstream_port')
72
+
73
+ if 'upstream_scheme' in kwargs:
74
+ self.__upstream_scheme = kwargs.get('upstream_scheme')
75
+
49
76
  @property
50
77
  def detached(self) -> bool:
51
78
  return not not self.__detached
@@ -66,12 +93,36 @@ class ServiceConfig(Config):
66
93
  def hostname(self, v):
67
94
  self.__hostname = v
68
95
 
96
+ @property
97
+ def local(self):
98
+ return self.__local
99
+
100
+ @local.setter
101
+ def local(self, v: bool):
102
+ self.__local = not not v
103
+
104
+ @property
105
+ def name(self):
106
+ return self.__name
107
+
108
+ @name.setter
109
+ def name(self, v: str):
110
+ if not v:
111
+ return
112
+
113
+ SERVICE_NAME_PATTERN = re.compile(r'^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$')
114
+
115
+ if not bool(SERVICE_NAME_PATTERN.fullmatch(v)):
116
+ raise ValueError(f"{v} must match {SERVICE_NAME_PATTERN}")
117
+
118
+ self.__name = v
119
+
69
120
  @property
70
121
  def port(self) -> int:
71
122
  if not self.__port:
72
- if self.scheme == 'https':
123
+ if self.__scheme == 'https':
73
124
  return 443
74
- elif self.scheme == 'http':
125
+ elif self.__scheme == 'http':
75
126
  return 80
76
127
 
77
128
  return self.__port
@@ -83,6 +134,8 @@ class ServiceConfig(Config):
83
134
  else:
84
135
  v = int(v)
85
136
  if v > 0 and v <= 65535:
137
+ if v == self.__upstream_port and self.local:
138
+ raise ValueError('port cannot be the same as upstream port')
86
139
  self.__port = v
87
140
 
88
141
  @property
@@ -107,57 +160,112 @@ class ServiceConfig(Config):
107
160
  if not self.hostname:
108
161
  return ''
109
162
 
110
- return f"reverse:{self.url}"
111
-
112
- @property
113
- def url(self):
114
- _url = f"{self.scheme}://{self.hostname}"
115
-
116
- if not self.port:
117
- return _url
118
-
119
- if self.scheme == 'http' and self.port == 80:
120
- return _url
121
-
122
- if self.scheme == 'https' and self.port == 443:
123
- return _url
124
-
125
- return f"{_url}:{self.port}"
163
+ return 'regular'
126
164
 
127
165
  @proxy_mode.setter
128
- def proxy_mode(self, v):
166
+ def proxy_mode(self, v: str):
167
+ if v and v not in ['regular', 'reverse']:
168
+ v = 'reverse'
129
169
  self.__proxy_mode = v
130
170
 
131
171
  @property
132
172
  def scheme(self):
173
+ if not self.__scheme and self.__port:
174
+ if self.port == 443:
175
+ return 'https'
176
+ else:
177
+ return 'http'
178
+
133
179
  return (self.__scheme or 'https').strip()
134
180
 
135
181
  @scheme.setter
136
182
  def scheme(self, v):
183
+ if v and not v in ['http', 'https']:
184
+ raise ValueError('scheme must be http or https')
137
185
  self.__scheme = v
138
186
 
139
187
  @property
140
188
  def tls(self) -> bool:
141
189
  return self.__scheme == 'https'
142
190
 
191
+ @property
192
+ def upstream_hostname(self) -> int:
193
+ if self.local:
194
+ return 'host.docker.internal'
195
+ return self.__upstream_hostname or self.hostname
196
+
197
+ @upstream_hostname.setter
198
+ def upstream_hostname(self, v: str):
199
+ self.__upstream_hostname = v
200
+
201
+ @property
202
+ def upstream_port(self) -> int:
203
+ return self.__upstream_port or self.port
204
+
205
+ @upstream_port.setter
206
+ def upstream_port(self, v: int):
207
+ if v == None:
208
+ self.__upstream_port = v
209
+ else:
210
+ v = int(v)
211
+ if v > 0 and v <= 65535:
212
+ if v == self.__port and self.local:
213
+ raise ValueError('upstream port cannot be the same as port')
214
+ self.__upstream_port = v
215
+ else:
216
+ raise ValueError('upstream port must be between 0 and 65535')
217
+
218
+ @property
219
+ def upstream_scheme(self):
220
+ return self.__upstream_scheme or self.scheme
221
+
222
+ @upstream_scheme.setter
223
+ def upstream_scheme(self, v):
224
+ self.__scheme = v
225
+
226
+ @property
227
+ def url(self):
228
+ _url = f"{self.scheme}://{self.hostname}"
229
+
230
+ if not self.port:
231
+ return _url
232
+
233
+ if self.scheme == 'http' and self.port == 80:
234
+ return _url
235
+
236
+ if self.scheme == 'https' and self.port == 443:
237
+ return _url
238
+
239
+ return f"{_url}:{self.port}"
240
+
143
241
  def load(self, config = None):
144
242
  config = config or self.read()
145
243
 
146
244
  self.detached = config.get(SERVICE_DETACHED_ENV)
147
245
  self.hostname = config.get(SERVICE_HOSTNAME_ENV)
246
+ self.local = config.get(SERVICE_LOCAL_ENV)
247
+ self.name = config.get(SERVICE_NAME_ENV)
148
248
  self.port = config.get(SERVICE_PORT_ENV)
149
249
  self.priority = config.get(SERVICE_PRIORITY_ENV)
150
250
  self.proxy_mode = config.get(SERVICE_PROXY_MODE_ENV)
151
251
  self.scheme = config.get(SERVICE_SCHEME_ENV)
252
+ self.upstream_hostname = config.get(SERVICE_UPSTREAM_HOSTNAME_ENV)
253
+ self.upstream_port = config.get(SERVICE_UPSTREAM_PORT_ENV)
254
+ self.upstream_scheme = config.get(SERVICE_UPSTREAM_SCHEME_ENV)
152
255
 
153
256
  def to_dict(self):
154
257
  return {
155
258
  'detached': self.detached,
156
259
  'hostname': self.hostname,
260
+ 'local': self.local,
261
+ 'name': self.name,
157
262
  'port': self.port,
158
263
  'priority': self.priority,
159
264
  'proxy_mode': self.proxy_mode,
160
265
  'scheme': self.scheme if self.hostname else '',
266
+ 'upstream_hostname': self.upstream_hostname,
267
+ 'upstream_port': self.upstream_port,
268
+ 'upstream_scheme': self.upstream_scheme,
161
269
  }
162
270
 
163
271
  def write(self):
@@ -166,6 +274,12 @@ class ServiceConfig(Config):
166
274
  if self.hostname:
167
275
  config[SERVICE_HOSTNAME_ENV] = self.hostname
168
276
 
277
+ if self.local:
278
+ config[SERVICE_LOCAL_ENV] = True
279
+
280
+ if self.name:
281
+ config[SERVICE_NAME_ENV] = self.name
282
+
169
283
  if self.port:
170
284
  config[SERVICE_PORT_ENV] = self.port
171
285
 
@@ -175,6 +289,15 @@ class ServiceConfig(Config):
175
289
  if self.scheme:
176
290
  config[SERVICE_SCHEME_ENV] = self.scheme
177
291
 
292
+ if self.upstream_hostname:
293
+ config[SERVICE_UPSTREAM_HOSTNAME_ENV] = self.upstream_hostname
294
+
295
+ if self.upstream_port:
296
+ config[SERVICE_UPSTREAM_PORT_ENV] = self.upstream_port
297
+
298
+ if self.upstream_scheme:
299
+ config[SERVICE_UPSTREAM_SCHEME_ENV] = self.upstream_scheme
300
+
178
301
  config[SERVICE_DETACHED_ENV] = bool(self.detached)
179
302
  config[SERVICE_ID_ENV] = self.id
180
303
  config[SERVICE_PROXY_MODE_ENV] = self.proxy_mode
@@ -2,6 +2,8 @@ import os
2
2
  import pdb
3
3
  import shutil
4
4
 
5
+ from copy import deepcopy
6
+
5
7
  from .app import App
6
8
  from .constants import WORKFLOW_MOCK_TYPE, WORKFLOW_RECORD_TYPE, WORKFLOW_TEST_TYPE
7
9
  from .docker.service.builder import ServiceBuilder
@@ -14,8 +16,13 @@ class ServiceCreateCommand(ServiceCommand):
14
16
  def __init__(self, app: App, **kwargs):
15
17
  super().__init__(app, **kwargs)
16
18
 
19
+ self.__upstream_port = kwargs.get('upstream_port') or []
17
20
  self.__env_vars = kwargs.get('env') or []
18
- self.__workflows = kwargs.get('workflow') or []
21
+ self.__workflows = kwargs.get('workflow') or [WORKFLOW_RECORD_TYPE, WORKFLOW_MOCK_TYPE, WORKFLOW_TEST_TYPE]
22
+
23
+ @property
24
+ def upstream_port(self):
25
+ return self.__upstream_port
19
26
 
20
27
  @property
21
28
  def env_vars(self):
@@ -45,7 +52,9 @@ class ServiceCreateCommand(ServiceCommand):
45
52
  self.__build_with_mock_workflow(service_builder, **workflow_kwargs)
46
53
 
47
54
  if WORKFLOW_RECORD_TYPE in self.workflows:
48
- self.__build_with_record_workflow(service_builder, **workflow_kwargs)
55
+ service_builder_copy = deepcopy(service_builder)
56
+ service_builder_copy.with_upstream_port(self.upstream_port)
57
+ self.__build_with_record_workflow(service_builder_copy, **workflow_kwargs)
49
58
 
50
59
  if WORKFLOW_TEST_TYPE in self.workflows:
51
60
  self.__build_with_test_workflow(service_builder, **workflow_kwargs)
@@ -0,0 +1,51 @@
1
+ import pdb
2
+ import re
3
+
4
+ from typing import Optional
5
+
6
+ class ServiceDependency():
7
+ def __init__(self, s: str):
8
+ parts = s.rsplit(":", 2) # split from the right, at most twice
9
+ if len(parts) != 3:
10
+ raise ValueError(f"Invalid format: {s}. Expected 'IMAGE:HOSTNAME:PORT'")
11
+
12
+ self.image, self.hostname, self.port = parts
13
+
14
+ # Basic validation
15
+ if not self.image:
16
+ raise ValueError("Image name cannot be empty")
17
+ if not self.hostname:
18
+ raise ValueError("Hostname cannot be empty")
19
+ if not self.port.isdigit():
20
+ raise ValueError(f"Port must be a number, got: {self.port}")
21
+
22
+ self.port = int(self.port)
23
+
24
+ def __repr__(self):
25
+ return f"ImageHostPort(image='{self.image}', hostname='{self.hostname}', port={self.port})"
26
+
27
+ @property
28
+ def image_name(self):
29
+ return DockerImage(self.image).name
30
+
31
+ class DockerImage:
32
+ def __init__(self, ref: str):
33
+ # Regex based on Docker image spec
34
+ pattern = r"""
35
+ ^(?:(?P<registry>[^/]+?)/)? # optional registry (no slash)
36
+ (?P<name>[^:@]+(?:/[^:@]+)*) # image name (can have slashes)
37
+ (?::(?P<tag>[^@]+))? # optional :tag
38
+ (?:@(?P<digest>.+))? # optional @digest
39
+ """
40
+ match = re.match(pattern, ref, re.VERBOSE)
41
+ if not match:
42
+ raise ValueError(f"Invalid Docker image reference: {ref}")
43
+
44
+ self.registry: Optional[str] = match.group("registry")
45
+ self.name: str = match.group("name")
46
+ self.tag: Optional[str] = match.group("tag")
47
+ self.digest: Optional[str] = match.group("digest")
48
+
49
+ def __repr__(self):
50
+ return (f"DockerImage(registry={self.registry!r}, "
51
+ f"name={self.name!r}, tag={self.tag!r}, digest={self.digest!r})")
@@ -1,4 +1,4 @@
1
- FROM stoobly/agent:1.9
1
+ FROM stoobly/agent:1.10
2
2
 
3
3
  ARG USER_ID
4
4
 
@@ -1,7 +1,17 @@
1
- # Define services here
1
+ # Define custom services here
2
2
  #
3
- # Container services that are intended to be run as part of a workflow need to have the workflow name added to the 'profiles' property
4
- # e.g. If we want to run a service as part of the 'mock' workflow, then the following should be added
5
- # profiles:
6
- # - mock
7
- services: {}
3
+ # 1. Uncomment the following
4
+ # 2. Replace <SERVICE-NAME> with the name of the service, this can be found in ../config.yml
5
+ # 3. Extend as needed
6
+ #
7
+ # To learn more, see https://docs.stoobly.com/core-concepts/scaffold
8
+
9
+ #services:
10
+ # <SERVICE-NAME>.<CUSTOM-SERVICE-NAME>:
11
+ # depends_on:
12
+ # <SERVICE-NAME>.configure:
13
+ # condition: service_completed_successfully
14
+ # networks:
15
+ # app.ingress: {}
16
+ # profiles:
17
+ # - ${WORKFLOW_NAME}
@@ -1,7 +1,17 @@
1
- # Define services here
1
+ # Define custom services here
2
2
  #
3
- # Container services that are intended to be run as part of a workflow need to have the workflow name added to the 'profiles' property
4
- # e.g. If we want to run a service as part of the 'record' workflow, then the following should be added
5
- # profiles:
6
- # - record
7
- services: {}
3
+ # 1. Uncomment the following
4
+ # 2. Replace <SERVICE-NAME> with the name of the service, this can be found in ../config.yml
5
+ # 3. Extend as needed
6
+ #
7
+ # To learn more, see https://docs.stoobly.com/core-concepts/scaffold
8
+
9
+ #services:
10
+ # <SERVICE-NAME>.<CUSTOM-SERVICE-NAME>:
11
+ # depends_on:
12
+ # <SERVICE-NAME>.configure:
13
+ # condition: service_completed_successfully
14
+ # networks:
15
+ # app.egress: {}
16
+ # profiles:
17
+ # - ${WORKFLOW_NAME}
@@ -1,7 +1,17 @@
1
- # Define services here
1
+ # Define custom services here
2
2
  #
3
- # Container services that are intended to be run as part of a workflow need to have the workflow name added to the 'profiles' property
4
- # e.g. If we want to run a service as part of the 'test' workflow, then the following should be added
5
- # profiles:
6
- # - test
7
- services: {}
3
+ # 1. Uncomment the following
4
+ # 2. Replace <SERVICE-NAME> with the name of the service, this can be found in ../config.yml
5
+ # 3. Extend as needed
6
+ #
7
+ # To learn more, see https://docs.stoobly.com/core-concepts/scaffold
8
+
9
+ #services:
10
+ # <SERVICE-NAME>.<CUSTOM-SERVICE-NAME>:
11
+ # depends_on:
12
+ # <SERVICE-NAME>.configure:
13
+ # condition: service_completed_successfully
14
+ # networks:
15
+ # app.ingress: {}
16
+ # profiles:
17
+ # - ${WORKFLOW_NAME}
@@ -1,11 +1,17 @@
1
- # Define services here
1
+ # Define custom services here
2
2
  #
3
- # If a container service needs access to a Stoobly defined service,
4
- # then the container service needs to add one of the scaffolded networks to the 'networks' property
5
- # See https://docs.stoobly.com/core-concepts/scaffold
6
- #
7
- # Container services that are intended to be run as part of a workflow need to have the workflow name added to the 'profiles' property
8
- # e.g. If we want to run a service as part of the 'mock' workflow, then the following should be added
9
- # profiles:
10
- # - mock
11
- services: {}
3
+ # 1. Uncomment the following
4
+ # 2. Replace <SERVICE-NAME> with the name of the service, this can be found in ../config.yml
5
+ # 3. Extend as needed
6
+ #
7
+ # To learn more, see https://docs.stoobly.com/core-concepts/scaffold
8
+
9
+ #services:
10
+ # <SERVICE-NAME>.<CUSTOM-SERVICE-NAME>:
11
+ # depends_on:
12
+ # <SERVICE-NAME>.configure:
13
+ # condition: service_completed_successfully
14
+ # networks:
15
+ # app.ingress: {}
16
+ # profiles:
17
+ # - ${WORKFLOW_NAME}
@@ -1,11 +1,17 @@
1
- # Define services here
1
+ # Define custom services here
2
2
  #
3
- # If a container service needs access to a Stoobly defined service,
4
- # then the container service needs to add one of the scaffolded networks to the 'networks' property
5
- # See https://docs.stoobly.com/core-concepts/scaffold
6
- #
7
- # Container services that are intended to be run as part of a workflow need to have the workflow name added to the 'profiles' property
8
- # e.g. If we want to run a service as part of the 'record' workflow, then the following should be added
9
- # profiles:
10
- # - record
11
- services: {}
3
+ # 1. Uncomment the following
4
+ # 2. Replace <SERVICE-NAME> with the name of the service, this can be found in ../config.yml
5
+ # 3. Extend as needed
6
+ #
7
+ # To learn more, see https://docs.stoobly.com/core-concepts/scaffold
8
+
9
+ #services:
10
+ # <SERVICE-NAME>.<CUSTOM-SERVICE-NAME>:
11
+ # depends_on:
12
+ # <SERVICE-NAME>.configure:
13
+ # condition: service_completed_successfully
14
+ # networks:
15
+ # app.egress: {}
16
+ # profiles:
17
+ # - ${WORKFLOW_NAME}
@@ -1,11 +1,17 @@
1
- # Define services here
1
+ # Define custom services here
2
2
  #
3
- # If a container service needs access to a Stoobly defined service,
4
- # then the container service needs to add one of the scaffolded networks to the 'networks' property
5
- # See https://docs.stoobly.com/core-concepts/scaffold
6
- #
7
- # Container services that are intended to be run as part of a workflow need to have the workflow name added to the 'profiles' property
8
- # e.g. If we want to run a service as part of the 'test' workflow, then the following should be added
9
- # profiles:
10
- # - test
11
- services: {}
3
+ # 1. Uncomment the following
4
+ # 2. Replace <SERVICE-NAME> with the name of the service, this can be found in ../config.yml
5
+ # 3. Extend as needed
6
+ #
7
+ # To learn more, see https://docs.stoobly.com/core-concepts/scaffold
8
+
9
+ #services:
10
+ # <SERVICE-NAME>.<CUSTOM-SERVICE-NAME>:
11
+ # depends_on:
12
+ # <SERVICE-NAME>.configure:
13
+ # condition: service_completed_successfully
14
+ # networks:
15
+ # app.ingress: {}
16
+ # profiles:
17
+ # - ${WORKFLOW_NAME}