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.
- stoobly_agent/__init__.py +1 -1
- stoobly_agent/app/api/__init__.py +4 -20
- stoobly_agent/app/api/application_http_request_handler.py +5 -2
- stoobly_agent/app/api/configs_controller.py +3 -3
- stoobly_agent/app/cli/decorators/exec.py +1 -1
- stoobly_agent/app/cli/helpers/handle_config_update_service.py +4 -0
- stoobly_agent/app/cli/intercept_cli.py +40 -7
- stoobly_agent/app/cli/scaffold/app_command.py +4 -0
- stoobly_agent/app/cli/scaffold/app_config.py +21 -3
- stoobly_agent/app/cli/scaffold/app_create_command.py +109 -2
- stoobly_agent/app/cli/scaffold/constants.py +14 -3
- stoobly_agent/app/cli/scaffold/docker/constants.py +4 -6
- stoobly_agent/app/cli/scaffold/docker/service/build_decorator.py +2 -2
- stoobly_agent/app/cli/scaffold/docker/service/builder.py +36 -10
- stoobly_agent/app/cli/scaffold/docker/workflow/builder.py +0 -27
- stoobly_agent/app/cli/scaffold/docker/workflow/command_decorator.py +25 -0
- stoobly_agent/app/cli/scaffold/docker/workflow/decorators_factory.py +7 -2
- stoobly_agent/app/cli/scaffold/docker/workflow/detached_decorator.py +42 -0
- stoobly_agent/app/cli/scaffold/docker/workflow/local_decorator.py +26 -0
- stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +9 -10
- stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py +5 -8
- stoobly_agent/app/cli/scaffold/service_config.py +133 -34
- stoobly_agent/app/cli/scaffold/service_create_command.py +11 -2
- stoobly_agent/app/cli/scaffold/service_dependency.py +51 -0
- stoobly_agent/app/cli/scaffold/service_docker_compose.py +3 -3
- stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py +10 -7
- stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +1 -1
- stoobly_agent/app/cli/scaffold/templates/app/build/.docker-compose.base.yml +2 -2
- stoobly_agent/app/cli/scaffold/templates/app/build/mock/bin/configure +1 -1
- stoobly_agent/app/cli/scaffold/templates/app/build/mock/docker-compose.yml +16 -6
- stoobly_agent/app/cli/scaffold/templates/app/build/record/bin/configure +26 -1
- stoobly_agent/app/cli/scaffold/templates/app/build/record/docker-compose.yml +16 -6
- stoobly_agent/app/cli/scaffold/templates/app/build/test/bin/configure +1 -1
- stoobly_agent/app/cli/scaffold/templates/app/build/test/docker-compose.yml +16 -6
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/.docker-compose.base.yml +2 -2
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/bin/configure +1 -1
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/docker-compose.yml +16 -10
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/bin/configure +1 -1
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/docker-compose.yml +16 -10
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/bin/configure +1 -1
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/docker-compose.yml +16 -10
- stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/.docker-compose.base.yml +2 -1
- stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/mock/.docker-compose.mock.yml +6 -3
- stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/record/.docker-compose.record.yml +6 -4
- stoobly_agent/app/cli/scaffold/templates/build/workflows/record/.configure +21 -1
- stoobly_agent/app/cli/scaffold/templates/constants.py +4 -0
- stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.Dockerfile.cypress +22 -0
- stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.docker-compose.test.yml +19 -0
- stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.Dockerfile.playwright +33 -0
- stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.docker-compose.test.yml +18 -0
- stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.entrypoint.sh +11 -0
- stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/configure +2 -10
- stoobly_agent/app/cli/scaffold/templates/workflow/mock/docker-compose.yml +17 -0
- stoobly_agent/app/cli/scaffold/templates/workflow/record/bin/configure +19 -45
- stoobly_agent/app/cli/scaffold/templates/workflow/record/docker-compose.yml +17 -0
- stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/configure +2 -10
- stoobly_agent/app/cli/scaffold/templates/workflow/test/docker-compose.yml +17 -0
- stoobly_agent/app/cli/scaffold/workflow_create_command.py +0 -1
- stoobly_agent/app/cli/scaffold/workflow_run_command.py +1 -1
- stoobly_agent/app/cli/scaffold_cli.py +85 -96
- stoobly_agent/app/proxy/handle_record_service.py +12 -3
- stoobly_agent/app/proxy/handle_replay_service.py +14 -2
- stoobly_agent/app/proxy/intercept_settings.py +12 -8
- stoobly_agent/app/proxy/record/upload_request_service.py +5 -8
- stoobly_agent/app/proxy/replay/replay_request_service.py +3 -0
- stoobly_agent/app/proxy/run.py +3 -28
- stoobly_agent/app/proxy/utils/allowed_request_service.py +3 -2
- stoobly_agent/app/proxy/utils/minimize_headers.py +47 -0
- stoobly_agent/app/proxy/utils/publish_change_service.py +22 -24
- stoobly_agent/app/proxy/utils/strategy.py +16 -0
- stoobly_agent/app/settings/__init__.py +15 -6
- stoobly_agent/app/settings/data_rules.py +25 -1
- stoobly_agent/app/settings/intercept_settings.py +5 -2
- stoobly_agent/app/settings/types/__init__.py +0 -1
- stoobly_agent/app/settings/ui_settings.py +5 -5
- stoobly_agent/cli.py +41 -16
- stoobly_agent/config/constants/custom_headers.py +1 -0
- stoobly_agent/config/constants/env_vars.py +4 -3
- stoobly_agent/config/constants/record_strategy.py +6 -0
- stoobly_agent/config/data_dir.py +1 -0
- stoobly_agent/config/settings.yml.sample +2 -3
- stoobly_agent/lib/logger.py +15 -5
- stoobly_agent/public/index.html +1 -1
- stoobly_agent/public/main-es2015.5a9aa16433404c3f423a.js +1 -0
- stoobly_agent/public/main-es5.5a9aa16433404c3f423a.js +1 -0
- stoobly_agent/test/app/cli/intercept/intercept_configure_test.py +231 -1
- stoobly_agent/test/app/cli/scaffold/cli_invoker.py +3 -2
- stoobly_agent/test/app/cli/scaffold/cli_test.py +3 -3
- stoobly_agent/test/app/cli/scaffold/e2e_test.py +11 -11
- stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
- stoobly_agent/test/app/proxy/utils/minimize_headers_test.py +342 -0
- {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.1.dist-info}/METADATA +2 -1
- {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.1.dist-info}/RECORD +96 -80
- stoobly_agent/public/main-es2015.089b46f303768fbe864f.js +0 -1
- stoobly_agent/public/main-es5.089b46f303768fbe864f.js +0 -1
- {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.1.dist-info}/LICENSE +0 -0
- {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.1.dist-info}/WHEEL +0 -0
- {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(
|
22
|
+
workflow_decorators.append(DetachedDecorator if service_config.detached else MockDecorator)
|
18
23
|
workflow_decorators.append(DnsDecorator)
|
19
24
|
else:
|
20
25
|
if service_config.hostname:
|
21
|
-
workflow_decorators.append(
|
26
|
+
workflow_decorators.append(DetachedDecorator if service_config.detached else MockDecorator)
|
22
27
|
|
23
28
|
return workflow_decorators
|
@@ -0,0 +1,42 @@
|
|
1
|
+
import os
|
2
|
+
import pdb
|
3
|
+
|
4
|
+
from ...constants import SERVICE_HOSTNAME, SERVICE_PORT, STOOBLY_CERTS_DIR
|
5
|
+
from .builder import WorkflowBuilder
|
6
|
+
from .command_decorator import CommandDecorator
|
7
|
+
|
8
|
+
class DetachedDecorator(CommandDecorator):
|
9
|
+
|
10
|
+
def __init__(self, workflow_builder: WorkflowBuilder):
|
11
|
+
super().__init__(workflow_builder)
|
12
|
+
|
13
|
+
@property
|
14
|
+
def service_builder(self):
|
15
|
+
return self.workflow_builder.service_builder
|
16
|
+
|
17
|
+
def decorate(self):
|
18
|
+
config = self.service_builder.config
|
19
|
+
|
20
|
+
command = [
|
21
|
+
'--headless',
|
22
|
+
'--lifecycle-hooks-path', 'lifecycle_hooks.py',
|
23
|
+
'--proxy-mode', self.proxy_mode,
|
24
|
+
'--proxy-port', f"{SERVICE_PORT}",
|
25
|
+
'--public-directory-path', 'public',
|
26
|
+
'--response-fixtures-path', 'fixtures.yml',
|
27
|
+
'--ssl-insecure'
|
28
|
+
]
|
29
|
+
|
30
|
+
if config.scheme == 'https':
|
31
|
+
command.append('--certs')
|
32
|
+
command.append(os.path.join(STOOBLY_CERTS_DIR, f"{SERVICE_HOSTNAME}-joined.pem"))
|
33
|
+
|
34
|
+
services = self.workflow_builder.services
|
35
|
+
proxy_name = self.workflow_builder.proxy
|
36
|
+
proxy_service = services.get(proxy_name) or {}
|
37
|
+
|
38
|
+
services[proxy_name] = {
|
39
|
+
**proxy_service,
|
40
|
+
**{ 'command': command },
|
41
|
+
}
|
42
|
+
|
@@ -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
|
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
|
-
|
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',
|
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.
|
39
|
-
proxy_name = self.
|
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,
|
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
|
-
|
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',
|
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
|
-
|
14
|
-
|
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.
|
118
|
+
if self.__scheme == 'https':
|
73
119
|
return 443
|
74
|
-
elif self.
|
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
|
104
|
-
if self.
|
105
|
-
|
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
|
-
|
108
|
-
|
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
|
-
|
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
|
-
|
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}/
|
15
|
-
self.init_script_path = f"{data_dir_path}/
|
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
|
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}
|
126
|
-
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
|
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 =
|
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
|
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}/
|
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():
|
@@ -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/
|
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/
|
19
|
+
working_dir: /home/stoobly/.stoobly/services/${SERVICE_NAME}/${WORKFLOW_NAME}
|