stoobly-agent 1.3.0__py3-none-any.whl → 1.4.1__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 (51) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/app/api/application_http_request_handler.py +3 -3
  3. stoobly_agent/app/api/proxy_controller.py +8 -7
  4. stoobly_agent/app/cli/config_cli.py +1 -1
  5. stoobly_agent/app/cli/helpers/certificate_authority.py +6 -1
  6. stoobly_agent/app/cli/helpers/print_service.py +17 -0
  7. stoobly_agent/app/cli/scaffold/app.py +2 -2
  8. stoobly_agent/app/cli/scaffold/constants.py +0 -2
  9. stoobly_agent/app/cli/scaffold/hosts_file_manager.py +185 -0
  10. stoobly_agent/app/cli/scaffold/service.py +0 -1
  11. stoobly_agent/app/cli/scaffold/service_config.py +10 -14
  12. stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py +3 -3
  13. stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +1 -1
  14. stoobly_agent/app/cli/scaffold/templates/app/.Makefile +77 -53
  15. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/bin/.services +9 -0
  16. stoobly_agent/app/cli/scaffold/workflow_create_command.py +0 -1
  17. stoobly_agent/app/cli/scaffold/workflow_run_command.py +6 -9
  18. stoobly_agent/app/cli/scaffold_cli.py +212 -69
  19. stoobly_agent/app/cli/snapshot_cli.py +1 -1
  20. stoobly_agent/app/proxy/handle_mock_service.py +2 -0
  21. stoobly_agent/app/proxy/handle_replay_service.py +2 -0
  22. stoobly_agent/app/proxy/mitmproxy/request_facade.py +1 -1
  23. stoobly_agent/app/proxy/mitmproxy/response_body_facade.py +19 -0
  24. stoobly_agent/app/proxy/mitmproxy/response_facade.py +90 -18
  25. stoobly_agent/app/proxy/record/join_request_service.py +1 -1
  26. stoobly_agent/app/settings/constants/request_component.py +2 -1
  27. stoobly_agent/config/constants/custom_headers.py +13 -13
  28. stoobly_agent/config/constants/headers.py +0 -2
  29. stoobly_agent/public/18-es2015.583f191cc7ad512ee262.js +1 -0
  30. stoobly_agent/public/18-es5.583f191cc7ad512ee262.js +1 -0
  31. stoobly_agent/public/35-es2015.8f79ff8748d4ff06ab03.js +1 -0
  32. stoobly_agent/public/35-es5.8f79ff8748d4ff06ab03.js +1 -0
  33. stoobly_agent/public/index.html +1 -1
  34. stoobly_agent/public/main-es2015.2cc16523aa3fcaba51e5.js +1 -0
  35. stoobly_agent/public/main-es5.2cc16523aa3fcaba51e5.js +1 -0
  36. stoobly_agent/public/{runtime-es2015.9addf49b79aca951b7e2.js → runtime-es2015.b914470164e4d6e75d96.js} +1 -1
  37. stoobly_agent/public/{runtime-es5.9addf49b79aca951b7e2.js → runtime-es5.b914470164e4d6e75d96.js} +1 -1
  38. stoobly_agent/test/app/cli/scaffold/{hosts_file_reader_test.py → hosts_file_manager_test.py} +20 -20
  39. stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
  40. {stoobly_agent-1.3.0.dist-info → stoobly_agent-1.4.1.dist-info}/METADATA +1 -1
  41. {stoobly_agent-1.3.0.dist-info → stoobly_agent-1.4.1.dist-info}/RECORD +44 -42
  42. stoobly_agent/app/cli/scaffold/hosts_file_reader.py +0 -65
  43. stoobly_agent/public/18-es2015.d3b430636a4d6f544d92.js +0 -1
  44. stoobly_agent/public/18-es5.d3b430636a4d6f544d92.js +0 -1
  45. stoobly_agent/public/35-es2015.f741ebce0bfc25f0ec99.js +0 -1
  46. stoobly_agent/public/35-es5.f741ebce0bfc25f0ec99.js +0 -1
  47. stoobly_agent/public/main-es2015.ccd46ac1b6638ddf2066.js +0 -1
  48. stoobly_agent/public/main-es5.ccd46ac1b6638ddf2066.js +0 -1
  49. {stoobly_agent-1.3.0.dist-info → stoobly_agent-1.4.1.dist-info}/LICENSE +0 -0
  50. {stoobly_agent-1.3.0.dist-info → stoobly_agent-1.4.1.dist-info}/WHEEL +0 -0
  51. {stoobly_agent-1.3.0.dist-info → stoobly_agent-1.4.1.dist-info}/entry_points.txt +0 -0
