kubeler 0.1.3__py3-none-any.whl → 0.1.4__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.
kubeler/main.py CHANGED
@@ -13,13 +13,14 @@ def main():
13
13
  install_parser.add_argument('--start-from', type=str, default=None, help='Start from a specific step')
14
14
  install_parser.add_argument('--steps', type=str, default=None, help='Only run specific steps, comma separated')
15
15
  install_parser.add_argument('--excludes', type=str, default=None, help='Exlude some steps, comma separated')
16
+ install_parser.add_argument('--watch', type=str, default=None, help='Watch for changes')
16
17
  args = parser.parse_args()
17
18
 
18
19
  # load .env content to environment variables
19
20
  load_dotenv()
20
21
 
21
22
  if args.command == 'install':
22
- installer = Installer(installer=args.installer, kube_config=args.kube_config, start_from=args.start_from, steps=args.steps, excludes=args.excludes)
23
+ installer = Installer(installer=args.installer, kube_config=args.kube_config, start_from=args.start_from, steps=args.steps, excludes=args.excludes, watch=args.watch)
23
24
  installer.install()
24
25
 
25
26
  if __name__ == "__main__":
@@ -1,16 +1,21 @@
1
1
  import yaml, os, sys, subprocess, shutil, jinja2
2
+ from kubernetes import client, config as k8sconfig
2
3
  from .models.kubeler import Kubeler
4
+ from .watchdog import watch_directory
3
5
 
4
6
  tmp_dir = "/tmp/kubeler"
7
+ k8sconfig.load_kube_config()
8
+ v1 = client.CoreV1Api()
5
9
 
6
10
  class Installer:
7
- def __init__(self, installer, kube_config, start_from, steps, excludes):
11
+ def __init__(self, installer, kube_config, start_from, steps, excludes, watch):
8
12
  self.installer = installer
9
13
  self.kube_config = kube_config
10
14
  self.start_from = start_from
11
15
  self.kube_config = kube_config
12
16
  self.steps = steps
13
17
  self.excludes = excludes
18
+ self.watch = watch
14
19
 
15
20
  # get the directory path of the installer and kube_config
16
21
  self.installer_dir_path = os.path.dirname(installer)
@@ -72,19 +77,25 @@ class Installer:
72
77
 
73
78
  # get commands defined in the file
74
79
  commands = self.load_file(file_path)
75
- for command in commands:
76
- print("Executing command: ", command)
77
- cmd = command.split()
78
- process = subprocess.Popen(cmd, cwd=execution_dir, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=1, universal_newlines=True)
79
- for line in process.stdout:
80
- print(line, end="")
81
- sys.stdout.flush()
82
- process.wait()
80
+ self.execute(commands, execution_dir)
83
81
 
84
82
  # if config_path exists, restore the file
85
83
  if os.path.exists(config_path):
86
84
  os.remove(config_path)
87
-
85
+
86
+ if kubeler.group.watch.enabled == True and self.watch == "true":
87
+ watch_directory(self, os.path.abspath(self.installer_dir_path), kubeler.group.watch.dir)
88
+
89
+ def execute(self, commands, execution_dir):
90
+ for command in commands:
91
+ print("Executing command: ", command)
92
+ cmd = command.split()
93
+ process = subprocess.Popen(cmd, cwd=execution_dir, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=1, universal_newlines=True)
94
+ for line in process.stdout:
95
+ print(line, end="")
96
+ sys.stdout.flush()
97
+ process.wait()
98
+
88
99
  # if there some files in the directory, load them
89
100
  def load_files_in_dir(self, dir):
90
101
  files = []
@@ -100,7 +111,9 @@ class Installer:
100
111
  for line in file:
101
112
  if line.startswith("#cmd:"):
102
113
  command = line.split(":", 1)[1].strip()
114
+ command = self.handle_helm_chart_idempotency(command)
103
115
  commands.append(command)
116
+
104
117
  return commands
105
118
 
106
119
  # load the configuration file and parse to Kubeler model
@@ -154,10 +167,10 @@ class Installer:
154
167
  # handle only run specific steps
155
168
  if self.steps != None:
156
169
  steps = self.steps.split(",")
157
- for step in kubeler.group.steps:
170
+ for step in kubeler.group.steps[:]:
158
171
  if step.name not in steps:
159
172
  kubeler.group.steps.remove(step)
160
-
173
+
161
174
  return kubeler
162
175
 
163
176
  # open the configuration file
@@ -167,4 +180,42 @@ class Installer:
167
180
  data = yaml.safe_load(stream)
168
181
  return data
169
182
  except yaml.YAMLError as exc:
