dfsync 0.4.5__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.5 → dfsync-0.4.7}/PKG-INFO +2 -1
- {dfsync-0.4.5 → dfsync-0.4.7}/dfsync/backends/kube.py +81 -54
- {dfsync-0.4.5 → dfsync-0.4.7}/dfsync/char_ui.py +13 -4
- dfsync-0.4.7/dfsync/diffui.py +63 -0
- {dfsync-0.4.5 → dfsync-0.4.7}/dfsync/kube_credentials.py +89 -2
- {dfsync-0.4.5 → dfsync-0.4.7}/dfsync/lib.py +4 -3
- {dfsync-0.4.5 → dfsync-0.4.7}/dfsync/monitor.py +3 -3
- dfsync-0.4.7/dfsync/test_dfsync.py +39 -0
- dfsync-0.4.7/dfsync/test_kube_credentials.py +107 -0
- dfsync-0.4.7/dfsync/test_lib.py +28 -0
- {dfsync-0.4.5 → dfsync-0.4.7}/pyproject.toml +7 -3
- {dfsync-0.4.5 → dfsync-0.4.7}/dfsync/__init__.py +0 -0
- {dfsync-0.4.5 → dfsync-0.4.7}/dfsync/backends/__init__.py +0 -0
- {dfsync-0.4.5 → dfsync-0.4.7}/dfsync/backends/kube_exec.py +0 -0
- {dfsync-0.4.5 → dfsync-0.4.7}/dfsync/backends/rsync.py +0 -0
- {dfsync-0.4.5 → dfsync-0.4.7}/dfsync/chcli.py +0 -0
- {dfsync-0.4.5 → dfsync-0.4.7}/dfsync/check_dns.py +0 -0
- {dfsync-0.4.5 → dfsync-0.4.7}/dfsync/cli.py +0 -0
- {dfsync-0.4.5 → dfsync-0.4.7}/dfsync/config.py +0 -0
- {dfsync-0.4.5 → dfsync-0.4.7}/dfsync/distribution.py +0 -0
- {dfsync-0.4.5 → dfsync-0.4.7}/dfsync/exp.py +0 -0
- {dfsync-0.4.5 → dfsync-0.4.7}/dfsync/filters.py +0 -0
- {dfsync-0.4.5 → dfsync-0.4.7}/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.7
|
|
4
4
|
Summary:
|
|
5
5
|
Author: Mihai Balint
|
|
6
6
|
Author-email: balint.mihai@gmail.com
|
|
@@ -10,6 +10,7 @@ Classifier: Programming Language :: Python :: 3.8
|
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.9
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.10
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
14
|
Requires-Dist: black (==23.7.0)
|
|
14
15
|
Requires-Dist: click (>=8,<9)
|
|
15
16
|
Requires-Dist: click-default-group (>=1.2.2,<2.0.0)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import copy
|
|
1
2
|
import sys
|
|
2
3
|
import os
|
|
3
4
|
import os.path
|
|
@@ -11,11 +12,27 @@ from kubernetes.client.exceptions import ApiException
|
|
|
11
12
|
from tenacity import retry, retry_if_exception_type, wait_exponential, stop_after_attempt
|
|
12
13
|
|
|
13
14
|
from dfsync.filters import GIT_FILTER
|
|
15
|
+
from dfsync.kube_credentials import KubeContextConfig
|
|
14
16
|
from .rsync import rsync_backend
|
|
15
17
|
|
|
16
18
|
DEFAULT_COMMAND = []
|
|
17
19
|
DEFAULT_PULL_POLICY = "Always"
|
|
18
|
-
|
|
20
|
+
DISABLED_PROBES = {
|
|
21
|
+
"readiness_probe": {
|
|
22
|
+
"_exec": {"command": ["true"]},
|
|
23
|
+
"timeout_seconds": 5,
|
|
24
|
+
"period_seconds": 15,
|
|
25
|
+
"initial_delay_seconds": 0,
|
|
26
|
+
"failure_threshold": 3,
|
|
27
|
+
},
|
|
28
|
+
"startup_probe": {
|
|
29
|
+
"_exec": {"command": ["true"]},
|
|
30
|
+
"timeout_seconds": 2,
|
|
31
|
+
"period_seconds": 1,
|
|
32
|
+
"initial_delay_seconds": 0,
|
|
33
|
+
"failure_threshold": 60,
|
|
34
|
+
},
|
|
35
|
+
}
|
|
19
36
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
20
37
|
|
|
21
38
|
|
|
@@ -147,88 +164,87 @@ class Generic:
|
|
|
147
164
|
return ["true"]
|
|
148
165
|
|
|
149
166
|
|
|
167
|
+
def clean_probe(probe: dict) -> client.V1Probe:
|
|
168
|
+
if probe is None:
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
probe_types = ["_exec", "http_get", "tcp_socket", "grpc"]
|
|
172
|
+
probe = copy.deepcopy(probe)
|
|
173
|
+
cleaned = {k: v for k, v in probe.items() if k not in probe_types or k is not None}
|
|
174
|
+
assert any(pt in cleaned for pt in probe_types), f"Probe missing probe type: {probe.keys()}"
|
|
175
|
+
return client.V1Probe(**cleaned)
|
|
176
|
+
|
|
177
|
+
|
|
150
178
|
def please_wait(msg="Please wait"):
|
|
151
179
|
print("⌛ {}...".format(msg))
|
|
152
180
|
|
|
153
181
|
|
|
154
|
-
def
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
configuration.verify_ssl = True
|
|
161
|
-
configuration.assert_hostname = False
|
|
162
|
-
return client.ApiClient(configuration)
|
|
163
|
-
|
|
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}")
|
|
164
188
|
|
|
165
|
-
def get_selected_kubernetes(kube_host=None):
|
|
166
189
|
contexts, active_context = config.list_kube_config_contexts()
|
|
167
190
|
if not contexts:
|
|
168
191
|
raise ValueError("Cannot find any kubernetes contexts in kube-config file")
|
|
169
192
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
active_context_api = None
|
|
193
|
+
k8sctx_configs = []
|
|
194
|
+
active_k8ctx = None
|
|
195
|
+
selected_k8ctx = None
|
|
174
196
|
multiple_matches = False
|
|
197
|
+
|
|
175
198
|
for c in contexts:
|
|
176
|
-
|
|
177
|
-
|
|
199
|
+
k8sctx = KubeContextConfig(c["name"])
|
|
200
|
+
k8sctx_configs.append(k8sctx)
|
|
178
201
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
202
|
+
if k8sctx.context_name == active_context["name"]:
|
|
203
|
+
active_k8ctx = k8sctx
|
|
204
|
+
k8sctx.is_active = True
|
|
182
205
|
|
|
183
|
-
if kube_host is not None and kube_host.lower() ==
|
|
184
|
-
if
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
187
210
|
else:
|
|
188
211
|
multiple_matches = True
|
|
189
212
|
|
|
190
|
-
if
|
|
191
|
-
|
|
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
|
|
192
217
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
elif c["name"] == active_context["name"]:
|
|
199
|
-
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}")
|
|
200
223
|
|
|
201
|
-
if len(tags) > 0:
|
|
202
|
-
tags_str = ",".join(tags)
|
|
203
|
-
print(f" * {c['name']} on {ctx_api_host} [{tags_str}]")
|
|
204
|
-
else:
|
|
205
|
-
print(f" * {c['name']} on {ctx_api_host}")
|
|
206
224
|
if kube_host is None:
|
|
207
|
-
ctx_api_host = active_context_api.api_client.configuration.host
|
|
208
225
|
print(
|
|
209
|
-
f"Add --kube-host={
|
|
226
|
+
f"Add --kube-host={active_k8ctx.host} to use a specific kubernetes API host\n"
|
|
210
227
|
f"Alternatively, use 'kubectl config set current-context <name>' to set the current context"
|
|
211
228
|
)
|
|
212
229
|
|
|
213
|
-
if kube_host is not None and
|
|
230
|
+
if kube_host is not None and selected_k8ctx is None:
|
|
214
231
|
raise ValueError(f"None of the kubernetes contexts match '{kube_host}'")
|
|
215
232
|
|
|
216
|
-
if
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
return
|
|
233
|
+
if selected_k8ctx is None:
|
|
234
|
+
raise ValueError("Failed to select a kubernetes context config")
|
|
235
|
+
|
|
236
|
+
return selected_k8ctx
|
|
220
237
|
|
|
221
238
|
|
|
222
239
|
class KubeReDeployer:
|
|
223
240
|
def __init__(self, kube_host=None, pod_timeout=30, container_command=None, full_sync=True, **kwargs):
|
|
224
|
-
|
|
225
|
-
selected_context, selected_context_api = get_selected_kubernetes(kube_host)
|
|
241
|
+
k8sctx = get_selected_kubernetes(kube_host)
|
|
226
242
|
|
|
227
|
-
self.context_name =
|
|
228
|
-
self.api =
|
|
229
|
-
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()
|
|
230
246
|
|
|
231
|
-
print(f"Using cluster: {
|
|
247
|
+
print(f"Using cluster: {k8sctx.prettified_str}")
|
|
232
248
|
self.rsync_backend_instance = rsync_backend()
|
|
233
249
|
self._image_distro = None
|
|
234
250
|
self.pod_timeout = pod_timeout
|
|
@@ -241,7 +257,16 @@ class KubeReDeployer:
|
|
|
241
257
|
return
|
|
242
258
|
|
|
243
259
|
command = self._image_distro.get_supervise_command(self.container_command)
|
|
244
|
-
deployments = self._edit_deployment(
|
|
260
|
+
deployments = self._edit_deployment(
|
|
261
|
+
pod,
|
|
262
|
+
spec,
|
|
263
|
+
status,
|
|
264
|
+
command=command,
|
|
265
|
+
image_pull_policy="Never",
|
|
266
|
+
startup_probe=None,
|
|
267
|
+
readiness_probe=None,
|
|
268
|
+
liveness_probe=None,
|
|
269
|
+
)
|
|
245
270
|
|
|
246
271
|
print("Supervisor installing on {}".format(" ".join(deployments)))
|
|
247
272
|
|
|
@@ -303,6 +328,8 @@ class KubeReDeployer:
|
|
|
303
328
|
existing_requests = container_spec.resources.requests or {}
|
|
304
329
|
new_requests = v.get("requests") or {}
|
|
305
330
|
container_spec.resources.requests = {**existing_requests, **new_requests}
|
|
331
|
+
elif k in ["readiness_probe", "startup_probe", "liveness_probe"]:
|
|
332
|
+
setattr(container_spec, k, clean_probe(v or DISABLED_PROBES.get(k)))
|
|
306
333
|
else:
|
|
307
334
|
setattr(container_spec, k, v)
|
|
308
335
|
|
|
@@ -315,7 +342,7 @@ class KubeReDeployer:
|
|
|
315
342
|
metadata=deployment.metadata,
|
|
316
343
|
spec=deployment.spec,
|
|
317
344
|
)
|
|
318
|
-
self.apps_api.
|
|
345
|
+
self.apps_api.replace_namespaced_deployment(
|
|
319
346
|
name=deployment.metadata.name,
|
|
320
347
|
namespace=namespace,
|
|
321
348
|
body=patch,
|
|
@@ -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):
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import difflib
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
text1 = """
|
|
5
|
+
The project, "micrograd", is currently at version 0.8.0 and was authored by Andrej Karpathy. It is structured into two main directories: "micrograd" and "test". The "micrograd" directory contains the main source code for the project, while the "test" directory contains the test code.
|
|
6
|
+
|
|
7
|
+
The project uses Python 3.10 and has dependencies on numpy, sklearn, and matplotlib. The build system requires "poetry-core" and uses "poetry.core.masonry.api" as the build backend.
|
|
8
|
+
|
|
9
|
+
The main files in the project are:
|
|
10
|
+
- `test/test_engine.py`: Contains tests for the engine component of the project.
|
|
11
|
+
- `micrograd/nn.py`: Contains code related to neural networks.
|
|
12
|
+
- `micrograd/__init__.py`: The initialization file for the "micrograd" package.
|
|
13
|
+
- `micrograd/engine.py`: Contains the main engine code for the project. It defines a `Value` class that stores a single scalar value and its gradient. This class is central to the project as it implements methods for basic mathematical operations, and more importantly, methods for automatic differentiation (autograd), which is a key component in training neural networks.
|
|
14
|
+
- `micrograd/demo.py`: Likely contains a demonstration or example usage of the project.
|
|
15
|
+
|
|
16
|
+
This project appears to be a minimalistic implementation of a deep learning library, focusing on the autograd feature. The `Value` class in `engine.py` is the heart of this library, implementing the forward and backward passes necessary for neural network training.
|
|
17
|
+
"""
|
|
18
|
+
text2 = """
|
|
19
|
+
The project, "micrograd", is currently at version 0.8.0 and was authored by Andrej Karpathy. It is structured into two main directories: "micrograd" and "test". The "micrograd" directory contains the main source code for the project, while the "test" directory contains the test code.
|
|
20
|
+
|
|
21
|
+
The project uses Python 3.10 and has dependencies on numpy, sklearn, and matplotlib. The build system requires "poetry-core" and uses "poetry.core.masonry.api" as the build backend.
|
|
22
|
+
|
|
23
|
+
The main files in the project are:
|
|
24
|
+
- `test/test_engine.py`: Contains tests for the engine component of the project.
|
|
25
|
+
- `micrograd/nn.py`: Contains code related to neural networks, including the definition of `Module`, `Neuron`, `Layer`, and `MLP` classes. These classes form the building blocks of the neural network functionality in the project. The `Module` class provides a base for other classes, with methods for zeroing gradients and retrieving parameters. The `Neuron` class represents a single neuron, the `Layer` class represents a layer of neurons, and the `MLP` class represents a multi-layer perceptron.
|
|
26
|
+
- `micrograd/__init__.py`: The initialization file for the "micrograd" package.
|
|
27
|
+
- `micrograd/engine.py`: Contains the main engine code for the project. It defines a `Value` class that stores a single scalar value and its gradient. This class is central to the project as it implements methods for basic mathematical operations, and more importantly, methods for automatic differentiation (autograd), which is a key component in training neural networks.
|
|
28
|
+
- `micrograd/demo.py`: Likely contains a demonstration or example usage of the project.
|
|
29
|
+
|
|
30
|
+
This project appears to be a minimalistic implementation of a deep learning library, focusing on the autograd feature. The `Value` class in `engine.py` is the heart of this library, implementing the forward and backward passes necessary for neural network training. The `nn.py` file extends this functionality by providing the necessary classes to build and manipulate neural networks.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def colored(diff_line: str) -> str:
|
|
35
|
+
line = diff_line if diff_line.endswith("\n") else f"{diff_line}\n"
|
|
36
|
+
red = "\033[38;2;255;0;0m"
|
|
37
|
+
green = "\033[38;2;0;255;0m"
|
|
38
|
+
reset_color = "\033[0m"
|
|
39
|
+
|
|
40
|
+
if line.startswith("+"):
|
|
41
|
+
return f"{green}{line[1:]}{reset_color}"
|
|
42
|
+
elif line.startswith("-"):
|
|
43
|
+
return f"{red}{line[1:]}{reset_color}"
|
|
44
|
+
else:
|
|
45
|
+
return line[1:]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def animate_diff(a: str, b: str):
|
|
49
|
+
a_lines = a.splitlines()
|
|
50
|
+
b_lines = b.splitlines()
|
|
51
|
+
diff = difflib.unified_diff(a_lines, b_lines, n=len(a_lines))
|
|
52
|
+
print("\033[H\033[J", end="") # Clear the screen
|
|
53
|
+
|
|
54
|
+
lines = []
|
|
55
|
+
for line in diff:
|
|
56
|
+
if line.startswith("---") or line.startswith("+++") or line.startswith("@@"):
|
|
57
|
+
continue
|
|
58
|
+
lines.append(colored(line))
|
|
59
|
+
print("".join(lines))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == "__main__":
|
|
63
|
+
animate_diff(text1, text2)
|
|
@@ -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)
|
|
@@ -70,7 +71,7 @@ def update_local_kube_config(new_kube_credentials):
|
|
|
70
71
|
credentials["users"].append(users_entry)
|
|
71
72
|
print(f"Adding entry to users section '{user_name}'")
|
|
72
73
|
|
|
73
|
-
if not credentials
|
|
74
|
+
if not credentials.get("current-context"):
|
|
74
75
|
credentials["current-context"] = contexts_entry["name"]
|
|
75
76
|
|
|
76
77
|
credentials_yaml = yaml.dump(credentials)
|
|
@@ -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,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from tempfile import NamedTemporaryFile
|
|
2
|
+
|
|
3
|
+
from dfsync.filters import EmacsBufferFilter, UntrackedGitFilesFilter
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_version():
|
|
7
|
+
assert False, "Test version not implemented"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_emacs_buffer_filter():
|
|
11
|
+
emacs = EmacsBufferFilter()
|
|
12
|
+
|
|
13
|
+
assert emacs.is_filtered("test_dfsync.py") is False
|
|
14
|
+
assert emacs.is_filtered("./test_dfsync.py") is False
|
|
15
|
+
assert emacs.is_filtered("./tests/test_dfsync.py") is False
|
|
16
|
+
assert emacs.is_filtered("tests/test_dfsync.py") is False
|
|
17
|
+
assert emacs.is_filtered("/tests/test_dfsync.py") is False
|
|
18
|
+
|
|
19
|
+
assert emacs.is_filtered("#test_dfsync.py#") is True
|
|
20
|
+
assert emacs.is_filtered("./#test_dfsync.py#") is True
|
|
21
|
+
assert emacs.is_filtered("./tests/#test_dfsync.py#") is True
|
|
22
|
+
assert emacs.is_filtered("tests/#test_dfsync.py#") is True
|
|
23
|
+
assert emacs.is_filtered("/tests/#test_dfsync.py#") is True
|
|
24
|
+
|
|
25
|
+
assert emacs.is_filtered("test_dfsync.py~") is True
|
|
26
|
+
assert emacs.is_filtered("./test_dfsync.py~") is True
|
|
27
|
+
assert emacs.is_filtered("./tests/test_dfsync.py~") is True
|
|
28
|
+
assert emacs.is_filtered("tests/test_dfsync.py~") is True
|
|
29
|
+
assert emacs.is_filtered("/tests/test_dfsync.py~") is True
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_untracked_git_files_filter():
|
|
33
|
+
git = UntrackedGitFilesFilter()
|
|
34
|
+
|
|
35
|
+
assert git.is_filtered(__name__) is False
|
|
36
|
+
assert git.is_filtered("does-not-exist.py") is False
|
|
37
|
+
|
|
38
|
+
with NamedTemporaryFile(dir=".") as f:
|
|
39
|
+
assert git.is_filtered(f.name) is True
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import yaml
|
|
3
|
+
import tempfile
|
|
4
|
+
|
|
5
|
+
from dfsync.kube_credentials import update_local_kube_config, normalized_k8s_url
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.fixture
|
|
9
|
+
def mock_kube_credentials(mocker):
|
|
10
|
+
with tempfile.NamedTemporaryFile(delete=True) as temp:
|
|
11
|
+
mocker.patch("dfsync.kube_credentials.LOCAL_CREDENTIALS_FILE", temp.name)
|
|
12
|
+
return temp.name
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_update_missing_kube_config(mock_kube_credentials):
|
|
16
|
+
new_kube_credentials = {
|
|
17
|
+
"clusters": [{"cluster": {"server": "test_server"}, "name": "test_cluster"}],
|
|
18
|
+
"contexts": [{"context": {"cluster": "test_cluster", "user": "test_user"}, "name": "test_context"}],
|
|
19
|
+
"users": [{"user": {"username": "test_user"}, "name": "test_user"}],
|
|
20
|
+
}
|
|
21
|
+
result = update_local_kube_config(new_kube_credentials)
|
|
22
|
+
assert "test_cluster" in result
|
|
23
|
+
assert "test_context" in result
|
|
24
|
+
assert "test_user" in result
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_update_empty_kube_config(mock_kube_credentials):
|
|
28
|
+
with open(mock_kube_credentials, "wb") as f:
|
|
29
|
+
empty_config = {
|
|
30
|
+
"clusters": [],
|
|
31
|
+
"contexts": [],
|
|
32
|
+
"users": [],
|
|
33
|
+
}
|
|
34
|
+
f.write(yaml.dump(empty_config).encode())
|
|
35
|
+
|
|
36
|
+
new_kube_credentials = {
|
|
37
|
+
"clusters": [{"name": "test_cluster", "cluster": {"server": "test_server"}}],
|
|
38
|
+
"contexts": [{"name": "test_context", "context": {"cluster": "test_cluster", "user": "test_user"}}],
|
|
39
|
+
"users": [{"name": "test_user", "user": {"username": "test_username", "password": "test_password"}}],
|
|
40
|
+
}
|
|
41
|
+
update_local_kube_config(new_kube_credentials)
|
|
42
|
+
|
|
43
|
+
# Verify the results
|
|
44
|
+
with open(mock_kube_credentials, "r") as f:
|
|
45
|
+
credentials = yaml.safe_load(f)
|
|
46
|
+
|
|
47
|
+
assert credentials["clusters"][0]["name"] == "test_cluster"
|
|
48
|
+
assert credentials["clusters"][0]["cluster"]["server"] == "test_server"
|
|
49
|
+
assert credentials["contexts"][0]["name"] == "test_context"
|
|
50
|
+
assert credentials["contexts"][0]["context"]["cluster"] == "test_cluster"
|
|
51
|
+
assert credentials["contexts"][0]["context"]["user"] == "test_user"
|
|
52
|
+
assert credentials["users"][0]["name"] == "test_user"
|
|
53
|
+
assert credentials["users"][0]["user"]["username"] == "test_username"
|
|
54
|
+
assert credentials["users"][0]["user"]["password"] == "test_password"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_update_existing_kube_config(mock_kube_credentials):
|
|
58
|
+
with open(mock_kube_credentials, "wb") as f:
|
|
59
|
+
existing_config = {
|
|
60
|
+
"clusters": [
|
|
61
|
+
{"name": "existing_cluster", "cluster": {"server": "existing_server"}},
|
|
62
|
+
],
|
|
63
|
+
"contexts": [
|
|
64
|
+
{"name": "existing_context", "context": {"cluster": "existing_cluster", "user": "existing_user"}}
|
|
65
|
+
],
|
|
66
|
+
"users": [
|
|
67
|
+
{"name": "existing_user", "user": {"username": "existing_username", "password": "existing_password"}}
|
|
68
|
+
],
|
|
69
|
+
}
|
|
70
|
+
f.write(yaml.dump(existing_config).encode())
|
|
71
|
+
|
|
72
|
+
new_kube_credentials = {
|
|
73
|
+
"clusters": [{"name": "new_cluster", "cluster": {"server": "new_server"}}],
|
|
74
|
+
"contexts": [{"name": "new_context", "context": {"cluster": "new_cluster", "user": "new_user"}}],
|
|
75
|
+
"users": [{"name": "new_user", "user": {"username": "new_username", "password": "new_password"}}],
|
|
76
|
+
}
|
|
77
|
+
update_local_kube_config(new_kube_credentials)
|
|
78
|
+
|
|
79
|
+
# Verify the results
|
|
80
|
+
with open(mock_kube_credentials, "r") as f:
|
|
81
|
+
credentials = yaml.safe_load(f)
|
|
82
|
+
|
|
83
|
+
assert len(credentials["clusters"]) == 2
|
|
84
|
+
assert len(credentials["contexts"]) == 2
|
|
85
|
+
assert len(credentials["users"]) == 2
|
|
86
|
+
|
|
87
|
+
assert credentials["clusters"][1]["name"] == "new_cluster"
|
|
88
|
+
assert credentials["clusters"][1]["cluster"]["server"] == "new_server"
|
|
89
|
+
assert credentials["contexts"][1]["name"] == "new_context"
|
|
90
|
+
assert credentials["contexts"][1]["context"]["cluster"] == "new_cluster"
|
|
91
|
+
assert credentials["contexts"][1]["context"]["user"] == "new_user"
|
|
92
|
+
assert credentials["users"][1]["name"] == "new_user"
|
|
93
|
+
assert credentials["users"][1]["user"]["username"] == "new_username"
|
|
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)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from dfsync.lib import ControlledThreadedOperation, ThreadedOperationsManager
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_threaded_operations_manager():
|
|
6
|
+
manager = ThreadedOperationsManager()
|
|
7
|
+
assert len(manager.threads) == 0
|
|
8
|
+
|
|
9
|
+
operation = ControlledThreadedOperation()
|
|
10
|
+
manager.register_threaded_operation(operation)
|
|
11
|
+
assert len(manager.threads) == 1
|
|
12
|
+
|
|
13
|
+
manager.stop()
|
|
14
|
+
assert operation.is_completed
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_controlled_threaded_operation():
|
|
18
|
+
operation = ControlledThreadedOperation()
|
|
19
|
+
assert not operation.is_running
|
|
20
|
+
assert operation.is_completed
|
|
21
|
+
|
|
22
|
+
operation.start()
|
|
23
|
+
assert operation.is_running
|
|
24
|
+
assert not operation.is_completed
|
|
25
|
+
|
|
26
|
+
operation.stop()
|
|
27
|
+
assert not operation.is_running
|
|
28
|
+
assert operation.is_completed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "dfsync"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.7"
|
|
4
4
|
description = ""
|
|
5
5
|
authors = ["Mihai Balint <balint.mihai@gmail.com>"]
|
|
6
6
|
|
|
@@ -17,12 +17,16 @@ paramiko = {version = "^3.2.0", extras = ["invoke"]}
|
|
|
17
17
|
black = "23.7.0"
|
|
18
18
|
|
|
19
19
|
[tool.poetry.dev-dependencies]
|
|
20
|
-
pytest = "^
|
|
20
|
+
pytest = "^7.4"
|
|
21
|
+
pytest_mock = "^3.12"
|
|
21
22
|
ipython = "^8"
|
|
22
|
-
ipdb = "^0.13
|
|
23
|
+
ipdb = "^0.13"
|
|
23
24
|
flake8 = "^4.0.1"
|
|
24
25
|
pycodestyle = "^2.8.0"
|
|
25
26
|
|
|
27
|
+
[tool.black]
|
|
28
|
+
line-length = 120
|
|
29
|
+
|
|
26
30
|
[tool.poetry.scripts]
|
|
27
31
|
dfsync = "dfsync.cli:dfsync"
|
|
28
32
|
[build-system]
|
|
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
|