dfsync 0.4.6__tar.gz → 0.4.7__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.1
2
2
  Name: dfsync
3
- Version: 0.4.6
3
+ Version: 0.4.7
4
4
  Summary:
5
5
  Author: Mihai Balint
6
6
  Author-email: balint.mihai@gmail.com
@@ -12,6 +12,7 @@ from kubernetes.client.exceptions import ApiException
12
12
  from tenacity import retry, retry_if_exception_type, wait_exponential, stop_after_attempt
13
13
 
14
14
  from dfsync.filters import GIT_FILTER
15
+ from dfsync.kube_credentials import KubeContextConfig
15
16
  from .rsync import rsync_backend
16
17
 
17
18
  DEFAULT_COMMAND = []
@@ -178,84 +179,72 @@ def please_wait(msg="Please wait"):
178
179
  print("⌛ {}...".format(msg))
179
180
 
180
181
 
181
- def _get_kube_api_client(context_name):
182
- configuration = client.Configuration()
183
- config.load_kube_config(
184
- context=context_name,
185
- client_configuration=configuration,
186
- )
187
- configuration.verify_ssl = True
188
- configuration.assert_hostname = False
189
- return client.ApiClient(configuration)
190
-
182
+ def get_selected_kubernetes(kube_host=None) -> KubeContextConfig:
183
+ try:
184
+ config.load_kube_config()
185
+ except Exception as e:
186
+ error_message = str(e)
187
+ print(f"Failed to load default kube config: {error_message}")
191
188
 
192
- def get_selected_kubernetes(kube_host=None):
193
189
  contexts, active_context = config.list_kube_config_contexts()
194
190
  if not contexts:
195
191
  raise ValueError("Cannot find any kubernetes contexts in kube-config file")
196
192
 
197
- context_apis = []
198
- selected_context = None
199
- selected_context_api = None
200
- active_context_api = None
193
+ k8sctx_configs = []
194
+ active_k8ctx = None
195
+ selected_k8ctx = None
201
196
  multiple_matches = False
197
+
202
198
  for c in contexts:
203
- ctx_client = client.CoreV1Api(api_client=_get_kube_api_client(c["name"]))
204
- context_apis.append([c, ctx_client])
199
+ k8sctx = KubeContextConfig(c["name"])
200
+ k8sctx_configs.append(k8sctx)
205
201
 
206
- ctx_api_host = ctx_client.api_client.configuration.host
207
- if c["name"] == active_context["name"]:
208
- active_context_api = ctx_client
202
+ if k8sctx.context_name == active_context["name"]:
203
+ active_k8ctx = k8sctx
204
+ k8sctx.is_active = True
209
205
 
210
- if kube_host is not None and kube_host.lower() == ctx_api_host.lower():
211
- if selected_context is None:
212
- selected_context = c
213
- selected_context_api = ctx_client
206
+ if kube_host is not None and kube_host.lower() == k8sctx.host.lower():
207
+ if selected_k8ctx is None:
208
+ selected_k8ctx = k8sctx
209
+ k8sctx.is_selected = True
214
210
  else:
215
211
  multiple_matches = True
216
212
 
217
- if len(contexts) > 1 or multiple_matches or (kube_host is not None and selected_context is None):
218
- print(f"Multiple kubernetes contexts in kube-config:")
213
+ if kube_host is None and selected_k8ctx is None and active_k8ctx is not None:
214
+ # Select the active context if no specific arg is given
215
+ selected_k8ctx = active_k8ctx
216
+ selected_k8ctx.is_selected = True
219
217
 
220
- for c, ctx_client in context_apis:
221
- ctx_api_host = ctx_client.api_client.configuration.host
222
- tags = []
223
- if c == selected_context:
224
- tags.append("ACTIVE-SELECTED")
225
- elif c["name"] == active_context["name"]:
226
- tags.append("ACTIVE")
218
+ if len(contexts) > 1 or multiple_matches or (kube_host is not None and selected_k8ctx is None):
219
+ print("Multiple kubernetes contexts in kube-config:")
220
+
221
+ for k8sctx in k8sctx_configs:
222
+ print(f" {k8sctx.active_glyph} {k8sctx.prettified_str}")
227
223
 
