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.
- 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/app/.Makefile +2 -2
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/docker-compose.yml +14 -1
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/docker-compose.yml +14 -1
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/docker-compose.yml +14 -1
- stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/bin/.run +2 -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 +17 -3
- stoobly_agent/app/cli/scaffold/workflow_validate_command.py +94 -0
- stoobly_agent/app/cli/scaffold_cli.py +48 -5
- stoobly_agent/config/data_dir.py +11 -2
- 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.7.dist-info → stoobly_agent-1.0.9.dist-info}/METADATA +2 -1
- {stoobly_agent-1.0.7.dist-info → stoobly_agent-1.0.9.dist-info}/RECORD +34 -24
- {stoobly_agent-1.0.7.dist-info → stoobly_agent-1.0.9.dist-info}/LICENSE +0 -0
- {stoobly_agent-1.0.7.dist-info → stoobly_agent-1.0.9.dist-info}/WHEEL +0 -0
- {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.
|
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,
|
@@ -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
|
-
|
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 $(
|
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: {}
|
@@ -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))
|
@@ -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
|
-
|
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
|
+
|