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
@@ -0,0 +1,25 @@
1
+ from ...constants import (
2
+ 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
+
19
+ if config.upstream_hostname == 'host.docker.internal':
20
+ return f"upstream:{SERVICE_UPSTREAM_SCHEME}://{SERVICE_UPSTREAM_HOSTNAME}:{SERVICE_UPSTREAM_PORT}"
21
+
22
+ if config.hostname != config.upstream_hostname or config.scheme != config.upstream_scheme or config.port != config.upstream_port:
23
+ return f"reverse:{SERVICE_UPSTREAM_SCHEME}://{SERVICE_UPSTREAM_HOSTNAME}:{SERVICE_UPSTREAM_PORT}"
24
+
25
+ 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
@@ -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,21 @@
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
- SERVICE_PROXY_MODE_ENV,
14
- SERVICE_SCHEME_ENV
15
+ SERVICE_SCHEME_ENV,
16
+ SERVICE_UPSTREAM_HOSTNAME_ENV,
17
+ SERVICE_UPSTREAM_PORT_ENV,
18
+ SERVICE_UPSTREAM_SCHEME_ENV,
15
19
  )
16
20
 
17
21
  class ServiceConfig(Config):
@@ -21,10 +25,14 @@ class ServiceConfig(Config):
21
25
 
22
26
  self.__detached = None
23
27
  self.__hostname = None
28
+ self.__local = None
29
+ self.__name = None
24
30
  self.__port = None
25
31
  self.__priority = None
26
- self.__proxy_mode = None
27
32
  self.__scheme = None
33
+ self.__upstream_hostname = None
34
+ self.__upstream_port = None
35
+ self.__upstream_scheme = None
28
36
 
29
37
  self.load()
30
38
 
@@ -34,18 +42,32 @@ class ServiceConfig(Config):
34
42
  if 'hostname' in kwargs:
35
43
  self.__hostname = kwargs.get('hostname')
36
44
 
45
+ if 'local' in kwargs:
46
+ self.__local = kwargs.get('local')
47
+
48
+ if 'name' in kwargs:
49
+ self.__name = kwargs.get('name')
50
+ elif 'service_name' in kwargs:
51
+ self.__name = kwargs.get('service_name')
52
+
37
53
  if 'port' in kwargs:
38
54
  self.__port = kwargs.get('port')
39
55
 
40
56
  if 'priority' in kwargs:
41
57
  self.__priority = kwargs.get('priority')
42
58
 
43
- if 'proxy_mode' in kwargs:
44
- self.__proxy_mode = kwargs.get('proxy_mode')
45
-
46
59
  if 'scheme' in kwargs:
47
60
  self.__scheme = kwargs.get('scheme')
48
61
 
62
+ if 'upstream_hostname' in kwargs:
63
+ self.__upstream_hostname = kwargs.get('upstream_hostname')
64
+
65
+ if 'upstream_port' in kwargs:
66
+ self.__upstream_port = kwargs.get('upstream_port')
67
+
68
+ if 'upstream_scheme' in kwargs:
69
+ self.__upstream_scheme = kwargs.get('upstream_scheme')
70
+
49
71
  @property
50
72
  def detached(self) -> bool:
51
73
  return not not self.__detached
@@ -66,12 +88,36 @@ class ServiceConfig(Config):
66
88
  def hostname(self, v):
67
89
  self.__hostname = v
68
90
 
91
+ @property
92
+ def local(self):
93
+ return self.__local
94
+
95
+ @local.setter
96
+ def local(self, v: bool):
97
+ self.__local = not not v
98
+
99
+ @property
100
+ def name(self):
101
+ return self.__name
102
+
103
+ @name.setter
104
+ def name(self, v: str):
105
+ if not v:
106
+ return
107
+
108
+ SERVICE_NAME_PATTERN = re.compile(r'^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$')
109
+
110
+ if not bool(SERVICE_NAME_PATTERN.fullmatch(v)):
111
+ raise ValueError(f"{v} must match {SERVICE_NAME_PATTERN}")
112
+
113
+ self.__name = v
114
+
69
115
  @property
70
116
  def port(self) -> int:
71
117
  if not self.__port:
72
- if self.scheme == 'https':
118
+ if self.__scheme == 'https':
73
119
  return 443
74
- elif self.scheme == 'http':
120
+ elif self.__scheme == 'http':
75
121
  return 80