228
- if len(tags) > 0:
229
- tags_str = ",".join(tags)
230
- print(f" * {c['name']} on {ctx_api_host} [{tags_str}]")
231
- else:
232
- print(f" * {c['name']} on {ctx_api_host}")
233
224
  if kube_host is None:
234
- ctx_api_host = active_context_api.api_client.configuration.host
235
225
  print(
236
- f"Add --kube-host={ctx_api_host} to use a specific kubernetes API host\n"
226
+ f"Add --kube-host={active_k8ctx.host} to use a specific kubernetes API host\n"
237
227
  f"Alternatively, use 'kubectl config set current-context <name>' to set the current context"
238
228
  )
239
229
 
240
- if kube_host is not None and selected_context is None:
230
+ if kube_host is not None and selected_k8ctx is None:
241
231
  raise ValueError(f"None of the kubernetes contexts match '{kube_host}'")
242
232
 
243
- if selected_context is None:
244
- selected_context = active_context
245
- selected_context_api = active_context_api
246
- return selected_context, selected_context_api
233
+ if selected_k8ctx is None:
234
+ raise ValueError("Failed to select a kubernetes context config")
235
+
236
+ return selected_k8ctx
247
237
 
248
238
 
249
239
  class KubeReDeployer:
250
240
  def __init__(self, kube_host=None, pod_timeout=30, container_command=None, full_sync=True, **kwargs):
251
- config.load_kube_config()
252
- selected_context, selected_context_api = get_selected_kubernetes(kube_host)
241
+ k8sctx = get_selected_kubernetes(kube_host)
253
242
 
254
- self.context_name = selected_context["name"]
255
- self.api = selected_context_api
256
- self.apps_api = client.AppsV1Api(api_client=_get_kube_api_client(self.context_name))
243
+ self.context_name = k8sctx.context_name
244
+ self.api = k8sctx.core_v1_api()
245
+ self.apps_api = k8sctx.apps_v1_api()
257
246
 
258
- print(f"Using cluster: {self.context_name} on {self.api.api_client.configuration.host}")
247
+ print(f"Using cluster: {k8sctx.prettified_str}")
259
248
  self.rsync_backend_instance = rsync_backend()
260
249
  self._image_distro = None
261
250
  self.pod_timeout = pod_timeout
@@ -60,7 +60,16 @@ class KeyHandlerException(Exception):
60
60
  pass
61
61
 
62
62
 
63
- KeyHandler = namedtuple("KeyHandler", "keys description action")
63
+ class KeyHandler:
64
+ def __init__(self, keys, description, action, strict_case):
65
+ self.keys = keys
66
+ self.description = description
67
+ self.action = action
68
+
69
+ self.event_keys = set(keys)
70
+ if not strict_case:
71
+ self.event_keys.update([k.lower() for k in self.keys])
72
+ self.event_keys.update([k.upper() for k in self.keys])
64
73
 
65
74
 
66
75
  class KeyController(ControlledThreadedOperation):
@@ -97,12 +106,12 @@ class KeyController(ControlledThreadedOperation):
97
106
  except (Exception, KeyboardInterrupt) as e:
98
107
  self._exception = e
99
108
 
100
- def on_key(self, *keys, description: str = None, action=None):
109
+ def on_key(self, *keys, description: str = None, action=None, strict_case=False):
101
110
  if action is None:
102
111
  raise ValueError("'action' cannot be None")
103
- handler = KeyHandler(keys=keys, description=description, action=action)
112
+ handler = KeyHandler(keys=keys, description=description, action=action, strict_case=strict_case)
104
113
  self.handlers.append(handler)
105
- for k in keys:
114
+ for k in handler.event_keys:
106
115
  self.key_handlers[k] = handler
107
116
 
108
117
  def raise_exceptions(self):
@@ -2,6 +2,7 @@ import os
2
2
  import os.path
3
3
  import yaml
