sshkube 0.2.4__tar.gz → 0.4.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.
- {sshkube-0.2.4 → sshkube-0.4.0}/PKG-INFO +1 -1
- {sshkube-0.2.4 → sshkube-0.4.0}/pyproject.toml +1 -1
- {sshkube-0.2.4 → sshkube-0.4.0}/sshkube/__main__.py +122 -38
- {sshkube-0.2.4 → sshkube-0.4.0}/LICENSE +0 -0
- {sshkube-0.2.4 → sshkube-0.4.0}/README.md +0 -0
- {sshkube-0.2.4 → sshkube-0.4.0}/sshkube/__init__.py +0 -0
- {sshkube-0.2.4 → sshkube-0.4.0}/sshkube/socat.py +0 -0
|
@@ -13,6 +13,7 @@ sshkube run kubectl get nodes
|
|
|
13
13
|
sshkube run helm list
|
|
14
14
|
'''
|
|
15
15
|
import os
|
|
16
|
+
import re
|
|
16
17
|
import sys
|
|
17
18
|
import yaml
|
|
18
19
|
import click
|
|
@@ -60,6 +61,16 @@ def wait_for_port(port, timeout=1, backoff=1, retries=3):
|
|
|
60
61
|
sock.close()
|
|
61
62
|
raise RuntimeError(f"Proxy server didn't start!")
|
|
62
63
|
|
|
64
|
+
def kubectl_livez(port, timeout=1):
|
|
65
|
+
import ssl, urllib.request, urllib.error
|
|
66
|
+
try:
|
|
67
|
+
with urllib.request.urlopen(f"https://127.0.0.1:{port}/livez", timeout=timeout, context=ssl._create_unverified_context()) as res:
|
|
68
|
+
return res.getcode()
|
|
69
|
+
except urllib.error.HTTPError as e:
|
|
70
|
+
return e.getcode()
|
|
71
|
+
except:
|
|
72
|
+
return 600
|
|
73
|
+
|
|
63
74
|
class PidFile:
|
|
64
75
|
pidfile = workdir/'pid'
|
|
65
76
|
def __init__(self, *, netloc, pid, port):
|
|
@@ -98,6 +109,53 @@ class PidFile:
|
|
|
98
109
|
os.kill(self.pid, signal.SIGINT)
|
|
99
110
|
PidFile.pidfile.unlink()
|
|
100
111
|
|
|
112
|
+
class SSHConfigFile:
|
|
113
|
+
file = workdir/'config'
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def init():
|
|
117
|
+
# include sshkube config in sshconfig
|
|
118
|
+
ssh_dir = pathlib.Path('~/.ssh/').expanduser()
|
|
119
|
+
ssh_dir.mkdir(parents=True, exist_ok=True, mode=700)
|
|
120
|
+
add_ssh_config = f"Include {str(workdir/'config')}"
|
|
121
|
+
ssh_config = (ssh_dir/'config').read_text() if (ssh_dir/'config').exists() else ''
|
|
122
|
+
if add_ssh_config not in ssh_config.splitlines():
|
|
123
|
+
ssh_config = add_ssh_config + '\n' + ssh_config
|
|
124
|
+
(ssh_dir/'config').write_text(ssh_config)
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def read():
|
|
128
|
+
SSHConfigFile.file.parent.mkdir(parents=True, exist_ok=True)
|
|
129
|
+
current_config = SSHConfigFile.file.read_text() if SSHConfigFile.file.exists() else ''
|
|
130
|
+
return {
|
|
131
|
+
m.group(1): m.group(0)
|
|
132
|
+
for m in re.finditer(r'Host (.+?)(\n +.+)+', current_config)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def hosts():
|
|
137
|
+
return SSHConfigFile.read().keys()
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def install(*, server, user, identity_file, use_env, verify):
|
|
141
|
+
SSHConfigFile.init()
|
|
142
|
+
hosts = SSHConfigFile.read()
|
|
143
|
+
hosts[server] = '\n'.join(filter(None, [
|
|
144
|
+
f"Host {server}",
|
|
145
|
+
user and f" User {user}",
|
|
146
|
+
f" IdentitiesOnly yes",
|
|
147
|
+
identity_file and f" IdentityFile {identity_file}",
|
|
148
|
+
use_env and f" ProxyCommand env \"PYTHONPATH={':'.join(sys.path)}\" \"{sys.executable}\" -m {__package__} openssl -s {server} --verify={verify}",
|
|
149
|
+
(not use_env) and f" ProxyCommand \"{sys.executable}\" -m {__package__} openssl -s {server} --verify={verify}",
|
|
150
|
+
]))
|
|
151
|
+
SSHConfigFile.file.write_text('\n\n'.join(hosts.values()))
|
|
152
|
+
|
|
153
|
+
@staticmethod
|
|
154
|
+
def uninstall(*, server):
|
|
155
|
+
hosts = SSHConfigFile.read()
|
|
156
|
+
del hosts[server]
|
|
157
|
+
SSHConfigFile.file.write_text('\n\n'.join(hosts.values()))
|
|
158
|
+
|
|
101
159
|
def make_ssh_cmd(*, server, cmd=[], flags=[]):
|
|
102
160
|
return ['ssh', *flags, server, *cmd]
|
|
103
161
|
|
|
@@ -109,47 +167,65 @@ def make_ssh_cmd(*, server, cmd=[], flags=[]):
|
|
|
109
167
|
@click.option('-e', '--use-env', type=bool, is_flag=True, default=False)
|
|
110
168
|
@click.option('-v', '--verbose', type=bool, is_flag=True, default=False)
|
|
111
169
|
def install(*, server, user, use_env, identity_file, verify, verbose):
|
|
170
|
+
''' Install a new server to use with sshkube
|
|
171
|
+
'''
|
|
112
172
|
_install(server=server, user=user, use_env=use_env, identity_file=identity_file, verify=verify, verbose=verbose)
|
|
113
173
|
|
|
114
174
|
def _install(*, server, user, use_env, identity_file, verify, verbose):
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
dotenv.set_key(workdir/'.env', 'SSHKUBE_SERVER', server)
|
|
118
|
-
|
|
119
|
-
# include sshkube config in sshconfig
|
|
120
|
-
ssh_dir = pathlib.Path('~/.ssh/').expanduser()
|
|
121
|
-
ssh_dir.mkdir(parents=True, exist_ok=True, mode=700)
|
|
122
|
-
add_ssh_config = f"Include {str(workdir/'config')}"
|
|
123
|
-
ssh_config = (ssh_dir/'config').read_text() if (ssh_dir/'config').exists() else ''
|
|
124
|
-
if add_ssh_config not in ssh_config.splitlines():
|
|
125
|
-
ssh_config = add_ssh_config + '\n' + ssh_config
|
|
126
|
-
(ssh_dir/'config').write_text(ssh_config)
|
|
127
|
-
|
|
128
|
-
# create sshkube sshconfig
|
|
129
|
-
(workdir/'config').write_text('\n'.join(filter(None, [
|
|
130
|
-
f"Host {server}",
|
|
131
|
-
user and f" User {user}",
|
|
132
|
-
f" IdentitiesOnly yes",
|
|
133
|
-
identity_file and f" IdentityFile {identity_file}",
|
|
134
|
-
use_env and f" ProxyCommand env \"PYTHONPATH={':'.join(sys.path)}\" \"{sys.executable}\" -m {__package__} openssl -s {server} --verify={verify}",
|
|
135
|
-
(not use_env) and f" ProxyCommand \"{sys.executable}\" -m {__package__} openssl -s {server} --verify={verify}",
|
|
136
|
-
]))+'\n')
|
|
175
|
+
# update sshkube sshconfig
|
|
176
|
+
SSHConfigFile.install(server=server, user=user, identity_file=identity_file, use_env=use_env, verify=verify)
|
|
137
177
|
|
|
138
178
|
# verify connection
|
|
139
179
|
try:
|
|
140
180
|
subprocess.check_call(make_ssh_cmd(server=server, flags=['-v'] if verbose else [], cmd=['echo', 'Success!']), stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr)
|
|
141
181
|
except subprocess.CalledProcessError as e:
|
|
142
182
|
raise click.UsageError('Failed to connect, check all options and any above errors') from e
|
|
183
|
+
|
|
184
|
+
_use(server=server)
|
|
185
|
+
|
|
186
|
+
@cli.command()
|
|
187
|
+
@click.option('-s', '--server', envvar='SSHKUBE_SERVER', type=str, required=True)
|
|
188
|
+
def list(*, server):
|
|
189
|
+
''' List currently installed servers
|
|
190
|
+
'''
|
|
191
|
+
_list(server=server)
|
|
192
|
+
|
|
193
|
+
def _list(*, server):
|
|
194
|
+
print('\n'.join(['Servers (* is in use):']+[
|
|
195
|
+
(' * ' if server == host else ' ') + host
|
|
196
|
+
for host in SSHConfigFile.hosts()
|
|
197
|
+
]), file=sys.stderr)
|
|
198
|
+
|
|
199
|
+
@cli.command()
|
|
200
|
+
@click.option('-s', '--server', envvar='SSHKUBE_SERVER', type=str, required=True)
|
|
201
|
+
def use(*, server):
|
|
202
|
+
''' Use a specific configured server
|
|
203
|
+
'''
|
|
204
|
+
_use(server=server)
|
|
205
|
+
|
|
206
|
+
def _use(*, server):
|
|
207
|
+
dotenv.set_key(workdir/'.env', 'SSHKUBE_SERVER', server)
|
|
208
|
+
_list(server=server)
|
|
143
209
|
|
|
210
|
+
@cli.command()
|
|
211
|
+
@click.option('-s', '--server', envvar='SSHKUBE_SERVER', type=str, required=True)
|
|
212
|
+
def uninstall(*, server):
|
|
213
|
+
''' Uninstall a previously installed server
|
|
214
|
+
'''
|
|
215
|
+
_uninstall(server=server)
|
|
216
|
+
|
|
217
|
+
def _uninstall(*, server):
|
|
218
|
+
_kill_server()
|
|
219
|
+
SSHConfigFile.uninstall(server=server)
|
|
144
220
|
|
|
145
221
|
@cli.command()
|
|
146
222
|
@click.option('-s', '--server', envvar='SSHKUBE_SERVER', type=str, required=True)
|
|
147
223
|
def kubeconfig(*, server):
|
|
224
|
+
''' [internal]: Obtain kubeconfig from remote
|
|
225
|
+
'''
|
|
148
226
|
_kubeconfig(server=server)
|
|
149
227
|
|
|
150
228
|
def _kubeconfig(*, server):
|
|
151
|
-
''' get kube config from remote
|
|
152
|
-
'''
|
|
153
229
|
try:
|
|
154
230
|
kube_config = subprocess.check_output(make_ssh_cmd(server=server, cmd=['cat', '~/.kube/config']))
|
|
155
231
|
except subprocess.CalledProcessError:
|
|
@@ -168,7 +244,10 @@ def start_server(*, server, force):
|
|
|
168
244
|
def _start_server(*, server, force):
|
|
169
245
|
pid = PidFile.read()
|
|
170
246
|
if pid:
|
|
171
|
-
if pid.netloc != server
|
|
247
|
+
if force or pid.netloc != server:
|
|
248
|
+
_kill_server()
|
|
249
|
+
elif kubectl_livez(pid.port) >= 500:
|
|
250
|
+
# permission denied error is also fine if connection is broken we get a 600
|
|
172
251
|
_kill_server()
|
|
173
252
|
else:
|
|
174
253
|
return
|
|
@@ -187,6 +266,8 @@ def _start_server(*, server, force):
|
|
|
187
266
|
proc = Popen(make_ssh_cmd(server=server, flags=[f"-NL{port}:{k8s_server_parsed.netloc}"]), start_new_session=True)
|
|
188
267
|
try:
|
|
189
268
|
wait_for_port(port)
|
|
269
|
+
if kubectl_livez(port) >= 500:
|
|
270
|
+
raise click.UsageError('Kubernetes not available')
|
|
190
271
|
PidFile(netloc=server, pid=proc.pid, port=port).write()
|
|
191
272
|
except RuntimeError as e:
|
|
192
273
|
proc.kill()
|
|
@@ -198,11 +279,13 @@ def _start_server(*, server, force):
|
|
|
198
279
|
|
|
199
280
|
@cli.command()
|
|
200
281
|
def kill_server():
|
|
282
|
+
''' [internal]: Explicitly kill the proxy server when something went wrong
|
|
283
|
+
|
|
284
|
+
Inspired by adb kill-server
|
|
285
|
+
'''
|
|
201
286
|
_kill_server()
|
|
202
287
|
|
|
203
288
|
def _kill_server():
|
|
204
|
-
''' Inspired by adb kill-server
|
|
205
|
-
'''
|
|
206
289
|
pid = PidFile.read()
|
|
207
290
|
if pid: pid.kill()
|
|
208
291
|
(workdir/'kube.config').unlink(missing_ok=True)
|
|
@@ -211,13 +294,15 @@ def _kill_server():
|
|
|
211
294
|
@cli.command()
|
|
212
295
|
@click.option('-s', '--server', envvar='SSHKUBE_SERVER', type=str, required=True)
|
|
213
296
|
def init(*, server):
|
|
214
|
-
|
|
297
|
+
''' Configure current shell to access the sshkube kubeconfig
|
|
215
298
|
|
|
216
|
-
def _init(*, server):
|
|
217
|
-
'''
|
|
218
299
|
Usage: eval "$(sshkube init)"
|
|
219
300
|
'''
|
|
301
|
+
_init(server=server)
|
|
302
|
+
|
|
303
|
+
def _init(*, server):
|
|
220
304
|
_start_server(server=server, force=False)
|
|
305
|
+
_list(server=server)
|
|
221
306
|
pid = PidFile.read()
|
|
222
307
|
assert pid is not None
|
|
223
308
|
#
|
|
@@ -239,12 +324,13 @@ def _init(*, server):
|
|
|
239
324
|
@click.option('-s', '--server', envvar='SSHKUBE_SERVER', type=str, required=True)
|
|
240
325
|
@click.argument('args', nargs=-1, type=click.UNPROCESSED)
|
|
241
326
|
def run(*, server, args):
|
|
242
|
-
|
|
327
|
+
''' Run a command ensuring that the kubeconfig is set properly
|
|
243
328
|
|
|
244
|
-
def _run(*, server, args):
|
|
245
|
-
'''
|
|
246
329
|
Usage: sshkube run kubectl help
|
|
247
330
|
'''
|
|
331
|
+
_run(server=server, args=args)
|
|
332
|
+
|
|
333
|
+
def _run(*, server, args):
|
|
248
334
|
pid = PidFile.read()
|
|
249
335
|
_start_server(server=server, force=False)
|
|
250
336
|
pid = PidFile.read()
|
|
@@ -259,14 +345,12 @@ def _run(*, server, args):
|
|
|
259
345
|
@click.option('-s', '--server', envvar='SSHKUBE_SERVER', type=str, required=True)
|
|
260
346
|
@click.option('--verify', type=int, default=1)
|
|
261
347
|
def openssl(*, server, verify):
|
|
348
|
+
''' [internal]: proxy stdin <-> ssl
|
|
349
|
+
'''
|
|
262
350
|
_openssl(server=server, verify=verify)
|
|
263
351
|
|
|
264
352
|
def _openssl(*, server, verify):
|
|
265
|
-
|
|
266
|
-
socat = shutil.which('socat')
|
|
267
|
-
if socat:
|
|
268
|
-
subprocess.run([socat, '-', f"openssl:{server}:443,verify={verify}"])
|
|
269
|
-
elif sys.platform == 'win32':
|
|
353
|
+
if sys.platform == 'win32':
|
|
270
354
|
import winloop
|
|
271
355
|
winloop.run(_async_openssl(server=server, verify=verify))
|
|
272
356
|
else:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|