76
122
 
77
123
  return self.__port
@@ -83,6 +129,8 @@ class ServiceConfig(Config):
83
129
  else:
84
130
  v = int(v)
85
131
  if v > 0 and v <= 65535:
132
+ if v == self.__upstream_port and self.local:
133
+ raise ValueError('port cannot be the same as upstream port')
86
134
  self.__port = v
87
135
 
88
136
  @property
@@ -100,14 +148,59 @@ class ServiceConfig(Config):
100
148
  self.__priority = v
101
149
 
102
150
  @property
103
- def proxy_mode(self) -> str:
104
- if self.__proxy_mode:
105
- return (self.__proxy_mode or '').strip()
151
+ def scheme(self):
152
+ if not self.__scheme and self.__port:
153
+ if self.port == 443:
154
+ return 'https'
155
+ else:
156
+ return 'http'
157
+
158
+ return (self.__scheme or 'https').strip()
159
+
160
+ @scheme.setter
161
+ def scheme(self, v):
162
+ if v and not v in ['http', 'https']:
163
+ raise ValueError('scheme must be http or https')
164
+ self.__scheme = v
165
+
166
+ @property
167
+ def tls(self) -> bool:
168
+ return self.__scheme == 'https'
106
169
 
107
- if not self.hostname:
108
- return ''
170
+ @property
171
+ def upstream_hostname(self) -> int:
172
+ if self.local:
173
+ return 'host.docker.internal'
174
+ return self.__upstream_hostname or self.hostname
109
175
 
110
- return f"reverse:{self.url}"
176
+ @upstream_hostname.setter
177
+ def upstream_hostname(self, v: str):
178
+ self.__upstream_hostname = v
179
+
180
+ @property
181
+ def upstream_port(self) -> int:
182
+ return self.__upstream_port or self.port
183
+
184
+ @upstream_port.setter
185
+ def upstream_port(self, v: int):
186
+ if v == None:
187
+ self.__upstream_port = v
188
+ else:
189
+ v = int(v)
190
+ if v > 0 and v <= 65535:
191
+ if v == self.__port and self.local:
192
+ raise ValueError('upstream port cannot be the same as port')
193
+ self.__upstream_port = v
194
+ else:
195
+ raise ValueError('upstream port must be between 0 and 65535')
196
+
197
+ @property
198
+ def upstream_scheme(self):
199
+ return self.__upstream_scheme or self.scheme
200
+
201
+ @upstream_scheme.setter
202
+ def upstream_scheme(self, v):
203
+ self.__upstream_scheme = v
111
204
 
112
205
  @property
113
206
  def url(self):
@@ -124,40 +217,32 @@ class ServiceConfig(Config):
124
217
 
125
218
  return f"{_url}:{self.port}"
126
219
 
127
- @proxy_mode.setter
128
- def proxy_mode(self, v):
129
- self.__proxy_mode = v
130
-
131
- @property
132
- def scheme(self):
133
- return (self.__scheme or 'https').strip()
134
-
135
- @scheme.setter
136
- def scheme(self, v):
137
- self.__scheme = v
138
-
139
- @property
140
- def tls(self) -> bool:
141
- return self.__scheme == 'https'
142
-
143
220
  def load(self, config = None):
144
221
  config = config or self.read()
145
222
 
146
223
  self.detached = config.get(SERVICE_DETACHED_ENV)
147
224
  self.hostname = config.get(SERVICE_HOSTNAME_ENV)
225
+ self.local = config.get(SERVICE_LOCAL_ENV)
226
+ self.name = config.get(SERVICE_NAME_ENV)
148
227
  self.port = config.get(SERVICE_PORT_ENV)
149
228
  self.priority = config.get(SERVICE_PRIORITY_ENV)
150
- self.proxy_mode = config.get(SERVICE_PROXY_MODE_ENV)
151
229
  self.scheme = config.get(SERVICE_SCHEME_ENV)
230
+ self.upstream_hostname = config.get(SERVICE_UPSTREAM_HOSTNAME_ENV)
231
+ self.upstream_port = config.get(SERVICE_UPSTREAM_PORT_ENV)
232
+ self.upstream_scheme = config.get(SERVICE_UPSTREAM_SCHEME_ENV)
152
233
 
153
234
  def to_dict(self):
