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.
Files changed (29) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/app/cli/scaffold/constants.py +9 -1
  3. stoobly_agent/app/cli/scaffold/docker/constants.py +6 -0
  4. stoobly_agent/app/cli/scaffold/docker/workflow/builder.py +0 -2
  5. stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +1 -1
  6. stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py +0 -1
  7. stoobly_agent/app/cli/scaffold/managed_services_docker_compose.py +9 -0
  8. stoobly_agent/app/cli/scaffold/service_command.py +3 -1
  9. stoobly_agent/app/cli/scaffold/service_config.py +2 -1
  10. stoobly_agent/app/cli/scaffold/service_docker_compose.py +15 -0
  11. stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py +234 -0
  12. stoobly_agent/app/cli/scaffold/templates/constants.py +3 -3
  13. stoobly_agent/app/cli/scaffold/validate_command.py +59 -0
  14. stoobly_agent/app/cli/scaffold/validate_exceptions.py +5 -0
  15. stoobly_agent/app/cli/scaffold/workflow.py +22 -1
  16. stoobly_agent/app/cli/scaffold/workflow_run_command.py +2 -1
  17. stoobly_agent/app/cli/scaffold/workflow_validate_command.py +94 -0
  18. stoobly_agent/app/cli/scaffold_cli.py +40 -2
  19. stoobly_agent/config/data_dir.py +14 -6
  20. stoobly_agent/test/app/cli/scaffold/e2e_test.py +428 -0
  21. stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
  22. stoobly_agent/test/mock_data/scaffold/docker-compose-assets-service.yml +18 -0
  23. stoobly_agent/test/mock_data/scaffold/docker-compose-local-service.yml +16 -0
  24. stoobly_agent/test/mock_data/scaffold/index.html +12 -0
  25. {stoobly_agent-1.0.6.dist-info → stoobly_agent-1.0.8.dist-info}/METADATA +2 -1
  26. {stoobly_agent-1.0.6.dist-info → stoobly_agent-1.0.8.dist-info}/RECORD +29 -19
  27. {stoobly_agent-1.0.6.dist-info → stoobly_agent-1.0.8.dist-info}/LICENSE +0 -0
  28. {stoobly_agent-1.0.6.dist-info → stoobly_agent-1.0.8.dist-info}/WHEEL +0 -0
  29. {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.6'
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,
@@ -37,4 +37,4 @@ class MockDecorator():
37
37
  services[proxy_name] = {
38
38
  **proxy_service,
39
39
  **{ 'command': command, 'hostname': f"{SERVICE_HOSTNAME}" },
40
- }
40
+ }
@@ -62,4 +62,3 @@ class ReverseProxyDecorator():
62
62
 
63
63
  services[proxy_name] = service
64
64
 
65
-
@@ -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
+
@@ -0,0 +1,5 @@
1
+
2
+
3
+ class ScaffoldValidateException(Exception):
4
+ pass
5
+
@@ -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))
@@ -137,4 +137,5 @@ class WorkflowRunCommand(WorkflowCommand):
137
137
 
138
138
  env_vars = self.config(_config)
139
139
  env_path = self.workflow_env_path
140
- Env(env_path).write(env_vars)
140
+ Env(env_path).write(env_vars)
141
+
@@ -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
+