kubernetes-watch 0.1.12__py3-none-any.whl → 0.1.13__py3-none-any.whl

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.
@@ -146,7 +146,45 @@ def restart_deployment(deployment, namespace):
146
146
  api_response = v1.patch_namespaced_deployment(name=deployment, namespace=namespace, body=body)
147
147
  logger.info(f"Deployment restarted. Name: {api_response.metadata.name}")
148
148
  except ApiException as e:
149
- logger.error(f"Exception when restarting deployment: {e}")
149
+ logger.error(
150
+ "Failed to restart deployment. status=%s reason=%s body=%s",
151
+ e.status, e.reason, e.body
152
+ )
153
+ # raise
154
+
155
+ except Exception as e:
156
+ # Everything else: connectivity, config, TLS, serialization, etc.
157
+ logger.error("Unexpected error restarting deployment: %s", e)
158
+ # raise
159
+
160
+
161
+ def delete_pod(namespace: str, label_selector: str, max_pods: int | None = 1):
162
+ v1 = client.CoreV1Api()
163
+
164
+
165
+ pods = v1.list_namespaced_pod(namespace=namespace, label_selector=label_selector)
166
+ if not pods.items:
167
+ raise RuntimeError(f"No pods found for selector: {label_selector}")
168
+
169
+
170
+ for i, pod in enumerate(pods.items):
171
+ if max_pods is not None and i >= max_pods:
172
+ break
173
+
174
+ try:
175
+ v1.delete_namespaced_pod(
176
+ name=pod,
177
+ namespace=namespace,
178
+ grace_period_seconds=30,
179
+ )
180
+ print(f"Deleted pod: {pod}")
181
+ except ApiException as e:
182
+ logger.error(f"Failed to delete pod: status={e.status} reason={e.reason} body={e.body}")
183
+ # raise
184
+ except Exception as e:
185
+ # Everything else: connectivity, config, TLS, serialization, etc.
186
+ logger.error("Unexpected error restarting deployment: %s", e)
187
+ # raise
150
188
 
151
189
 
152
190
  def has_mismatch_image_digest(repo_digest, label_selector, namespace):
@@ -1,33 +1,65 @@
1
1
  import os
2
2
  import shutil
3
+ import time
3
4
  import subprocess
4
5
  import tempfile
5
6
  from pathlib import Path
6
7
 
8
+ import jwt
9
+ import requests
10
+ from git import Repo
11
+
7
12
  from prefect import get_run_logger
8
13
 
9
14
  logger = get_run_logger()
10
15
 
11
16
 
12
- def clone_pat_repo(git_pat, git_url, clone_base_path):
17
+ def is_ssh_clone(git_method: str) -> bool:
18
+ """ Determine if the git clone method is SSH based on the provided method string. """
19
+ result = git_method.lower() == 'ssh'
20
+ logger.info(f"Checking if git method '{git_method}' is SSH: {result}")
21
+ return result
22
+
23
+ def is_pat_clone(git_method: str) -> bool:
24
+ """ Determine if the git clone method is PAT based on the provided method string. """
25
+ result = git_method.lower() == 'pat'
26
+ logger.info(f"Checking if git method '{git_method}' is PAT: {result}")
27
+ return result
28
+
29
+ def is_gh_apps_clone(git_method: str) -> bool:
30
+ """ Determine if the git clone method is GitHub App based on the provided method string. """
31
+ result = git_method.lower() == 'apps'
32
+ logger.info(f"Checking if git method '{git_method}' is GitHub Apps: {result}")
33
+ return result
34
+
35
+
36
+ def clone_repo_pat(git_pat, git_url, clone_base_path):
13
37
  """ Clone a Git repository using a Personal Access Token (PAT) for authentication."""
38
+ logger.info(f"Starting PAT-based git clone for URL: {git_url}")
39
+ logger.info(f"Clone base path: {clone_base_path}")
40
+
14
41
  # Retrieve environment variables
15
42
  access_token = git_pat # os.environ.get('GIT_PAT')
16
43
  repo_url = git_url # os.environ.get('GIT_URL')
17
44
 
18
45
  if not access_token or not repo_url:
46
+ logger.error("Missing required parameters: git_pat or git_url")
19
47
  raise ValueError("Environment variables GIT_PAT or GIT_URL are not set")
20
48
 
21
49
  # Correctly format the URL with the PAT
