kubernetes-watch 0.1.12__tar.gz → 0.1.14__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.
Files changed (38) hide show
  1. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/PKG-INFO +1 -1
  2. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/kube_watch/modules/clusters/kube.py +39 -1
  3. kubernetes_watch-0.1.14/kube_watch/modules/database/model.py +13 -0
  4. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/kube_watch/modules/database/postgre.py +45 -12
  5. kubernetes_watch-0.1.14/kube_watch/modules/providers/git.py +309 -0
  6. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/kube_watch/standalone/metarecogen/ckan_to_gn.py +1 -1
  7. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/pyproject.toml +1 -1
  8. kubernetes_watch-0.1.12/kube_watch/modules/database/model.py +0 -12
  9. kubernetes_watch-0.1.12/kube_watch/modules/providers/git.py +0 -109
  10. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/LICENSE +0 -0
  11. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/README.md +0 -0
  12. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/kube_watch/__init__.py +0 -0
  13. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/kube_watch/enums/__init__.py +0 -0
  14. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/kube_watch/enums/kube.py +0 -0
  15. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/kube_watch/enums/logic.py +0 -0
  16. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/kube_watch/enums/providers.py +0 -0
  17. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/kube_watch/enums/workflow.py +0 -0
  18. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/kube_watch/models/__init__.py +0 -0
  19. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/kube_watch/models/common.py +0 -0
  20. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/kube_watch/models/workflow.py +0 -0
  21. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/kube_watch/modules/__init__.py +0 -0
  22. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/kube_watch/modules/clusters/__init__.py +0 -0
  23. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/kube_watch/modules/database/__init__.py +0 -0
  24. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/kube_watch/modules/logic/actions.py +0 -0
  25. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/kube_watch/modules/logic/checks.py +0 -0
  26. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/kube_watch/modules/logic/load.py +0 -0
  27. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/kube_watch/modules/logic/merge.py +0 -0
  28. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/kube_watch/modules/logic/scheduler.py +0 -0
  29. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/kube_watch/modules/logic/trasnform.py +0 -0
  30. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/kube_watch/modules/mock/__init__.py +0 -0
  31. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/kube_watch/modules/mock/mock_generator.py +0 -0
  32. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/kube_watch/modules/providers/__init__.py +0 -0
  33. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/kube_watch/modules/providers/aws.py +0 -0
  34. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/kube_watch/modules/providers/github.py +0 -0
  35. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/kube_watch/modules/providers/vault.py +0 -0
  36. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/kube_watch/watch/__init__.py +0 -0
  37. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/kube_watch/watch/helpers.py +0 -0
  38. {kubernetes_watch-0.1.12 → kubernetes_watch-0.1.14}/kube_watch/watch/workflow.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: kubernetes-watch
3
- Version: 0.1.12
3
+ Version: 0.1.14
4
4
  Summary:
5
5
  Author: bmotevalli
6
6
  Author-email: b.motevalli@gmail.com
@@ -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):
@@ -0,0 +1,13 @@
1
+ from typing import Union, Optional
2
+ from pydantic import BaseModel
3
+
4
+ class TableQuery(BaseModel):
5
+ table_name: str
6
+ column_name: str
7
+ db_url: Optional[str] = None
8
+ db_host: Optional[str] = None
9
+ db_port: Optional[int] = None
10
+ db_name: Optional[str] = None
11
+ db_user: Optional[str] = None
12
+ db_pass: Optional[str] = None
13
+
@@ -1,23 +1,26 @@
1
1
  import psycopg2
2
2
  import psycopg2.extras
3
3
  from prefect import get_run_logger
4
+ from urllib.parse import urlparse, unquote
4
5
 
5
6
  from .model import TableQuery
6
7
 
7
8
  logger = get_run_logger()
8
9
 
9
10
 
