dfsync 0.4.6__tar.gz → 0.4.8__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.8}/PKG-INFO +2 -2
- {dfsync-0.4.6 → dfsync-0.4.8}/dfsync/backends/kube.py +40 -51
- {dfsync-0.4.6 → dfsync-0.4.8}/dfsync/char_ui.py +13 -4
- {dfsync-0.4.6 → dfsync-0.4.8}/dfsync/distribution.py +50 -8
- {dfsync-0.4.6 → dfsync-0.4.8}/dfsync/kube_credentials.py +91 -2
- {dfsync-0.4.6 → dfsync-0.4.8}/dfsync/lib.py +4 -3
- {dfsync-0.4.6 → dfsync-0.4.8}/dfsync/monitor.py +40 -9
- {dfsync-0.4.6 → dfsync-0.4.8}/dfsync/test_dfsync.py +0 -4
- dfsync-0.4.8/dfsync/test_distribution.py +30 -0
- {dfsync-0.4.6 → dfsync-0.4.8}/dfsync/test_kube_credentials.py +31 -4
- {dfsync-0.4.6 → dfsync-0.4.8}/pyproject.toml +3 -2
- {dfsync-0.4.6 → dfsync-0.4.8}/dfsync/__init__.py +0 -0
- {dfsync-0.4.6 → dfsync-0.4.8}/dfsync/backends/__init__.py +0 -0
- {dfsync-0.4.6 → dfsync-0.4.8}/dfsync/backends/kube_exec.py +0 -0
- {dfsync-0.4.6 → dfsync-0.4.8}/dfsync/backends/rsync.py +0 -0
- {dfsync-0.4.6 → dfsync-0.4.8}/dfsync/chcli.py +0 -0
- {dfsync-0.4.6 → dfsync-0.4.8}/dfsync/check_dns.py +0 -0
- {dfsync-0.4.6 → dfsync-0.4.8}/dfsync/cli.py +0 -0
- {dfsync-0.4.6 → dfsync-0.4.8}/dfsync/config.py +0 -0
- {dfsync-0.4.6 → dfsync-0.4.8}/dfsync/diffui.py +0 -0
- {dfsync-0.4.6 → dfsync-0.4.8}/dfsync/exp.py +0 -0
- {dfsync-0.4.6 → dfsync-0.4.8}/dfsync/filters.py +0 -0
- {dfsync-0.4.6 → dfsync-0.4.8}/dfsync/test_lib.py +0 -0
- {dfsync-0.4.6 → dfsync-0.4.8}/dfsync/transactions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: dfsync
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.8
|
|
4
4
|
Summary:
|
|
5
5
|
Author: Mihai Balint
|
|
6
6
|
Author-email: balint.mihai@gmail.com
|
|
@@ -11,7 +11,7 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.10
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
-
Requires-Dist: black (==
|
|
14
|
+
Requires-Dist: black (==24.3.0)
|
|
15
15
|
Requires-Dist: click (>=8,<9)
|
|
16
16
|
Requires-Dist: click-default-group (>=1.2.2,<2.0.0)
|
|
17
17
|
Requires-Dist: gitpython (>=3.1.14,<4.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):
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import click
|
|
1
2
|
import json
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
2
5
|
import urllib.request
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
3
8
|
from dfsync.lib import ControlledThreadedOperation
|
|
4
9
|
|
|
5
10
|
|
|
@@ -8,14 +13,14 @@ def get_package_version(package_name):
|
|
|
8
13
|
from importlib.metadata import version
|
|
9
14
|
|
|
10
15
|
return version(package_name)
|
|
11
|
-
except:
|
|
16
|
+
except Exception:
|
|
12
17
|
pass
|
|
13
18
|
|
|
14
19
|
try:
|
|
15
20
|
import pkg_resources
|
|
16
21
|
|
|
17
22
|
return pkg_resources.get_distribution(package_name).version
|
|
18
|
-
except:
|
|
23
|
+
except Exception:
|
|
19
24
|
pass
|
|
20
25
|
|
|
21
26
|
return None
|
|
@@ -41,7 +46,7 @@ def get_latest_version():
|
|
|
41
46
|
raise ValueError("No version visible on github")
|
|
42
47
|
|
|
43
48
|
return versions[0]
|
|
44
|
-
except Exception
|
|
49
|
+
except Exception:
|
|
45
50
|
return get_installed_version()
|
|
46
51
|
|
|
47
52
|
|
|
@@ -52,7 +57,7 @@ def parse_version(version_str):
|
|
|
52
57
|
for part in version_str.split("."):
|
|
53
58
|
try:
|
|
54
59
|
parsed_version.append(int(part))
|
|
55
|
-
except:
|
|
60
|
+
except Exception:
|
|
56
61
|
parsed_version.append(part)
|
|
57
62
|
return parsed_version
|
|
58
63
|
|
|
@@ -80,15 +85,52 @@ class AsyncVersionChecker(ControlledThreadedOperation):
|
|
|
80
85
|
self.installed = get_installed_version()
|
|
81
86
|
self.latest = get_latest_version()
|
|
82
87
|
self.installed_is_older = is_older_version(self.installed, self.latest)
|
|
88
|
+
self._is_install_editable = is_installed_in_editable_mode("dfsync")
|
|
83
89
|
self._cta_count = 0
|
|
84
90
|
self.stop()
|
|
85
91
|
|
|
86
92
|
@property
|
|
87
|
-
def
|
|
93
|
+
def should_emit_update_warning(self):
|
|
88
94
|
return self.is_completed and self.installed_is_older and self._cta_count < 1
|
|
89
95
|
|
|
90
|
-
def
|
|
96
|
+
def set_emitted_update_warning(self):
|
|
91
97
|
self._cta_count += 1
|
|
92
98
|
|
|
93
|
-
def
|
|
94
|
-
|
|
99
|
+
def get_update_warning(self):
|
|
100
|
+
if self._is_install_editable:
|
|
101
|
+
return f"dfsync ver. {self.latest} is available, please update! ({self.installed} is installed)"
|
|
102
|
+
else:
|
|
103
|
+
return f"dfsync ver. {self.latest} is available ({self.installed} is installed), please update using `dfsync self-update`"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _read_editable_location_from_pip_show_output(out: str) -> Optional[str]:
|
|
107
|
+
editable_label = "Editable project location: "
|
|
108
|
+
site_packages_label = "site-packages"
|
|
109
|
+
site_packages_lines = []
|
|
110
|
+
|
|
111
|
+
for line in out.split("\n"):
|
|
112
|
+
if line.startswith("Location: "):
|
|
113
|
+
continue
|
|
114
|
+
if line.startswith(editable_label):
|
|
115
|
+
return line[len(editable_label) :].strip()
|
|
116
|
+
if site_packages_label in line:
|
|
117
|
+
site_packages_lines.append(line)
|
|
118
|
+
|
|
119
|
+
return "UNKNOWN-EDITABLE-LOCATION" if len(site_packages_lines) > 0 else None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def is_installed_in_editable_mode(package_name: str) -> bool:
|
|
123
|
+
try:
|
|
124
|
+
out = subprocess.check_output([sys.executable, "-m", "pip", "show", "-f", package_name]).decode()
|
|
125
|
+
editable_location = _read_editable_location_from_pip_show_output(out)
|
|
126
|
+
return editable_location is not None
|
|
127
|
+
except Exception:
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def update_package(package_name: str):
|
|
132
|
+
click.echo(f"Running {sys.executable} -m pip install --upgrade {package_name}")
|
|
133
|
+
try:
|
|
134
|
+
subprocess.check_call([sys.executable, "-m", "pip", "install", "--upgrade", package_name])
|
|
135
|
+
except Exception as e:
|
|
136
|
+
click.echo(f"Failed to update {package_name}: {e}")
|
|
@@ -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)
|
|
@@ -76,14 +77,16 @@ def update_local_kube_config(new_kube_credentials):
|
|
|
76
77
|
credentials_yaml = yaml.dump(credentials)
|
|
77
78
|
with open(f"{LOCAL_CREDENTIALS_FILE}.new", "w") as f:
|
|
78
79
|
f.write(credentials_yaml)
|
|
80
|
+
os.chmod(f"{LOCAL_CREDENTIALS_FILE}.new", 0o600)
|
|
79
81
|
|
|
80
82
|
if os.path.isfile(LOCAL_CREDENTIALS_FILE):
|
|
81
83
|
if os.path.isfile(f"{LOCAL_CREDENTIALS_FILE}.old"):
|
|
82
84
|
os.remove(f"{LOCAL_CREDENTIALS_FILE}.old")
|
|
83
85
|
os.rename(f"{LOCAL_CREDENTIALS_FILE}", f"{LOCAL_CREDENTIALS_FILE}.old")
|
|
86
|
+
os.chmod(f"{LOCAL_CREDENTIALS_FILE}.old", 0o600)
|
|
84
87
|
|
|
85
88
|
os.rename(f"{LOCAL_CREDENTIALS_FILE}.new", f"{LOCAL_CREDENTIALS_FILE}")
|
|
86
|
-
|
|
89
|
+
os.chmod(LOCAL_CREDENTIALS_FILE, 0o600)
|
|
87
90
|
return credentials_yaml
|
|
88
91
|
|
|
89
92
|
|
|
@@ -132,3 +135,89 @@ def contextualize_kube_credentials(kube_host, credentials_file):
|
|
|
132
135
|
print(exc)
|
|
133
136
|
|
|
134
137
|
return {}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def normalized_k8s_url(k8s_url: str) -> str:
|
|
141
|
+
parsed = None
|
|
142
|
+
result = k8s_url
|
|
143
|
+
if k8s_url is None:
|
|
144
|
+
return None
|
|
145
|
+
elif not k8s_url:
|
|
146
|
+
raise ValueError(f"Invalid k8s URL: `{k8s_url}`")
|
|
147
|
+
elif "://" not in k8s_url.lower():
|
|
148
|
+
result = f"https://{k8s_url}"
|
|
149
|
+
parsed = urlparse(result)
|
|
150
|
+
else:
|
|
151
|
+
parsed = urlparse(k8s_url)
|
|
152
|
+
|
|
153
|
+
if all([parsed.scheme, parsed.netloc]):
|
|
154
|
+
return result
|
|
155
|
+
else:
|
|
156
|
+
raise ValueError(f"Invalid k8s URL: `{k8s_url}`")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
RED = "\033[38;2;255;0;0m"
|
|
160
|
+
GREEN = "\033[38;2;128;255;128m"
|
|
161
|
+
GRAY = "\033[38;2;128;128;128m"
|
|
162
|
+
NEUTRAL = "\033[0m"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class KubeContextConfig:
|
|
166
|
+
def __init__(self, context_name: str) -> None:
|
|
167
|
+
self._api_client = None
|
|
168
|
+
self._load_failure = None
|
|
169
|
+
self._configuration = client.Configuration()
|
|
170
|
+
self.context_name = context_name
|
|
171
|
+
self._adjust_ssl()
|
|
172
|
+
self.is_active = False
|
|
173
|
+
self.is_selected = False
|
|
174
|
+
|
|
175
|
+
def _adjust_ssl(self):
|
|
176
|
+
self._configuration.verify_ssl = True
|
|
177
|
+
self._configuration.assert_hostname = False
|
|
178
|
+
|
|
179
|
+
def _load(self):
|
|
180
|
+
if self._api_client is not None:
|
|
181
|
+
return
|
|
182
|
+
try:
|
|
183
|
+
config.load_kube_config(
|
|
184
|
+
context=self.context_name,
|
|
185
|
+
client_configuration=self._configuration,
|
|
186
|
+
)
|
|
187
|
+
self._adjust_ssl()
|
|
188
|
+
self._api_client = client.ApiClient(self._configuration)
|
|
189
|
+
self._load_failure = None
|
|
190
|
+
except Exception as e:
|
|
191
|
+
self._load_failure = str(e)
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def host(self):
|
|
195
|
+
self._load()
|
|
196
|
+
return self._configuration.host
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def api_client(self):
|
|
200
|
+
self._load()
|
|
201
|
+
return self._api_client
|
|
202
|
+
|
|
203
|
+
def apps_v1_api(self):
|
|
204
|
+
return client.AppsV1Api(api_client=self.api_client)
|
|
205
|
+
|
|
206
|
+
def core_v1_api(self):
|
|
207
|
+
return client.CoreV1Api(api_client=self.api_client)
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def active_glyph(self):
|
|
211
|
+
glyph = "@" if self.is_active else "*"
|
|
212
|
+
COLOR = GREEN if self.is_selected else GRAY
|
|
213
|
+
return f"{COLOR}{glyph}{NEUTRAL}"
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def prettified_str(self, include_failure=True):
|
|
217
|
+
fail_text = ""
|
|
218
|
+
if self._load_failure and include_failure:
|
|
219
|
+
fail_text = f"{RED}{self._load_failure[:80]}{NEUTRAL}"
|
|
220
|
+
|
|
221
|
+
txt = f"{self.context_name} on {self.host}"
|
|
222
|
+
COLOR = GREEN if self.is_selected else GRAY
|
|
223
|
+
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):
|
|
@@ -13,11 +13,18 @@ from watchdog.observers import Observer
|
|
|
13
13
|
from watchdog.events import FileSystemEventHandler
|
|
14
14
|
|
|
15
15
|
from dfsync.backends import rsync_backend, kube_backend
|
|
16
|
-
from dfsync.distribution import
|
|
16
|
+
from dfsync.distribution import (
|
|
17
|
+
get_installed_version,
|
|
18
|
+
get_latest_version,
|
|
19
|
+
is_older_version,
|
|
20
|
+
AsyncVersionChecker,
|
|
21
|
+
is_installed_in_editable_mode,
|
|
22
|
+
update_package,
|
|
23
|
+
)
|
|
17
24
|
from dfsync.filters import add_user_ignored_patterns_filter, ALL_FILTERS
|
|
18
25
|
from dfsync.config import read_config
|
|
19
26
|
from dfsync.char_ui import KeyController
|
|
20
|
-
from dfsync.kube_credentials import contextualize_kube_credentials, update_local_kube_config
|
|
27
|
+
from dfsync.kube_credentials import contextualize_kube_credentials, update_local_kube_config, normalized_k8s_url
|
|
21
28
|
from dfsync.lib import ControlledThreadedOperation, thread_manager
|
|
22
29
|
|
|
23
30
|
logging.basicConfig(level=logging.WARN)
|
|
@@ -253,12 +260,36 @@ def main():
|
|
|
253
260
|
|
|
254
261
|
@main.command()
|
|
255
262
|
def version():
|
|
263
|
+
"""
|
|
264
|
+
Print the currently installed version
|
|
265
|
+
"""
|
|
256
266
|
installed_version = get_installed_version()
|
|
257
267
|
click.echo(f"{installed_version}")
|
|
258
268
|
|
|
259
269
|
latest_version = get_latest_version()
|
|
260
|
-
if is_older_version(installed_version, latest_version):
|
|
261
|
-
|
|
270
|
+
if not is_older_version(installed_version, latest_version):
|
|
271
|
+
return
|
|
272
|
+
elif is_installed_in_editable_mode("dfsync"):
|
|
273
|
+
click.echo(f"Latest version is {latest_version}, please update!")
|
|
274
|
+
else:
|
|
275
|
+
click.echo(f"Latest version is {latest_version}, please update using `dfsync self-update`")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@main.command()
|
|
279
|
+
def self_update():
|
|
280
|
+
"""
|
|
281
|
+
Update dfsync to the latest version published on pypi
|
|
282
|
+
"""
|
|
283
|
+
latest_version = get_latest_version()
|
|
284
|
+
installed_version = get_installed_version()
|
|
285
|
+
if not is_older_version(installed_version, latest_version):
|
|
286
|
+
click.echo(f"The latest version is already installed: {installed_version}")
|
|
287
|
+
return
|
|
288
|
+
elif is_installed_in_editable_mode("dfsync"):
|
|
289
|
+
click.echo("dfsync is installed in editable mode, please update manually")
|
|
290
|
+
return
|
|
291
|
+
else:
|
|
292
|
+
update_package("dfsync")
|
|
262
293
|
|
|
263
294
|
|
|
264
295
|
@main.command()
|
|
@@ -271,7 +302,7 @@ def import_kube_host(kube_host=None, credentials=None):
|
|
|
271
302
|
|
|
272
303
|
ssh user@kube-host -C "sudo cat /root/.kube/config" | dfsync import-kube-host --kube-host=https://kube-host:6443
|
|
273
304
|
"""
|
|
274
|
-
patch = contextualize_kube_credentials(kube_host, credentials)
|
|
305
|
+
patch = contextualize_kube_credentials(normalized_k8s_url(kube_host), credentials)
|
|
275
306
|
update_local_kube_config(patch)
|
|
276
307
|
|
|
277
308
|
|
|
@@ -342,7 +373,7 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync):
|
|
|
342
373
|
backend_options = dict(
|
|
343
374
|
destination_dir=destination_dir,
|
|
344
375
|
supervisor=supervisor,
|
|
345
|
-
kube_host=kube_host,
|
|
376
|
+
kube_host=normalized_k8s_url(kube_host),
|
|
346
377
|
pod_timeout=pod_timeout,
|
|
347
378
|
container_command=config.container_command,
|
|
348
379
|
full_sync=full_sync,
|
|
@@ -419,9 +450,9 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync):
|
|
|
419
450
|
no_errors = False
|
|
420
451
|
break
|
|
421
452
|
with controller.getch_lock():
|
|
422
|
-
if checker.
|
|
423
|
-
checker.
|
|
424
|
-
click.echo(checker.
|
|
453
|
+
if checker.should_emit_update_warning:
|
|
454
|
+
checker.set_emitted_update_warning()
|
|
455
|
+
click.echo(checker.get_update_warning())
|
|
425
456
|
controller.raise_exceptions()
|
|
426
457
|
|
|
427
458
|
except KeyboardInterrupt:
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from dfsync.distribution import is_installed_in_editable_mode, get_package_version, update_package
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_is_installed_in_editable_mode():
|
|
6
|
+
assert is_installed_in_editable_mode("dfsync") is True
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_is_installed_in_editable_mode_missing_package():
|
|
10
|
+
assert is_installed_in_editable_mode("does-not-exist") is False
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_is_installed_in_editable_mode_required_and_installed_package():
|
|
14
|
+
assert pytest is not None, "pytest must be installed"
|
|
15
|
+
assert is_installed_in_editable_mode("pytest") is False
|
|
16
|
+
|
|
17
|
+
assert is_installed_in_editable_mode("click") is False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_update_package():
|
|
21
|
+
tqdm_version = get_package_version("tqdm")
|
|
22
|
+
|
|
23
|
+
update_package("tqdm==4.66.2")
|
|
24
|
+
assert get_package_version("tqdm") == "4.66.2"
|
|
25
|
+
|
|
26
|
+
update_package("tqdm==4.66.0")
|
|
27
|
+
assert get_package_version("tqdm") == "4.66.0"
|
|
28
|
+
|
|
29
|
+
update_package(f"tqdm=={tqdm_version}")
|
|
30
|
+
assert get_package_version("tqdm") == tqdm_version
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import os
|
|
2
|
+
import pytest
|
|
3
|
+
import tempfile
|
|
4
|
+
import yaml
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
from dfsync.kube_credentials import update_local_kube_config
|
|
6
|
+
from dfsync.kube_credentials import update_local_kube_config, normalized_k8s_url
|
|
5
7
|
|
|
6
8
|
|
|
7
9
|
@pytest.fixture
|
|
@@ -11,7 +13,7 @@ def mock_kube_credentials(mocker):
|
|
|
11
13
|
return temp.name
|
|
12
14
|
|
|
13
15
|
|
|
14
|
-
def
|
|
16
|
+
def test_update_missing_kube_config_and_check_permissions(mock_kube_credentials):
|
|
15
17
|
new_kube_credentials = {
|
|
16
18
|
"clusters": [{"cluster": {"server": "test_server"}, "name": "test_cluster"}],
|
|
17
19
|
"contexts": [{"context": {"cluster": "test_cluster", "user": "test_user"}, "name": "test_context"}],
|
|
@@ -22,6 +24,9 @@ def test_update_missing_kube_config(mock_kube_credentials):
|
|
|
22
24
|
assert "test_context" in result
|
|
23
25
|
assert "test_user" in result
|
|
24
26
|
|
|
27
|
+
# Check file permissions
|
|
28
|
+
assert oct(os.stat(mock_kube_credentials).st_mode)[-3:] == "600"
|
|
29
|
+
|
|
25
30
|
|
|
26
31
|
def test_update_empty_kube_config(mock_kube_credentials):
|
|
27
32
|
with open(mock_kube_credentials, "wb") as f:
|
|
@@ -52,6 +57,15 @@ def test_update_empty_kube_config(mock_kube_credentials):
|
|
|
52
57
|
assert credentials["users"][0]["user"]["username"] == "test_username"
|
|
53
58
|
assert credentials["users"][0]["user"]["password"] == "test_password"
|
|
54
59
|
|
|
60
|
+
# Verify that the old file was saved
|
|
61
|
+
with open(f"{mock_kube_credentials}.old", "r") as f:
|
|
62
|
+
credentials = yaml.safe_load(f)
|
|
63
|
+
assert credentials == empty_config
|
|
64
|
+
|
|
65
|
+
# Check file permissions for both old and new config file
|
|
66
|
+
assert oct(os.stat(mock_kube_credentials).st_mode)[-3:] == "600"
|
|
67
|
+
assert oct(os.stat(f"{mock_kube_credentials}.old").st_mode)[-3:] == "600"
|
|
68
|
+
|
|
55
69
|
|
|
56
70
|
def test_update_existing_kube_config(mock_kube_credentials):
|
|
57
71
|
with open(mock_kube_credentials, "wb") as f:
|
|
@@ -91,3 +105,16 @@ def test_update_existing_kube_config(mock_kube_credentials):
|
|
|
91
105
|
assert credentials["users"][1]["name"] == "new_user"
|
|
92
106
|
assert credentials["users"][1]["user"]["username"] == "new_username"
|
|
93
107
|
assert credentials["users"][1]["user"]["password"] == "new_password"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_normalized_k8s_url():
|
|
111
|
+
assert normalized_k8s_url("http://example.com") == "http://example.com"
|
|
112
|
+
assert normalized_k8s_url("https://example.com") == "https://example.com"
|
|
113
|
+
assert normalized_k8s_url("example.com") == "https://example.com"
|
|
114
|
+
assert normalized_k8s_url("http://example.com") == "http://example.com"
|
|
115
|
+
assert normalized_k8s_url("https://192.168.0.123:443") == "https://192.168.0.123:443"
|
|
116
|
+
assert normalized_k8s_url("192.168.0.123:6443") == "https://192.168.0.123:6443"
|
|
117
|
+
with pytest.raises(ValueError):
|
|
118
|
+
normalized_k8s_url("")
|
|
119
|
+
|
|
120
|
+
assert normalized_k8s_url(None) is None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "dfsync"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.8"
|
|
4
4
|
description = ""
|
|
5
5
|
authors = ["Mihai Balint <balint.mihai@gmail.com>"]
|
|
6
6
|
|
|
@@ -14,7 +14,7 @@ toml = "^0.10.2"
|
|
|
14
14
|
tenacity = "^8.0.1"
|
|
15
15
|
click-default-group = "^1.2.2"
|
|
16
16
|
paramiko = {version = "^3.2.0", extras = ["invoke"]}
|
|
17
|
-
black = "
|
|
17
|
+
black = "24.3.0"
|
|
18
18
|
|
|
19
19
|
[tool.poetry.dev-dependencies]
|
|
20
20
|
pytest = "^7.4"
|
|
@@ -23,6 +23,7 @@ ipython = "^8"
|
|
|
23
23
|
ipdb = "^0.13"
|
|
24
24
|
flake8 = "^4.0.1"
|
|
25
25
|
pycodestyle = "^2.8.0"
|
|
26
|
+
tqdm = "4.66.0"
|
|
26
27
|
|
|
27
28
|
[tool.black]
|
|
28
29
|
line-length = 120
|
|
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
|