kubernetes-watch 0.1.9__tar.gz → 0.1.11__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 (37) hide show
  1. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/PKG-INFO +14 -2
  2. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/README.md +12 -0
  3. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/kube_watch/modules/clusters/kube.py +73 -1
  4. kubernetes_watch-0.1.11/kube_watch/modules/logic/checks.py +15 -0
  5. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/kube_watch/modules/logic/merge.py +4 -3
  6. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/kube_watch/modules/providers/aws.py +45 -0
  7. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/kube_watch/modules/providers/git.py +35 -47
  8. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/pyproject.toml +2 -2
  9. kubernetes_watch-0.1.9/kube_watch/modules/logic/checks.py +0 -8
  10. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/LICENSE +0 -0
  11. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/kube_watch/__init__.py +0 -0
  12. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/kube_watch/enums/__init__.py +0 -0
  13. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/kube_watch/enums/kube.py +0 -0
  14. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/kube_watch/enums/logic.py +0 -0
  15. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/kube_watch/enums/providers.py +0 -0
  16. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/kube_watch/enums/workflow.py +0 -0
  17. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/kube_watch/models/__init__.py +0 -0
  18. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/kube_watch/models/common.py +0 -0
  19. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/kube_watch/models/workflow.py +0 -0
  20. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/kube_watch/modules/__init__.py +0 -0
  21. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/kube_watch/modules/clusters/__init__.py +0 -0
  22. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/kube_watch/modules/database/__init__.py +0 -0
  23. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/kube_watch/modules/database/model.py +0 -0
  24. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/kube_watch/modules/database/postgre.py +0 -0
  25. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/kube_watch/modules/logic/actions.py +0 -0
  26. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/kube_watch/modules/logic/load.py +0 -0
  27. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/kube_watch/modules/logic/scheduler.py +0 -0
  28. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/kube_watch/modules/logic/trasnform.py +0 -0
  29. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/kube_watch/modules/mock/__init__.py +0 -0
  30. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/kube_watch/modules/mock/mock_generator.py +0 -0
  31. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/kube_watch/modules/providers/__init__.py +0 -0
  32. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/kube_watch/modules/providers/github.py +0 -0
  33. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/kube_watch/modules/providers/vault.py +0 -0
  34. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/kube_watch/standalone/metarecogen/ckan_to_gn.py +0 -0
  35. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/kube_watch/watch/__init__.py +0 -0
  36. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/kube_watch/watch/helpers.py +0 -0
  37. {kubernetes_watch-0.1.9 → kubernetes_watch-0.1.11}/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.9
3
+ Version: 0.1.11
4
4
  Summary:
5
5
  Author: bmotevalli
6
6
  Author-email: b.motevalli@gmail.com
@@ -15,7 +15,7 @@ Requires-Dist: boto3 (>=1.34.68,<2.0.0)
15
15
  Requires-Dist: humps (>=0.2.2,<0.3.0)
16
16
  Requires-Dist: hvac (>=2.1.0,<3.0.0)
17
17
  Requires-Dist: kubernetes (>=29.0.0,<30.0.0)
18
- Requires-Dist: prefect (>=3.4.17,<4.0.0)
18
+ Requires-Dist: prefect (==3.4.18)
19
19
  Requires-Dist: psycopg2 (>=2.9.10,<3.0.0)
20
20
  Requires-Dist: pydantic (>=2.11.7,<3.0.0)
21
21
  Requires-Dist: requests (>=2.32.3,<3.0.0)
@@ -81,6 +81,14 @@ workflow:
81
81
  inputsArgType: arg
82
82
  conditional:
83
83
  tasks: ["Task_B"]
84
+
85
+ - name: Task_external
86
+ module: <module_external_path>
87
+ plugin_path: <path_to_module>
88
+ task: <func_name>
89
+ inputsArgType: arg
90
+ conditional:
91
+ tasks: ["Task_B"]
84
92
  ```
85
93
 
86
94
 
@@ -88,6 +96,10 @@ workflow:
88
96
 
89
97
  **module**: all modules are located in 'modules' directory in kube_watch. This is where you can extend the library and add new tasks / modules. Below modules, there are submodules such as providers, clusters, and logic. Within each of this submodules, specific modules are defined. For example: providers.aws contains a series of tasks related to AWS. In this case, <module_path> = providers.aws. To add new tasks, add a new module with a similar pattern and refer the path in your task block.
90
98
 
99
+ **plugin_path**: you can add new funtions outside the library without needing to modify this library. If plugin_path is defined, the
100
+ library assumes you are referring to a module outside the library (e.g. in your app or workflow). The plugin_path is path to your
101
+ py script (the folder). Then, provide your script name as module and function name as task.
102
+
91
103
  **task**: task is simply the name function that you put in the <module_path>. i.e. as you define a function in a module, you can simply start to use it in your manifests.
92
104
 
93
105
  **inputArgType**: arg | dict | list: if the task functions accept known-fixed number of parameters, then use arg.
@@ -58,6 +58,14 @@ workflow:
58
58
  inputsArgType: arg
59
59
  conditional:
60
60
  tasks: ["Task_B"]
61
+
62
+ - name: Task_external
63
+ module: <module_external_path>
64
+ plugin_path: <path_to_module>
65
+ task: <func_name>
66
+ inputsArgType: arg
67
+ conditional:
68
+ tasks: ["Task_B"]
61
69
  ```
