mantis-cli 22.3.0__tar.gz → 22.5.0__tar.gz
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.
- {mantis_cli-22.3.0/mantis_cli.egg-info → mantis_cli-22.5.0}/PKG-INFO +37 -27
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/README.md +33 -4
- mantis_cli-22.5.0/mantis/__init__.py +1 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis/managers.py +84 -9
- {mantis_cli-22.3.0 → mantis_cli-22.5.0/mantis_cli.egg-info}/PKG-INFO +38 -28
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis_cli.egg-info/entry_points.txt +1 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/tests/test_managers.py +35 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/tests/test_tunnel.py +70 -1
- mantis_cli-22.3.0/mantis/__init__.py +0 -1
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/LICENSE +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/MANIFEST.in +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis/__main__.py +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis/app.py +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis/command_line.py +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis/commands/__init__.py +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis/commands/compose.py +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis/commands/configuration.py +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis/commands/connection.py +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis/commands/containers.py +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis/commands/core.py +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis/commands/django.py +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis/commands/images.py +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis/commands/nginx.py +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis/commands/postgres.py +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis/commands/secrets.py +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis/commands/services.py +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis/commands/volumes.py +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis/config.py +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis/cryptography.py +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis/environment.py +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis/extensions/__init__.py +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis/extensions/django.py +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis/extensions/nginx.py +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis/extensions/postgres.py +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis/helpers.py +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis/mantis.tpl +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis/schema.py +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis_cli.egg-info/SOURCES.txt +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis_cli.egg-info/dependency_links.txt +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis_cli.egg-info/requires.txt +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/mantis_cli.egg-info/top_level.txt +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/setup.cfg +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/setup.py +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/tests/__init__.py +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/tests/test_build_config.py +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/tests/test_command_line.py +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/tests/test_config.py +0 -0
- {mantis_cli-22.3.0 → mantis_cli-22.5.0}/tests/test_helpers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: mantis_cli
|
|
3
|
-
Version: 22.
|
|
3
|
+
Version: 22.5.0
|
|
4
4
|
Summary: Management command to build and deploy webapps, especially based on Django
|
|
5
5
|
Home-page: https://github.com/PragmaticMates/mantis-cli
|
|
6
6
|
Author: Erik Telepovský
|
|
@@ -9,6 +9,7 @@ Maintainer: Pragmatic Mates
|
|
|
9
9
|
Maintainer-email: info@pragmaticmates.com
|
|
10
10
|
License: GNU General Public License (GPL)
|
|
11
11
|
Keywords: management deployment docker command
|
|
12
|
+
Platform: UNKNOWN
|
|
12
13
|
Classifier: Programming Language :: Python
|
|
13
14
|
Classifier: Operating System :: OS Independent
|
|
14
15
|
Classifier: Environment :: Web Environment
|
|
@@ -18,27 +19,6 @@ Classifier: License :: OSI Approved :: GNU General Public License (GPL)
|
|
|
18
19
|
Classifier: Development Status :: 5 - Production/Stable
|
|
19
20
|
Description-Content-Type: text/markdown
|
|
20
21
|
License-File: LICENSE
|
|
21
|
-
Requires-Dist: cffi
|
|
22
|
-
Requires-Dist: click
|
|
23
|
-
Requires-Dist: cryptography
|
|
24
|
-
Requires-Dist: pycryptodome
|
|
25
|
-
Requires-Dist: pydantic
|
|
26
|
-
Requires-Dist: PyYAML
|
|
27
|
-
Requires-Dist: rich
|
|
28
|
-
Requires-Dist: typer
|
|
29
|
-
Dynamic: author
|
|
30
|
-
Dynamic: author-email
|
|
31
|
-
Dynamic: classifier
|
|
32
|
-
Dynamic: description
|
|
33
|
-
Dynamic: description-content-type
|
|
34
|
-
Dynamic: home-page
|
|
35
|
-
Dynamic: keywords
|
|
36
|
-
Dynamic: license
|
|
37
|
-
Dynamic: license-file
|
|
38
|
-
Dynamic: maintainer
|
|
39
|
-
Dynamic: maintainer-email
|
|
40
|
-
Dynamic: requires-dist
|
|
41
|
-
Dynamic: summary
|
|
42
22
|
|
|
43
23
|
# mantis-cli
|
|
44
24
|
|
|
@@ -210,10 +190,39 @@ alone: `ssh -L` to a unix socket succeeds even when nothing listens on the far e
|
|
|
210
190
|
tunnel only proves itself once the daemon has replied. Set `enabled` explicitly once
|
|
211
191
|
`check-tunnel` (below) confirms a server, and that round-trip goes away.
|
|
212
192
|
|
|
213
|
-
Requirements on the server:
|
|
214
|
-
(typically membership in the `docker` group), and
|
|
215
|
-
be disabled — it defaults to `yes`. Whenever the
|
|
216
|
-
falls back to a connection per docker command, so
|
|
193
|
+
Requirements on the server: it must be an OpenSSH server (see below), the connecting user
|
|
194
|
+
must be able to read the docker socket (typically membership in the `docker` group), and
|
|
195
|
+
`AllowStreamLocalForwarding` must not be disabled — it defaults to `yes`. Whenever the
|
|
196
|
+
tunnel cannot be used, mantis warns and falls back to a connection per docker command, so
|
|
197
|
+
deployments keep working either way.
|
|
198
|
+
|
|
199
|
+
#### Servers that cannot forward a unix socket
|
|
200
|
+
|
|
201
|
+
Forwarding one is `direct-streamlocal@openssh.com`, an OpenSSH extension, and SSH front-ends
|
|
202
|
+
built on other implementations do not have it. [ssh2incus](https://ssh2incus.com), which
|
|
203
|
+
routes connections into Incus instances by login name, rejects the channel outright:
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
channel 2: open failed: unknown channel type: unsupported channel type
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
No `remote_socket` helps there — the socket is never reached. What works instead is
|
|
210
|
+
multiplexing the per-command connections, which needs no forward at all:
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
Host <server>
|
|
214
|
+
ControlMaster auto
|
|
215
|
+
ControlPath ~/.ssh/mantis-%r@%h:%p
|
|
216
|
+
ControlPersist 5m
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Docker spawns a plain `ssh` per command and passes it no options of its own, so the ssh
|
|
220
|
+
client reads this block by itself and every command reuses one connection. Measured against
|
|
221
|
+
an ssh2incus server, that took a docker command from 0.29 s to 0.09 s. This is a weaker
|
|
222
|
+
guarantee than the tunnel — sessions do count against sshd's `MaxSessions`, where forwarded
|
|
223
|
+
channels do not — so on OpenSSH prefer the tunnel and keep multiplexing for the servers that
|
|
224
|
+
cannot offer it. Set `"tunnel": {"enabled": false}` there, so no run pays for a probe that
|
|
225
|
+
cannot succeed.
|
|
217
226
|
|
|
218
227
|
The check ignores `tunnel.enabled`, so it answers "would this work" rather than "is this
|
|
219
228
|
on" — use it before setting `enabled` to `true`:
|
|
@@ -611,3 +620,4 @@ Works as follows:
|
|
|
611
620
|
## Release notes
|
|
612
621
|
|
|
613
622
|
Mantis uses semantic versioning. See more in [changelog](https://github.com/PragmaticMates/mantis-cli/blob/master/CHANGES.md).
|
|
623
|
+
|
|
@@ -168,10 +168,39 @@ alone: `ssh -L` to a unix socket succeeds even when nothing listens on the far e
|
|
|
168
168
|
tunnel only proves itself once the daemon has replied. Set `enabled` explicitly once
|
|
169
169
|
`check-tunnel` (below) confirms a server, and that round-trip goes away.
|
|
170
170
|
|
|
171
|
-
Requirements on the server:
|
|
172
|
-
(typically membership in the `docker` group), and
|
|
173
|
-
be disabled — it defaults to `yes`. Whenever the
|
|
174
|
-
falls back to a connection per docker command, so
|
|
171
|
+
Requirements on the server: it must be an OpenSSH server (see below), the connecting user
|
|
172
|
+
must be able to read the docker socket (typically membership in the `docker` group), and
|
|
173
|
+
`AllowStreamLocalForwarding` must not be disabled — it defaults to `yes`. Whenever the
|
|
174
|
+
tunnel cannot be used, mantis warns and falls back to a connection per docker command, so
|
|
175
|
+
deployments keep working either way.
|
|
176
|
+
|
|
177
|
+
#### Servers that cannot forward a unix socket
|
|
178
|
+
|
|
179
|
+
Forwarding one is `direct-streamlocal@openssh.com`, an OpenSSH extension, and SSH front-ends
|
|
180
|
+
built on other implementations do not have it. [ssh2incus](https://ssh2incus.com), which
|
|
181
|
+
routes connections into Incus instances by login name, rejects the channel outright:
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
channel 2: open failed: unknown channel type: unsupported channel type
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
No `remote_socket` helps there — the socket is never reached. What works instead is
|
|
188
|
+
multiplexing the per-command connections, which needs no forward at all:
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
Host <server>
|
|
192
|
+
ControlMaster auto
|
|
193
|
+
ControlPath ~/.ssh/mantis-%r@%h:%p
|
|
194
|
+
ControlPersist 5m
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Docker spawns a plain `ssh` per command and passes it no options of its own, so the ssh
|
|
198
|
+
client reads this block by itself and every command reuses one connection. Measured against
|
|
199
|
+
an ssh2incus server, that took a docker command from 0.29 s to 0.09 s. This is a weaker
|
|
200
|
+
guarantee than the tunnel — sessions do count against sshd's `MaxSessions`, where forwarded
|
|
201
|
+
channels do not — so on OpenSSH prefer the tunnel and keep multiplexing for the servers that
|
|
202
|
+
cannot offer it. Set `"tunnel": {"enabled": false}` there, so no run pays for a probe that
|
|
203
|
+
cannot succeed.
|
|
175
204
|
|
|
176
205
|
The check ignores `tunnel.enabled`, so it answers "would this work" rather than "is this
|
|
177
206
|
on" — use it before setting `enabled` to `true`:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = '22.5.0'
|
|
@@ -136,6 +136,10 @@ class AbstractManager(object):
|
|
|
136
136
|
def tunnel_config(self) -> Dict[str, Any]:
|
|
137
137
|
return self.config.get('tunnel', {})
|
|
138
138
|
|
|
139
|
+
@property
|
|
140
|
+
def tunnel_remote_socket(self) -> str:
|
|
141
|
+
return self.tunnel_config.get('remote_socket', '/var/run/docker.sock')
|
|
142
|
+
|
|
139
143
|
def ensure_tunnel(self) -> Optional[str]:
|
|
140
144
|
"""
|
|
141
145
|
Returns path of a local unix socket forwarded to the remote docker socket,
|
|
@@ -187,8 +191,9 @@ class AbstractManager(object):
|
|
|
187
191
|
CLI.success(f'Tunnel is available (docker {version}). '
|
|
188
192
|
f'Enable it in config to skip this check.')
|
|
189
193
|
else:
|
|
194
|
+
reason, remedy = self.diagnose_tunnel()
|
|
190
195
|
CLI.warning(
|
|
191
|
-
'
|
|
196
|
+
f'{reason}\n{remedy}\n'
|
|
192
197
|
'Falling back to a separate SSH connection per docker command.'
|
|
193
198
|
)
|
|
194
199
|
self.stop_tunnel()
|
|
@@ -212,7 +217,7 @@ class AbstractManager(object):
|
|
|
212
217
|
Opens a single backgrounded SSH master forwarding the remote docker socket.
|
|
213
218
|
Every docker command then reuses it instead of opening its own connection.
|
|
214
219
|
"""
|
|
215
|
-
remote_socket = self.
|
|
220
|
+
remote_socket = self.tunnel_remote_socket
|
|
216
221
|
ssh_options = self.tunnel_config.get('ssh_options', [])
|
|
217
222
|
|
|
218
223
|
# unix socket paths are limited to ~104 characters, so prefer a shallow base
|
|
@@ -224,7 +229,9 @@ class AbstractManager(object):
|
|
|
224
229
|
control_socket = str(Path(self._tunnel_dir) / 'ssh.ctl')
|
|
225
230
|
|
|
226
231
|
command = [
|
|
227
|
-
|
|
232
|
+
# -v, because a channel the server refuses is only reported at verbose level,
|
|
233
|
+
# and the master writes to a file nobody reads unless something goes wrong
|
|
234
|
+
'ssh', '-f', '-N', '-M', '-v',
|
|
228
235
|
'-S', control_socket,
|
|
229
236
|
'-o', 'ExitOnForwardFailure=yes',
|
|
230
237
|
'-o', 'ServerAliveInterval=30',
|
|
@@ -251,7 +258,7 @@ class AbstractManager(object):
|
|
|
251
258
|
|
|
252
259
|
if result.returncode != 0:
|
|
253
260
|
CLI.warning(
|
|
254
|
-
f'Failed to open SSH tunnel: {output_file.read_text()
|
|
261
|
+
f'Failed to open SSH tunnel: {self.ssh_log_errors(output_file.read_text())}\n'
|
|
255
262
|
f'Falling back to a separate SSH connection per docker command.'
|
|
256
263
|
)
|
|
257
264
|
self.remove_tunnel_dir()
|
|
@@ -281,6 +288,62 @@ class AbstractManager(object):
|
|
|
281
288
|
shutil.rmtree(self._tunnel_dir, ignore_errors=True)
|
|
282
289
|
self._tunnel_dir = None
|
|
283
290
|
|
|
291
|
+
@staticmethod
|
|
292
|
+
def ssh_log_errors(log: str) -> str:
|
|
293
|
+
"""Keeps the lines worth showing, dropping the debug chatter -v produces."""
|
|
294
|
+
return '\n'.join(line for line in log.splitlines() if not line.startswith('debug')).strip()
|
|
295
|
+
|
|
296
|
+
def read_tunnel_log(self) -> str:
|
|
297
|
+
"""
|
|
298
|
+
Returns whatever the SSH master has written so far.
|
|
299
|
+
|
|
300
|
+
The master is backgrounded with -f and keeps writing to the same file long after it
|
|
301
|
+
exited successfully, so a channel it refuses later is only ever reported there.
|
|
302
|
+
"""
|
|
303
|
+
if not self._tunnel_dir:
|
|
304
|
+
return ''
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
return (Path(self._tunnel_dir) / 'ssh.log').read_text()
|
|
308
|
+
except OSError:
|
|
309
|
+
return ''
|
|
310
|
+
|
|
311
|
+
def diagnose_tunnel(self) -> Tuple[str, str]:
|
|
312
|
+
"""
|
|
313
|
+
Explains why nothing answered through the forward, as (reason, remedy).
|
|
314
|
+
|
|
315
|
+
Opening the forward proves nothing: `ssh -L` to a unix socket succeeds even when the
|
|
316
|
+
far end is dead, so the reason only appears once a connection is made through it.
|
|
317
|
+
Reads the master's log, and therefore has to run before the tunnel is stopped.
|
|
318
|
+
"""
|
|
319
|
+
log = self.read_tunnel_log()
|
|
320
|
+
|
|
321
|
+
if 'unknown channel type' in log:
|
|
322
|
+
return (
|
|
323
|
+
'The server refused to forward a unix socket: it does not implement '
|
|
324
|
+
'direct-streamlocal, so it is not an OpenSSH server. SSH front-ends such as '
|
|
325
|
+
'ssh2incus reject that channel outright, and no remote_socket can help.',
|
|
326
|
+
'Share one connection without a forward instead, by adding to ~/.ssh/config:\n'
|
|
327
|
+
f' Host {self.host}\n'
|
|
328
|
+
' ControlMaster auto\n'
|
|
329
|
+
' ControlPath ~/.ssh/mantis-%r@%h:%p\n'
|
|
330
|
+
' ControlPersist 5m\n'
|
|
331
|
+
'Every docker command then reuses that connection, and setting '
|
|
332
|
+
'"tunnel": {"enabled": false} skips this check from now on.',
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
if 'administratively prohibited' in log:
|
|
336
|
+
return (
|
|
337
|
+
'The server refused the forward.',
|
|
338
|
+
'Check "AllowStreamLocalForwarding" in its sshd_config. It defaults to "yes", '
|
|
339
|
+
'so it has been turned off.',
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
return (
|
|
343
|
+
f'The forward works, but the docker daemon did not answer on {self.tunnel_remote_socket}.',
|
|
344
|
+
f'Make sure user "{self.user}" can access it (usually by being in the "docker" group).',
|
|
345
|
+
)
|
|
346
|
+
|
|
284
347
|
def check_tunnel(self) -> bool:
|
|
285
348
|
"""
|
|
286
349
|
Verifies the SSH tunnel can be used for this connection, regardless of whether it
|
|
@@ -297,7 +360,7 @@ class AbstractManager(object):
|
|
|
297
360
|
if not self.connection.startswith('ssh://'):
|
|
298
361
|
CLI.error(f'Tunnel requires an ssh:// connection, but this one is "{self.connection}".')
|
|
299
362
|
|
|
300
|
-
remote_socket = self.
|
|
363
|
+
remote_socket = self.tunnel_remote_socket
|
|
301
364
|
|
|
302
365
|
if self.tunnel_config.get('enabled') is False:
|
|
303
366
|
CLI.warning('Tunnel is disabled in config, checking anyway...')
|
|
@@ -317,12 +380,14 @@ class AbstractManager(object):
|
|
|
317
380
|
|
|
318
381
|
version = self.probe_tunnel()
|
|
319
382
|
|
|
383
|
+
# the master's log goes away with the tunnel directory
|
|
384
|
+
reason, remedy = ('', '') if version else self.diagnose_tunnel()
|
|
385
|
+
|
|
320
386
|
self.stop_tunnel()
|
|
321
387
|
|
|
322
388
|
if not version:
|
|
323
389
|
CLI.danger('Tunnel is NOT available.')
|
|
324
|
-
CLI.info(f'
|
|
325
|
-
f'Make sure user "{self.user}" can access it (usually by being in the "docker" group).')
|
|
390
|
+
CLI.info(f'{reason}\n{remedy}')
|
|
326
391
|
return False
|
|
327
392
|
|
|
328
393
|
CLI.success(f'Tunnel is available (docker {version}).')
|
|
@@ -521,6 +586,16 @@ class AbstractManager(object):
|
|
|
521
586
|
except OSError as e:
|
|
522
587
|
CLI.error(f"{error_message}: {e}")
|
|
523
588
|
|
|
589
|
+
@property
|
|
590
|
+
def quiet_flag(self) -> str:
|
|
591
|
+
"""
|
|
592
|
+
Layer progress is meant to be repainted in place, which needs a terminal. Redirected
|
|
593
|
+
to a log file or a CI pipe, docker prints every progress event on its own line
|
|
594
|
+
instead -- hundreds per image -- so ask for no progress at all when there is nobody
|
|
595
|
+
to watch it move. Errors and the final result are not progress and still print.
|
|
596
|
+
"""
|
|
597
|
+
return '' if sys.stdout.isatty() else '--quiet'
|
|
598
|
+
|
|
524
599
|
def build_docker_command(self, command: str, use_connection: bool = True) -> str:
|
|
525
600
|
docker_connection = self.docker_connection if use_connection else ''
|
|
526
601
|
|
|
@@ -1229,7 +1304,7 @@ class BaseManager(AbstractManager):
|
|
|
1229
1304
|
CLI.info(f'Services = {params}')
|
|
1230
1305
|
|
|
1231
1306
|
# Push using docker compose
|
|
1232
|
-
self.docker_compose(f'push {params}', use_connection=False)
|
|
1307
|
+
self.docker_compose(f'push {self.quiet_flag} {params}', use_connection=False)
|
|
1233
1308
|
|
|
1234
1309
|
def pull(self, services: Optional[List[str]] = None) -> None:
|
|
1235
1310
|
"""
|
|
@@ -1240,7 +1315,7 @@ class BaseManager(AbstractManager):
|
|
|
1240
1315
|
CLI.info(f'Services = {params}')
|
|
1241
1316
|
|
|
1242
1317
|
# Pull using docker compose
|
|
1243
|
-
self.docker_compose(f'pull {params}')
|
|
1318
|
+
self.docker_compose(f'pull {self.quiet_flag} {params}')
|
|
1244
1319
|
|
|
1245
1320
|
def upload(self) -> None:
|
|
1246
1321
|
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
2
|
-
Name:
|
|
3
|
-
Version: 22.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: mantis-cli
|
|
3
|
+
Version: 22.5.0
|
|
4
4
|
Summary: Management command to build and deploy webapps, especially based on Django
|
|
5
5
|
Home-page: https://github.com/PragmaticMates/mantis-cli
|
|
6
6
|
Author: Erik Telepovský
|
|
@@ -9,6 +9,7 @@ Maintainer: Pragmatic Mates
|
|
|
9
9
|
Maintainer-email: info@pragmaticmates.com
|
|
10
10
|
License: GNU General Public License (GPL)
|
|
11
11
|
Keywords: management deployment docker command
|
|
12
|
+
Platform: UNKNOWN
|
|
12
13
|
Classifier: Programming Language :: Python
|
|
13
14
|
Classifier: Operating System :: OS Independent
|
|
14
15
|
Classifier: Environment :: Web Environment
|
|
@@ -18,27 +19,6 @@ Classifier: License :: OSI Approved :: GNU General Public License (GPL)
|
|
|
18
19
|
Classifier: Development Status :: 5 - Production/Stable
|
|
19
20
|
Description-Content-Type: text/markdown
|
|
20
21
|
License-File: LICENSE
|
|
21
|
-
Requires-Dist: cffi
|
|
22
|
-
Requires-Dist: click
|
|
23
|
-
Requires-Dist: cryptography
|
|
24
|
-
Requires-Dist: pycryptodome
|
|
25
|
-
Requires-Dist: pydantic
|
|
26
|
-
Requires-Dist: PyYAML
|
|
27
|
-
Requires-Dist: rich
|
|
28
|
-
Requires-Dist: typer
|
|
29
|
-
Dynamic: author
|
|
30
|
-
Dynamic: author-email
|
|
31
|
-
Dynamic: classifier
|
|
32
|
-
Dynamic: description
|
|
33
|
-
Dynamic: description-content-type
|
|
34
|
-
Dynamic: home-page
|
|
35
|
-
Dynamic: keywords
|
|
36
|
-
Dynamic: license
|
|
37
|
-
Dynamic: license-file
|
|
38
|
-
Dynamic: maintainer
|
|
39
|
-
Dynamic: maintainer-email
|
|
40
|
-
Dynamic: requires-dist
|
|
41
|
-
Dynamic: summary
|
|
42
22
|
|
|
43
23
|
# mantis-cli
|
|
44
24
|
|
|
@@ -210,10 +190,39 @@ alone: `ssh -L` to a unix socket succeeds even when nothing listens on the far e
|
|
|
210
190
|
tunnel only proves itself once the daemon has replied. Set `enabled` explicitly once
|
|
211
191
|
`check-tunnel` (below) confirms a server, and that round-trip goes away.
|
|
212
192
|
|
|
213
|
-
Requirements on the server:
|
|
214
|
-
(typically membership in the `docker` group), and
|
|
215
|
-
be disabled — it defaults to `yes`. Whenever the
|
|
216
|
-
falls back to a connection per docker command, so
|
|
193
|
+
Requirements on the server: it must be an OpenSSH server (see below), the connecting user
|
|
194
|
+
must be able to read the docker socket (typically membership in the `docker` group), and
|
|
195
|
+
`AllowStreamLocalForwarding` must not be disabled — it defaults to `yes`. Whenever the
|
|
196
|
+
tunnel cannot be used, mantis warns and falls back to a connection per docker command, so
|
|
197
|
+
deployments keep working either way.
|
|
198
|
+
|
|
199
|
+
#### Servers that cannot forward a unix socket
|
|
200
|
+
|
|
201
|
+
Forwarding one is `direct-streamlocal@openssh.com`, an OpenSSH extension, and SSH front-ends
|
|
202
|
+
built on other implementations do not have it. [ssh2incus](https://ssh2incus.com), which
|
|
203
|
+
routes connections into Incus instances by login name, rejects the channel outright:
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
channel 2: open failed: unknown channel type: unsupported channel type
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
No `remote_socket` helps there — the socket is never reached. What works instead is
|
|
210
|
+
multiplexing the per-command connections, which needs no forward at all:
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
Host <server>
|
|
214
|
+
ControlMaster auto
|
|
215
|
+
ControlPath ~/.ssh/mantis-%r@%h:%p
|
|
216
|
+
ControlPersist 5m
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Docker spawns a plain `ssh` per command and passes it no options of its own, so the ssh
|
|
220
|
+
client reads this block by itself and every command reuses one connection. Measured against
|
|
221
|
+
an ssh2incus server, that took a docker command from 0.29 s to 0.09 s. This is a weaker
|
|
222
|
+
guarantee than the tunnel — sessions do count against sshd's `MaxSessions`, where forwarded
|
|
223
|
+
channels do not — so on OpenSSH prefer the tunnel and keep multiplexing for the servers that
|
|
224
|
+
cannot offer it. Set `"tunnel": {"enabled": false}` there, so no run pays for a probe that
|
|
225
|
+
cannot succeed.
|
|
217
226
|
|
|
218
227
|
The check ignores `tunnel.enabled`, so it answers "would this work" rather than "is this
|
|
219
228
|
on" — use it before setting `enabled` to `true`:
|
|
@@ -611,3 +620,4 @@ Works as follows:
|
|
|
611
620
|
## Release notes
|
|
612
621
|
|
|
613
622
|
Mantis uses semantic versioning. See more in [changelog](https://github.com/PragmaticMates/mantis-cli/blob/master/CHANGES.md).
|
|
623
|
+
|
|
@@ -512,3 +512,38 @@ class TestResolveContainers:
|
|
|
512
512
|
manager.get_containers = lambda *args, **kwargs: pytest.fail('containers listed again')
|
|
513
513
|
|
|
514
514
|
assert manager.resolve_container('app', ['itfitness-app']) == 'itfitness-app'
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
class TestQuietProgress:
|
|
518
|
+
"""Layer progress is repainted in place, which only works on a terminal."""
|
|
519
|
+
|
|
520
|
+
@staticmethod
|
|
521
|
+
def _manager():
|
|
522
|
+
manager = BaseManager.__new__(BaseManager)
|
|
523
|
+
manager.commands = []
|
|
524
|
+
manager.docker_compose = lambda command, **kwargs: manager.commands.append(
|
|
525
|
+
' '.join(command.split())
|
|
526
|
+
)
|
|
527
|
+
return manager
|
|
528
|
+
|
|
529
|
+
@patch('sys.stdout', new_callable=MagicMock)
|
|
530
|
+
def test_push_and_pull_are_quiet_off_a_terminal(self, mock_stdout):
|
|
531
|
+
"""A log file or a CI pipe would otherwise get every progress event as its own line."""
|
|
532
|
+
mock_stdout.isatty.return_value = False
|
|
533
|
+
manager = self._manager()
|
|
534
|
+
|
|
535
|
+
manager.push()
|
|
536
|
+
manager.pull()
|
|
537
|
+
|
|
538
|
+
assert manager.commands == ['push --quiet', 'pull --quiet']
|
|
539
|
+
|
|
540
|
+
@patch('sys.stdout', new_callable=MagicMock)
|
|
541
|
+
def test_push_and_pull_keep_progress_on_a_terminal(self, mock_stdout):
|
|
542
|
+
"""Interactively the progress is the point, so nothing changes."""
|
|
543
|
+
mock_stdout.isatty.return_value = True
|
|
544
|
+
manager = self._manager()
|
|
545
|
+
|
|
546
|
+
manager.push(['app'])
|
|
547
|
+
manager.pull(['app'])
|
|
548
|
+
|
|
549
|
+
assert manager.commands == ['push app', 'pull app']
|
|
@@ -114,6 +114,8 @@ class TestTunnelEnabled:
|
|
|
114
114
|
assert command[0] == 'ssh'
|
|
115
115
|
# backgrounded master with a control socket we can close deterministically
|
|
116
116
|
assert '-f' in command and '-N' in command and '-M' in command
|
|
117
|
+
# a refused channel is only reported at verbose level, and only in the log file
|
|
118
|
+
assert '-v' in command
|
|
117
119
|
assert command[command.index('-S') + 1] == f'{ssh.dir}/ssh.ctl'
|
|
118
120
|
# a refused forward must fail loudly instead of yielding a dead socket
|
|
119
121
|
assert 'ExitOnForwardFailure=yes' in command
|
|
@@ -207,6 +209,17 @@ class TestTunnelAutoDetection:
|
|
|
207
209
|
|
|
208
210
|
assert 'did not answer' in warning.call_args[0][0]
|
|
209
211
|
|
|
212
|
+
def test_fallback_says_why(self, ssh):
|
|
213
|
+
ssh.result.stdout = ''
|
|
214
|
+
ssh.result.output = 'channel 2: open failed: unknown channel type: unsupported channel type\n'
|
|
215
|
+
manager = _manager(tunnel={})
|
|
216
|
+
|
|
217
|
+
with patch('mantis.managers.CLI.warning') as warning:
|
|
218
|
+
manager.docker_connection
|
|
219
|
+
|
|
220
|
+
assert 'direct-streamlocal' in warning.call_args[0][0]
|
|
221
|
+
assert 'ControlMaster' in warning.call_args[0][0]
|
|
222
|
+
|
|
210
223
|
def test_failed_detection_closes_the_tunnel(self, ssh):
|
|
211
224
|
ssh.result.stdout = ''
|
|
212
225
|
manager = _manager(tunnel={})
|
|
@@ -259,7 +272,7 @@ class TestTunnelAutoDetection:
|
|
|
259
272
|
|
|
260
273
|
|
|
261
274
|
class TestCheckTunnel:
|
|
262
|
-
"""check-tunnel tells apart
|
|
275
|
+
"""check-tunnel tells apart the reasons a tunnel can be unusable."""
|
|
263
276
|
|
|
264
277
|
def test_reports_available(self, ssh):
|
|
265
278
|
ssh.result.stdout = '24.0.6\n'
|
|
@@ -311,6 +324,62 @@ class TestCheckTunnel:
|
|
|
311
324
|
assert 'docker' in info.call_args[0][0]
|
|
312
325
|
assert 'deploy' in info.call_args[0][0]
|
|
313
326
|
|
|
327
|
+
def test_debug_chatter_is_not_reported_to_the_user(self, ssh):
|
|
328
|
+
"""The master runs with -v, so its log cannot be shown as it is."""
|
|
329
|
+
ssh.result.returncode = 255
|
|
330
|
+
ssh.result.output = (
|
|
331
|
+
'debug1: Connecting to example.com port 2222.\n'
|
|
332
|
+
'ssh: connect to host example.com port 2222: Connection refused\n'
|
|
333
|
+
)
|
|
334
|
+
manager = _manager(tunnel={'enabled': True})
|
|
335
|
+
|
|
336
|
+
with patch('mantis.managers.CLI.warning') as warning, patch('mantis.managers.CLI.danger'), \
|
|
337
|
+
patch('mantis.managers.CLI.info'):
|
|
338
|
+
manager.check_tunnel()
|
|
339
|
+
|
|
340
|
+
message = warning.call_args[0][0]
|
|
341
|
+
|
|
342
|
+
assert 'Connection refused' in message
|
|
343
|
+
assert 'debug1' not in message
|
|
344
|
+
|
|
345
|
+
def test_unsupported_channel_points_at_multiplexing(self, ssh):
|
|
346
|
+
"""Not every SSH server is OpenSSH: some reject the forward channel outright."""
|
|
347
|
+
ssh.result.stdout = ''
|
|
348
|
+
ssh.result.output = 'channel 2: open failed: unknown channel type: unsupported channel type\n'
|
|
349
|
+
manager = _manager(tunnel={'enabled': True})
|
|
350
|
+
|
|
351
|
+
with patch('mantis.managers.CLI.danger'), patch('mantis.managers.CLI.info') as info:
|
|
352
|
+
assert manager.check_tunnel() is False
|
|
353
|
+
|
|
354
|
+
message = info.call_args[0][0]
|
|
355
|
+
|
|
356
|
+
# no remote_socket can fix this, so the advice must not be about the socket
|
|
357
|
+
assert 'direct-streamlocal' in message
|
|
358
|
+
assert 'ControlMaster' in message
|
|
359
|
+
assert 'docker' not in message.split('ControlMaster')[0]
|
|
360
|
+
|
|
361
|
+
def test_prohibited_forward_points_at_sshd_config(self, ssh):
|
|
362
|
+
ssh.result.stdout = ''
|
|
363
|
+
ssh.result.output = 'channel 2: open failed: administratively prohibited: open failed\n'
|
|
364
|
+
manager = _manager(tunnel={'enabled': True})
|
|
365
|
+
|
|
366
|
+
with patch('mantis.managers.CLI.danger'), patch('mantis.managers.CLI.info') as info:
|
|
367
|
+
assert manager.check_tunnel() is False
|
|
368
|
+
|
|
369
|
+
assert 'AllowStreamLocalForwarding' in info.call_args[0][0]
|
|
370
|
+
|
|
371
|
+
def test_reason_is_read_before_the_tunnel_is_removed(self, ssh):
|
|
372
|
+
"""The log lives in the tunnel directory, which stop_tunnel deletes."""
|
|
373
|
+
ssh.result.stdout = ''
|
|
374
|
+
ssh.result.output = 'channel 2: open failed: unknown channel type: unsupported channel type\n'
|
|
375
|
+
manager = _manager(tunnel={'enabled': True})
|
|
376
|
+
|
|
377
|
+
with patch('mantis.managers.CLI.danger'), patch('mantis.managers.CLI.info') as info:
|
|
378
|
+
manager.check_tunnel()
|
|
379
|
+
|
|
380
|
+
assert not ssh.dir.exists()
|
|
381
|
+
assert 'direct-streamlocal' in info.call_args[0][0]
|
|
382
|
+
|
|
314
383
|
def test_non_ssh_connection_is_an_error(self, ssh):
|
|
315
384
|
manager = _manager(connection='context://production', tunnel={'enabled': True})
|
|
316
385
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
VERSION = '22.3.0'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|