4
4
  from urllib.parse import urlparse
5
+ from kubernetes import client, config
5
6
 
6
7
  LOCAL_CREDENTIALS_FILE = os.path.expanduser("~/.kube/config")
7
8
  EMPTY_CREDENTIALS = """apiVersion: v1
@@ -18,7 +19,7 @@ def update_local_kube_config(new_kube_credentials):
18
19
  try:
19
20
  with open(LOCAL_CREDENTIALS_FILE, "r") as f:
20
21
  credentials = yaml.safe_load(f)
21
- except FileNotFoundError as e:
22
+ except FileNotFoundError:
22
23
  credentials = yaml.safe_load(EMPTY_CREDENTIALS)
23
24
  kube_dir, _ = os.path.split(LOCAL_CREDENTIALS_FILE)
24
25
  os.makedirs(kube_dir, exist_ok=True)
@@ -132,3 +133,89 @@ def contextualize_kube_credentials(kube_host, credentials_file):
132
133
  print(exc)
133
134
 
134
135
  return {}
136
+
137
+
138
+ def normalized_k8s_url(k8s_url: str) -> str:
139
+ parsed = None
140
+ result = k8s_url
141
+ if k8s_url is None:
142
+ return None
143
+ elif not k8s_url:
144
+ raise ValueError(f"Invalid k8s URL: `{k8s_url}`")
145
+ elif "://" not in k8s_url.lower():
146
+ result = f"https://{k8s_url}"
147
+ parsed = urlparse(result)
148
+ else:
149
+ parsed = urlparse(k8s_url)
150
+
151
+ if all([parsed.scheme, parsed.netloc]):
152
+ return result
153
+ else:
154
+ raise ValueError(f"Invalid k8s URL: `{k8s_url}`")
155
+
156
+
157
+ RED = "\033[38;2;255;0;0m"
158
+ GREEN = "\033[38;2;128;255;128m"
159
+ GRAY = "\033[38;2;128;128;128m"
160
+ NEUTRAL = "\033[0m"
161
+
162
+
163
+ class KubeContextConfig:
164
+ def __init__(self, context_name: str) -> None:
165
+ self._api_client = None
166
+ self._load_failure = None
167
+ self._configuration = client.Configuration()
168
+ self.context_name = context_name
169
+ self._adjust_ssl()
170
+ self.is_active = False
171
+ self.is_selected = False
172
+
173
+ def _adjust_ssl(self):
174
+ self._configuration.verify_ssl = True
175
+ self._configuration.assert_hostname = False
176
+
177
+ def _load(self):
178
+ if self._api_client is not None:
179
+ return
180
+ try:
181
+ config.load_kube_config(
182
+ context=self.context_name,
183
+ client_configuration=self._configuration,
184
+ )
185
+ self._adjust_ssl()
186
+ self._api_client = client.ApiClient(self._configuration)
187
+ self._load_failure = None
188
+ except Exception as e:
189
+ self._load_failure = str(e)
190
+
191
+ @property
192
+ def host(self):
193
+ self._load()
194
+ return self._configuration.host
195
+
196
+ @property
197
+ def api_client(self):
198
+ self._load()
199
+ return self._api_client
200
+
201
+ def apps_v1_api(self):
202
+ return client.AppsV1Api(api_client=self.api_client)
203
+
204
+ def core_v1_api(self):
205
+ return client.CoreV1Api(api_client=self.api_client)
206
+
207
+ @property
208
+ def active_glyph(self):
209
+ glyph = "@" if self.is_active else "*"
210
+ COLOR = GREEN if self.is_selected else GRAY
211
+ return f"{COLOR}{glyph}{NEUTRAL}"
212
+
213
+ @property
214
+ def prettified_str(self, include_failure=True):
215
+ fail_text = ""
216
+ if self._load_failure and include_failure:
217
+ fail_text = f"{RED}{self._load_failure[:80]}{NEUTRAL}"
218
+
219
+ txt = f"{self.context_name} on {self.host}"
220
+ COLOR = GREEN if self.is_selected else GRAY
221
+ return f"{COLOR}{txt}{NEUTRAL} {fail_text}"
@@ -1,4 +1,5 @@
1
- import time, threading
1
+ import time
2
+ import threading
2
3
 
3
4
 
4
5
  class ThreadedOperationsManager:
@@ -35,13 +36,13 @@ class ControlledThreadedOperation:
35
36
 
36
37
  @property
37
38
  def is_completed(self):
38
- return self._running == False
39
+ return self._running is False
39
40
 
40
41
  def run(self):
41
42
  while self._running:
42
43
  try:
43
44
  self._run_once()
44
- except:
45
+ except Exception:
45
46
  time.sleep(0.001)
46
47
 
47
48
  def _run_once(self):
@@ -17,7 +17,7 @@ from dfsync.distribution import get_installed_version, get_latest_version, is_ol
17
17
  from dfsync.filters import add_user_ignored_patterns_filter, ALL_FILTERS
18
18
  from dfsync.config import read_config
19
19
  from dfsync.char_ui import KeyController
20
- from dfsync.kube_credentials import contextualize_kube_credentials, update_local_kube_config
20
+ from dfsync.kube_credentials import contextualize_kube_credentials, update_local_kube_config, normalized_k8s_url
21
21
  from dfsync.lib import ControlledThreadedOperation, thread_manager
22
22
 
23
23
  logging.basicConfig(level=logging.WARN)
@@ -271,7 +271,7 @@ def import_kube_host(kube_host=None, credentials=None):
271
271
 
272
272
  ssh user@kube-host -C "sudo cat /root/.kube/config" | dfsync import-kube-host --kube-host=https://kube-host:6443
273
273
  """