62
70
 
63
71
 
@@ -65,6 +73,10 @@ workflow:
65
73
 
66
74
  **module**: all modules are located in 'modules' directory in kube_watch. This is where you can extend the library and add new tasks / modules. Below modules, there are submodules such as providers, clusters, and logic. Within each of this submodules, specific modules are defined. For example: providers.aws contains a series of tasks related to AWS. In this case, <module_path> = providers.aws. To add new tasks, add a new module with a similar pattern and refer the path in your task block.
67
75
 
76
+ **plugin_path**: you can add new funtions outside the library without needing to modify this library. If plugin_path is defined, the
77
+ library assumes you are referring to a module outside the library (e.g. in your app or workflow). The plugin_path is path to your
78
+ py script (the folder). Then, provide your script name as module and function name as task.
79
+
68
80
  **task**: task is simply the name function that you put in the <module_path>. i.e. as you define a function in a module, you can simply start to use it in your manifests.
69
81
 
70
82
  **inputArgType**: arg | dict | list: if the task functions accept known-fixed number of parameters, then use arg.
@@ -183,4 +183,76 @@ def has_mismatch_image_digest(repo_digest, label_selector, namespace):
183
183
  logger.info("Images are in-sync.")
184
184
  logger.info(f"Repo digest: {repo_digest}")
185
185
  logger.info(f"Curr digest: {current_image_id.split('@')[-1]}")
186
- return False
186
+ return False
187
+
188
+
189
+ def update_deployment_image_if_needed(namespace, deployment_name, container_name, new_tag):
190
+ """
191
+ Updates the deployment's container image tag only if it differs from the current one.
192
+ The repository URI is extracted automatically from the current image reference.
193
+
194
+ Args:
195
+ namespace: Kubernetes namespace where the deployment resides.
196
+ deployment_name: Name of the deployment to patch.
197
+ container_name: Name of the container inside the deployment.
198
+ new_tag: The new image tag to set, e.g. 'v1.2.3'
199
+
200
+ Returns:
201
+ True if the image was updated and rollout triggered, False otherwise.
202
+ """
203
+ try:
204
+ # Load kube config (works both in-cluster and local)
205
+ try:
206
+ config.load_incluster_config()
207
+ except config.ConfigException:
208
+ config.load_kube_config()
209
+
210
+ apps_v1 = client.AppsV1Api()
211
+
212
+ # Read the current deployment
213
+ deployment = apps_v1.read_namespaced_deployment(deployment_name, namespace)
214
+
215
+ # Find the container we want to update
216
+ containers = deployment.spec.template.spec.containers
217
+ target_container = next((c for c in containers if c.name == container_name), None)
218
+ if not target_container:
219
+ logger.error(f"Container '{container_name}' not found in deployment '{deployment_name}'.")
220
+ return False
221
+
222
+ current_image = target_container.image
223
+ if ':' in current_image:
224
+ repo_uri, current_tag = current_image.rsplit(':', 1)
225
+ else:
226
+ repo_uri, current_tag = current_image, "latest"
227
+
228
+ if current_tag == new_tag:
229
+ logger.info(f"No update needed. Deployment '{deployment_name}' already using tag '{new_tag}'.")
230
+ return False
231
+
232
+ # Build the new image reference using same repo
233
+ new_image = f"{repo_uri}:{new_tag}"
234
+ patch = {
235
+ "spec": {
236
+ "template": {
237
+ "spec": {
238
+ "containers": [
239
+ {"name": container_name, "image": new_image}
240
+ ]
241
+ }
242
+ }
243
+ }
244
+ }
245
+
246
+ logger.info(f"Updating '{deployment_name}' from '{current_image}' to '{new_image}'")
247
+ apps_v1.patch_namespaced_deployment(
248
+ name=deployment_name,
249
+ namespace=namespace,
250
+ body=patch
251
+ )
252
+
253
+ logger.info(f"Deployment '{deployment_name}' successfully updated to {new_image}")
254
+ return True
255
+
256
+ except Exception as e:
257
+ logger.error(f"Failed to update deployment '{deployment_name}': {e}")
258
+ return False
@@ -0,0 +1,15 @@
1
+ from prefect import get_run_logger
2
+ logger = get_run_logger()
3
+
4
+ def dicts_has_diff(dict_a, dict_b):
5
+ return dict_a != dict_b
6
+
7
+
8
+ def remove_keys(d, keys):
9
+ return {k: v for k, v in d.items() if k not in keys}
10
+
11
+
12
+ def print_data(data, indicator = None):
13
+ if indicator:
14
+ logger.info(indicator)
15
+ logger.info(data)
@@ -1,6 +1,6 @@
1
1
  from typing import Any, List, Dict