170
- raise ValueError("Failed to load configuration")
183
+ raise ValueError("Failed to load configuration")
184
+
185
+ def is_helm_chart_installed(self, release_name, namespace="default"):
186
+ secrets = v1.list_namespaced_secret(namespace).items
187
+ for secret in secrets:
188
+ labels = secret.metadata.labels or {}
189
+ if labels.get("owner") == "helm" and labels.get("name") == release_name:
190
+ return True
191
+ return False
192
+
193
+ def handle_helm_chart_idempotency(self, command):
194
+ # check if it's helm from: #cmd: helm install ...
195
+ if command.startswith("helm install"):
196
+ # get namespace from some scenarios:
197
+ # -n <namespace>
198
+ # --namespace <namespace>
199
+ # --namespace=<namespace>
200
+ namespace = None
201
+ if "-n" in command:
202
+ namespace = command.split("-n")[1].split()[0]
203
+ elif "--namespace" in command:
204
+ namespace = command.split("--namespace")[1].split()[0]
205
+ elif "--namespace=" in command:
206
+ namespace = command.split("--namespace=")[1].split()[0]
207
+
208
+ # get release name from: #cmd: helm install <release_name> ...
209
+ # get after `helm install` or `helm upgrade`
210
+ release_name = None
211
+ if "install" in command:
212
+ release_name = command.split("install")[1].split()[0]
213
+ elif "upgrade" in command:
214
+ release_name = command.split("upgrade")[1].split()[0]
215
+
216
+ if namespace != None:
217
+ if self.is_helm_chart_installed(release_name, namespace):
218
+ # remove --install and replace install with upgrade
219
+ command = command.replace("--install", "").replace("install", "upgrade")
220
+
221
+ return command
@@ -16,9 +16,14 @@ class Step(BaseModel):
16
16
  vars: List[Variable] | None = Field(default=None,title="Variables to be passed to the step")
17
17
  exclude: str | bool | None = Field(default=None, title="Exclude the step from execution")
18
18
 
19
+ class Watch(BaseModel):
20
+ enabled: bool = Field(title="Watch enabled")
21
+ dir: List[str] = Field(default=None, title="Directory to be watched")
22
+
19
23
  class Group(BaseModel):
20
24
  name: str = Field(title="Initial Command")
21
25
  steps: List[Step] = Field(title="List of steps to be executed")
26
+ watch: Watch | None = Field(default=None,title="Watch configuration")
22
27
 
23
28
  class Kubeler(BaseModel):
24
29
  init: Init | None = Field(default=None,title="Initial Command")
@@ -0,0 +1,7 @@
1
+ from pydantic import BaseModel
2
+ from pydantic.fields import Field
3
+
4
+ class Resource(BaseModel):
5
+ type: str = Field(title="Type of the resource", min_length=3)
6
+ name: str | bool | int = Field(title="Name of the resource", min_length=3)
7
+ namespace: str | bool | int = Field(title="Namespace of the resource", min_length=3)
@@ -0,0 +1,36 @@
1
+ import time, os
2
+ from watchdog.observers import Observer
3
+ from watchdog.events import FileSystemEventHandler
4
+
5
+ class WatchHandler(FileSystemEventHandler):
6
+ def __init__(self, handler):
7
+ self.handler = handler
8
+
9
+ def on_modified(self, event):
10
+ if not event.is_directory:
11
+ print(f"File modified: {event.src_path}")
12
+ commands = self.handler.load_file(event.src_path)
13
+ self.handler.execute(commands, os.path.dirname(event.src_path))
14
+
15
+ def on_created(self, event):
16
+ if not event.is_directory:
17
+ print(f"File created: {event.src_path}")
18
+
19
+ def on_deleted(self, event):
20
+ if not event.is_directory:
21
+ print(f"File deleted: {event.src_path}")
22
+
23
+ def watch_directory(handler, dir, paths=["."]):
24
+ event_handler = WatchHandler(handler)
25
+ observer = Observer()
26
+ for path in paths:
27
+ dir_path = os.path.join(dir, path)
28
+ observer.schedule(event_handler, dir_path, recursive=True)
29
+ observer.start()
30
+
31
+ try:
32
+ while True:
33
+ time.sleep(1)
34
+ except KeyboardInterrupt:
35
+ observer.stop()
36
+ observer.join()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: kubeler
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: A dead simple Kubernetes Resources installer
5
5
  Home-page: https://github.com/glendmaatita/kubeler
6
6
  Author: Glend Maatita
@@ -14,6 +14,8 @@ License-File: LICENSE
14
14
  Requires-Dist: jinja2>=3.1.5
15
15
  Requires-Dist: kubernetes>=32.0.0
16
16
  Requires-Dist: pydantic>=2.10.6
17
+ Requires-Dist: python-dotenv>=1.0.1
18
+ Requires-Dist: watchdog>=6.0.0
17
19
  Dynamic: author
18
20
  Dynamic: author-email
19
21
  Dynamic: classifier
