kubernetes-watch 0.1.12__py3-none-any.whl → 0.1.14__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.
- kube_watch/modules/clusters/kube.py +39 -1
- kube_watch/modules/database/model.py +8 -7
- kube_watch/modules/database/postgre.py +45 -12
- kube_watch/modules/providers/git.py +202 -2
- kube_watch/standalone/metarecogen/ckan_to_gn.py +1 -1
- {kubernetes_watch-0.1.12.dist-info → kubernetes_watch-0.1.14.dist-info}/METADATA +1 -1
- {kubernetes_watch-0.1.12.dist-info → kubernetes_watch-0.1.14.dist-info}/RECORD +9 -9
- {kubernetes_watch-0.1.12.dist-info → kubernetes_watch-0.1.14.dist-info}/LICENSE +0 -0
- {kubernetes_watch-0.1.12.dist-info → kubernetes_watch-0.1.14.dist-info}/WHEEL +0 -0
|
@@ -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(
|
|
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,12 +1,13 @@
|
|
|
1
|
-
from typing import Union
|
|
1
|
+
from typing import Union, Optional
|
|
2
2
|
from pydantic import BaseModel
|
|
3
3
|
|
|
4
4
|
class TableQuery(BaseModel):
|
|
5
|
-
|
|
5
|
+
table_name: str
|
|
6
6
|
column_name: str
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
12
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,
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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 '
|
|
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=
|
|
137
|
-
port=
|
|
138
|
-
database=
|
|
139
|
-
user=
|
|
140
|
-
password=
|
|
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()
|
|
@@ -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
|
|
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
|
|
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
|
+
}
|
|
@@ -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/
|
|
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),
|
|
@@ -9,10 +9,10 @@ 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=
|
|
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
|
-
kube_watch/modules/database/model.py,sha256=
|
|
15
|
-
kube_watch/modules/database/postgre.py,sha256=
|
|
14
|
+
kube_watch/modules/database/model.py,sha256=sHJdDykrEGqBemCTAlPt0_sqSz1l30FPrfxgAvJAOtw,341
|
|
15
|
+
kube_watch/modules/database/postgre.py,sha256=EK5LlYDarsHquNH4NOh9kh4ernj3RJ890PPKElTrELg,9905
|
|
16
16
|
kube_watch/modules/logic/actions.py,sha256=hMvqqzR2EzcZ68_O8GdyLaPSLftA-tWAPaJnKdUMj-k,2416
|
|
17
17
|
kube_watch/modules/logic/checks.py,sha256=7tIR5ipZgnYsAI3ref0qfeFmzXpSyWJNclnO45OUizs,327
|
|
18
18
|
kube_watch/modules/logic/load.py,sha256=XC-SsWIChhW-QXJeCGMsLuLsn9v5nRni5Y6utwYLt48,781
|
|
@@ -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=
|
|
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
|
-
kube_watch/standalone/metarecogen/ckan_to_gn.py,sha256=
|
|
29
|
+
kube_watch/standalone/metarecogen/ckan_to_gn.py,sha256=OcA2jkjIVhgrdaCmjEpAdR60-XTGYvY2uGJPd3bkHTA,4631
|
|
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.
|
|
34
|
-
kubernetes_watch-0.1.
|
|
35
|
-
kubernetes_watch-0.1.
|
|
36
|
-
kubernetes_watch-0.1.
|
|
33
|
+
kubernetes_watch-0.1.14.dist-info/LICENSE,sha256=_H2QdL-2dXbivDmOpJ11DnqJewSFhSJwGpHx_WAE-CA,1075
|
|
34
|
+
kubernetes_watch-0.1.14.dist-info/METADATA,sha256=-cY70V1_b1yFRYc7ovGyLWGl69Hlwub7b4HnrVtogOM,5600
|
|
35
|
+
kubernetes_watch-0.1.14.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
36
|
+
kubernetes_watch-0.1.14.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|