50
+ logger.info("Formatting URL with PAT authentication")
22
51
  if 'https://' in repo_url:
23
52
  # Splitting the URL and inserting the PAT
24
53
  parts = repo_url.split('https://', 1)
25
54
  repo_url = f'https://{access_token}@{parts[1]}'
55
+ logger.info("Successfully formatted URL with PAT")
26
56
  else:
57
+ logger.error(f"Invalid URL format: {repo_url}. Must begin with https://")
27
58
  raise ValueError("URL must begin with https:// for PAT authentication")
28
59
 
29
60
  # Directory where the repo will be cloned
30
61
  repo_path = os.path.join(clone_base_path, 'manifest-repo')
62
+ logger.info(f"Target repository path: {repo_path}")
31
63
 
32
64
  # Clone the repository
33
65
  if not os.path.exists(repo_path):
@@ -38,7 +70,7 @@ def clone_pat_repo(git_pat, git_url, clone_base_path):
38
70
  logger.info(f"Repository already exists at {repo_path}")
39
71
 
40
72
 
41
- def clone_ssh_repo(
73
+ def clone_repo_ssh(
42
74
  git_url: str,
43
75
  clone_base_path: str,
44
76
  repo_dir_name: str = "manifest-repo",
@@ -51,23 +83,33 @@ def clone_ssh_repo(
51
83
  - GIT_SSH_PRIVATE_KEY: full private key (BEGIN/END ...)
52
84
  - GIT_SSH_KNOWN_HOSTS: lines from `ssh-keyscan github.com`
53
85
  """
86
+ logger.info(f"Starting SSH-based git clone for URL: {git_url}")
87
+ logger.info(f"Clone base path: {clone_base_path}, repo directory: {repo_dir_name}")
88
+ logger.info(f"Clone depth: {depth}")
89
+
54
90
  if not git_url.startswith("git@"):
91
+ logger.error(f"Invalid SSH URL format: {git_url}")
55
92
  raise ValueError("git_url must be an SSH URL like 'git@github.com:org/repo.git'")
56
93
 
94
+ logger.info(f"Reading SSH credentials from environment variables: {ssh_key_env}, {known_hosts_env}")
57
95
  priv_key = os.environ.get(ssh_key_env)
58
96
  kh_data = os.environ.get(known_hosts_env)
59
97
  if not priv_key:
98
+ logger.error(f"Missing SSH private key environment variable: {ssh_key_env}")
60
99
  raise ValueError(f"Missing env var {ssh_key_env}")
61
100
  if not kh_data:
101
+ logger.error(f"Missing SSH known hosts environment variable: {known_hosts_env}")
62
102
  raise ValueError(f"Missing env var {known_hosts_env}")
63
103
 
64
104
  base = Path(clone_base_path).expanduser().resolve()
65
105
  base.mkdir(parents=True, exist_ok=True)
66
106
  repo_path = base / repo_dir_name
107
+ logger.info(f"Target repository path: {repo_path}")
67
108
 
68
109
  tmpdir = Path(tempfile.mkdtemp(prefix="git_ssh_"))
69
110
  key_path = tmpdir / "id_rsa"
70
111
  kh_path = tmpdir / "known_hosts"
112
+ logger.info(f"Created temporary directory for SSH keys: {tmpdir}")
71
113
 
72
114
  try:
73
115
  key_path.write_text(priv_key, encoding="utf-8")
@@ -88,18 +130,28 @@ def clone_ssh_repo(
88
130
  env["GIT_SSH_COMMAND"] = ssh_cmd
89
131
 
90
132
  if not repo_path.exists():
133
+ logger.info(f"Repository doesn't exist, performing fresh clone")
91
134
  cmd = ["git", "clone"]
92
135
  if depth and depth > 0:
93
136
  cmd += ["--depth", str(depth)]
94
137
  cmd += [git_url, str(repo_path)]
138
+ logger.info(f"Executing git clone command: {' '.join(cmd[:-1])} <repo_path>")
95
139
  subprocess.check_call(cmd, env=env)
140
+ logger.info("Git clone completed successfully")
96
141
  else:
142
+ logger.info(f"Repository already exists at {repo_path}, updating existing repo")
97
143
  if not (repo_path / ".git").exists():
144
+ logger.error(f"Path exists but is not a git repository: {repo_path}")
98
145
  raise RuntimeError(f"Path exists but is not a git repo: {repo_path}")
146
+ logger.info("Updating remote URL")
99
147
  subprocess.check_call(["git", "remote", "set-url", "origin", git_url], cwd=repo_path, env=env)
148
+ logger.info("Fetching latest changes")
100
149
  subprocess.check_call(["git", "fetch", "--all", "--prune"], cwd=repo_path, env=env)
150
+ logger.info("Pulling latest changes")
101
151
  subprocess.check_call(["git", "pull", "--ff-only", "origin"], cwd=repo_path, env=env)
152
+ logger.info("Repository update completed successfully")
102
153
 
154
+ logger.info(f"SSH clone operation completed, returning path: {repo_path}")
103
155
  return repo_path
104
156
 
105
157
  finally:
@@ -107,3 +159,151 @@ def clone_ssh_repo(
107
159
  shutil.rmtree(tmpdir)
108
160
  except Exception:
109
161
  pass
162
+
163
+
164
+
165
+ def _ghapp_installation_token(app_id: str, installation_id: str, private_key_pem: str) -> str:
166
+ """Create an App-signed JWT and exchange it for a short-lived installation token (~1h)."""
167
+ logger.info(f"Creating GitHub App installation token for app_id: {app_id}, installation_id: {installation_id}")
168
+
169
+ now = int(time.time())
170
+ payload = {"iat": now - 60, "exp": now + 9 * 60, "iss": app_id} # JWT max 10m
171
+ app_jwt = jwt.encode(payload, private_key_pem, algorithm="RS256")
172
+ logger.info("JWT token created successfully")
173
+
174
+ headers = {"Authorization": f"Bearer {app_jwt}", "Accept": "application/vnd.github+json"}
175
+ logger.info("Requesting installation access token from GitHub API")
176
+ resp = requests.post(
177
+ f"https://api.github.com/app/installations/{installation_id}/access_tokens",
178
+ headers=headers,
179
+ timeout=20,
180
+ )
181
+ resp.raise_for_status()
182
+ logger.info("Installation access token retrieved successfully")
183
+ return resp.json()["token"] # valid ~1 hour
184
+
185
+
186
+ def _to_https_url(git_url: str) -> str:
187
+ """
188
+ Accepts either SSH ('git@github.com:org/repo.git') or HTTPS.
189
+ Returns HTTPS form required for GitHub App tokens:
190
+ 'https://github.com/org/repo.git'
191
+ """
192
+ logger.info(f"Converting git URL to HTTPS format: {git_url}")
193
+
194
+ if git_url.startswith("git@github.com:"):
195
+ org_repo = git_url.split("git@github.com:", 1)[1]
196
+ https_url = f"https://github.com/{org_repo}"
197
+ logger.info(f"Converted SSH URL to HTTPS: {https_url}")
198
+ return https_url
199
+ if git_url.startswith("https://"):
200
+ logger.info("URL is already in HTTPS format")
201
+ return git_url
202
+
203
+ logger.error(f"Invalid git URL format: {git_url}")
204
+ raise ValueError("git_url must be an SSH url for github.com or an https:// URL")
205
+
206
+
207
+ def clone_repo_github_app(
208
+ git_url: str,
209
+ clone_base_path: str,
210
+ repo_dir_name: str = "manifest-repo",
211
+ depth: int = 1,
212
+ app_id_env: str = "GITHUB_APP_ID",
213
+ installation_id_env: str = "GITHUB_INSTALLATION_ID",
214
+ private_key_env: str = "GITHUB_APP_PRIVATE_KEY",
215
+ ) -> Path:
216
+ """
217
+ Clone/update a repo using a GitHub App installation token (HTTPS).
218
+
219
+ Env vars required:
220
+ - GITHUB_APP_ID : the App ID (numeric string)
221
+ - GITHUB_INSTALLATION_ID : the installation ID (numeric string)
222
+ - GITHUB_APP_PRIVATE_KEY : the App private key PEM (BEGIN/END ...)
223
+
224
+ Notes:
225
+ - Token is minted on the fly and embedded in the clone URL:
226
+ https://x-access-token:<token>@github.com/org/repo.git
227
+ - No SSH keys / known_hosts needed.
228
+ """
229
+ logger.info(f"Starting GitHub App-based git clone for URL: {git_url}")
230
+ logger.info(f"Clone base path: {clone_base_path}, repo directory: {repo_dir_name}")
231
+ logger.info(f"Clone depth: {depth}")
232
+
233
+ logger.info(f"Reading GitHub App credentials from environment variables: {app_id_env}, {installation_id_env}, {private_key_env}")
234
+ app_id = os.environ.get(app_id_env)
235
+ inst_id = os.environ.get(installation_id_env)
236
+ pem = os.environ.get(private_key_env)
237
+
238
+ if not app_id or not inst_id or not pem:
239
+ logger.error(f"Missing GitHub App environment variables. Required: {app_id_env}, {installation_id_env}, {private_key_env}")
240
+ raise ValueError(f"Missing one of env vars: {app_id_env}, {installation_id_env}, {private_key_env}")
241
+
242
+ https_url = _to_https_url(git_url)
243
+ logger.info("Obtaining GitHub App installation token")
244
+ token = _ghapp_installation_token(app_id, inst_id, pem)
245
+
246
+ # Build an authenticated URL (avoids credential helpers)
247
+ authed_url = f"https://x-access-token:{token}@{https_url.split('https://',1)[1]}"
248
+ logger.info("Successfully created authenticated URL with installation token")
249
+
250
+ base = Path(clone_base_path).expanduser().resolve()
251
+ base.mkdir(parents=True, exist_ok=True)
252
+ repo_path = base / repo_dir_name
253
+ logger.info(f"Target repository path: {repo_path}")
254
+
255
+ # Ensure git won't prompt for creds in CI
256
+ env = os.environ.copy()
257
+ env.setdefault("GIT_TERMINAL_PROMPT", "0")
258
+ env.setdefault("GCM_INTERACTIVE", "Never") # in case Git Credential Manager is present
259
+
260
+ if not repo_path.exists():
261
+ logger.info(f"Repository doesn't exist, performing fresh clone")
262
+ cmd = ["git", "clone"]
263
+ if depth and depth > 0:
264
+ cmd += ["--depth", str(depth)]
265
+ cmd += [authed_url, str(repo_path)]
266
+ logger.info(f"Executing git clone command with GitHub App authentication")
267
+ subprocess.check_call(cmd, env=env)
268
+ logger.info("Git clone completed successfully")
269
+ else:
270
+ logger.info(f"Repository already exists at {repo_path}, updating existing repo")
271
+ if not (repo_path / ".git").exists():
272
+ logger.error(f"Path exists but is not a git repository: {repo_path}")
273
+ raise RuntimeError(f"Path exists but is not a git repo: {repo_path}")
274
+ # Refresh remote URL & pull default branch
275
+ logger.info("Updating remote URL with new GitHub App token")
276
+ subprocess.check_call(["git", "remote", "set-url", "origin", authed_url], cwd=repo_path, env=env)
277
+ logger.info("Fetching latest changes")
278
+ subprocess.check_call(["git", "fetch", "--all", "--prune"], cwd=repo_path, env=env)
279
+ logger.info("Pulling latest changes")
280
+ subprocess.check_call(["git", "pull", "--ff-only", "origin"], cwd=repo_path, env=env)
281
+ logger.info("Repository update completed successfully")
282
+
283
+ logger.info(f"GitHub App clone operation completed, returning path: {repo_path}")
284
+ return repo_path
285
+
286
+
287
+
288
+ def generate_github_creds(
289
+ app_id_env: str = "GITHUB_APP_ID",
290
+ installation_id_env: str = "GITHUB_INSTALLATION_ID",
291
+ private_key_env: str = "GITHUB_APP_PRIVATE_KEY",
292
+ ):
293
+ """
294
+ Generate GitHub credentials using a GitHub App installation token.
295
+ """
296
+ app_id = os.environ.get(app_id_env)
297
+ installation_id = os.environ.get(installation_id_env)
298
+ private_key = os.environ.get(private_key_env)
299
+
300
+ if not app_id or not installation_id or not private_key:
301
+ logger.error("Missing GitHub App environment variables.")
302
+ raise ValueError(f"Missing one of env vars: {app_id_env}, {installation_id_env}, {private_key_env}")
303
+
304
+ token = _ghapp_installation_token(app_id, installation_id, private_key)
305
+
306
+ return {
307
+ "username": "x-access-token",
308
+ "password": token
309
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: kubernetes-watch
3
- Version: 0.1.12
3
+ Version: 0.1.13
4
4
  Summary:
5
5
  Author: bmotevalli
6
6
  Author-email: b.motevalli@gmail.com
@@ -9,7 +9,7 @@ kube_watch/models/common.py,sha256=FQktpX552zSCigMxEzm4S07SvrHv5RA7YwVJHgv7uuI,5
9
9
  kube_watch/models/workflow.py,sha256=ZFBMz_LmYgROcbz2amSvms38K770njnyZC6h1bpTXGU,1634
10
10
  kube_watch/modules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  kube_watch/modules/clusters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- kube_watch/modules/clusters/kube.py,sha256=ZvWpLrdgftLHexuigil8aa23BjqTZDmyeb6ZCAW6Y7w,10205
12
+ kube_watch/modules/clusters/kube.py,sha256=DiHhXiX8p4UX7fbyu1xhjNWcZPm3nkHTI_NPW-JeLb4,11467
13
13
  kube_watch/modules/database/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  kube_watch/modules/database/model.py,sha256=MWG9UW6g0KuBzW6MjzPBtknAk7GmuncQrdAq6HHarTo,207
15
15
  kube_watch/modules/database/postgre.py,sha256=1Sq2YFwgCJM_FWKabV2S1bFAIl2GBwytTtPCuLfVhu8,8182
@@ -23,14 +23,14 @@ kube_watch/modules/mock/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG
23
23
  kube_watch/modules/mock/mock_generator.py,sha256=BKKQFCxxQgFW_GFgeIbkyIbuNU4328xTTaFfTwFLsS8,1262
24
24
  kube_watch/modules/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
25
  kube_watch/modules/providers/aws.py,sha256=qKL7oGKeKOUO5AQRQqcrR35kzOpq42uNXuXumeU-gtw,10224
26
- kube_watch/modules/providers/git.py,sha256=5d7vkeTbVD_XN-mpCoPTRmM6lwgrwkDwcCIsjqYwmaQ,3778
26
+ kube_watch/modules/providers/git.py,sha256=E1XJ1QN0ekoud7XQkwRkQTuZo3_5F-v7c_NJ9CjTos4,12934
27
27
  kube_watch/modules/providers/github.py,sha256=eQY8sLy2U6bOWMpFxA73DFCPVuswhTXSG25KmYSuo5s,5212
28
28
  kube_watch/modules/providers/vault.py,sha256=etzzHbTrUDsTUpeUN-xg0Xh8ulqC0-1FA3tHRZinIOo,7193
29
29
  kube_watch/standalone/metarecogen/ckan_to_gn.py,sha256=LWd7ikyxRIC1IGt6CtALnDOEoyuG07a8NoDHhgMkX4o,4635
30
30
  kube_watch/watch/__init__.py,sha256=9KE0Sf1nLUTNaFvXbiQCgf11vpG8Xgmb5ddeMAmak3Q,88
31
31
  kube_watch/watch/helpers.py,sha256=8BQnQ6AeLHs0JEq54iKYDvWURb1F-kROJxwIcl_nv_Y,6276
32
32
  kube_watch/watch/workflow.py,sha256=CaXHFuEWVsFjBv5dU4IfVMeTlGJWyKaE1But9-YzVWk,9769
33
- kubernetes_watch-0.1.12.dist-info/LICENSE,sha256=_H2QdL-2dXbivDmOpJ11DnqJewSFhSJwGpHx_WAE-CA,1075
34
- kubernetes_watch-0.1.12.dist-info/METADATA,sha256=zG9wEStvXgfYthz6Aa5i05Oyoh7ygmIMWtI8kdzRM00,5600
35
- kubernetes_watch-0.1.12.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
36
- kubernetes_watch-0.1.12.dist-info/RECORD,,
33
+ kubernetes_watch-0.1.13.dist-info/LICENSE,sha256=_H2QdL-2dXbivDmOpJ11DnqJewSFhSJwGpHx_WAE-CA,1075
34
+ kubernetes_watch-0.1.13.dist-info/METADATA,sha256=wMEwoVLfacKPKBOppPCM6kEWYRmynt-LnjoxCOC7KVU,5600
35
+ kubernetes_watch-0.1.13.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
36
+ kubernetes_watch-0.1.13.dist-info/RECORD,,