@@ -190,3 +192,27 @@ Use argument `--steps` if you want to only execute some steps and exclude the ot
190
192
  ```
191
193
  --installer=./../examples/installer.yaml --steps=argocd,redis
192
194
  ```
195
+
196
+ ## Watching for file changes
197
+
198
+ Use the `--watch` argument to monitor a directory and automatically apply changes when they occur.
199
+
200
+ Update `installer.yaml` to set `watch` attribute
201
+
202
+ ```
203
+ group:
204
+ name: k8s
205
+ watch:
206
+ enabled: true
207
+ dir:
208
+ - ./applications
209
+ - ./cluster
210
+ ```
211
+
212
+ and then, run
213
+
214
+ ```
215
+ kubeler install --installer=./examples/installer.yaml --watch=true
216
+ ```
217
+
218
+
@@ -0,0 +1,14 @@
1
+ kubeler/__init__.py,sha256=xuYziAiR_MvvngneqgskzBPoVTHLwxRs6l--kORxv7s,7
2
+ kubeler/main.py,sha256=6Bn1OguVUMjx24plZ3J9PCeVbQrcMAxLP7ukIg9CrOI,1437
3
+ kubeler/scripts/__init__.py,sha256=xuYziAiR_MvvngneqgskzBPoVTHLwxRs6l--kORxv7s,7
4
+ kubeler/scripts/installer.py,sha256=ercx3I4ZCs02-QMxoSGm21TIyNJwRvwZeQWTk1hnM9Y,8937
5
+ kubeler/scripts/watchdog.py,sha256=t-iSqNtYm0_i4BOnnDScGdYvder9QUjEid346zoiW9E,1123
6
+ kubeler/scripts/models/__init__.py,sha256=xuYziAiR_MvvngneqgskzBPoVTHLwxRs6l--kORxv7s,7
7
+ kubeler/scripts/models/kubeler.py,sha256=xdpHTxt1wNwjBNHrRu9alj7M8Jp6UDXwPxe_adPH5kc,1357
8
+ kubeler/scripts/models/kubernetes.py,sha256=-G6ppVwXu2Z2zfXXf2Zl9T6fXofSbF25MuYaKdsclV0,327
9
+ kubeler-0.1.4.dist-info/LICENSE,sha256=g8iEDGIIhDh0wHq4B7md4cWUJ69HC13i010cmASC9Lg,1059
10
+ kubeler-0.1.4.dist-info/METADATA,sha256=UagyoubabOH9YjXEj6WLaMGVcZJ_6CS8PFt6-zzBUUY,4661
11
+ kubeler-0.1.4.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
12
+ kubeler-0.1.4.dist-info/entry_points.txt,sha256=-HM8L2j9zOFjn8DGCEqANNS-rSvA8dXlyzWhTZD_02Q,46
13
+ kubeler-0.1.4.dist-info/top_level.txt,sha256=Chw1LcDOq_cKEex9nHyQOMq8-C6QpLdJjK1s_1MnDEk,8
14
+ kubeler-0.1.4.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- kubeler/__init__.py,sha256=xuYziAiR_MvvngneqgskzBPoVTHLwxRs6l--kORxv7s,7
2
- kubeler/main.py,sha256=PN7Q2SmnMcF_HKbdUMNsk7mbVj9pTsu5IFMjyQjuUmc,1326
3
- kubeler/scripts/__init__.py,sha256=xuYziAiR_MvvngneqgskzBPoVTHLwxRs6l--kORxv7s,7
4
- kubeler/scripts/installer.py,sha256=iITHzlaKJ4rOT9vIEVHDcGn9tbh9z7fCSr9hT65fcZI,6736
5
- kubeler/scripts/models/__init__.py,sha256=xuYziAiR_MvvngneqgskzBPoVTHLwxRs6l--kORxv7s,7
6
- kubeler/scripts/models/kubeler.py,sha256=Tq5Ttz1VdfBaKgdVH5DWKUPT9yTm4uPR_CcvmLjtuJY,1135
7
- kubeler-0.1.3.dist-info/LICENSE,sha256=g8iEDGIIhDh0wHq4B7md4cWUJ69HC13i010cmASC9Lg,1059
8
- kubeler-0.1.3.dist-info/METADATA,sha256=DqgtM6m8Xz9PvabjIpJLMRT39jIyzKWgcqOPvi1LPug,4215
9
- kubeler-0.1.3.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
10
- kubeler-0.1.3.dist-info/entry_points.txt,sha256=-HM8L2j9zOFjn8DGCEqANNS-rSvA8dXlyzWhTZD_02Q,46
11
- kubeler-0.1.3.dist-info/top_level.txt,sha256=Chw1LcDOq_cKEex9nHyQOMq8-C6QpLdJjK1s_1MnDEk,8
12
- kubeler-0.1.3.dist-info/RECORD,,