stoobly-agent 1.0.14__py3-none-any.whl → 1.1.0__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 +7 -1
- stoobly_agent/app/cli/scaffold/docker/workflow/builder.py +6 -6
- stoobly_agent/app/cli/scaffold/hosts_file_reader.py +65 -0
- stoobly_agent/app/cli/scaffold/service_delete_command.py +35 -0
- stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py +37 -20
- stoobly_agent/app/cli/scaffold/templates/app/.Makefile +19 -1
- stoobly_agent/app/cli/scaffold/templates/app/gateway/.docker-compose.base.yml +3 -3
- stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/bin/.logs +10 -0
- stoobly_agent/app/cli/scaffold/workflow_command.py +1 -5
- stoobly_agent/app/cli/scaffold/workflow_log_command.py +30 -5
- stoobly_agent/app/cli/scaffold/workflow_run_command.py +0 -1
- stoobly_agent/app/cli/scaffold_cli.py +53 -23
- stoobly_agent/app/proxy/replay/multipart.py +3 -0
- stoobly_agent/test/app/cli/scaffold/cli_invoker.py +134 -0
- stoobly_agent/test/app/cli/scaffold/cli_test.py +128 -0
- stoobly_agent/test/app/cli/scaffold/e2e_test.py +19 -143
- stoobly_agent/test/app/cli/scaffold/hosts_file_reader_test.py +79 -0
- stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
- {stoobly_agent-1.0.14.dist-info → stoobly_agent-1.1.0.dist-info}/METADATA +9 -10
- {stoobly_agent-1.0.14.dist-info → stoobly_agent-1.1.0.dist-info}/RECORD +24 -18
- {stoobly_agent-1.0.14.dist-info → stoobly_agent-1.1.0.dist-info}/WHEEL +1 -1
- {stoobly_agent-1.0.14.dist-info → stoobly_agent-1.1.0.dist-info}/LICENSE +0 -0
- {stoobly_agent-1.0.14.dist-info → stoobly_agent-1.1.0.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.1.0'
|
@@ -36,7 +36,13 @@ USER_ID_ENV = 'USER_ID'
|
|
36
36
|
VIRTUAL_HOST_ENV = 'VIRTUAL_HOST'
|
37
37
|
VIRTUAL_PORT_ENV = 'VIRTUAL_PORT'
|
38
38
|
VIRTUAL_PROTO_ENV = 'VIRTUAL_PROTO'
|
39
|
-
|
39
|
+
WORKFLOW_CONTAINER_CONFIGURE = 'configure'
|
40
|
+
WORKFLOW_CONTAINER_INIT = 'init'
|
41
|
+
WORKFLOW_CONTAINER_PROXY = 'proxy'
|
42
|
+
WORKFLOW_CONTAINER_TEMPLATE = '{service_name}.{container}'
|
43
|
+
WORKFLOW_CONTAINER_CONFIGURE_TEMPLATE = '{service_name}.' + WORKFLOW_CONTAINER_CONFIGURE
|
44
|
+
WORKFLOW_CONTAINER_INIT_TEMPLATE = '{service_name}.' + WORKFLOW_CONTAINER_INIT
|
45
|
+
WORKFLOW_CONTAINER_PROXY_TEMPLATE = '{service_name}.' + WORKFLOW_CONTAINER_PROXY
|
40
46
|
WORKFLOW_MOCK_TYPE = 'mock'
|
41
47
|
WORKFLOW_NAME_ENV = 'WORKFLOW_NAME'
|
42
48
|
WORKFLOW_RECORD_TYPE = 'record'
|
@@ -4,11 +4,11 @@ import pdb
|
|
4
4
|
from typing import List
|
5
5
|
|
6
6
|
from ...constants import (
|
7
|
-
COMPOSE_TEMPLATE, SERVICE_HOSTNAME, SERVICE_HOSTNAME_ENV, SERVICE_NAME_ENV,
|
8
|
-
|
7
|
+
COMPOSE_TEMPLATE, STOOBLY_HOME_DIR, SERVICE_HOSTNAME, SERVICE_HOSTNAME_ENV, SERVICE_NAME_ENV,
|
8
|
+
SERVICE_PORT, SERVICE_PORT_ENV, SERVICE_SCHEME, SERVICE_SCHEME_ENV,
|
9
|
+
WORKFLOW_CONTAINER_CONFIGURE_TEMPLATE, WORKFLOW_CONTAINER_INIT_TEMPLATE, WORKFLOW_CONTAINER_PROXY_TEMPLATE, WORKFLOW_NAME_ENV
|
9
10
|
)
|
10
11
|
from ...templates.constants import SERVICE_HOSTNAME_BUILD_ARG
|
11
|
-
from ...workflow_env import WorkflowEnv
|
12
12
|
from ..builder import Builder
|
13
13
|
from ..service.builder import ServiceBuilder
|
14
14
|
|
@@ -44,7 +44,7 @@ class WorkflowBuilder(Builder):
|
|
44
44
|
|
45
45
|
@property
|
46
46
|
def init(self):
|
47
|
-
return
|
47
|
+
return WORKFLOW_CONTAINER_INIT_TEMPLATE.format(service_name=self.namespace)
|
48
48
|
|
49
49
|
@property
|
50
50
|
def config(self):
|
@@ -52,7 +52,7 @@ class WorkflowBuilder(Builder):
|
|
52
52
|
|
53
53
|
@property
|
54
54
|
def configure(self):
|
55
|
-
return
|
55
|
+
return WORKFLOW_CONTAINER_CONFIGURE_TEMPLATE.format(service_name=self.namespace)
|
56
56
|
|
57
57
|
@property
|
58
58
|
def context(self):
|
@@ -79,7 +79,7 @@ class WorkflowBuilder(Builder):
|
|
79
79
|
|
80
80
|
@property
|
81
81
|
def proxy(self):
|
82
|
-
return
|
82
|
+
return WORKFLOW_CONTAINER_PROXY_TEMPLATE.format(service_name=self.namespace)
|
83
83
|
|
84
84
|
@property
|
85
85
|
def proxy_build(self):
|
@@ -0,0 +1,65 @@
|
|
1
|
+
import pdb
|
2
|
+
import platform
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from typing import Union
|
5
|
+
|
6
|
+
|
7
|
+
class HostsFileReader():
|
8
|
+
|
9
|
+
@dataclass
|
10
|
+
class IpAddressToHostnames:
|
11
|
+
ip_address: str
|
12
|
+
hostnames: list[str]
|
13
|
+
|
14
|
+
def __get_hosts_file_path(self) -> str:
|
15
|
+
system = platform.system()
|
16
|
+
if system == 'Linux' or system == 'Darwin':
|
17
|
+
return '/etc/hosts'
|
18
|
+
else:
|
19
|
+
print(f"Unsupported system: {system}, for hosts file validation, skipping")
|
20
|
+
|
21
|
+
return ''
|
22
|
+
|
23
|
+
# Split IP address and hostnames. Don't include inline comments
|
24
|
+
def __split_hosts_line(self, line: str) -> list[str]:
|
25
|
+
ip_addr_hosts_split = line.split('#')[0].split()
|
26
|
+
return ip_addr_hosts_split
|
27
|
+
|
28
|
+
|
29
|
+
# Parses hosts file and returns a mapping of IP address to hostnames in a list.
|
30
|
+
def get_hosts(self) -> list[IpAddressToHostnames]:
|
31
|
+
hosts_file_path = self.__get_hosts_file_path()
|
32
|
+
|
33
|
+
if not hosts_file_path:
|
34
|
+
return []
|
35
|
+
|
36
|
+
with open(hosts_file_path, 'r') as f:
|
37
|
+
hostlines = f.readlines()
|
38
|
+
|
39
|
+
# Skip comments and empty lines
|
40
|
+
hostlines = [line.strip() for line in hostlines
|
41
|
+
if not line.startswith('#') and line.strip() != '']
|
42
|
+
|
43
|
+
hosts = []
|
44
|
+
for line in hostlines:
|
45
|
+
ip_addr_hosts_split = self.__split_hosts_line(line)
|
46
|
+
ip_address = ip_addr_hosts_split[0]
|
47
|
+
hostnames = ip_addr_hosts_split[1:]
|
48
|
+
ipAddressToHostnames = self.IpAddressToHostnames(ip_address, hostnames)
|
49
|
+
|
50
|
+
hosts.append(ipAddressToHostnames)
|
51
|
+
|
52
|
+
return hosts
|
53
|
+
|
54
|
+
def find_host(self, hostname) -> Union[IpAddressToHostnames, None]:
|
55
|
+
hosts = self.get_hosts()
|
56
|
+
|
57
|
+
for mapping in hosts:
|
58
|
+
if ((mapping.ip_address == '0.0.0.0' or mapping.ip_address == '127.0.0.1') and
|
59
|
+
hostname in mapping.hostnames):
|
60
|
+
|
61
|
+
return mapping
|
62
|
+
|
63
|
+
return None
|
64
|
+
|
65
|
+
|
@@ -0,0 +1,35 @@
|
|
1
|
+
import glob
|
2
|
+
import os
|
3
|
+
import pdb
|
4
|
+
import shutil
|
5
|
+
|
6
|
+
from .app import App
|
7
|
+
from .service_command import ServiceCommand
|
8
|
+
from stoobly_agent.lib.logger import Logger
|
9
|
+
|
10
|
+
|
11
|
+
LOG_ID = 'ServiceDeleteCommand'
|
12
|
+
|
13
|
+
class ServiceDeleteCommand(ServiceCommand):
|
14
|
+
def __init__(self, app: App, **kwargs):
|
15
|
+
super().__init__(app, **kwargs)
|
16
|
+
|
17
|
+
def delete(self) -> None:
|
18
|
+
Logger.instance(LOG_ID).debug(f"Deleting service: {self.service_name}, path: {self.service_path}")
|
19
|
+
|
20
|
+
# Delete service directory
|
21
|
+
shutil.rmtree(self.service_path)
|
22
|
+
|
23
|
+
# Delete certs for that hostname if HTTPS
|
24
|
+
if self.service_config.scheme == 'https':
|
25
|
+
certs_dir_path_escaped = glob.escape(self.app.certs_dir_path)
|
26
|
+
hostname = self.service_config.hostname
|
27
|
+
pattern = os.path.join(certs_dir_path_escaped, f"{hostname}*")
|
28
|
+
|
29
|
+
cert_paths = glob.glob(pattern)
|
30
|
+
for cert_path in cert_paths:
|
31
|
+
Logger.instance(LOG_ID).debug(f"Deleting cert_path: {cert_path}")
|
32
|
+
os.remove(cert_path)
|
33
|
+
|
34
|
+
return None
|
35
|
+
|
@@ -1,6 +1,8 @@
|
|
1
1
|
import os
|
2
2
|
import pdb
|
3
|
+
import socket
|
3
4
|
import ssl
|
5
|
+
import time
|
4
6
|
from collections import Counter
|
5
7
|
from pathlib import Path
|
6
8
|
|
@@ -20,6 +22,7 @@ from stoobly_agent.app.cli.scaffold.constants import (
|
|
20
22
|
WORKFLOW_RECORD_TYPE,
|
21
23
|
WORKFLOW_TEST_TYPE,
|
22
24
|
)
|
25
|
+
from stoobly_agent.app.cli.scaffold.hosts_file_reader import HostsFileReader
|
23
26
|
from stoobly_agent.app.cli.scaffold.service_command import ServiceCommand
|
24
27
|
from stoobly_agent.app.cli.scaffold.service_docker_compose import ServiceDockerCompose
|
25
28
|
from stoobly_agent.app.cli.scaffold.validate_command import ValidateCommand
|
@@ -68,26 +71,40 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
|
|
68
71
|
def is_external(self):
|
69
72
|
return not self.is_local()
|
70
73
|
|
71
|
-
def hostname_reachable(self,
|
72
|
-
|
73
|
-
|
74
|
-
retries =
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
74
|
+
def hostname_reachable(self, hostname: str, port: int) -> bool:
|
75
|
+
print(f"Validating connection to hostname: {hostname}, port: {port}")
|
76
|
+
timeout = 1
|
77
|
+
retries = 15
|
78
|
+
delay = 0.100
|
79
|
+
|
80
|
+
for attempt in range(retries):
|
81
|
+
try:
|
82
|
+
with socket.create_connection((hostname, port), timeout=timeout):
|
83
|
+
return True
|
84
|
+
except (socket.timeout, socket.error):
|
85
|
+
if attempt < retries - 1:
|
86
|
+
time.sleep(delay)
|
87
|
+
|
88
|
+
raise ScaffoldValidateException(f"Connection failed to hostname: {hostname}, port: {port}")
|
89
|
+
|
90
|
+
# Check if hostname is defined in hosts file
|
91
|
+
def hostname_exists(self, hostname: str) -> bool:
|
92
|
+
print(f"Validating hostname exists in hosts file for hostname: {hostname}")
|
93
|
+
|
94
|
+
hosts_file_reader = HostsFileReader()
|
95
|
+
host_mapping = hosts_file_reader.find_host(hostname)
|
96
|
+
if host_mapping:
|
97
|
+
print(f"Correct hosts mapping found for {hostname}")
|
98
|
+
return True
|
99
|
+
|
100
|
+
raise ScaffoldValidateException(f"Missing hosts mapping for {hostname}")
|
87
101
|
|
88
|
-
def validate_hostname(self,
|
89
|
-
print(f"Validating hostname: {
|
90
|
-
|
102
|
+
def validate_hostname(self, hostname: str, port: int) -> None:
|
103
|
+
print(f"Validating hostname: {hostname}")
|
104
|
+
|
105
|
+
self.hostname_exists(hostname)
|
106
|
+
|
107
|
+
self.hostname_reachable(hostname, port)
|
91
108
|
|
92
109
|
# TODO: check logs of proxy. lifecycle hook for custom logging? Does mitmproxy support json logging?
|
93
110
|
|
@@ -189,7 +206,7 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
|
|
189
206
|
url = f"{self.service_config.scheme}://{self.hostname}"
|
190
207
|
|
191
208
|
if self.service_config.hostname and self.workflow_name not in [WORKFLOW_TEST_TYPE]:
|
192
|
-
self.validate_hostname(
|
209
|
+
self.validate_hostname(self.hostname, self.service_config.port)
|
193
210
|
|
194
211
|
# Test workflow won't expose services that are detached and have a hostname to the host such as assets.
|
195
212
|
# Need to test connection from inside the Docker network
|
@@ -28,7 +28,7 @@ docker_compose_command=docker compose
|
|
28
28
|
source_env=set -a; [ -f .env ] && source .env; set +a
|
29
29
|
|
30
30
|
docker_compose_file_path=$(app_data_dir)/docker/stoobly-ui/exec/.docker-compose.exec.yml
|
31
|
-
stoobly_exec_args=--profile $(EXEC_WORKFLOW_NAME) -p $(EXEC_WORKFLOW_NAME) up --build --remove-orphans --pull always
|
31
|
+
stoobly_exec_args=--profile $(EXEC_WORKFLOW_NAME) -p $(EXEC_WORKFLOW_NAME) up --build --remove-orphans --pull always
|
32
32
|
stoobly_exec_env=export CONTEXT_DIR=$(context_dir) && export USER_ID=$$UID && export CA_CERTS_DIR="$(ca_certs_dir)"
|
33
33
|
stoobly_exec=$(stoobly_exec_env) && $(source_env) && $(docker_compose_command) -f "$(docker_compose_file_path)" $(stoobly_exec_args)
|
34
34
|
|
@@ -68,6 +68,12 @@ mock: nameservers
|
|
68
68
|
export EXEC_ARGS="mock" && \
|
69
69
|
$(stoobly_exec_run) && \
|
70
70
|
$(workflow_run)
|
71
|
+
mock/logs:
|
72
|
+
@export EXEC_COMMAND=bin/.logs && \
|
73
|
+
export EXEC_OPTIONS="$(options)" && \
|
74
|
+
export EXEC_ARGS="mock" && \
|
75
|
+
$(stoobly_exec_run) && \
|
76
|
+
$(workflow_run)
|
71
77
|
mock/stop:
|
72
78
|
@export EXEC_COMMAND=bin/.stop && \
|
73
79
|
export EXEC_OPTIONS="$(options)" && \
|
@@ -80,6 +86,12 @@ record: nameservers
|
|
80
86
|
export EXEC_ARGS="record" && \
|
81
87
|
$(stoobly_exec_run) && \
|
82
88
|
$(workflow_run)
|
89
|
+
record/logs:
|
90
|
+
@export EXEC_COMMAND=bin/.logs && \
|
91
|
+
export EXEC_OPTIONS="$(options)" && \
|
92
|
+
export EXEC_ARGS="record" && \
|
93
|
+
$(stoobly_exec_run) && \
|
94
|
+
$(workflow_run)
|
83
95
|
record/stop:
|
84
96
|
@export EXEC_COMMAND=bin/.stop && \
|
85
97
|
export EXEC_OPTIONS="$(options)" && \
|
@@ -121,6 +133,12 @@ test:
|
|
121
133
|
export EXEC_ARGS="test" && \
|
122
134
|
$(stoobly_exec_run) && \
|
123
135
|
$(workflow_run)
|
136
|
+
test/logs:
|
137
|
+
@export EXEC_COMMAND=bin/.logs && \
|
138
|
+
export EXEC_OPTIONS="$(options)" && \
|
139
|
+
export EXEC_ARGS="test" && \
|
140
|
+
$(stoobly_exec_run) && \
|
141
|
+
$(workflow_run)
|
124
142
|
test/stop:
|
125
143
|
@export EXEC_COMMAND=bin/.stop && \
|
126
144
|
export EXEC_OPTIONS="$(options)" && \
|
@@ -39,7 +39,6 @@ class WorkflowCommand(ServiceCommand):
|
|
39
39
|
|
40
40
|
@property
|
41
41
|
def containers(self):
|
42
|
-
_containers = []
|
43
42
|
if not os.path.exists(self.compose_path):
|
44
43
|
return []
|
45
44
|
|
@@ -49,11 +48,8 @@ class WorkflowCommand(ServiceCommand):
|
|
49
48
|
if not isinstance(services, dict):
|
50
49
|
return []
|
51
50
|
|
52
|
-
|
53
|
-
_containers.append(f"{self.workflow_name}-{service}-1")
|
51
|
+
return services
|
54
52
|
|
55
|
-
return _containers
|
56
|
-
|
57
53
|
@property
|
58
54
|
def custom_compose(self):
|
59
55
|
if os.path.exists(self.custom_compose_path):
|
@@ -1,21 +1,46 @@
|
|
1
|
-
import os
|
2
1
|
import pdb
|
3
|
-
|
2
|
+
|
3
|
+
from typing import TypedDict
|
4
|
+
|
5
|
+
from stoobly_agent.lib.logger import DEBUG
|
4
6
|
|
5
7
|
from .app import App
|
8
|
+
from .constants import WORKFLOW_CONTAINER_TEMPLATE
|
6
9
|
from .workflow_command import WorkflowCommand
|
7
10
|
|
11
|
+
class WorkflowLogCommand(TypedDict):
|
12
|
+
container: str
|
13
|
+
follow: bool
|
14
|
+
namespace: str
|
15
|
+
|
8
16
|
class WorkflowLogCommand(WorkflowCommand):
|
9
17
|
def __init__(self, app: App, **kwargs):
|
10
18
|
super().__init__(app, **kwargs)
|
11
19
|
|
12
|
-
def
|
20
|
+
def build(self, **options: WorkflowLogCommand):
|
13
21
|
commands = []
|
14
22
|
containers = self.containers
|
23
|
+
allowed_containers = list(
|
24
|
+
map(
|
25
|
+
lambda container: WORKFLOW_CONTAINER_TEMPLATE.format(
|
26
|
+
container=container, service_name=self.service_name
|
27
|
+
), options.get('containers') or []
|
28
|
+
)
|
29
|
+
)
|
15
30
|
|
16
|
-
for container in containers:
|
17
|
-
|
31
|
+
for index, container in enumerate(containers):
|
32
|
+
if container not in allowed_containers:
|
33
|
+
continue
|
18
34
|
|
35
|
+
container_name = self.container_name(container, options.get('namespace'))
|
36
|
+
commands.append(f"echo \"=== Logging {container_name}\"")
|
37
|
+
if options.get('follow') and index == len(containers) - 1:
|
38
|
+
command = ['docker', 'logs', '--follow', container_name]
|
39
|
+
else:
|
40
|
+
command = ['docker', 'logs', container_name]
|
19
41
|
commands.append(' '.join(command))
|
20
42
|
|
21
43
|
return commands
|
44
|
+
|
45
|
+
def container_name(self, container, namespace=None):
|
46
|
+
return f"{namespace or self.workflow_name}-{container}-1"
|
@@ -9,15 +9,15 @@ 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
|
13
12
|
from stoobly_agent.app.cli.scaffold.constants import (
|
14
|
-
DOCKER_NAMESPACE,
|
13
|
+
DOCKER_NAMESPACE, WORKFLOW_CONTAINER_PROXY, WORKFLOW_MOCK_TYPE, WORKFLOW_RECORD_TYPE, WORKFLOW_TEST_TYPE
|
15
14
|
)
|
16
15
|
from stoobly_agent.app.cli.scaffold.docker.service.set_gateway_ports import set_gateway_ports
|
17
16
|
from stoobly_agent.app.cli.scaffold.docker.workflow.decorators_factory import get_workflow_decorators
|
18
17
|
from stoobly_agent.app.cli.scaffold.service import Service
|
19
18
|
from stoobly_agent.app.cli.scaffold.service_config import ServiceConfig
|
20
19
|
from stoobly_agent.app.cli.scaffold.service_create_command import ServiceCreateCommand
|
20
|
+
from stoobly_agent.app.cli.scaffold.service_delete_command import ServiceDeleteCommand
|
21
21
|
from stoobly_agent.app.cli.scaffold.service_workflow_validate_command import ServiceWorkflowValidateCommand
|
22
22
|
from stoobly_agent.app.cli.scaffold.templates.constants import CORE_SERVICES
|
23
23
|
from stoobly_agent.app.cli.scaffold.validate_exceptions import ScaffoldValidateException
|
@@ -143,6 +143,24 @@ def create(**kwargs):
|
|
143
143
|
else:
|
144
144
|
print(f"{service.dir_path} already exists, use option '--force' to continue")
|
145
145
|
|
146
|
+
@service.command(
|
147
|
+
help="Delete a service",
|
148
|
+
)
|
149
|
+
@click.option('--app-dir-path', default=os.getcwd(), help='Path to application directory.')
|
150
|
+
@click.argument('service_name')
|
151
|
+
def delete(**kwargs):
|
152
|
+
__validate_app_dir(kwargs['app_dir_path'])
|
153
|
+
|
154
|
+
app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
|
155
|
+
service = Service(kwargs['service_name'], app)
|
156
|
+
|
157
|
+
if not os.path.exists(service.dir_path):
|
158
|
+
print(f"Service does not exist, so not deleting")
|
159
|
+
else:
|
160
|
+
print(f"Deleting service: {service.service_name}")
|
161
|
+
__scaffold_delete(app, **kwargs)
|
162
|
+
print(f"Successfully deleted service: {service.service_name}")
|
163
|
+
|
146
164
|
@service.command(
|
147
165
|
help="Update a service config"
|
148
166
|
)
|
@@ -227,8 +245,6 @@ def copy(**kwargs):
|
|
227
245
|
@click.option('--app-dir-path', default=os.getcwd(), help='Path to application directory.')
|
228
246
|
@click.option('--context-dir-path', default=DataDir.instance().context_dir_path, help='Path to Stoobly data directory.')
|
229
247
|
@click.option('--dry-run', default=False, is_flag=True)
|
230
|
-
|
231
|
-
@click.option('--filter', multiple=True, type=click.Choice([WORKFLOW_CUSTOM_FILTER]), help='Select which service groups to run. Defaults to all.')
|
232
248
|
@click.option('--log-level', default=INFO, type=click.Choice([DEBUG, INFO, WARNING, ERROR]), help='''
|
233
249
|
Log levels can be "debug", "info", "warning", or "error"
|
234
250
|
''')
|
@@ -250,7 +266,7 @@ def stop(**kwargs):
|
|
250
266
|
sys.exit(1)
|
251
267
|
|
252
268
|
workflow = Workflow(kwargs['workflow_name'], app)
|
253
|
-
services = __get_services(workflow.services,
|
269
|
+
services = __get_services(workflow.services, service=kwargs['service'])
|
254
270
|
|
255
271
|
commands: List[WorkflowRunCommand] = []
|
256
272
|
for service in services:
|
@@ -276,11 +292,18 @@ def stop(**kwargs):
|
|
276
292
|
|
277
293
|
@workflow.command()
|
278
294
|
@click.option('--app-dir-path', default=os.getcwd(), help='Path to application directory.')
|
279
|
-
@click.option(
|
280
|
-
|
295
|
+
@click.option(
|
296
|
+
'--container', multiple=True, help=f"Select which containers to log. Defaults to '{WORKFLOW_CONTAINER_PROXY}'"
|
297
|
+
)
|
298
|
+
@click.option('--dry-run', default=False, is_flag=True, help='If set, prints commands.')
|
299
|
+
@click.option('--follow', is_flag=True, help='Follow last container log output.')
|
300
|
+
@click.option('--namespace', help='Workflow namespace.')
|
281
301
|
@click.option('--service', multiple=True, help='Select which services to log. Defaults to all.')
|
282
302
|
@click.argument('workflow_name')
|
283
303
|
def logs(**kwargs):
|
304
|
+
if len(kwargs['container']) == 0:
|
305
|
+
kwargs['container'] = [WORKFLOW_CONTAINER_PROXY]
|
306
|
+
|
284
307
|
app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
|
285
308
|
|
286
309
|
if not app.exists:
|
@@ -288,12 +311,18 @@ def logs(**kwargs):
|
|
288
311
|
sys.exit(1)
|
289
312
|
|
290
313
|
workflow = Workflow(kwargs['workflow_name'], app)
|
291
|
-
services = __get_services(workflow.services,
|
314
|
+
services = __get_services(workflow.services, service=kwargs['service'])
|
292
315
|
|
293
316
|
commands: List[WorkflowLogCommand] = []
|
294
317
|
for service in services:
|
295
|
-
if len(kwargs['service'])
|
296
|
-
|
318
|
+
if len(kwargs['service']) == 0:
|
319
|
+
# If no filter is specified, ignore CORE_SERVICES
|
320
|
+
if service in CORE_SERVICES:
|
321
|
+
continue
|
322
|
+
else:
|
323
|
+
# If a filter is specified, ignore all other services
|
324
|
+
if service not in kwargs['service']:
|
325
|
+
continue
|
297
326
|
|
298
327
|
config = { **kwargs }
|
299
328
|
config['service_name'] = service
|
@@ -302,10 +331,15 @@ def logs(**kwargs):
|
|
302
331
|
|
303
332
|
commands = sorted(commands, key=lambda command: command.service_config.priority)
|
304
333
|
|
305
|
-
for command in commands:
|
334
|
+
for index, command in enumerate(commands):
|
306
335
|
__print_header(f"SERVICE {command.service_name}")
|
307
336
|
|
308
|
-
|
337
|
+
follow = kwargs['follow'] and index == len(commands) - 1
|
338
|
+
shell_commands = command.build(
|
339
|
+
containers=kwargs['container'], follow=follow, namespace=kwargs['namespace']
|
340
|
+
)
|
341
|
+
|
342
|
+
for shell_command in shell_commands:
|
309
343
|
print(shell_command)
|
310
344
|
|
311
345
|
if not kwargs['dry_run']:
|
@@ -317,7 +351,6 @@ def logs(**kwargs):
|
|
317
351
|
@click.option('--certs-dir-path', help='Path to certs directory. Defaults to the certs dir of the context.')
|
318
352
|
@click.option('--context-dir-path', default=DataDir.instance().context_dir_path, help='Path to Stoobly data directory.')
|
319
353
|
@click.option('--detached', is_flag=True, help='If set, will not run the highest priority service in the foreground.')
|
320
|
-
@click.option('--filter', multiple=True, type=click.Choice([WORKFLOW_CUSTOM_FILTER]), help='Select which service groups to run. Defaults to all.')
|
321
354
|
@click.option('--dry-run', default=False, is_flag=True, help='If set, prints commands.')
|
322
355
|
@click.option('--extra-compose-path', help='Path to extra compose configuration files.')
|
323
356
|
@click.option('--log-level', default=INFO, type=click.Choice([DEBUG, INFO, WARNING, ERROR]), help='''
|
@@ -352,7 +385,7 @@ def run(**kwargs):
|
|
352
385
|
sys.exit(1)
|
353
386
|
|
354
387
|
workflow = Workflow(kwargs['workflow_name'], app)
|
355
|
-
services = __get_services(workflow.services,
|
388
|
+
services = __get_services(workflow.services, service=kwargs['service'])
|
356
389
|
|
357
390
|
# Gateway ports are dynamically set depending on the workflow run
|
358
391
|
set_gateway_ports(workflow.service_paths_from_services(services))
|
@@ -432,17 +465,9 @@ def __get_services(services: List[str], **kwargs):
|
|
432
465
|
if missing_services:
|
433
466
|
Logger.instance(LOG_ID).warn(f"Service(s) {','.join(missing_services)} are not found")
|
434
467
|
|
435
|
-
filter = kwargs.get('filter') or []
|
436
468
|
if kwargs['service']:
|
437
469
|
# If service is specified, run only those services
|
438
470
|
services = list(kwargs['service'])
|
439
|
-
|
440
|
-
if WORKFLOW_CUSTOM_FILTER not in filter:
|
441
|
-
services += CORE_SERVICES
|
442
|
-
else:
|
443
|
-
if WORKFLOW_CUSTOM_FILTER in filter:
|
444
|
-
# If this filter is set, then the user does not want to run core services
|
445
|
-
services = list(filter(lambda service: service not in CORE_SERVICES, services))
|
446
471
|
|
447
472
|
return list(set(services))
|
448
473
|
|
@@ -457,6 +482,11 @@ def __scaffold_build(app, **kwargs):
|
|
457
482
|
|
458
483
|
command.build()
|
459
484
|
|
485
|
+
def __scaffold_delete(app, **kwargs):
|
486
|
+
command = ServiceDeleteCommand(app, **kwargs)
|
487
|
+
|
488
|
+
command.delete()
|
489
|
+
|
460
490
|
def __validate_app_dir(app_dir_path):
|
461
491
|
if not os.path.exists(app_dir_path):
|
462
492
|
print(f"Error: {app_dir_path} does not exist", file=sys.stderr)
|
@@ -476,4 +506,4 @@ def __workflow_build(app, **kwargs):
|
|
476
506
|
headless=kwargs['headless'],
|
477
507
|
template=kwargs['template'],
|
478
508
|
workflow_decorators=workflow_decorators
|
479
|
-
)
|
509
|
+
)
|