274
- patch = contextualize_kube_credentials(kube_host, credentials)
274
+ patch = contextualize_kube_credentials(normalized_k8s_url(kube_host), credentials)
275
275
  update_local_kube_config(patch)
276
276
 
277
277
 
@@ -342,7 +342,7 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync):
342
342
  backend_options = dict(
343
343
  destination_dir=destination_dir,
344
344
  supervisor=supervisor,
345
- kube_host=kube_host,
345
+ kube_host=normalized_k8s_url(kube_host),
346
346
  pod_timeout=pod_timeout,
347
347
  container_command=config.container_command,
348
348
  full_sync=full_sync,
@@ -1,7 +1,8 @@
1
- import pytest, yaml, tempfile
1
+ import pytest
2
+ import yaml
3
+ import tempfile
2
4
 
3
- import dfsync.kube_credentials
4
- from dfsync.kube_credentials import update_local_kube_config
5
+ from dfsync.kube_credentials import update_local_kube_config, normalized_k8s_url
5
6
 
6
7
 
7
8
  @pytest.fixture
@@ -91,3 +92,16 @@ def test_update_existing_kube_config(mock_kube_credentials):
91
92
  assert credentials["users"][1]["name"] == "new_user"
92
93
  assert credentials["users"][1]["user"]["username"] == "new_username"
93
94
  assert credentials["users"][1]["user"]["password"] == "new_password"
95
+
96
+
97
+ def test_normalized_k8s_url():
98
+ assert normalized_k8s_url("http://example.com") == "http://example.com"
99
+ assert normalized_k8s_url("https://example.com") == "https://example.com"
100
+ assert normalized_k8s_url("example.com") == "https://example.com"
101
+ assert normalized_k8s_url("http://example.com") == "http://example.com"
102
+ assert normalized_k8s_url("https://192.168.0.123:443") == "https://192.168.0.123:443"
103
+ assert normalized_k8s_url("192.168.0.123:6443") == "https://192.168.0.123:6443"
104
+ with pytest.raises(ValueError):
105
+ normalized_k8s_url("")
106
+ with pytest.raises(ValueError):
107
+ normalized_k8s_url(None)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "dfsync"
3
- version = "0.4.6"
3
+ version = "0.4.7"
4
4
  description = ""
5
5
  authors = ["Mihai Balint <balint.mihai@gmail.com>"]
6
6
 
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