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.
- {dfsync-0.4.6 → dfsync-0.4.7}/PKG-INFO +1 -1
- {dfsync-0.4.6 → dfsync-0.4.7}/dfsync/backends/kube.py +40 -51
- {dfsync-0.4.6 → dfsync-0.4.7}/dfsync/char_ui.py +13 -4
- {dfsync-0.4.6 → dfsync-0.4.7}/dfsync/kube_credentials.py +88 -1
- {dfsync-0.4.6 → dfsync-0.4.7}/dfsync/lib.py +4 -3
- {dfsync-0.4.6 → dfsync-0.4.7}/dfsync/monitor.py +3 -3
- {dfsync-0.4.6 → dfsync-0.4.7}/dfsync/test_kube_credentials.py +17 -3
- {dfsync-0.4.6 → dfsync-0.4.7}/pyproject.toml +1 -1
- {dfsync-0.4.6 → dfsync-0.4.7}/dfsync/__init__.py +0 -0
- {dfsync-0.4.6 → dfsync-0.4.7}/dfsync/backends/__init__.py +0 -0
- {dfsync-0.4.6 → dfsync-0.4.7}/dfsync/backends/kube_exec.py +0 -0
- {dfsync-0.4.6 → dfsync-0.4.7}/dfsync/backends/rsync.py +0 -0
- {dfsync-0.4.6 → dfsync-0.4.7}/dfsync/chcli.py +0 -0
- {dfsync-0.4.6 → dfsync-0.4.7}/dfsync/check_dns.py +0 -0
- {dfsync-0.4.6 → dfsync-0.4.7}/dfsync/cli.py +0 -0
- {dfsync-0.4.6 → dfsync-0.4.7}/dfsync/config.py +0 -0
- {dfsync-0.4.6 → dfsync-0.4.7}/dfsync/diffui.py +0 -0
- {dfsync-0.4.6 → dfsync-0.4.7}/dfsync/distribution.py +0 -0
- {dfsync-0.4.6 → dfsync-0.4.7}/dfsync/exp.py +0 -0
- {dfsync-0.4.6 → dfsync-0.4.7}/dfsync/filters.py +0 -0
- {dfsync-0.4.6 → dfsync-0.4.7}/dfsync/test_dfsync.py +0 -0
- {dfsync-0.4.6 → dfsync-0.4.7}/dfsync/test_lib.py +0 -0
- {dfsync-0.4.6 → dfsync-0.4.7}/dfsync/transactions.py +0 -0
|
@@ -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
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
204
|
-
|
|
199
|
+
k8sctx = KubeContextConfig(c["name"])
|
|
200
|
+
k8sctx_configs.append(k8sctx)
|
|
205
201
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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() ==
|
|
211
|
-
if
|
|
212
|
-
|
|
213
|
-
|
|
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
|
|
218
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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={
|
|
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
|
|
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
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
return
|
|
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
|
-
|
|
252
|
-
selected_context, selected_context_api = get_selected_kubernetes(kube_host)
|
|
241
|
+
k8sctx = get_selected_kubernetes(kube_host)
|
|
253
242
|
|
|
254
|
-
self.context_name =
|
|
255
|
-
self.api =
|
|
256
|
-
self.apps_api =
|
|
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: {
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1
|
+
import pytest
|
|
2
|
+
import yaml
|
|
3
|
+
import tempfile
|
|
2
4
|
|
|
3
|
-
|
|
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)
|
|
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
|