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.
Files changed (24) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/app/cli/scaffold/constants.py +7 -1
  3. stoobly_agent/app/cli/scaffold/docker/workflow/builder.py +6 -6
  4. stoobly_agent/app/cli/scaffold/hosts_file_reader.py +65 -0
  5. stoobly_agent/app/cli/scaffold/service_delete_command.py +35 -0
  6. stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py +37 -20
  7. stoobly_agent/app/cli/scaffold/templates/app/.Makefile +19 -1
  8. stoobly_agent/app/cli/scaffold/templates/app/gateway/.docker-compose.base.yml +3 -3
  9. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/bin/.logs +10 -0
  10. stoobly_agent/app/cli/scaffold/workflow_command.py +1 -5
  11. stoobly_agent/app/cli/scaffold/workflow_log_command.py +30 -5
  12. stoobly_agent/app/cli/scaffold/workflow_run_command.py +0 -1
  13. stoobly_agent/app/cli/scaffold_cli.py +53 -23
  14. stoobly_agent/app/proxy/replay/multipart.py +3 -0
  15. stoobly_agent/test/app/cli/scaffold/cli_invoker.py +134 -0
  16. stoobly_agent/test/app/cli/scaffold/cli_test.py +128 -0
  17. stoobly_agent/test/app/cli/scaffold/e2e_test.py +19 -143
  18. stoobly_agent/test/app/cli/scaffold/hosts_file_reader_test.py +79 -0
  19. stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
  20. {stoobly_agent-1.0.14.dist-info → stoobly_agent-1.1.0.dist-info}/METADATA +9 -10
  21. {stoobly_agent-1.0.14.dist-info → stoobly_agent-1.1.0.dist-info}/RECORD +24 -18
  22. {stoobly_agent-1.0.14.dist-info → stoobly_agent-1.1.0.dist-info}/WHEEL +1 -1
  23. {stoobly_agent-1.0.14.dist-info → stoobly_agent-1.1.0.dist-info}/LICENSE +0 -0
  24. {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.14'
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
- WORKFLOW_CUSTOM_FILTER = 'custom'
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, SERVICE_PORT, SERVICE_PORT_ENV, SERVICE_SCHEME,
8
- SERVICE_SCHEME_ENV, STOOBLY_HOME_DIR, WORKFLOW_NAME_ENV
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 f"{self.namespace}.init"
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 f"{self.namespace}.configure"
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 f"{self.namespace}.proxy"
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, 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}")
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, url: str) -> None:
89
- print(f"Validating hostname: {url}")
90
- self.hostname_reachable(url)
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(url)
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)" && \
@@ -1,9 +1,9 @@
1
1
  services:
2
2
  gateway_base:
3
+ environment:
4
+ TRUST_DOWNSTREAM_PROXY: true
3
5
  image: nginxproxy/nginx-proxy:1.5
4
- ports:
5
- - '80:80'
6
- - '443:443' # Entry port
6
+ ports: {}
7
7
  profiles:
8
8
  - gateway_base
9
9
  volumes:
@@ -0,0 +1,10 @@
1
+ #!/bin/bash
2
+
3
+ extra_options=$EXEC_OPTIONS
4
+ workflow=$1
5
+
6
+ stoobly-agent scaffold workflow logs \
7
+ --app-dir-path "$(pwd)" \
8
+ --dry-run \
9
+ $extra_options \
10
+ $workflow > .stoobly/tmp/run.sh
@@ -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
- for service in services:
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
- import yaml
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 all(self):
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
- command = ['docker', 'logs', container]
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"
@@ -142,7 +142,6 @@ class WorkflowRunCommand(WorkflowCommand):
142
142
  command.append(option)
143
143
 
144
144
  command.append('--build')
145
- command.append('--pull always')
146
145
 
147
146
  self.write_env()
148
147
 
@@ -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, WORKFLOW_CUSTOM_FILTER, WORKFLOW_MOCK_TYPE, WORKFLOW_RECORD_TYPE, WORKFLOW_TEST_TYPE
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, filter=kwargs['filter'], service=kwargs['service'])
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('--dry-run', default=False, is_flag=True)
280
- @click.option('--filter', multiple=True, type=click.Choice([WORKFLOW_CUSTOM_FILTER]), help='Select which service groups to run. Defaults to all.')
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, filter=kwargs['filter'], service=kwargs['service'])
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']) > 0 and service not in kwargs['service']:
296
- continue
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
- for shell_command in command.all():
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, filter=kwargs['filter'], service=kwargs['service'])
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
+ )
@@ -73,4 +73,7 @@ def decode(hdrs, content):
73
73
  r.append((part.name, part.raw))
74
74
  else:
75
75
  r.append((part.name, part.value))
76
+
77
+ # Free up resources after use
78
+ part.close()
76
79
  return r