2
2
  from kube_watch.enums.logic import Operations
3
-
3
+ from copy import deepcopy
4
4
 
5
5
  def merge_logical_outputs(inp_dict: Dict):
6
6
  if 'operation' not in inp_dict.keys():
@@ -25,7 +25,8 @@ def partial_dict_update(orig_data, new_data):
25
25
  This function is used when some key value pairs in orig_data should
26
26
  be updated from new_data.
27
27
  """
28
+ orig_data_copy = deepcopy(orig_data)
28
29
  for k, v in new_data.items():
29
- orig_data[k] = v
30
+ orig_data_copy[k] = v
30
31
 
31
- return orig_data
32
+ return orig_data_copy
@@ -3,6 +3,7 @@
3
3
  #========================================================================
4
4
  import json
5
5
  import base64
6
+ from packaging import version
6
7
  from datetime import datetime , timezone, timedelta
7
8
  import boto3
8
9
  from botocore.exceptions import ClientError
@@ -73,6 +74,50 @@ def task_get_latest_image_digest(session, resource, region, repository_name, tag
73
74
 
74
75
  raise ValueError('Unknown resource')
75
76
 
77
+
78
+ def task_get_highest_version_tag(session, resource, region, repository_name, tag_pattern=r"^v?(?P<version>\d+\.\d+\.\d+([-+][0-9A-Za-z.-]+)?)$"):
79
+ """
80
+ Fetches the highest versioned tag from an ECR repository, similar to FluxCD's semver filter.
81
+
82
+ Args:
83
+ session: boto3 session
84
+ resource: AwsResources enum
85
+ region: AWS region
86
+ repository_name: Name of the ECR repository
87
+ tag_pattern: Regex pattern to extract version component (default: semver)
88
+
89
+ Returns:
90
+ The highest matching tag or None if not found.
91
+ """
92
+ if resource == AwsResources.ECR:
93
+ ecr_client = session.client('ecr', region_name=region)
94
+ try:
95
+ pattern = re.compile(tag_pattern)
96
+ paginator = ecr_client.get_paginator('describe_images')
97
+ matched_tags = []
98
+
99
+ for page in paginator.paginate(repositoryName=repository_name, filter={'tagStatus': 'TAGGED'}):
100
+ for image in page.get('imageDetails', []):
101
+ for tag in image.get('imageTags', []):
102
+ match = pattern.match(tag)
103
+ if match:
104
+ ver_str = match.group('version')
105
+ matched_tags.append((tag, version.parse(ver_str)))
106
+
107
+ if not matched_tags:
108
+ logger.info(f"No tags matching pattern '{tag_pattern}' found in repository {repository_name}.")
109
+ return None
110
+
111
+ # Sort by semantic version and pick the highest
112
+ highest_tag, _ = max(matched_tags, key=lambda t: t[1])
113
+ return highest_tag
114
+
115
+ except Exception as e:
116
+ logger.error(f"Error fetching highest version tag from {repository_name}: {e}")
117
+ return None
118
+
119
+ raise ValueError('Unknown resource')
120
+
76
121
  #========================================================================================
77
122
  # IAM Cred update
78
123
  #========================================================================================
@@ -1,5 +1,4 @@
1
1
  import os
2
- from git import Repo
3
2
  import shutil
4
3
  import subprocess
5
4
  import tempfile
@@ -10,6 +9,35 @@ from prefect import get_run_logger
10
9
  logger = get_run_logger()
11
10
 
12
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
+
13
41
  def clone_ssh_repo(
14
42
  git_url: str,
15
43
  clone_base_path: str,
@@ -19,20 +47,9 @@ def clone_ssh_repo(
19
47
  known_hosts_env: str = "GIT_SSH_KNOWN_HOSTS",
20
48
  ) -> Path:
21
49
  """