154
235
  return {
155
236
  'detached': self.detached,
156
237
  'hostname': self.hostname,
238
+ 'local': self.local,
239
+ 'name': self.name,
157
240
  'port': self.port,
158
241
  'priority': self.priority,
159
- 'proxy_mode': self.proxy_mode,
160
242
  'scheme': self.scheme if self.hostname else '',
243
+ 'upstream_hostname': self.upstream_hostname,
244
+ 'upstream_port': self.upstream_port,
245
+ 'upstream_scheme': self.upstream_scheme,
161
246
  }
162
247
 
163
248
  def write(self):
@@ -166,6 +251,12 @@ class ServiceConfig(Config):
166
251
  if self.hostname:
167
252
  config[SERVICE_HOSTNAME_ENV] = self.hostname
168
253
 
254
+ if self.local:
255
+ config[SERVICE_LOCAL_ENV] = True
256
+
257
+ if self.name:
258
+ config[SERVICE_NAME_ENV] = self.name
259
+
169
260
  if self.port:
170
261
  config[SERVICE_PORT_ENV] = self.port
171
262
 
@@ -175,8 +266,16 @@ class ServiceConfig(Config):
175
266
  if self.scheme:
176
267
  config[SERVICE_SCHEME_ENV] = self.scheme
177
268
 
269
+ if self.upstream_hostname:
270
+ config[SERVICE_UPSTREAM_HOSTNAME_ENV] = self.upstream_hostname
271
+
272
+ if self.upstream_port:
273
+ config[SERVICE_UPSTREAM_PORT_ENV] = self.upstream_port
274
+
275
+ if self.upstream_scheme:
276
+ config[SERVICE_UPSTREAM_SCHEME_ENV] = self.upstream_scheme
277
+
178
278
  config[SERVICE_DETACHED_ENV] = bool(self.detached)
179
279
  config[SERVICE_ID_ENV] = self.id
180
- config[SERVICE_PROXY_MODE_ENV] = self.proxy_mode
181
280
 
182
281
  super().write(config)
@@ -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,6 +1,6 @@
1
+ from stoobly_agent.app.cli.scaffold.constants import SERVICES_NAMESPACE
1
2
  from stoobly_agent.config.data_dir import DataDir
2
3
 
3
-
4
4
  class ServiceDockerCompose():
5
5
  def __init__(self, app_dir_path, target_workflow_name, service_name, hostname):
6
6
  self.service_name = service_name
@@ -11,5 +11,5 @@ class ServiceDockerCompose():
11
11
  self.configure_container_name = f"{target_workflow_name}-{service_name}.configure-1"
12
12
 
13
13
  data_dir_path = DataDir.instance(app_dir_path).path
14
- self.docker_compose_path = f"{data_dir_path}/docker/{service_name}/{target_workflow_name}/docker-compose.yml"
15
- self.init_script_path = f"{data_dir_path}/docker/{service_name}/{target_workflow_name}/bin/init"
14
+ self.docker_compose_path = f"{data_dir_path}/{SERVICES_NAMESPACE}/{service_name}/{target_workflow_name}/docker-compose.yml"
15
+ self.init_script_path = f"{data_dir_path}/{SERVICES_NAMESPACE}/{service_name}/{target_workflow_name}/bin/init"
@@ -12,11 +12,12 @@ from docker.models.containers import Container
12
12
 
13
13
  from stoobly_agent.app.cli.scaffold.constants import (
14
14
  PUBLIC_FOLDER_NAME,
15
+ SERVICES_NAMESPACE,
15
16
  STOOBLY_DATA_DIR,
16
17
  WORKFLOW_RECORD_TYPE,
17
18
  WORKFLOW_TEST_TYPE,
18
19
  )
19
- from stoobly_agent.app.cli.scaffold.docker.constants import APP_EGRESS_NETWORK_TEMPLATE
20
+ from stoobly_agent.app.cli.scaffold.docker.constants import APP_INGRESS_NETWORK_TEMPLATE
20
21
  from stoobly_agent.app.cli.scaffold.hosts_file_manager import HostsFileManager
21
22
  from stoobly_agent.app.cli.scaffold.service_command import ServiceCommand
22
23
  from stoobly_agent.app.cli.scaffold.service_docker_compose import ServiceDockerCompose
@@ -118,12 +119,14 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
118
119
  print(f"Validating hostname inside Docker network, url: {url}")
119
120
 