10
- def execute_query(db_user, db_pass, db_query, db_host="localhost", db_port=5432, db_name="postgres"):
11
+ def execute_query(db_query, db_url=None, db_user=None, db_pass=None, db_host="localhost", db_port=5432, db_name="postgres"):
11
12
  """
12
13
  Connect to PostgreSQL database, execute a query, and return status message.
13
14
 
14
15
  Args:
15
- db_user (str): Database username
16
- db_pass (str): Database password
17
16
  db_query (str): SQL query to execute
18
- db_host (str): Database host (default: localhost)
19
- db_port (int): Database port (default: 5432)
20
- db_name (str): Database name (default: postgres)
17
+ db_url (str, optional): Database connection URL (e.g., postgresql://user:pass@host:port/dbname)
18
+ Supports postgresql+asyncpg:// format as well
19
+ db_user (str, optional): Database username (used if db_url is not provided)
20
+ db_pass (str, optional): Database password (used if db_url is not provided)
21
+ db_host (str): Database host (default: localhost, used if db_url is not provided)
22
+ db_port (int): Database port (default: 5432, used if db_url is not provided)
23
+ db_name (str): Database name (default: postgres, used if db_url is not provided)
21
24
 
22
25
  Returns:
23
26
  dict: Status message with success/failure information
@@ -26,6 +29,18 @@ def execute_query(db_user, db_pass, db_query, db_host="localhost", db_port=5432,
26
29
  cursor = None
27
30
 
28
31
  try:
32
+ # Parse connection URL if provided
33
+ if db_url:
34
+ # Handle SQLAlchemy-style URLs (postgresql+asyncpg://)
35
+ url = db_url.replace('postgresql+asyncpg://', 'postgresql://')
36
+ parsed = urlparse(url)
37
+
38
+ db_host = parsed.hostname
39
+ db_port = parsed.port or 5432
40
+ db_name = parsed.path.lstrip('/')
41
+ db_user = unquote(parsed.username) if parsed.username else None
42
+ db_pass = unquote(parsed.password) if parsed.password else None
43
+
29
44
  # Establish database connection
30
45
  connection = psycopg2.connect(
31
46
  host=db_host,
@@ -125,19 +140,37 @@ def delete_on_retention_period(table_delete: dict, batch_size: int = 100000, int
125
140
  table_query = TableQuery(**table_delete)
126
141
  except Exception as e:
127
142
  logger.error(f"Error creating TableQuery object: {str(e)}")
128
- raise ValueError("Invalid table_delete data format. Expected a dictionary with 'name', 'column_name', 'db_host', 'db_port', 'db_name', 'db_user', and 'db_pass' keys.")
143
+ raise ValueError("Invalid table_delete data format. Expected a dictionary with 'table_name', 'column_name', and either 'db_url' or ('db_host', 'db_port', 'db_name', 'db_user', 'db_pass') keys.")
129
144
 
130
145
  connection = None
131
146
  cursor = None
132
147
 
133
148
  try:
149
+ # Parse connection URL if provided
150
+ if table_query.db_url:
151
+ # Handle SQLAlchemy-style URLs (postgresql+asyncpg://)
152
+ url = table_query.db_url.replace('postgresql+asyncpg://', 'postgresql://')
153
+ parsed = urlparse(url)
154
+
155
+ db_host = parsed.hostname
156
+ db_port = parsed.port or 5432
157
+ db_name = parsed.path.lstrip('/')
158
+ db_user = unquote(parsed.username) if parsed.username else None
159
+ db_pass = unquote(parsed.password) if parsed.password else None
160
+ else:
161
+ db_host = table_query.db_host
162
+ db_port = table_query.db_port
163
+ db_name = table_query.db_name
164
+ db_user = table_query.db_user
165
+ db_pass = table_query.db_pass
166
+
134
167
  # Establish database connection
135
168
  connection = psycopg2.connect(
136
- host=table_query.db_host,
137
- port=table_query.db_port,
138
- database=table_query.db_name,
139
- user=table_query.db_user,
140
- password=table_query.db_pass
169
+ host=db_host,
170
+ port=db_port,
171
+ database=db_name,
172
+ user=db_user,
173
+ password=db_pass
141
174
  )
142
175
 
143
176
  cursor = connection.cursor()
@@ -0,0 +1,309 @@
1
+ import os
2
+ import shutil
3
+ import time
4
+ import subprocess
5
+ import tempfile
6
+ from pathlib import Path
7
+
8
+ import jwt
9
+ import requests
10
+ from git import Repo
11
+
12
+ from prefect import get_run_logger
13
+
14
+ logger = get_run_logger()
15
+
16
+
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):
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
+
41
+ # Retrieve environment variables
42
+ access_token = git_pat # os.environ.get('GIT_PAT')
43
+ repo_url = git_url # os.environ.get('GIT_URL')
44
+
45
+ if not access_token or not repo_url:
46
+ logger.error("Missing required parameters: git_pat or git_url")
47
+ raise ValueError("Environment variables GIT_PAT or GIT_URL are not set")
48
+
49
+ # Correctly format the URL with the PAT
50
+ logger.info("Formatting URL with PAT authentication")
51
+ if 'https://' in repo_url:
52
+ # Splitting the URL and inserting the PAT
53
+ parts = repo_url.split('https://', 1)
54
+ repo_url = f'https://{access_token}@{parts[1]}'
55
+ logger.info("Successfully formatted URL with PAT")
56
+ else:
57
+ logger.error(f"Invalid URL format: {repo_url}. Must begin with https://")
58
+ raise ValueError("URL must begin with https:// for PAT authentication")
59
+
60
+ # Directory where the repo will be cloned
61
+ repo_path = os.path.join(clone_base_path, 'manifest-repo')
62
+ logger.info(f"Target repository path: {repo_path}")
63
+
64
+ # Clone the repository
65
+ if not os.path.exists(repo_path):
66
+ logger.info(f"Cloning repository into {repo_path}")
67
+ repo = Repo.clone_from(repo_url, repo_path)
68
+ logger.info("Repository cloned successfully.")
69
+ else:
70
+ logger.info(f"Repository already exists at {repo_path}")
71
+
72
+
73
+ def clone_repo_ssh(
74
+ git_url: str,
75
+ clone_base_path: str,
76
+ repo_dir_name: str = "manifest-repo",
77
+ depth: int = 1,
78
+ ssh_key_env: str = "GIT_SSH_PRIVATE_KEY",
79
+ known_hosts_env: str = "GIT_SSH_KNOWN_HOSTS",
80
+ ) -> Path:
81
+ """
82
+ Clone/update a repo via SSH using key + known_hosts from env vars.
83
+ - GIT_SSH_PRIVATE_KEY: full private key (BEGIN/END ...)
84
+ - GIT_SSH_KNOWN_HOSTS: lines from `ssh-keyscan github.com`
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
+
90
+ if not git_url.startswith("git@"):
91
+ logger.error(f"Invalid SSH URL format: {git_url}")
92
+ raise ValueError("git_url must be an SSH URL like 'git@github.com:org/repo.git'")
93
+
94
+ logger.info(f"Reading SSH credentials from environment variables: {ssh_key_env}, {known_hosts_env}")
95
+ priv_key = os.environ.get(ssh_key_env)
96
+ kh_data = os.environ.get(known_hosts_env)
97
+ if not priv_key:
98
+ logger.error(f"Missing SSH private key environment variable: {ssh_key_env}")
99
+ raise ValueError(f"Missing env var {ssh_key_env}")
100
+ if not kh_data:
101
+ logger.error(f"Missing SSH known hosts environment variable: {known_hosts_env}")
102
+ raise ValueError(f"Missing env var {known_hosts_env}")
103
+
104
+ base = Path(clone_base_path).expanduser().resolve()
105
+ base.mkdir(parents=True, exist_ok=True)
106
+ repo_path = base / repo_dir_name
107
+ logger.info(f"Target repository path: {repo_path}")
108
+
109
+ tmpdir = Path(tempfile.mkdtemp(prefix="git_ssh_"))
110
+ key_path = tmpdir / "id_rsa"
111
+ kh_path = tmpdir / "known_hosts"
112
+ logger.info(f"Created temporary directory for SSH keys: {tmpdir}")
113
+
114
+ try:
115
+ key_path.write_text(priv_key, encoding="utf-8")
116
+ kh_path.write_text(kh_data, encoding="utf-8")
117
+ try:
118
+ os.chmod(key_path, 0o600)
119
+ except PermissionError:
120
+ pass # ignore on platforms where chmod doesn't apply
121
+
122
+ # Note: no StrictModes here
123
+ ssh_cmd = (
124
+ f"ssh -i {key_path} -o IdentitiesOnly=yes "
125
+ f"-o UserKnownHostsFile={kh_path} "
126
+ f"-o StrictHostKeyChecking=yes"
127
+ )
128
+
129
+ env = os.environ.copy()
130
+ env["GIT_SSH_COMMAND"] = ssh_cmd
131
+
132
+ if not repo_path.exists():
133
+ logger.info(f"Repository doesn't exist, performing fresh clone")
134
+ cmd = ["git", "clone"]
135
+ if depth and depth > 0:
136
+ cmd += ["--depth", str(depth)]
137
+ cmd += [git_url, str(repo_path)]
138
+ logger.info(f"Executing git clone command: {' '.join(cmd[:-1])} <repo_path>")
139
+ subprocess.check_call(cmd, env=env)
140
+ logger.info("Git clone completed successfully")
141
+ else:
142
+ logger.info(f"Repository already exists at {repo_path}, updating existing repo")
143
+ if not (repo_path / ".git").exists():
144
+ logger.error(f"Path exists but is not a git repository: {repo_path}")
145
+ raise RuntimeError(f"Path exists but is not a git repo: {repo_path}")
146
+ logger.info("Updating remote URL")
147
+ subprocess.check_call(["git", "remote", "set-url", "origin", git_url], cwd=repo_path, env=env)
148
+ logger.info("Fetching latest changes")
149
+ subprocess.check_call(["git", "fetch", "--all", "--prune"], cwd=repo_path, env=env)
150
+ logger.info("Pulling latest changes")
151
+ subprocess.check_call(["git", "pull", "--ff-only", "origin"], cwd=repo_path, env=env)
152
+ logger.info("Repository update completed successfully")
153
+
154
+ logger.info(f"SSH clone operation completed, returning path: {repo_path}")
155
+ return repo_path
156
+
157
+ finally:
158
+ try:
159
+ shutil.rmtree(tmpdir)
160
+ except Exception:
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
+ }
@@ -92,7 +92,7 @@ def insert_gn_record(session, xsrf_token, xml_string):
92
92
  }