stoobly_agent/__init__.py CHANGED
@@ -1,2 +1,2 @@
1
1
  COMMAND = 'stoobly-agent'
2
- VERSION = '1.3.0'
2
+ VERSION = '1.4.1'
@@ -7,7 +7,7 @@ from mitmproxy.coretypes.multidict import MultiDict
7
7
  from urllib.parse import urlparse, parse_qs
8
8
 
9
9
  from stoobly_agent.app.proxy.replay.body_parser_service import decode_response
10
- from stoobly_agent.config.constants import headers
10
+ from stoobly_agent.config.constants import custom_headers, headers
11
11
 
12
12
  from .routes import ROUTES
13
13
  from .simple_http_request_handler import SimpleHTTPRequestHandler
@@ -102,11 +102,11 @@ class ApplicationHTTPRequestHandler(SimpleHTTPRequestHandler):
102
102
  'Content-Type',
103
103
  headers.ACCESS_TOKEN.title(),
104
104
  headers.CLIENT.title(),
105
- headers.DO_PROXY.title(),
105
+ custom_headers.DO_PROXY.title(),
106
106
  headers.EXPIRY.title(),
107
107
  headers.PROXY_HEADERS.title(),
108
108
  headers.REQUEST_PATH.title(),
109
- headers.SERVICE_URL.title(),
109
+ custom_headers.SERVICE_URL.title(),
110
110
  headers.TOKEN_TYPE.title(),
111
111
  headers.UID.title(),
112
112
  ])
@@ -3,7 +3,8 @@ import requests
3
3
  from http.cookies import SimpleCookie
4
4
  from urllib3.exceptions import InsecureRequestWarning
5
5
 
6
- from stoobly_agent.config.constants import headers
6
+ from stoobly_agent.app.api.simple_http_request_handler import SimpleHTTPRequestHandler
7
+ from stoobly_agent.config.constants import custom_headers, headers
7
8
  from stoobly_agent.config.mitmproxy import MitmproxyConfig
8
9
  from stoobly_agent.lib.logger import Logger
9
10
 
@@ -46,7 +47,7 @@ class ProxyController:
46
47
  def do_PUT(self, context):
47
48
  self.__proxy(context, requests.put)
48
49
 
49
- def __proxy(self, context, method):
50
+ def __proxy(self, context: SimpleHTTPRequestHandler, method):
50
51
  url = self.__get_url(context)
51
52
 
52
53
  if url:
@@ -110,7 +111,7 @@ class ProxyController:
110
111
  status = status,
111
112
  )
112
113
 
113
- def __get_headers(self, context):
114
+ def __get_headers(self, context: SimpleHTTPRequestHandler):
114
115
  request_headers = dict(context.headers)
115
116
 
116
117
  headers_white_list = []
@@ -125,12 +126,12 @@ class ProxyController:
125
126
 
126
127
  return white_listed_headers
127
128
 
128
- def __get_url(self, context):
129
- service_url = context.headers.get(headers.SERVICE_URL)
129
+ def __get_url(self, context: SimpleHTTPRequestHandler):
130
+ service_url = context.headers.get(custom_headers.SERVICE_URL)
130
131
 
131
132
  if not service_url:
132
133
  context.render(
133
- plain = f"Invalid {headers.SERVICE_URL} header {service_url}",
134
+ plain = f"Invalid {custom_headers.SERVICE_URL} header {service_url}",
134
135
  status = 400
135
136
  )
136
137
  return None
@@ -146,7 +147,7 @@ class ProxyController:
146
147
 
147
148
  return f"{service_url}{request_path}"
148
149
 
149
- def __get_cookies(self, context):
150
+ def __get_cookies(self, context: SimpleHTTPRequestHandler):
150
151
  return SimpleCookie(context.headers.get('Cookie'))
151
152
 
152
153
  def __get_body(self, context):
@@ -180,7 +180,7 @@ def rewrite(ctx):
180
180
  @click.option('--project-key', help='Project to add rewrite rule to.')
