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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sshkube
3
- Version: 0.2.4
3
+ Version: 0.4.0
4
4
  Summary: Access kubernetes clusters over ssh
5
5
  Author: Daniel J. B. Clarke
6
6
  Author-email: u8sand@gmail.com
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sshkube"
3
- version = "0.2.4"
3
+ version = "0.4.0"
4
4
  description = "Access kubernetes clusters over ssh"
5
5
  authors = [
6
6
  {name = "Daniel J. B. Clarke",email = "u8sand@gmail.com"}
@@ -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
- # instal sshkube server
116
- workdir.mkdir(parents=True, exist_ok=True)
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 or force:
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
- _init(server=server)
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
- _run(server=server, args=args)
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
- import shutil
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