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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dfsync
3
- Version: 0.4.6
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 (==23.7.0)
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 _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):
@@ -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 as e:
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 should_emit_upgrade_warning(self):
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 set_emitted_upgrade_warning(self):
96
+ def set_emitted_update_warning(self):
91
97
  self._cta_count += 1
92
98
 
93
- def get_upgrade_warning(self):
94
- return f"dfsync ver. {self.latest} is available, please upgrade! ({self.installed} is installed)"
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 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)
@@ -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, 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):
@@ -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 get_installed_version, get_latest_version, is_older_version, AsyncVersionChecker
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
- click.echo(f"Latest version is {latest_version}, please upgrade!")
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.should_emit_upgrade_warning:
423
- checker.set_emitted_upgrade_warning()
424
- click.echo(checker.get_upgrade_warning())
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:
@@ -3,10 +3,6 @@ from tempfile import NamedTemporaryFile
3
3
  from dfsync.filters import EmacsBufferFilter, UntrackedGitFilesFilter
4
4
 
5
5
 
6
- def test_version():
7
- assert False, "Test version not implemented"
8
-
9
-
10
6
  def test_emacs_buffer_filter():
11
7
  emacs = EmacsBufferFilter()
12
8
 
@@ -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 pytest, yaml, tempfile
1
+ import os
2
+ import pytest
3
+ import tempfile
4
+ import yaml
2
5
 
3
- import dfsync.kube_credentials
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 test_update_missing_kube_config(mock_kube_credentials):
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.6"
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 = "23.7.0"
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