120
121
  # See WorkflowRunCommand for how 'network' is generated
122
+ # Debug command=f"curl -k --max-time {timeout_seconds} {url} --verbose",
121
123
  network = f"{self.workflow_name}.{self.app.network}"
122
124
  timeout_seconds = 1
125
+ http_code_format = '"%{http_code}"'
123
126
  output = self.docker_client.containers.run(
124
127
  image='curlimages/curl:8.11.0',
125
- command=f"curl --max-time {timeout_seconds} {url} --verbose",
126
- network=APP_EGRESS_NETWORK_TEMPLATE.format(network=network),
128
+ command=f"curl -k -s -o /dev/null -w {http_code_format} --max-time {timeout_seconds} {url}",
129
+ network=APP_INGRESS_NETWORK_TEMPLATE.format(network=network),
127
130
  stderr=True,
128
131
  remove=True,
129
132
  )
@@ -131,7 +134,7 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
131
134
  # Note: 499 error could also mean success because it shows the proxy
132
135
  # connection is working, but we haven't recorded anything yet
133
136
  logs = output.decode('ascii')
134
- if ('200 OK' not in logs) and ('499' not in logs):
137
+ if logs != '200' and logs != '499':
135
138
  raise ScaffoldValidateException(f"Error reaching {url} from inside Docker network")
136
139
 
137
140
  # Check public folder exists in container
@@ -190,14 +193,14 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
190
193
  def validate(self) -> bool:
191
194
  print(f"Validating service: {self.service_name}")
192
195
 
193
- url = f"{self.service_config.scheme}://{self.hostname}"
196
+ url = self.service_config.url
194
197
 
195
198
  if self.service_config.hostname and self.workflow_name not in [WORKFLOW_TEST_TYPE]:
196
199
  self.validate_hostname(self.hostname, self.service_config.port)
197
200
 
198
201
  # Test workflow won't expose services that are detached and have a hostname to the host such as assets.
199
202
  # Need to test connection from inside the Docker network
200
- if self.service_config.hostname and self.workflow_name == WORKFLOW_TEST_TYPE and self.service_config.detached:
203
+ if self.service_config.hostname and self.workflow_name == WORKFLOW_TEST_TYPE:
201
204
  self.validate_internal_hostname(url)
202
205
 
203
206
  self.validate_init_containers(self.service_docker_compose.init_container_name, self.service_docker_compose.configure_container_name)
@@ -231,7 +234,7 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
231
234
  if self.is_local():
232
235
  print(f"Validating local user defined service: {self.service_name}")
233
236
  # Validate docker-compose path exists
234
- docker_compose_path = f"{self.app_dir_path}/{DATA_DIR_NAME}/docker/{self.service_docker_compose.service_name}/{self.workflow_name}/docker-compose.yml"
237
+ docker_compose_path = f"{self.app_dir_path}/{DATA_DIR_NAME}/{SERVICES_NAMESPACE}/{self.service_docker_compose.service_name}/{self.workflow_name}/docker-compose.yml"
235
238
  destination_path = Path(docker_compose_path)
236
239
 
237
240
  if not destination_path.exists():
@@ -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
 
@@ -6,7 +6,7 @@ services:
6
6
  extends:
7
7
  file: ../.docker-compose.base.yml
8
8
  service: context_base
9
- working_dir: /home/stoobly/.stoobly/docker/${SERVICE_NAME}/${WORKFLOW_NAME}
9
+ working_dir: /home/stoobly/.stoobly/services/${SERVICE_NAME}/${WORKFLOW_NAME}
10
10
  build.init_base:
11
11
  command:
12
12
  - ${SERVICE_SCRIPTS}/${SERVICE_NAME}/${WORKFLOW_TEMPLATE}/.init
@@ -16,4 +16,4 @@ services:
16
16
  service: context_base
17
17
  volumes:
18
18
  - ${APP_DIR}:/app
19
- working_dir: /home/stoobly/.stoobly/docker/${SERVICE_NAME}/${WORKFLOW_NAME}
19
+ working_dir: /home/stoobly/.stoobly/services/${SERVICE_NAME}/${WORKFLOW_NAME}
@@ -1,3 +1,3 @@
1
1
  #!/bin/bash
2
2
 
3
- # Add custom configuration here
3
+ # Add custom Stoobly configuration here, to learn more: https://docs.stoobly.com/core-concepts/agent/proxy-settings