stoobly-agent 1.0.7__py3-none-any.whl → 1.0.9__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 (34) 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/app/.Makefile +2 -2
  13. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/docker-compose.yml +14 -1
  14. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/docker-compose.yml +14 -1
  15. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/docker-compose.yml +14 -1
  16. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/bin/.run +2 -0
  17. stoobly_agent/app/cli/scaffold/templates/constants.py +3 -3
  18. stoobly_agent/app/cli/scaffold/validate_command.py +59 -0
  19. stoobly_agent/app/cli/scaffold/validate_exceptions.py +5 -0
  20. stoobly_agent/app/cli/scaffold/workflow.py +22 -1
  21. stoobly_agent/app/cli/scaffold/workflow_run_command.py +17 -3
  22. stoobly_agent/app/cli/scaffold/workflow_validate_command.py +94 -0
  23. stoobly_agent/app/cli/scaffold_cli.py +48 -5
  24. stoobly_agent/config/data_dir.py +11 -2
  25. stoobly_agent/test/app/cli/scaffold/e2e_test.py +428 -0
  26. stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
  27. stoobly_agent/test/mock_data/scaffold/docker-compose-assets-service.yml +18 -0
  28. stoobly_agent/test/mock_data/scaffold/docker-compose-local-service.yml +16 -0
  29. stoobly_agent/test/mock_data/scaffold/index.html +12 -0
  30. {stoobly_agent-1.0.7.dist-info → stoobly_agent-1.0.9.dist-info}/METADATA +2 -1
  31. {stoobly_agent-1.0.7.dist-info → stoobly_agent-1.0.9.dist-info}/RECORD +34 -24
  32. {stoobly_agent-1.0.7.dist-info → stoobly_agent-1.0.9.dist-info}/LICENSE +0 -0
  33. {stoobly_agent-1.0.7.dist-info → stoobly_agent-1.0.9.dist-info}/WHEEL +0 -0
  34. {stoobly_agent-1.0.7.dist-info → stoobly_agent-1.0.9.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.7'
2
+ VERSION = '1.0.9'
@@ -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
+
@@ -8,7 +8,7 @@
8
8
 
9
9
  # Constants
10
10
  DIR := $(dir $(realpath $(lastword $(MAKEFILE_LIST))))
11
- WORKFLOW_NAME := exec
11
+ EXEC_WORKFLOW_NAME := exec
12
12
 
13
13
  CONTEXT_DIR_DEFAULT := $(realpath $(DIR)/../..)
14
14
 
@@ -27,7 +27,7 @@ docker_compose_command=docker compose
27
27
  source_env=set -a; [ -f .env ] && source .env; set +a
28
28
 
29
29
  docker_compose_file_path=$(app_data_dir)/docker/stoobly-ui/exec/.docker-compose.exec.yml
30
- stoobly_exec_args=--profile $(WORKFLOW_NAME) -p $(WORKFLOW_NAME) up --build --remove-orphans
30
+ stoobly_exec_args=--profile $(EXEC_WORKFLOW_NAME) -p $(EXEC_WORKFLOW_NAME) up --build --remove-orphans
31
31
  stoobly_exec_env=export CONTEXT_DIR=$(context_dir) && export USER_ID=$$UID && export CA_CERTS_DIR="$(ca_certs_dir)"
32
32
  stoobly_exec=$(stoobly_exec_env) && $(source_env) && $(docker_compose_command) -f "$(docker_compose_file_path)" $(stoobly_exec_args)
33
33
 
@@ -1 +1,14 @@
1
- # Define services here
1
+ # Define networks and services here
2
+ #
3
+ # If a container service needs access to a Stoobly defined service,
4
+ # then the container service needs to add the service name to the 'networks' property
5
+ # e.g. If we have defined a 'upstream' service, then the following should be added
6
+ # networks:
7
+ # - upstream
8
+ #
9
+ # Container services that are intended to be run as part of a workflow need to have the workflow name added to the 'profiles' property
10
+ # e.g. If we want to run a service as part of the 'mock' workflow, then the following should be added
11
+ # profiles:
12
+ # - mock
13
+ networks: {}
14
+ services: {}
@@ -1 +1,14 @@
1
- # Define services here
1
+ # Define networks and services here
2
+ #
3
+ # If a container service needs access to a Stoobly defined service,
4
+ # then the container service needs to add the service name to the 'networks' property
5
+ # e.g. If we have defined a 'upstream' service, then the following should be added
6
+ # networks:
7
+ # - upstream
8
+ #
9
+ # Container services that are intended to be run as part of a workflow need to have the workflow name added to the 'profiles' property
10
+ # e.g. If we want to run a service as part of the 'record' workflow, then the following should be added
11
+ # profiles:
12
+ # - record
13
+ networks: {}
14
+ services: {}
@@ -1 +1,14 @@
1
- # Define services here
1
+ # Define networks and services here
2
+ #
3
+ # If a container service needs access to a Stoobly defined service,
4
+ # then the container service needs to add the service name to the 'networks' property
5
+ # e.g. If we have defined a 'upstream' service, then the following should be added
6
+ # networks:
7
+ # - upstream
8
+ #
9
+ # Container services that are intended to be run as part of a workflow need to have the workflow name added to the 'profiles' property
10
+ # e.g. If we want to run a service as part of the 'test' workflow, then the following should be added
11
+ # profiles:
12
+ # - test
13
+ networks: {}
14
+ services: {}
@@ -3,6 +3,8 @@
3
3
  extra_options=$EXEC_OPTIONS
4
4
  workflow=$1
5
5
 
6
+ mkdir -p .stoobly/tmp
7
+
6
8
  stoobly-agent scaffold workflow run \
7
9
  --app-dir-path "$(pwd)" \
8
10
  --dry-run \
@@ -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))
@@ -1,6 +1,8 @@
1
1
  import os