181
181
  @click.option(
182
182
  '--type',
183
- type=click.Choice([request_component.BODY_PARAM, request_component.HEADER, request_component.QUERY_PARAM]),
183
+ type=click.Choice([request_component.BODY_PARAM, request_component.HEADER, request_component.QUERY_PARAM, request_component.RESPONSE_HEADER, request_component.RESPONSE_PARAM]),
184
184
  help='Request component type.'
185
185
  )
186
186
  @click.option(
@@ -94,6 +94,9 @@ class CertificateAuthority():
94
94
  subprocess.run("sudo update-ca-trust extract".split(), check=True)
95
95
 
96
96
  def install(self):
97
+ if not self.certs_generated:
98
+ self.generate_certs()
99
+
97
100
  distro_name = distro.name(pretty=True)
98
101
 
99
102
  # Ubuntu or other Debian based
@@ -105,7 +108,9 @@ class CertificateAuthority():
105
108
  # elif distro.id() == 'rhel':
106
109
  # return
107
110
  else:
108
- raise TypeError(f"{distro_name} is not supported yet for automatic CA cert installation.")
111
+ raise TypeError(
112
+ f"{distro_name} is not supported yet for automatic installation. For manual installation, see https://docs.mitmproxy.org/stable/concepts-certificates/"
113
+ )
109
114
 
110
115
  def sign(self, hostname: str, dest = None, port = 443):
111
116
  dest = dest or self.certs_dir
@@ -89,6 +89,23 @@ def print_scenarios(scenarios, **kwargs: TabulatePrintOptions):
89
89
  select=kwargs.get('select') or []
90
90
  )
91
91
 
92
+ def print_services(services, **kwargs: TabulatePrintOptions):
93
+ filter = []
94
+ format = kwargs.get('format')
95
+
96
+ if format == JSON_FORMAT:
97
+ json_print(services, **{
98
+ 'filter': filter,
99
+ **kwargs
100
+ })
101
+ else:
102
+ tabulate_print(
103
+ services,
104
+ filter=filter,
105
+ headers=not kwargs.get('without_headers'),
106
+ select=kwargs.get('select') or []
107
+ )
108
+
92
109
  def print_snapshots(snapshots, **kwargs: TabulatePrintOptions):
93
110
  filter = ['resource_uuid']
94
111
  format = kwargs.get('format')
@@ -54,8 +54,8 @@ class App():
54
54
  return os.path.join(self.context_dir_path, DATA_DIR_NAME)
55
55
 
56
56
  @property
57
- def exists(self):
58
- return os.path.exists(self.dir_path)
57
+ def valid(self):
58
+ return os.path.exists(self.scaffold_namespace_path)
59
59
 
60
60
  @property
61
61
  def scaffold_namespace(self):
@@ -18,8 +18,6 @@ FIXTURES_FOLDER_NAME = 'fixtures'
18
18
  NAMESERVERS_FILE = '.nameservers'
19
19
  SERVICE_DETACHED = '${SERVICE_DETACHED}'
20
20
  SERVICE_DETACHED_ENV = 'SERVICE_DETACHED'
21
- SERVICE_DOCKER_COMPOSE_PATH = '${SERVICE_DOCKER_COMPOSE_PATH}'
22
- SERVICE_DOCKER_COMPOSE_PATH_ENV = 'SERVICE_DOCKER_COMPOSE_PATH'
23
21
  SERVICE_HOSTNAME = '${SERVICE_HOSTNAME}'
24
22
  SERVICE_HOSTNAME_ENV = 'SERVICE_HOSTNAME'
25
23
  SERVICE_DNS = '${SERVICE_DNS}'