93
93
 
94
94
  # Send a put request to the endpoint to create record
95
- response = session.put(GN_URL + '/geonetwork/srv/api/0.1/records',
95
+ response = session.put(GN_URL + '/geonetwork/srv/api/records',
96
96
  data=xml_string,
97
97
  params=params,
98
98
  auth=(GN_USERNAME, GN_PASSWORD),
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "kubernetes-watch"
3
- version = "0.1.12"
3
+ version = "0.1.14"
4
4
  description = ""
5
5
  authors = ["bmotevalli <b.motevalli@gmail.com>"]
6
6
  packages = [{include = "kube_watch"}]
@@ -1,12 +0,0 @@
1
- from typing import Union
2
- from pydantic import BaseModel
3
-
4
- class TableQuery(BaseModel):
5
- name: str
6
- column_name: str
7
- db_host: str
8
- db_port: int
9
- db_name: str
10
- db_user: str
11
- db_pass: str
12
-
@@ -1,109 +0,0 @@
1
- import os
2
- import shutil
3
- import subprocess
4
- import tempfile
5
- from pathlib import Path
6
-
7
- from prefect import get_run_logger
8
-
9
- logger = get_run_logger()
10
-
11
-
12
- def clone_pat_repo(git_pat, git_url, clone_base_path):
13
- """ Clone a Git repository using a Personal Access Token (PAT) for authentication."""
14
- # Retrieve environment variables
15
- access_token = git_pat # os.environ.get('GIT_PAT')
16
- repo_url = git_url # os.environ.get('GIT_URL')
17
-
18
- if not access_token or not repo_url:
19
- raise ValueError("Environment variables GIT_PAT or GIT_URL are not set")
20
-
21
- # Correctly format the URL with the PAT
22
- if 'https://' in repo_url:
23
- # Splitting the URL and inserting the PAT
24
- parts = repo_url.split('https://', 1)
25
- repo_url = f'https://{access_token}@{parts[1]}'
26
- else:
27
- raise ValueError("URL must begin with https:// for PAT authentication")
28
-
29
- # Directory where the repo will be cloned
30
- repo_path = os.path.join(clone_base_path, 'manifest-repo')
31
-
32
- # Clone the repository
33
- if not os.path.exists(repo_path):
34
- logger.info(f"Cloning repository into {repo_path}")
35
- repo = Repo.clone_from(repo_url, repo_path)
36
- logger.info("Repository cloned successfully.")
37
- else:
38
- logger.info(f"Repository already exists at {repo_path}")
39
-
40
-
41
- def clone_ssh_repo(
42
- git_url: str,
43
- clone_base_path: str,
44
- repo_dir_name: str = "manifest-repo",
45
- depth: int = 1,
46
- ssh_key_env: str = "GIT_SSH_PRIVATE_KEY",
47
- known_hosts_env: str = "GIT_SSH_KNOWN_HOSTS",
48
- ) -> Path:
49
- """
50
- Clone/update a repo via SSH using key + known_hosts from env vars.
51
- - GIT_SSH_PRIVATE_KEY: full private key (BEGIN/END ...)
52
- - GIT_SSH_KNOWN_HOSTS: lines from `ssh-keyscan github.com`
53
- """
54
- if not git_url.startswith("git@"):
55
- raise ValueError("git_url must be an SSH URL like 'git@github.com:org/repo.git'")
56
-
57
- priv_key = os.environ.get(ssh_key_env)
58
- kh_data = os.environ.get(known_hosts_env)
59
- if not priv_key:
60
- raise ValueError(f"Missing env var {ssh_key_env}")
61
- if not kh_data:
62
- raise ValueError(f"Missing env var {known_hosts_env}")
63
-
64
- base = Path(clone_base_path).expanduser().resolve()
65
- base.mkdir(parents=True, exist_ok=True)
66
- repo_path = base / repo_dir_name
67
-
68
- tmpdir = Path(tempfile.mkdtemp(prefix="git_ssh_"))
69
- key_path = tmpdir / "id_rsa"
70
- kh_path = tmpdir / "known_hosts"
71
-
72
- try:
73
- key_path.write_text(priv_key, encoding="utf-8")
74
- kh_path.write_text(kh_data, encoding="utf-8")
75
- try:
76
- os.chmod(key_path, 0o600)
77
- except PermissionError:
78
- pass # ignore on platforms where chmod doesn't apply
79
-
80
- # Note: no StrictModes here
81
- ssh_cmd = (
82
- f"ssh -i {key_path} -o IdentitiesOnly=yes "
83
- f"-o UserKnownHostsFile={kh_path} "
84
- f"-o StrictHostKeyChecking=yes"
85
- )
86
-
87
- env = os.environ.copy()
88
- env["GIT_SSH_COMMAND"] = ssh_cmd
89
-
90
- if not repo_path.exists():
91
- cmd = ["git", "clone"]
92
- if depth and depth > 0:
93
- cmd += ["--depth", str(depth)]
94
- cmd += [git_url, str(repo_path)]
95
- subprocess.check_call(cmd, env=env)
96
- else:
97
- if not (repo_path / ".git").exists():
98
- raise RuntimeError(f"Path exists but is not a git repo: {repo_path}")
99
- subprocess.check_call(["git", "remote", "set-url", "origin", git_url], cwd=repo_path, env=env)
100
- subprocess.check_call(["git", "fetch", "--all", "--prune"], cwd=repo_path, env=env)
101
- subprocess.check_call(["git", "pull", "--ff-only", "origin"], cwd=repo_path, env=env)
102
-
103
- return repo_path
104
-
105
- finally:
106
- try:
107
- shutil.rmtree(tmpdir)
108
- except Exception:
109
- pass