stoobly-agent 1.0.6__py3-none-any.whl → 1.0.8__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/cli/scaffold/constants.py +9 -1
- stoobly_agent/app/cli/scaffold/docker/constants.py +6 -0
- stoobly_agent/app/cli/scaffold/docker/workflow/builder.py +0 -2
- stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +1 -1
- stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py +0 -1
- stoobly_agent/app/cli/scaffold/managed_services_docker_compose.py +9 -0
- stoobly_agent/app/cli/scaffold/service_command.py +3 -1
- stoobly_agent/app/cli/scaffold/service_config.py +2 -1
- stoobly_agent/app/cli/scaffold/service_docker_compose.py +15 -0
- stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py +234 -0
- stoobly_agent/app/cli/scaffold/templates/constants.py +3 -3
- stoobly_agent/app/cli/scaffold/validate_command.py +59 -0
- stoobly_agent/app/cli/scaffold/validate_exceptions.py +5 -0
- stoobly_agent/app/cli/scaffold/workflow.py +22 -1
- stoobly_agent/app/cli/scaffold/workflow_run_command.py +2 -1
- stoobly_agent/app/cli/scaffold/workflow_validate_command.py +94 -0
- stoobly_agent/app/cli/scaffold_cli.py +40 -2
- stoobly_agent/config/data_dir.py +14 -6
- stoobly_agent/test/app/cli/scaffold/e2e_test.py +428 -0
- stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
- stoobly_agent/test/mock_data/scaffold/docker-compose-assets-service.yml +18 -0
- stoobly_agent/test/mock_data/scaffold/docker-compose-local-service.yml +16 -0
- stoobly_agent/test/mock_data/scaffold/index.html +12 -0
- {stoobly_agent-1.0.6.dist-info → stoobly_agent-1.0.8.dist-info}/METADATA +2 -1
- {stoobly_agent-1.0.6.dist-info → stoobly_agent-1.0.8.dist-info}/RECORD +29 -19
- {stoobly_agent-1.0.6.dist-info → stoobly_agent-1.0.8.dist-info}/LICENSE +0 -0
- {stoobly_agent-1.0.6.dist-info → stoobly_agent-1.0.8.dist-info}/WHEEL +0 -0
- {stoobly_agent-1.0.6.dist-info → stoobly_agent-1.0.8.dist-info}/entry_points.txt +0 -0
stoobly_agent/__init__.py
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
COMMAND = 'stoobly-agent'
|
2
|
-
VERSION = '1.0.
|
2
|
+
VERSION = '1.0.8'
|
@@ -1,5 +1,8 @@
|
|
1
1
|
from typing import Literal
|
2
2
|
|
3
|
+
from stoobly_agent.config.data_dir import DATA_DIR_NAME
|
4
|
+
|
5
|
+
|
3
6
|
APP_NETWORK_ENV = 'APP_NETWORK'
|
4
7
|
CA_CERTS_DIR_ENV = 'CA_CERTS_DIR'
|
5
8
|
CERTS_DIR_ENV = 'CERTS_DIR'
|
@@ -26,11 +29,16 @@ SERVICE_PORT = '${SERVICE_PORT}'
|
|
26
29
|
SERVICE_PORT_ENV = 'SERVICE_PORT'
|
27
30
|
SERVICE_PRIORITY_ENV = 'SERVICE_PRIORITY'
|
28
31
|
STOOBLY_HOME_DIR = '/home/stoobly'
|
32
|
+
STOOBLY_DATA_DIR = f"{STOOBLY_HOME_DIR}/{DATA_DIR_NAME}"
|
29
33
|
USER_ID_ENV = 'USER_ID'
|
34
|
+
VIRTUAL_HOST_ENV = 'VIRTUAL_HOST'
|
35
|
+
VIRTUAL_PORT_ENV = 'VIRTUAL_PORT'
|
36
|
+
VIRTUAL_PROTO_ENV = 'VIRTUAL_PROTO'
|
30
37
|
WORKFLOW_CUSTOM_FILTER = 'custom'
|
31
38
|
WORKFLOW_MOCK_TYPE = 'mock'
|
32
39
|
WORKFLOW_NAME_ENV = 'WORKFLOW_NAME'
|
33
40
|
WORKFLOW_RECORD_TYPE = 'record'
|
34
41
|
WORKFLOW_TEST_TYPE = 'test'
|
35
42
|
|
36
|
-
WORKFLOW_TEMPLATE = Literal[WORKFLOW_MOCK_TYPE, WORKFLOW_RECORD_TYPE, WORKFLOW_TEST_TYPE]
|
43
|
+
WORKFLOW_TEMPLATE = Literal[WORKFLOW_MOCK_TYPE, WORKFLOW_RECORD_TYPE, WORKFLOW_TEST_TYPE]
|
44
|
+
|
@@ -5,3 +5,9 @@ DOCKERFILE_CONTEXT = '.Dockerfile.context'
|
|
5
5
|
DOCKERFILE_PROXY = '.Dockerfile.proxy'
|
6
6
|
DOCKERFILE_SERVICE = 'Dockerfile.source'
|
7
7
|
GATEWAY_NETWORK = 'gateway'
|
8
|
+
|
9
|
+
# TODO: add scaffold container name templates here
|
10
|
+
|
11
|
+
# Example:
|
12
|
+
# COMPOSE_TEMPLATE = 'docker-compose.{workflow}.yml'
|
13
|
+
|
@@ -182,7 +182,6 @@ class WorkflowBuilder(Builder):
|
|
182
182
|
|
183
183
|
depends_on = {}
|
184
184
|
environment = { **self.env_dict() }
|
185
|
-
extra_hosts = []
|
186
185
|
networks = [self.service_builder.service_name]
|
187
186
|
volumes = []
|
188
187
|
|
@@ -190,7 +189,6 @@ class WorkflowBuilder(Builder):
|
|
190
189
|
'build': self.proxy_build,
|
191
190
|
'depends_on': depends_on,
|
192
191
|
'environment': environment,
|
193
|
-
'extra_hosts': extra_hosts,
|
194
192
|
'extends': self.service_builder.build_extends_proxy_base(self.dir_path),
|
195
193
|
'networks': networks,
|
196
194
|
'profiles': self.profiles,
|
@@ -0,0 +1,9 @@
|
|
1
|
+
|
2
|
+
class ManagedServicesDockerCompose():
|
3
|
+
def __init__(self, target_workflow_name):
|
4
|
+
self.init_container_name = f"{target_workflow_name}-build.init-1"
|
5
|
+
self.configure_container_name = f"{target_workflow_name}-build.configure-1"
|
6
|
+
self.gateway_container_name = f"{target_workflow_name}-gateway.service-1"
|
7
|
+
self.mock_ui_container_name = f"{target_workflow_name}-stoobly_ui.service-1"
|
8
|
+
self.entrypoint_init_container_name = f"{target_workflow_name}-entrypoint.init-1"
|
9
|
+
self.entrypoint_configure_container_name = f"{target_workflow_name}-entrypoint.configure-1"
|
@@ -5,6 +5,7 @@ from .app import App
|
|
5
5
|
from .app_command import AppCommand
|
6
6
|
from .service_config import ServiceConfig
|
7
7
|
|
8
|
+
|
8
9
|
class ServiceCommand(AppCommand):
|
9
10
|
|
10
11
|
def __init__(self, app: App, **kwargs):
|
@@ -47,4 +48,5 @@ class ServiceCommand(AppCommand):
|
|
47
48
|
_config = self.app_config.read()
|
48
49
|
_config.update(self.service_config.read())
|
49
50
|
_config.update(_c)
|
50
|
-
return _config
|
51
|
+
return _config
|
52
|
+
|
@@ -210,4 +210,5 @@ class ServiceConfig(Config):
|
|
210
210
|
|
211
211
|
# Split the DNS servers string into a list
|
212
212
|
dns_servers = match[0].strip().split("\n")
|
213
|
-
return list(map(lambda dns_server: dns_server.strip(), dns_servers))
|
213
|
+
return list(map(lambda dns_server: dns_server.strip(), dns_servers))
|
214
|
+
|
@@ -0,0 +1,15 @@
|
|
1
|
+
from stoobly_agent.config.data_dir import DataDir
|
2
|
+
|
3
|
+
|
4
|
+
class ServiceDockerCompose():
|
5
|
+
def __init__(self, app_dir_path, target_workflow_name, service_name, hostname):
|
6
|
+
self.service_name = service_name
|
7
|
+
self.hostname = hostname
|
8
|
+
self.container_name = f"{target_workflow_name}-{service_name}-1"
|
9
|
+
self.proxy_container_name = f"{target_workflow_name}-{service_name}.proxy-1"
|
10
|
+
self.init_container_name = f"{target_workflow_name}-{service_name}.init-1"
|
11
|
+
self.configure_container_name = f"{target_workflow_name}-{service_name}.configure-1"
|
12
|
+
|
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"
|
@@ -0,0 +1,234 @@
|
|
1
|
+
import os
|
2
|
+
import pdb
|
3
|
+
import ssl
|
4
|
+
from collections import Counter
|
5
|
+
from pathlib import Path
|
6
|
+
|
7
|
+
import requests
|
8
|
+
import yaml
|
9
|
+
from docker.models.containers import Container
|
10
|
+
from requests.adapters import HTTPAdapter
|
11
|
+
from urllib3 import Retry
|
12
|
+
|
13
|
+
from stoobly_agent.app.cli.scaffold.constants import (
|
14
|
+
FIXTURES_FOLDER_NAME,
|
15
|
+
STOOBLY_DATA_DIR,
|
16
|
+
STOOBLY_HOME_DIR,
|
17
|
+
VIRTUAL_HOST_ENV,
|
18
|
+
VIRTUAL_PORT_ENV,
|
19
|
+
VIRTUAL_PROTO_ENV,
|
20
|
+
WORKFLOW_RECORD_TYPE,
|
21
|
+
WORKFLOW_TEST_TYPE,
|
22
|
+
)
|
23
|
+
from stoobly_agent.app.cli.scaffold.service_command import ServiceCommand
|
24
|
+
from stoobly_agent.app.cli.scaffold.service_docker_compose import ServiceDockerCompose
|
25
|
+
from stoobly_agent.app.cli.scaffold.validate_command import ValidateCommand
|
26
|
+
from stoobly_agent.app.cli.scaffold.validate_exceptions import ScaffoldValidateException
|
27
|
+
from stoobly_agent.config.data_dir import DATA_DIR_NAME
|
28
|
+
|
29
|
+
from .app import App
|
30
|
+
|
31
|
+
|
32
|
+
class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
|
33
|
+
def __init__(self, app: App, **kwargs):
|
34
|
+
ServiceCommand.__init__(self, app, **kwargs)
|
35
|
+
ValidateCommand.__init__(self)
|
36
|
+
|
37
|
+
self.workflow_name = kwargs['workflow_name']
|
38
|
+
self.hostname = self.service_config.hostname
|
39
|
+
self.service_docker_compose = ServiceDockerCompose(app_dir_path=app.dir_path, target_workflow_name=self.workflow_name, service_name=self.service_name, hostname=self.hostname)
|
40
|
+
|
41
|
+
@property
|
42
|
+
def fixtures_dir_path(self):
|
43
|
+
return os.path.join(self.workflow_path, FIXTURES_FOLDER_NAME)
|
44
|
+
|
45
|
+
@property
|
46
|
+
def workflow_path(self):
|
47
|
+
return os.path.join(
|
48
|
+
self.scaffold_dir_path,
|
49
|
+
self.workflow_relative_path
|
50
|
+
)
|
51
|
+
@property
|
52
|
+
def workflow_relative_path(self):
|
53
|
+
return os.path.join(
|
54
|
+
self.service_relative_path,
|
55
|
+
self.workflow_name
|
56
|
+
)
|
57
|
+
|
58
|
+
def is_local(self):
|
59
|
+
with open (self.service_docker_compose.docker_compose_path,'rb') as f:
|
60
|
+
docker_compose_file_content = yaml.safe_load(f)
|
61
|
+
if docker_compose_file_content and docker_compose_file_content.get('services'):
|
62
|
+
return True
|
63
|
+
|
64
|
+
# We can potentially check the port too someday
|
65
|
+
|
66
|
+
return False
|
67
|
+
|
68
|
+
def is_external(self):
|
69
|
+
return not self.is_local()
|
70
|
+
|
71
|
+
def hostname_reachable(self, url: str) -> None:
|
72
|
+
# Retry HTTP request. Source: https://stackoverflow.com/questions/15431044/can-i-set-max-retries-for-requests-request
|
73
|
+
session = requests.Session()
|
74
|
+
retries = Retry(total=5,
|
75
|
+
backoff_factor=0.1,
|
76
|
+
status_forcelist=[ 500, 502, 503, 504 ])
|
77
|
+
session.mount('http://', HTTPAdapter(max_retries=retries))
|
78
|
+
session.mount('https://', HTTPAdapter(max_retries=retries))
|
79
|
+
|
80
|
+
# Use default OpenSSL path to CA certs on the system
|
81
|
+
default_ssl_verify_paths = ssl.get_default_verify_paths()
|
82
|
+
default_capath = default_ssl_verify_paths.capath
|
83
|
+
|
84
|
+
response = session.get(url=url, verify=default_capath)
|
85
|
+
if not response.ok:
|
86
|
+
raise ScaffoldValidateException(f"Host is not reachable: {url}")
|
87
|
+
|
88
|
+
def validate_hostname(self, url: str) -> None:
|
89
|
+
print(f"Validating hostname: {url}")
|
90
|
+
self.hostname_reachable(url)
|
91
|
+
|
92
|
+
# TODO: check logs of proxy. lifecycle hook for custom logging? Does mitmproxy support json logging?
|
93
|
+
|
94
|
+
def validate_internal_hostname(self, url: str) -> None:
|
95
|
+
print(f"Validating hostname inside Docker network, url: {url}")
|
96
|
+
|
97
|
+
timeout_seconds = 1
|
98
|
+
output = self.docker_client.containers.run(
|
99
|
+
image='curlimages/curl:8.11.0',
|
100
|
+
command=f"curl --max-time {timeout_seconds} {url} --verbose",
|
101
|
+
network=self.app_config.network,
|
102
|
+
stderr=True,
|
103
|
+
remove=True,
|
104
|
+
)
|
105
|
+
|
106
|
+
# Note: 499 error could also mean success because it shows the proxy
|
107
|
+
# connection is working, but we haven't recorded anything yet
|
108
|
+
logs = output.decode('ascii')
|
109
|
+
if ('200 OK' not in logs) and ('499' not in logs):
|
110
|
+
raise ScaffoldValidateException(f"Error reaching {url} from inside Docker network")
|
111
|
+
|
112
|
+
# Check fixtures folder mounted into container
|
113
|
+
def validate_fixtures_folder(self, container: Container):
|
114
|
+
|
115
|
+
if self.workflow_name == WORKFLOW_RECORD_TYPE:
|
116
|
+
print(f"Skipping validating fixtures folder in workflow: {self.workflow_name}, container: {container.name}")
|
117
|
+
return
|
118
|
+
|
119
|
+
print(f"Validating fixtures folder in container: {container.name}")
|
120
|
+
|
121
|
+
data_dir_mounted = False
|
122
|
+
volume_mounts = container.attrs['Mounts']
|
123
|
+
|
124
|
+
for volume_mount in volume_mounts:
|
125
|
+
if volume_mount['Destination'] == STOOBLY_DATA_DIR:
|
126
|
+
data_dir_mounted = True
|
127
|
+
break
|
128
|
+
if not data_dir_mounted:
|
129
|
+
raise ScaffoldValidateException(f"Data directory is not mounted for: {container.name}")
|
130
|
+
|
131
|
+
# Only the running proxy containers will be checkable
|
132
|
+
if container.status == 'exited':
|
133
|
+
print(f"Skipping validating fixtures folder contents because container is exited: {container.name}")
|
134
|
+
return
|
135
|
+
|
136
|
+
# Check contents of fixtures folder to confirm it's shared
|
137
|
+
fixtures_folder_path = f"{STOOBLY_HOME_DIR}/{self.workflow_name}/{FIXTURES_FOLDER_NAME}"
|
138
|
+
exec_result = container.exec_run(f"ls -A {fixtures_folder_path}")
|
139
|
+
output = exec_result.output
|
140
|
+
|
141
|
+
fixtures_folder_contents_container = output.decode('ascii').split('\n')
|
142
|
+
if fixtures_folder_contents_container[-1] == '':
|
143
|
+
fixtures_folder_contents_container.pop()
|
144
|
+
fixtures_folder_contents_scaffold = os.listdir(self.fixtures_dir_path)
|
145
|
+
|
146
|
+
if Counter(fixtures_folder_contents_container) != Counter(fixtures_folder_contents_scaffold):
|
147
|
+
raise ScaffoldValidateException(f"Fixtures was not mounted properly, expected {self.fixtures_dir_path} to exist in container path {fixtures_folder_path}")
|
148
|
+
|
149
|
+
# Note: might not need this if the hostname is reachable and working
|
150
|
+
def proxy_environment_variables_exist(self, container: Container) -> None:
|
151
|
+
environment_variables = container.attrs['Config']['Env']
|
152
|
+
virtual_host_exists = False
|
153
|
+
virtual_port_exists = False
|
154
|
+
virtual_proto_exists = False
|
155
|
+
|
156
|
+
for environment_variable in environment_variables:
|
157
|
+
environment_variable_name, environment_variable_value = environment_variable.split('=')
|
158
|
+
if environment_variable_name == VIRTUAL_HOST_ENV:
|
159
|
+
virtual_host_exists = True
|
160
|
+
elif environment_variable_name == VIRTUAL_PORT_ENV:
|
161
|
+
virtual_port_exists = True
|
162
|
+
elif environment_variable_name == VIRTUAL_PROTO_ENV:
|
163
|
+
virtual_proto_exists = True
|
164
|
+
|
165
|
+
if not virtual_host_exists:
|
166
|
+
raise ScaffoldValidateException(f"VIRTUAL_HOST environment variable is missing from container: {container.name}")
|
167
|
+
if not virtual_port_exists:
|
168
|
+
raise ScaffoldValidateException(f"VIRTUAL_POST environment variable is missing from container: {container.name}")
|
169
|
+
if not virtual_proto_exists:
|
170
|
+
raise ScaffoldValidateException(f"VIRTUAL_PROTO environment variable is missing from container: {container.name}")
|
171
|
+
|
172
|
+
|
173
|
+
def validate_proxy_container(self, service_proxy_container: Container):
|
174
|
+
print(f"Validating proxy container: {service_proxy_container.name}")
|
175
|
+
if not service_proxy_container.attrs:
|
176
|
+
raise ScaffoldValidateException(f"Container attributes are missing for: {container.name}")
|
177
|
+
|
178
|
+
if not self.service_config.detached:
|
179
|
+
self.validate_fixtures_folder(service_proxy_container)
|
180
|
+
|
181
|
+
self.proxy_environment_variables_exist(service_proxy_container)
|
182
|
+
|
183
|
+
def validate_service_container(self):
|
184
|
+
pass
|
185
|
+
|
186
|
+
def validate(self) -> bool:
|
187
|
+
print(f"Validating service: {self.service_name}")
|
188
|
+
|
189
|
+
url = f"{self.service_config.scheme}://{self.hostname}"
|
190
|
+
|
191
|
+
if self.service_config.hostname and self.workflow_name not in [WORKFLOW_TEST_TYPE]:
|
192
|
+
self.validate_hostname(url)
|
193
|
+
|
194
|
+
# Test workflow won't expose services that are detached and have a hostname to the host such as assets.
|
195
|
+
# Need to test connection from inside the Docker network
|
196
|
+
if self.service_config.hostname and self.workflow_name == WORKFLOW_TEST_TYPE and self.service_config.detached:
|
197
|
+
self.validate_internal_hostname(url)
|
198
|
+
|
199
|
+
self.validate_init_containers(self.service_docker_compose.init_container_name, self.service_docker_compose.configure_container_name)
|
200
|
+
|
201
|
+
# Service init containers have a mounted dist folder unlike the core init container
|
202
|
+
init_container = self.docker_client.containers.get(self.service_docker_compose.init_container_name)
|
203
|
+
self.validate_fixtures_folder(init_container)
|
204
|
+
|
205
|
+
if self.service_config.hostname:
|
206
|
+
service_proxy_container = self.docker_client.containers.get(self.service_docker_compose.proxy_container_name)
|
207
|
+
self.validate_proxy_container(service_proxy_container)
|
208
|
+
|
209
|
+
if self.is_local():
|
210
|
+
print(f"Validating local user defined service: {self.service_name}")
|
211
|
+
# Validate docker-compose path exists
|
212
|
+
docker_compose_path = f"{self.app_dir_path}/{DATA_DIR_NAME}/docker/{self.service_docker_compose.service_name}/{self.workflow_name}/docker-compose.yml"
|
213
|
+
destination_path = Path(docker_compose_path)
|
214
|
+
if not destination_path.is_file():
|
215
|
+
raise ScaffoldValidateException(f"Docker compose path is not a file: {destination_path}")
|
216
|
+
|
217
|
+
# Validate docker-compose.yml file has the service defined
|
218
|
+
with open(destination_path) as f:
|
219
|
+
if self.service_name not in f.read():
|
220
|
+
raise ScaffoldValidateException(f"Local service is not defined in Docker Compose file: {destination_path}")
|
221
|
+
|
222
|
+
service_container = self.docker_client.containers.get(self.service_docker_compose.container_name)
|
223
|
+
if service_container.status == 'exited':
|
224
|
+
return False
|
225
|
+
|
226
|
+
if self.service_config.detached:
|
227
|
+
service_container = self.docker_client.containers.get(self.service_docker_compose.container_name)
|
228
|
+
self.validate_detached(service_container)
|
229
|
+
|
230
|
+
print(f"Done validating service: {self.service_name}, success!")
|
231
|
+
print()
|
232
|
+
|
233
|
+
return True
|
234
|
+
|
@@ -1,9 +1,9 @@
|
|
1
1
|
import os
|
2
2
|
|
3
3
|
CORE_BUILD_SERVICE_NAME = 'build'
|
4
|
-
CORE_MOCK_UI_SERVICE_NAME = 'stoobly-ui'
|
5
|
-
CORE_GATEWAY_SERVICE_NAME = 'gateway'
|
6
4
|
CORE_ENTRYPOINT_SERVICE_NAME = 'entrypoint'
|
5
|
+
CORE_GATEWAY_SERVICE_NAME = 'gateway'
|
6
|
+
CORE_MOCK_UI_SERVICE_NAME = 'stoobly-ui'
|
7
7
|
CORE_SERVICES = [
|
8
8
|
CORE_BUILD_SERVICE_NAME, CORE_ENTRYPOINT_SERVICE_NAME, CORE_MOCK_UI_SERVICE_NAME, CORE_GATEWAY_SERVICE_NAME
|
9
9
|
]
|
@@ -60,4 +60,4 @@ TEST_WORKFLOW_CUSTOM_FILES = [
|
|
60
60
|
CUSTOM_LIFECYCLE_HOOKS
|
61
61
|
]
|
62
62
|
|
63
|
-
SERVICE_HOSTNAME_BUILD_ARG = 'SERVICE_HOSTNAME'
|
63
|
+
SERVICE_HOSTNAME_BUILD_ARG = 'SERVICE_HOSTNAME'
|
@@ -0,0 +1,59 @@
|
|
1
|
+
import pdb
|
2
|
+
from time import sleep
|
3
|
+
|
4
|
+
import docker
|
5
|
+
from docker import errors as docker_errors
|
6
|
+
from docker.models.containers import Container
|
7
|
+
|
8
|
+
from stoobly_agent.app.cli.scaffold.validate_exceptions import ScaffoldValidateException
|
9
|
+
from stoobly_agent.config.data_dir import DATA_DIR_NAME
|
10
|
+
|
11
|
+
|
12
|
+
class ValidateCommand():
|
13
|
+
def __init__(self):
|
14
|
+
self.docker_client = docker.from_env()
|
15
|
+
|
16
|
+
# Some containers like init and configure can take longer than expected to finish so retry
|
17
|
+
def __get_container(self, container_name: str) -> Container:
|
18
|
+
tries = 30
|
19
|
+
for _ in range(tries):
|
20
|
+
try:
|
21
|
+
container = self.docker_client.containers.get(container_name)
|
22
|
+
return container
|
23
|
+
except docker_errors.NotFound:
|
24
|
+
sleep(0.5)
|
25
|
+
|
26
|
+
raise ScaffoldValidateException(f"Container not found: {container_name}")
|
27
|
+
|
28
|
+
def validate_init_containers(self, init_container_name, configure_container_name) -> None:
|
29
|
+
print(f"Validating setup containers: {init_container_name}, {configure_container_name}")
|
30
|
+
|
31
|
+
|
32
|
+
init_container = self.__get_container(init_container_name)
|
33
|
+
logs = init_container.logs()
|
34
|
+
if logs:
|
35
|
+
raise ScaffoldValidateException(f"Error logs potentially detected in: {init_container_name}")
|
36
|
+
if init_container.status != 'exited' or init_container.attrs['State']['ExitCode'] != 0:
|
37
|
+
raise ScaffoldValidateException(f"init container has not exited like expected: {init_container_name}")
|
38
|
+
|
39
|
+
configure_container = self.__get_container(configure_container_name)
|
40
|
+
|
41
|
+
configure_container_ran = False
|
42
|
+
if configure_container.status == 'exited' and configure_container.attrs['State']['ExitCode'] == 0:
|
43
|
+
configure_container_ran = True
|
44
|
+
if not configure_container_ran:
|
45
|
+
raise ScaffoldValidateException(f"Configure container has not ran as expected: {configure_container_name}")
|
46
|
+
|
47
|
+
def validate_detached(self, container: Container) -> None:
|
48
|
+
print(f"Validating detached for: {container.name}")
|
49
|
+
|
50
|
+
if not container.attrs:
|
51
|
+
raise ScaffoldValidateException(f"Container is missing: {container.name}")
|
52
|
+
|
53
|
+
volume_mounts = container.attrs['Mounts']
|
54
|
+
for volume_mount in volume_mounts:
|
55
|
+
if DATA_DIR_NAME in volume_mount['Source']:
|
56
|
+
return
|
57
|
+
|
58
|
+
raise ScaffoldValidateException(f"Data directory is missing from container: {container.name}")
|
59
|
+
|
@@ -1,9 +1,10 @@
|
|
1
1
|
import os
|
2
|
-
|
2
|
+
import pdb
|
3
3
|
from typing import List
|
4
4
|
|
5
5
|
from .app import App
|
6
6
|
|
7
|
+
|
7
8
|
class Workflow():
|
8
9
|
|
9
10
|
def __init__(self, workflow_name: str, app: App):
|
@@ -34,6 +35,26 @@ class Workflow():
|
|
34
35
|
def service_paths(self):
|
35
36
|
return self.app.service_paths
|
36
37
|
|
38
|
+
# TODO: merge into 1 services property
|
39
|
+
|
40
|
+
# Returns services that run in this specific workflow
|
41
|
+
@property
|
42
|
+
def services_ran(self) -> List[str]:
|
43
|
+
services_dir = os.path.join(self.app.scaffold_dir_path, self.app.namespace)
|
44
|
+
|
45
|
+
services = []
|
46
|
+
for filename in os.listdir(services_dir):
|
47
|
+
path = os.path.join(services_dir, filename)
|
48
|
+
if not os.path.isdir(path):
|
49
|
+
continue
|
50
|
+
|
51
|
+
for sub_path in os.scandir(path):
|
52
|
+
if os.path.isdir(sub_path):
|
53
|
+
if sub_path.name == self.workflow_name:
|
54
|
+
services.append(filename)
|
55
|
+
|
56
|
+
return services
|
57
|
+
|
37
58
|
def service_paths_from_services(self, services: List[str]):
|
38
59
|
app_namespace_path = self.app.namespace_path
|
39
60
|
return list(map(lambda service: os.path.join(app_namespace_path, service), services))
|
@@ -0,0 +1,94 @@
|
|
1
|
+
import pdb
|
2
|
+
|
3
|
+
from docker import errors as docker_errors
|
4
|
+
|
5
|
+
from stoobly_agent.app.cli.scaffold.constants import WORKFLOW_TEST_TYPE
|
6
|
+
from stoobly_agent.app.cli.scaffold.managed_services_docker_compose import (
|
7
|
+
ManagedServicesDockerCompose,
|
8
|
+
)
|
9
|
+
from stoobly_agent.app.cli.scaffold.templates.constants import (
|
10
|
+
CORE_ENTRYPOINT_SERVICE_NAME,
|
11
|
+
CORE_GATEWAY_SERVICE_NAME,
|
12
|
+
CORE_MOCK_UI_SERVICE_NAME,
|
13
|
+
CORE_SERVICES,
|
14
|
+
)
|
15
|
+
from stoobly_agent.app.cli.scaffold.validate_command import ValidateCommand
|
16
|
+
from stoobly_agent.app.cli.scaffold.validate_exceptions import ScaffoldValidateException
|
17
|
+
from stoobly_agent.app.cli.scaffold.workflow_command import WorkflowCommand
|
18
|
+
|
19
|
+
from .app import App
|
20
|
+
|
21
|
+
|
22
|
+
class WorkflowValidateCommand(WorkflowCommand, ValidateCommand):
|
23
|
+
def __init__(self, app: App, **kwargs):
|
24
|
+
WorkflowCommand.__init__(self, app, **kwargs)
|
25
|
+
ValidateCommand.__init__(self)
|
26
|
+
self.managed_services_docker_compose = ManagedServicesDockerCompose(target_workflow_name=self.workflow_name)
|
27
|
+
|
28
|
+
def validate_core_components(self):
|
29
|
+
print(f"Validating core component: {CORE_GATEWAY_SERVICE_NAME}")
|
30
|
+
gateway_container_name = self.managed_services_docker_compose.gateway_container_name
|
31
|
+
gateway_container = self.docker_client.containers.get(gateway_container_name)
|
32
|
+
if not gateway_container or (gateway_container.status != 'running'):
|
33
|
+
raise ScaffoldValidateException(f"Container '{gateway_container_name}' not found for service '{CORE_GATEWAY_SERVICE_NAME}'")
|
34
|
+
|
35
|
+
print(f"Validating core component: {CORE_MOCK_UI_SERVICE_NAME}")
|
36
|
+
mock_ui_container_name = self.managed_services_docker_compose.mock_ui_container_name
|
37
|
+
mock_ui_container = self.docker_client.containers.get(mock_ui_container_name)
|
38
|
+
if not mock_ui_container or (mock_ui_container.status != 'running'):
|
39
|
+
raise ScaffoldValidateException(f"Container '{mock_ui_container_name}' not found for service '{CORE_MOCK_UI_SERVICE_NAME}'")
|
40
|
+
|
41
|
+
def validate_no_core_components(self):
|
42
|
+
try:
|
43
|
+
core_gateway_container = self.docker_client.containers.get(self.managed_services_docker_compose.gateway_container_name)
|
44
|
+
if core_gateway_container:
|
45
|
+
raise ScaffoldValidateException(f"Gateway container is running when it shouldn't: {core_gateway_container.name}")
|
46
|
+
except docker_errors.NotFound:
|
47
|
+
pass
|
48
|
+
|
49
|
+
try:
|
50
|
+
core_mock_ui_container_name = self.docker_client.containers.get(self.managed_services_docker_compose.mock_ui_container_name)
|
51
|
+
if core_mock_ui_container_name:
|
52
|
+
raise ScaffoldValidateException(f"Stoobly UI container is running when it shouldn't: {core_mock_ui_container_name.name}")
|
53
|
+
except docker_errors.NotFound:
|
54
|
+
pass
|
55
|
+
|
56
|
+
print(f"Skipping validating core component: {CORE_GATEWAY_SERVICE_NAME}")
|
57
|
+
print(f"Skipping validating core component: {CORE_MOCK_UI_SERVICE_NAME}")
|
58
|
+
|
59
|
+
|
60
|
+
def validate(self) -> bool:
|
61
|
+
print(f"Validating workflow: {self.workflow_name}")
|
62
|
+
print(f"Validating core components: {CORE_SERVICES}")
|
63
|
+
|
64
|
+
if self.workflow_name == WORKFLOW_TEST_TYPE:
|
65
|
+
# Don't validate the gateway and mock_ui core components in the "test" workflow
|
66
|
+
self.validate_no_core_components()
|
67
|
+
else:
|
68
|
+
self.validate_core_components()
|
69
|
+
|
70
|
+
self.validate_init_containers(self.managed_services_docker_compose.init_container_name, self.managed_services_docker_compose.configure_container_name)
|
71
|
+
|
72
|
+
print(f"Validating core component: {CORE_ENTRYPOINT_SERVICE_NAME}")
|
73
|
+
|
74
|
+
try:
|
75
|
+
core_entrypoint_init_container_name = self.managed_services_docker_compose.entrypoint_init_container_name
|
76
|
+
entrypoint_init_container = self.docker_client.containers.get(core_entrypoint_init_container_name)
|
77
|
+
except docker_errors.NotFound:
|
78
|
+
raise ScaffoldValidateException(f"Container not found: {core_entrypoint_init_container_name}")
|
79
|
+
|
80
|
+
try:
|
81
|
+
core_entrypoint_configure_container_name = self.managed_services_docker_compose.entrypoint_configure_container_name
|
82
|
+
entrypoint_configure_container = self.docker_client.containers.get(core_entrypoint_configure_container_name)
|
83
|
+
except docker_errors.NotFound:
|
84
|
+
raise ScaffoldValidateException(f"Container not found: {core_entrypoint_configure_container_name}")
|
85
|
+
|
86
|
+
# NOTE: we should check the correct workflow mode is enabled one day
|
87
|
+
# That's not currently queryable
|
88
|
+
|
89
|
+
print(f"Done validating workflow: {self.workflow_name}, success!")
|
90
|
+
print()
|
91
|
+
|
92
|
+
return True
|
93
|
+
|
94
|
+
|
@@ -9,6 +9,7 @@ from stoobly_agent.app.cli.helpers.certificate_authority import CertificateAutho
|
|
9
9
|
from stoobly_agent.app.cli.helpers.shell import exec_stream
|
10
10
|
from stoobly_agent.app.cli.scaffold.app import App
|
11
11
|
from stoobly_agent.app.cli.scaffold.app_create_command import AppCreateCommand
|
12
|
+
from stoobly_agent.app.cli.scaffold.constants import DOCKER_NAMESPACE, WORKFLOW_MOCK_TYPE, WORKFLOW_RECORD_TYPE, WORKFLOW_TEST_TYPE
|
12
13
|
from stoobly_agent.app.cli.scaffold.constants import (
|
13
14
|
DOCKER_NAMESPACE, WORKFLOW_CUSTOM_FILTER, WORKFLOW_MOCK_TYPE, WORKFLOW_RECORD_TYPE, WORKFLOW_TEST_TYPE
|
14
15
|
)
|
@@ -17,12 +18,15 @@ from stoobly_agent.app.cli.scaffold.docker.workflow.decorators_factory import ge
|
|
17
18
|
from stoobly_agent.app.cli.scaffold.service import Service
|
18
19
|
from stoobly_agent.app.cli.scaffold.service_config import ServiceConfig
|
19
20
|
from stoobly_agent.app.cli.scaffold.service_create_command import ServiceCreateCommand
|
21
|
+
from stoobly_agent.app.cli.scaffold.service_workflow_validate_command import ServiceWorkflowValidateCommand
|
20
22
|
from stoobly_agent.app.cli.scaffold.templates.constants import CORE_SERVICES
|
23
|
+
from stoobly_agent.app.cli.scaffold.validate_exceptions import ScaffoldValidateException
|
21
24
|
from stoobly_agent.app.cli.scaffold.workflow import Workflow
|
22
25
|
from stoobly_agent.app.cli.scaffold.workflow_create_command import WorkflowCreateCommand
|
23
26
|
from stoobly_agent.app.cli.scaffold.workflow_copy_command import WorkflowCopyCommand
|
24
27
|
from stoobly_agent.app.cli.scaffold.workflow_log_command import WorkflowLogCommand
|
25
28
|
from stoobly_agent.app.cli.scaffold.workflow_run_command import WorkflowRunCommand
|
29
|
+
from stoobly_agent.app.cli.scaffold.workflow_validate_command import WorkflowValidateCommand
|
26
30
|
from stoobly_agent.config.constants import env_vars
|
27
31
|
from stoobly_agent.config.data_dir import DataDir
|
28
32
|
from stoobly_agent.lib.logger import bcolors, DEBUG, ERROR, INFO, Logger, WARNING
|
@@ -324,6 +328,9 @@ def logs(**kwargs):
|
|
324
328
|
def run(**kwargs):
|
325
329
|
cwd = os.getcwd()
|
326
330
|
|
331
|
+
# Create the certs_dir_path if it doesn't exist
|
332
|
+
DataDir.instance().certs_dir_path
|
333
|
+
|
327
334
|
if not os.getenv(env_vars.LOG_LEVEL):
|
328
335
|
os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
|
329
336
|
|
@@ -376,7 +383,37 @@ def run(**kwargs):
|
|
376
383
|
exec_stream(exec_command)
|
377
384
|
else:
|
378
385
|
print(exec_command)
|
379
|
-
|
386
|
+
|
387
|
+
@workflow.command(
|
388
|
+
help="Validate a scaffold workflow"
|
389
|
+
)
|
390
|
+
@click.option('--app-dir-path', default=os.getcwd(), help='Path to validate the app scaffold.')
|
391
|
+
@click.argument('workflow_name')
|
392
|
+
def validate(**kwargs):
|
393
|
+
app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
|
394
|
+
workflow = Workflow(kwargs['workflow_name'], app)
|
395
|
+
|
396
|
+
config = { **kwargs }
|
397
|
+
config['service_name'] = 'build'
|
398
|
+
|
399
|
+
try:
|
400
|
+
command = WorkflowValidateCommand(app, **config)
|
401
|
+
command.validate()
|
402
|
+
except ScaffoldValidateException as sve:
|
403
|
+
print(f"\nFatal Scaffold Validation Exception: {sve}", file=sys.stderr)
|
404
|
+
sys.exit(1)
|
405
|
+
|
406
|
+
try:
|
407
|
+
for service in workflow.services_ran:
|
408
|
+
if service not in CORE_SERVICES:
|
409
|
+
config['service_name'] = service
|
410
|
+
command = ServiceWorkflowValidateCommand(app, **config)
|
411
|
+
command.validate()
|
412
|
+
except ScaffoldValidateException as sve:
|
413
|
+
print(f"\nFatal Scaffold Validation Exception: {sve}", file=sys.stderr)
|
414
|
+
sys.exit(1)
|
415
|
+
|
416
|
+
|
380
417
|
scaffold.add_command(app)
|
381
418
|
scaffold.add_command(service)
|
382
419
|
scaffold.add_command(workflow)
|
@@ -431,4 +468,5 @@ def __workflow_build(app, **kwargs):
|
|
431
468
|
headless=kwargs['headless'],
|
432
469
|
template=kwargs['template'],
|
433
470
|
workflow_decorators=workflow_decorators
|
434
|
-
)
|
471
|
+
)
|
472
|
+
|