@@ -0,0 +1,185 @@
1
+ import os
2
+ import pdb
3
+ import sys
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Union
7
+
8
+ SCAFFOLD_HOSTS_DELIMITTER_BEGIN = "##### STOOBLY SCAFFOLD HOSTS BEGIN #####\n"
9
+ SCAFFOLD_HOSTS_DELIMITTER_END = "##### STOOBLY SCAFFOLD HOSTS END #####\n"
10
+
11
+ class HostsFileManager():
12
+
13
+ @dataclass
14
+ class IpAddressToHostnames:
15
+ ip_address: str
16
+ hostnames: list[str]
17
+
18
+ def __get_hosts_file_path(self) -> str:
19
+ file_path = '/etc/hosts'
20
+ if not os.path.exists(file_path):
21
+ print(f"Error: File {file_path} not found.", file=sys.stderr)
22
+ sys.exit(1)
23
+ return file_path
24
+
25
+ # Split IP address and hostnames. Don't include inline comments
26
+ def __split_hosts_line(self, line: str) -> list[str]:
27
+ ip_addr_hosts_split = line.split('#')[0].split()
28
+ return ip_addr_hosts_split
29
+
30
+ # Parses hosts file and returns a mapping of IP address to hostnames in a list.
31
+ def get_hosts(self) -> list[IpAddressToHostnames]:
32
+ hosts_file_path = self.__get_hosts_file_path()
33
+
34
+ if not hosts_file_path:
35
+ return []
36
+
37
+ with open(hosts_file_path, 'r') as f:
38
+ hostlines = f.readlines()
39
+
40
+ # Skip comments and empty lines
41
+ hostlines = [line.strip() for line in hostlines
42
+ if not line.startswith('#') and line.strip() != '']
43
+
44
+ hosts = []
45
+ for line in hostlines:
46
+ ip_addr_hosts_split = self.__split_hosts_line(line)
47
+ ip_address = ip_addr_hosts_split[0]
48
+ hostnames = ip_addr_hosts_split[1:]
49
+ ipAddressToHostnames = self.IpAddressToHostnames(ip_address, hostnames)
50
+
51
+ hosts.append(ipAddressToHostnames)
52
+
53
+ return hosts
54
+
55
+ def find_host(self, hostname) -> Union[IpAddressToHostnames, None]:
56
+ hosts = self.get_hosts()
57
+
58
+ for mapping in hosts:
59
+ if ((mapping.ip_address == '0.0.0.0' or mapping.ip_address == '127.0.0.1') and
60
+ hostname in mapping.hostnames):
61
+
62
+ return mapping
63
+
64
+ return None
65
+
66
+ def install_hostnames(self, hostnames: list[str]) -> None:
67
+ hosts_file_path = self.__get_hosts_file_path()
68
+
69
+ self.__add_lines_between_markers(
70
+ hosts_file_path, SCAFFOLD_HOSTS_DELIMITTER_BEGIN, SCAFFOLD_HOSTS_DELIMITTER_END, hostnames
71
+ )
72
+
73
+ def uninstall_hostnames(self, hostnames: list[str] = []) -> None:
74
+ hosts_file_path = self.__get_hosts_file_path()
75
+
76
+ self.__remove_lines_between_markers(
77
+ hosts_file_path, SCAFFOLD_HOSTS_DELIMITTER_BEGIN, SCAFFOLD_HOSTS_DELIMITTER_END, hostnames
78
+ )
79
+
80
+ def __remove_lines_between_markers(self, file_path, start_marker, end_marker, hostnames = []):
81
+ with open(file_path, "r") as file:
82
+ lines = file.readlines()
83
+
84
+ filtered_lines = []
85
+ index = 0
86
+
87
+ # Continue until we reach start_marker
88
+ for line in lines:
89
+ index += 1
90
+
91
+ if start_marker in line:
92
+ break
93
+
94
+ filtered_lines.append(line)
95
+
96
+ # Continue until we reach end_marker
97
+ found_hostnames = {}
98
+ section = []
99
+ for line in lines[index:]:
100
+ index += 1
101
+
102
+ if end_marker in line:
103
+ break
104
+
105
+ found = False
106
+ for hostname in hostnames:
107
+ if hostname in line:
108
+ if hostname not in found_hostnames:
109
+ print(f"Removing hostname {hostname}")
110
+
111
+ found_hostnames[hostname] = True
112
+ found = True
113
+
114
+ if not found:
115
+ section.append(line)
116
+
117
+ # If there are still lines in the section
118
+ if len(section):
119
+ filtered_lines.append(start_marker)
120
+ section.append(end_marker)
121
+ filtered_lines += section
122
+
123
+ for line in lines[index:]:
124
+ filtered_lines.append(line)
125
+
126
+ with open(file_path, "w") as file:
127
+ file.writelines(filtered_lines)
128
+
129
+ def __add_lines_between_markers(self, file_path, start_marker, end_marker, hostnames = []):
130
+ with open(file_path, "r") as file:
131
+ lines = file.readlines()
132
+
133
+ filtered_lines = []
134
+ index = 0
135
+
136
+ # Continue until we reach start_marker
137
+ for line in lines:
138
+ index += 1
139
+ if start_marker in line:
140
+ break
141
+
142
+ filtered_lines.append(line)
143
+
144
+ # If no empty line before start_marker, add one
145
+ if len(filtered_lines):
146
+ last_line = filtered_lines[-1]
147
+
148
+ if last_line != "\n":
149
+ filtered_lines.append("\n")
150
+
151
+ filtered_lines.append(start_marker)
152
+
153
+ # Continue until we reach end_marker
154
+ found_hostnames = {}
155
+ for line in lines[index:]:
156
+ index += 1
157
+
158
+ if end_marker in line:
159
+ break
160
+
161
+ found = False
162
+ for hostname in hostnames:
163
+ if hostname in line:
164
+ filtered_lines.append(line)
165
+ found_hostnames[hostname] = True
166
+ found = True
167
+
168
+ if not found:
169
+ filtered_lines.append(line)
170
+
171
+ for hostname in hostnames:
172
+ if hostname in found_hostnames:
173
+ continue
174
+
175
+ print(f"Installing hostname {hostname}")
176
+ filtered_lines.append(f"127.0.0.1 {hostname}\n")
177
+ filtered_lines.append(f"::1 {hostname}\n")
178
+
179
+ filtered_lines.append(end_marker)
180
+
181
+ for line in lines[index:]:
182
+ filtered_lines.append(line)
183
+
184
+ with open(file_path, "w") as file:
185
+ file.writelines(filtered_lines)
@@ -20,6 +20,5 @@ class Service():
20
20
  def service_name(self):