22
- Clone or update a Git repo using an SSH private key & known_hosts provided via env vars.
23
-
24
- Env vars:
25
- - GIT_SSH_PRIVATE_KEY: the full private key (RSA/ED25519) including BEGIN/END lines
26
- - GIT_SSH_KNOWN_HOSTS: one or more known_hosts lines (from `ssh-keyscan github.com`)
27
-
28
- Args:
29
- git_url: SSH URL like 'git@github.com:your-org/your-repo.git'
30
- clone_base_path: directory where repo_dir_name will be created
31
- repo_dir_name: folder name for the clone
32
- depth: shallow clone depth (1 == latest)
33
-
34
- Returns:
35
- Path to the cloned/updated repository.
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`
36
53
  """
37
54
  if not git_url.startswith("git@"):
38
55
  raise ValueError("git_url must be an SSH URL like 'git@github.com:org/repo.git'")
@@ -48,7 +65,6 @@ def clone_ssh_repo(
48
65
  base.mkdir(parents=True, exist_ok=True)
49
66
  repo_path = base / repo_dir_name
50
67
 
51
- # Secure temp dir for SSH material
52
68
  tmpdir = Path(tempfile.mkdtemp(prefix="git_ssh_"))
53
69
  key_path = tmpdir / "id_rsa"
54
70
  kh_path = tmpdir / "known_hosts"
@@ -56,16 +72,16 @@ def clone_ssh_repo(
56
72
  try:
57
73
  key_path.write_text(priv_key, encoding="utf-8")
58
74
  kh_path.write_text(kh_data, encoding="utf-8")
59
-
60
75
  try:
61
76
  os.chmod(key_path, 0o600)
62
77
  except PermissionError:
63
- pass # Windows quirk
78
+ pass # ignore on platforms where chmod doesn't apply
64
79
 
80
+ # Note: no StrictModes here
65
81
  ssh_cmd = (
66
82
  f"ssh -i {key_path} -o IdentitiesOnly=yes "
67
83
  f"-o UserKnownHostsFile={kh_path} "
68
- f"-o StrictHostKeyChecking=yes -o StrictModes=no"
84
+ f"-o StrictHostKeyChecking=yes"
69
85
  )
70
86
 
71
87
  env = os.environ.copy()
@@ -91,31 +107,3 @@ def clone_ssh_repo(
91
107
  shutil.rmtree(tmpdir)
92
108
  except Exception:
93
109
  pass
94
-
95
-
96
- def clone_pat_repo(git_pat, git_url, clone_base_path):
97
- # Retrieve environment variables
98
- access_token = git_pat # os.environ.get('GIT_PAT')
99
- repo_url = git_url # os.environ.get('GIT_URL')
100
-
101
- if not access_token or not repo_url:
102
- raise ValueError("Environment variables GIT_PAT or GIT_URL are not set")
103
-
104
- # Correctly format the URL with the PAT
105
- if 'https://' in repo_url:
106
- # Splitting the URL and inserting the PAT
107
- parts = repo_url.split('https://', 1)
108
- repo_url = f'https://{access_token}@{parts[1]}'
109
- else:
110
- raise ValueError("URL must begin with https:// for PAT authentication")
111
-
112
- # Directory where the repo will be cloned
113
- repo_path = os.path.join(clone_base_path, 'manifest-repo')
114
-
115
- # Clone the repository
116
- if not os.path.exists(repo_path):
117
- logger.info(f"Cloning repository into {repo_path}")
118
- repo = Repo.clone_from(repo_url, repo_path)
119
- logger.info("Repository cloned successfully.")
120
- else:
121
- logger.info(f"Repository already exists at {repo_path}")
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "kubernetes-watch"
3
- version = "0.1.9"
3
+ version = "0.1.11"
4
4
  description = ""
5
5
  authors = ["bmotevalli <b.motevalli@gmail.com>"]
6
6
  packages = [{include = "kube_watch"}]
@@ -8,7 +8,7 @@ readme = "README.md"
8
8
 
9
9
  [tool.poetry.dependencies]
10
10
  python = ">=3.10,<3.14"
11
- prefect = "^3.4.17"
11
+ prefect = "3.4.18"
12
12
  boto3 = "^1.34.68"
13
13
  hvac = "^2.1.0"
14
14
  humps = "^0.2.2"
@@ -1,8 +0,0 @@
1
-
2
-
3
- def dicts_has_diff(dict_a, dict_b):
4
- return dict_a != dict_b
5
-
6
-
7
- def remove_keys(d, keys):
8
- return {k: v for k, v in d.items() if k not in keys}