2
2
  import pdb
3
3
 
4
+ from typing import TypedDict
5
+
4
6
  from stoobly_agent.config.data_dir import DataDir
5
7
  from stoobly_agent.lib.logger import Logger
6
8
 
@@ -11,6 +13,10 @@ from .workflow_command import WorkflowCommand
11
13
 
12
14
  LOG_ID = 'WorkflowRunCommand'
13
15
 
16
+ class UpOptions(TypedDict):
17
+ attached: bool
18
+ namespace: str
19
+
14
20
  class WorkflowRunCommand(WorkflowCommand):
15
21
  def __init__(self, app: App, **kwargs):
16
22
  super().__init__(app, **kwargs)
@@ -63,7 +69,7 @@ class WorkflowRunCommand(WorkflowCommand):
63
69
  def create_network(self):
64
70
  return f"docker network create {self.network} 2> /dev/null"
65
71
 
66
- def up(self):
72
+ def up(self, **options: UpOptions):
67
73
  if not os.path.exists(self.compose_path):
68
74
  return ''
69
75
 
@@ -94,8 +100,15 @@ class WorkflowRunCommand(WorkflowCommand):
94
100
  command.append(f"-f {os.path.relpath(self.extra_compose_path, self.__current_working_dir)}")
95
101
 
96
102
  command.append(f"--profile {self.workflow_name}")
103
+
104
+ if options.get('namespace'):
105
+ command.append(f"-p {options['namespace']}")
106
+
97
107
  command.append('up')
98
- command.append('-d')
108
+
109
+ if not options.get('attached'):
110
+ command.append('-d')
111
+
99
112
  command.append('--build')
100
113
 
101
114
  self.write_env()
@@ -137,4 +150,5 @@ class WorkflowRunCommand(WorkflowCommand):
137
150
 
138
151
  env_vars = self.config(_config)
139
152
  env_path = self.workflow_env_path
140
- Env(env_path).write(env_vars)
153
+ Env(env_path).write(env_vars)
154
+