21
21
  return self.__service_name
22
22
 
23
- @property
24
23
  def workflow_dir_path(self, workflow_name: str):
25
24
  return os.path.join(self.dir_path, workflow_name)
@@ -4,7 +4,6 @@ import pdb
4
4
  from .config import Config
5
5
  from .constants import (
6
6
  SERVICE_DETACHED_ENV,
7
- SERVICE_DOCKER_COMPOSE_PATH_ENV,
8
7
  SERVICE_HOSTNAME_ENV,
9
8
  SERVICE_PRIORITY_ENV,
10
9
  SERVICE_PORT_ENV,
@@ -18,7 +17,6 @@ class ServiceConfig(Config):
18
17
  super().__init__(dir)
19
18
 
20
19
  self.__detached = None
21
- self.__docker_compose_path = None
22
20
  self.__hostname = None
23
21
  self.__port = None
24
22
  self.__priority = None
@@ -53,14 +51,6 @@ class ServiceConfig(Config):
53
51
  def detached(self, v):
54
52
  self.__detached = v
55
53
 
56
- @property
57
- def docker_compose_path(self):
58
- return self.__docker_compose_path
59
-
60
- @docker_compose_path.setter
61
- def docker_compose_path(self, v):
62
- self.__docker_compose_path = v
63
-
64
54
  @property
65
55
  def hostname(self):
66
56
  return (self.__hostname or '').strip()
@@ -120,19 +110,25 @@ class ServiceConfig(Config):
120
110
  config = config or self.read()
121
111
 
122
112
  self.detached = config.get(SERVICE_DETACHED_ENV)
123
- self.docker_compose_path = config.get(SERVICE_DOCKER_COMPOSE_PATH_ENV)
124
113
  self.hostname = config.get(SERVICE_HOSTNAME_ENV)
125
114
  self.port = config.get(SERVICE_PORT_ENV)
126
115
  self.priority = config.get(SERVICE_PRIORITY_ENV)
127
116
  self.proxy_mode = config.get(SERVICE_PROXY_MODE_ENV)
128
117
  self.scheme = config.get(SERVICE_SCHEME_ENV)
129
118
 
119
+ def to_dict(self):
120
+ return {
121
+ 'detached': self.detached,
122
+ 'hostname': self.hostname,
123
+ 'port': self.port,
124
+ 'priority': self.priority,
125
+ 'proxy_mode': self.proxy_mode,
126
+ 'scheme': self.scheme,
127
+ }
128
+
130
129
  def write(self):
131
130
  config = {}
132
131
 
133
- if self.docker_compose_path:
134
- config[SERVICE_DOCKER_COMPOSE_PATH_ENV] = self.docker_compose_path
135
-
136
132
  if self.hostname:
137
133
  config[SERVICE_HOSTNAME_ENV] = self.hostname
138
134
 
@@ -18,7 +18,7 @@ from stoobly_agent.app.cli.scaffold.constants import (
18
18
  WORKFLOW_RECORD_TYPE,
19
19
  WORKFLOW_TEST_TYPE,
20
20
  )
21
- from stoobly_agent.app.cli.scaffold.hosts_file_reader import HostsFileReader
21
+ from stoobly_agent.app.cli.scaffold.hosts_file_manager import HostsFileManager
22
22
  from stoobly_agent.app.cli.scaffold.service_command import ServiceCommand
23
23
  from stoobly_agent.app.cli.scaffold.service_docker_compose import ServiceDockerCompose
24
24
  from stoobly_agent.app.cli.scaffold.validate_command import ValidateCommand
@@ -89,8 +89,8 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
89
89
  def hostname_exists(self, hostname: str) -> bool:
90
90
  print(f"Validating hostname exists in hosts file for hostname: {hostname}")
91
91
 
92
- hosts_file_reader = HostsFileReader()
93
- host_mapping = hosts_file_reader.find_host(hostname)
92
+ hosts_file_manager = HostsFileManager()
93
+ host_mapping = hosts_file_manager.find_host(hostname)
94
94
  if host_mapping:
95
95
  print(f"Correct hosts mapping found for {hostname}")
96
96
  return True
@@ -1,4 +1,4 @@
1
- FROM stoobly/agent:1.3
1
+ FROM stoobly/agent:1.4
2
2
 
3
3
  ARG USER_ID
4
4
 
@@ -4,7 +4,7 @@
4
4
  # STOOBLY_CA_CERTS_DIR: path to folder where ca certs are stored
5
5
  # STOOBLY_CERTS_DIR: path to a folder to store certs
6
6
  # STOOBLY_CONTEXT_DIR: path to the folder containing the .stoobly folder
7
- # STOOBLY_WORKFLOW_OPTIONS: extra options to pass to 'stoobly-agent scaffold workflow' commands
7
+ # STOOBLY_WORKFLOW_SERVICE_OPTIONS: extra --service options to pass 'stoobly-agent scaffold workflow' commands
8
8
 
9
9
  # Constants
10
10
  DIR := $(dir $(realpath $(lastword $(MAKEFILE_LIST))))
@@ -24,7 +24,7 @@ context_dir_option=--context-dir-path $(context_dir)
24
24
  user_id_option=--user-id $(USER_ID)
25
25
  stoobly_exec_options=--profile $(EXEC_WORKFLOW_NAME) -p $(EXEC_WORKFLOW_NAME)
26
26
  workflow_down_options=$(user_id_option)
27
- workflow_options=$${STOOBLY_WORKFLOW_OPTIONS:+$$STOOBLY_WORKFLOW_OPTIONS }
27
+ workflow_service_options=$(shell echo $$STOOBLY_WORKFLOW_SERVICE_OPTIONS)
28
28
  workflow_up_options=$(context_dir_option) --ca-certs-dir-path $(ca_certs_dir) --certs-dir-path $(certs_dir) --from-make $(user_id_option)
29
29
 
30
30
  app_data_dir=$(app_dir)/.stoobly
@@ -60,9 +60,16 @@ stoobly_exec_run_env=$(source_env) && $(exec_env) && export CONTEXT_DIR="$(app_d
60
60
  # Workflow run
61
61
  workflow_run=$(source_env) && bash "$(workflow_run_script)"
62
62
 
63
+ ca-cert/install: stoobly/install
64
+ @echo "Running stoobly-agent ca-cert install..."; \
65
+ stoobly-agent ca-cert install
63
66
  certs:
64
67
  @export EXEC_COMMAND=bin/.mkcert && \
65
68
  $(stoobly_exec)
69
+ command/install:
70
+ $(eval COMMAND=install)
71
+ command/uninstall:
72
+ $(eval COMMAND=uninstall)
66
73
  nameservers: tmpdir
67
74
  @if [ -f /etc/resolv.conf ]; then \
68
75
  nameserver=$$(grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' /etc/resolv.conf) && \
@@ -72,7 +79,8 @@ nameservers: tmpdir
72
79
  fi; \
73
80
  echo "$$nameserver" > $(app_tmp_dir)/.nameservers; \
74
81
  else \
75
- echo "/etc/resolv.conf not found."; \
82
+ echo "/etc/resolv.conf not found." >&2; \
83
+ exit 1; \
76
84
  fi
77
85
  intercept/disable:
78
86
  @export EXEC_COMMAND=bin/.disable && \
@@ -81,42 +89,24 @@ intercept/enable:
81
89
  @export EXEC_COMMAND=bin/.enable && \
82
90
  export EXEC_ARGS=$(scenario_key) && \
83
91
  $(stoobly_exec)
84
- mock: nameservers
85
- @export EXEC_COMMAND=bin/.up && \
86
- export EXEC_OPTIONS="$(workflow_up_options) $(workflow_options)$(options)" && \
87
- export EXEC_ARGS="mock" && \
88
- $(stoobly_exec_run) && \
89
- $(workflow_run)
90
- mock/logs:
91
- @export EXEC_COMMAND=bin/.logs && \
92
- export EXEC_OPTIONS="$(workflow_options)$(options)" && \
93
- export EXEC_ARGS="mock" && \
94
- $(stoobly_exec_run) && \
95
- $(workflow_run)
96
- mock/down:
97
- @export EXEC_COMMAND=bin/.down && \
98
- export EXEC_OPTIONS="$(workflow_down_options) $(workflow_options)$(options)" && \
99
- export EXEC_ARGS="mock" && \
100
- $(stoobly_exec_run) && \
101
- $(workflow_run)
102
- record: nameservers
103
- @export EXEC_COMMAND=bin/.up && \
104
- export EXEC_OPTIONS="$(workflow_up_options) $(workflow_options)$(options)" && \
105
- export EXEC_ARGS="record" && \
106
- $(stoobly_exec_run) && \
107
- $(workflow_run)
108
- record/logs:
109
- @export EXEC_COMMAND=bin/.logs && \
110
- export EXEC_OPTIONS="$(workflow_options)$(options)" && \
111
- export EXEC_ARGS="record" && \
112
- $(stoobly_exec_run) && \
113
- $(workflow_run)
114
- record/down:
115
- @export EXEC_COMMAND=bin/.down && \
116
- export EXEC_OPTIONS="$(workflow_down_options) $(workflow_options)$(options)" && \
117
- export EXEC_ARGS="record" && \
118
- $(stoobly_exec_run) && \
119
- $(workflow_run)
92
+ mock: workflow/mock workflow/hostname/install nameservers workflow/up
93
+ mock/services: workflow/mock workflow/services
94
+ mock/logs: workflow/mock workflow/logs
95
+ mock/down: workflow/mock workflow/down workflow/hostname/uninstall
96
+ pipx/install:
97
+ @if ! command -v pipx >/dev/null 2>&1; then \
98
+ echo "pipx is not installed. Installing pipx..."; \
99
+ python3 -m pip install --user pipx && python3 -m pipx ensurepath; \
100
+ fi
101
+ python/validate:
102
+ @if ! python3 --version | grep -Eq 'Python 3\.(10|11|12)'; then \
103
+ echo "Error: Python 3.10, 3.11, or 3.12 is required."; \
104
+ exit 1; \
105
+ fi
106
+ record: workflow/record workflow/hostname/install nameservers workflow/up
107
+ record/down: workflow/record workflow/down workflow/hostname/uninstall
108
+ record/services: workflow/record workflow/services
109
+ record/logs: workflow/record workflow/logs
120
110
  scenario/create:
121
111
  # Create a scenario
122
112
  @export EXEC_COMMAND=bin/.create && \
@@ -146,23 +136,57 @@ scenario/snapshot:
146
136
  export EXEC_OPTIONS="$(options)" && \
147
137
  export EXEC_ARGS="$(key)" && \
148
138
  $(stoobly_exec)
149
- test:
150
- @export EXEC_COMMAND=bin/.up && \
151
- export EXEC_OPTIONS="$(workflow_up_options) $(workflow_options)$(options)" && \
152
- export EXEC_ARGS="test" && \
139
+ stoobly/install: python/validate pipx/install
140
+ @if ! pipx list 2> /dev/null | grep -q 'stoobly-agent'; then \
141
+ echo "stoobly-agent not found. Installing..."; \
142
+ pipx install stoobly-agent; \
143
+ fi
144
+ test: workflow/test workflow/up
145
+ test/services: workflow/test workflow/services
146
+ test/logs: workflow/test workflow/logs
147
+ test/down: workflow/test workflow/down
148
+ tmpdir:
149
+ @mkdir -p $(app_tmp_dir)
150
+ workflow/down:
151
+ @export EXEC_COMMAND=bin/.down && \
152
+ export EXEC_OPTIONS="$(workflow_down_options) $(workflow_service_options) $(options)" && \
153
+ export EXEC_ARGS="$(WORKFLOW)" && \
153
154
  $(stoobly_exec_run) && \
154
155
  $(workflow_run)
155
- test/logs:
156
+ workflow/hostname: stoobly/install
157
+ @read -p "Do you want to $(COMMAND) hostname(s) in /etc/hosts? (y/N) " confirm && \
158
+ if [ "$$confirm" = "y" ] || [ "$$confirm" = "Y" ]; then \
159
+ CURRENT_VERSION=$$(stoobly-agent --version); \
160
+ REQUIRED_VERSION="1.4.0"; \
161
+ if [ "$$(printf '%s\n' "$$REQUIRED_VERSION" "$$CURRENT_VERSION" | sort -V | head -n 1)" != "$$REQUIRED_VERSION" ]; then \
162
+ echo "stoobly-agent version $$REQUIRED_VERSION required. Please run: pipx upgrade stoobly-agent"; \
163
+ exit 1; \
164
+ fi; \
165
+ echo "Running stoobly-agent scaffold hostname $(COMMAND) $(workflow_service_options)"; \
166
+ stoobly-agent scaffold hostname $(COMMAND) --app-dir-path $(app_dir) --workflow $(WORKFLOW) $(workflow_service_options); \
167
+ fi
168
+ workflow/hostname/install: command/install workflow/hostname
169
+ workflow/hostname/uninstall: command/uninstall workflow/hostname
170
+ workflow/logs:
156
171
  @export EXEC_COMMAND=bin/.logs && \
157
- export EXEC_OPTIONS="$(workflow_options)$(options)" && \
158
- export EXEC_ARGS="test" && \
172
+ export EXEC_OPTIONS="$(workflow_service_options) $(options)" && \
173
+ export EXEC_ARGS="$(WORKFLOW)" && \
159
174
  $(stoobly_exec_run) && \
160
175
  $(workflow_run)
161
- test/down:
162
- @export EXEC_COMMAND=bin/.down && \
163
- export EXEC_OPTIONS="$(workflow_down_options) $(workflow_options)$(options)" && \
164
- export EXEC_ARGS="test" && \
176
+ workflow/mock:
177
+ $(eval WORKFLOW=mock)
178
+ workflow/record:
179
+ $(eval WORKFLOW=record)
180
+ workflow/services:
181
+ @export EXEC_COMMAND=bin/.services && \
182
+ export EXEC_OPTIONS="$(workflow_service_options) $(options)" && \
183
+ export EXEC_ARGS="$(WORKFLOW)" && \
184
+ $(stoobly_exec_run)
185
+ workflow/test:
186
+ $(eval WORKFLOW=test)
187
+ workflow/up:
188
+ @export EXEC_COMMAND=bin/.up && \
189
+ export EXEC_OPTIONS="$(workflow_up_options) $(workflow_service_options) $(options)" && \
190
+ export EXEC_ARGS="$(WORKFLOW)" && \
165
191
  $(stoobly_exec_run) && \
166
- $(workflow_run)
167
- tmpdir:
168
- @mkdir -p $(app_tmp_dir)
192
+ $(workflow_run)
@@ -0,0 +1,9 @@
1
+ #!/bin/bash
2
+
3
+ extra_options=$EXEC_OPTIONS
4
+ workflow=$1
5
+
6
+ stoobly-agent scaffold service list \
7
+ --app-dir-path "$(pwd)" \
8
+ --workflow $workflow \
9
+ $extra_options
@@ -17,7 +17,6 @@ CORE_WORKFLOWS = [WORKFLOW_MOCK_TYPE, WORKFLOW_RECORD_TYPE, WORKFLOW_TEST_TYPE]
17
17
 
18
18
  class BuildOptions(TypedDict):
19
19
  builder_class: type
20
- headless: bool
21
20
  service_builder: ServiceBuilder
22
21
  template: WORKFLOW_TEMPLATE
23
22
  workflow_decorators: List[Union[MockDecorator, ReverseProxyDecorator]]