Skip to content

Commit 1b5d839

Browse files
Alessandro De Mariaademariag
authored andcommitted
Improve generators
1 parent 4623ad4 commit 1b5d839

File tree

9 files changed

+559
-157
lines changed

9 files changed

+559
-157
lines changed

kubernetes/argocd.py

Lines changed: 10 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010

1111
class ArgoCDApplicationConfigSpec(KubernetesResourceSpec):
1212
project: str = "default"
13-
destination: Dict = None
13+
destination: dict = None
1414
source: dict
1515
sync_policy: dict = None
16-
ignore_differences: dict = None
16+
ignore_differences: list[dict] = None
1717

1818

1919
class ArgoCDApplication(KubernetesResource):
@@ -27,7 +27,7 @@ def body(self):
2727
self.root.spec.project = self.config.project
2828
destination = self.config.destination
2929
self.root.spec.destination.name = self.cluster.display_name or destination.name
30-
self.root.spec.destination.namespace = destination.namespace
30+
self.root.spec.destination.namespace = destination.get("namespace")
3131
self.root.spec.source = self.config.source
3232
self.root.spec.syncPolicy = self.config.sync_policy
3333

@@ -44,7 +44,6 @@ def body(self):
4444
path="generators.argocd.applications",
4545
global_generator=True,
4646
activation_path="argocd.app_of_apps",
47-
apply_patches=["generators.argocd.defaults.application"],
4847
)
4948
class GenArgoCDApplication(kgenlib.BaseStore):
5049
def body(self):
@@ -53,6 +52,11 @@ def body(self):
5352
name = config.get("name", self.name)
5453
enabled = config.get("enabled", True)
5554
clusters = self.inventory.parameters.get("clusters", {})
55+
single_cluster = config.get("single_cluster", False)
56+
if single_cluster:
57+
cluster = self.inventory.parameters.get("cluster", {})
58+
cluster_id = cluster.get("user", None)
59+
clusters = {cluster_id: cluster}
5660

5761
if enabled:
5862
for cluster in clusters.keys():
@@ -72,40 +76,6 @@ def body(self):
7276
self.add(argo_application)
7377

7478

75-
@kgenlib.register_generator(
76-
path="isoflow.graphs",
77-
global_generator=True,
78-
activation_path="argocd.app_of_apps",
79-
)
80-
class GenArgoCDApplicationPerResource(kgenlib.BaseStore):
81-
def body(self):
82-
graph_config = self.config
83-
argocd_application_config = graph_config.get("argocd_application_config", {})
84-
85-
# Applies generator defaults to the config of the argocd application
86-
kgenlib.patch_config(
87-
argocd_application_config,
88-
self.inventory,
89-
"parameters.generators.argocd.defaults.application",
90-
)
91-
92-
graph_name = graph_config.get("name", self.name)
93-
argocd_application_config.setdefault("source", {}).setdefault("directory", {})[
94-
"include"
95-
] = f"{graph_name}.yml"
96-
argocd_application_config["source"].pop("plugin", None)
97-
98-
namespace = argocd_application_config["destination"]["namespace"]
99-
cluster = argocd_application_config["destination"]["name"]
100-
101-
graph_name = f"{graph_name}.{namespace}.{cluster}"
102-
103-
argo_application = ArgoCDApplication(
104-
name=graph_name, config=argocd_application_config
105-
)
106-
self.add(argo_application)
107-
108-
10979
class ArgoCDProjectConfigSpec(KubernetesResourceSpec):
11080
source_repos: list = []
11181
destinations: list = []
@@ -130,7 +100,8 @@ def body(self):
130100

131101
@kgenlib.register_generator(
132102
path="generators.argocd.projects",
133-
apply_patches=["generators.argocd.defaults.project"],
103+
global_generator=True,
104+
activation_path="argocd.app_of_apps",
134105
)
135106
class GenArgoCDProject(kgenlib.BaseStore):
136107
def body(self):

kubernetes/autoscaling.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
logger = logging.getLogger(__name__)
44

5-
from .common import KubernetesResource
5+
from .common import KubernetesResource, kgenlib
66

77

88
class KedaScaledObject(KubernetesResource):
@@ -17,6 +17,16 @@ def body(self):
1717
self.root.spec.scaleTargetRef.kind = workload.root.kind
1818
self.root.spec.scaleTargetRef.apiVersion = workload.root.apiVersion
1919
self.root.spec.update(config.keda_scaled_object)
20+
if self.root.spec.maxReplicaCount == 0:
21+
# keda supports pausing autoscaling
22+
# but doesn't support setting maxReplicaCount to 0
23+
self.root.metadata.annotations.update(
24+
{
25+
"autoscaling.keda.sh/paused-replicas": "0",
26+
"autoscaling.keda.sh/paused": "true",
27+
}
28+
)
29+
self.root.spec.maxReplicaCount = 1
2030

2131
# remove replica from workload because HPA is managing it
2232
workload.root.spec.pop("replicas")
@@ -83,3 +93,19 @@ def body(self):
8393

8494
# remove replica from workload because HPA is managing it
8595
workload.root.spec.pop("replicas")
96+
97+
98+
@kgenlib.register_generator(path="generators.kubernetes.vpa")
99+
class VerticalPodAutoscalerGenerator(kgenlib.BaseStore):
100+
def body(self):
101+
name = self.config.get("name", self.name)
102+
self.config.vpa.update_mode = self.config.update_mode
103+
self.config.vpa.resource_policy = self.config.resource_policy
104+
105+
workload = KubernetesResource(
106+
name=name, kind=self.config.kind, api_version=self.config.api_version
107+
)
108+
109+
self.add(
110+
VerticalPodAutoscaler(name=name, config=self.config, workload=workload)
111+
)

kubernetes/common.py

Lines changed: 128 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
logger = logging.getLogger(__name__)
44
from enum import StrEnum, auto
5-
from typing import Any, Dict, List, Optional
5+
from typing import Annotated, Any, Dict, List, Optional, Union
66

77
from kapitan.inputs.kadet import BaseObj, load_from_search_paths
8-
from pydantic import DirectoryPath, Field, FilePath
8+
from pydantic import ConfigDict, DirectoryPath, Field, FilePath, model_validator
9+
from pydantic.alias_generators import to_camel
910

1011
kgenlib = load_from_search_paths("kgenlib")
1112

@@ -68,6 +69,8 @@ def body(self):
6869
self.root.kind = self.kind
6970

7071
self.root.metadata.name = self.rendered_name or self.name
72+
self.root.metadata.labels
73+
self.root.metadata.annotations
7174
self.add_label("name", self.name)
7275
if self.config:
7376
self.add_labels(self.config.labels)
@@ -141,13 +144,17 @@ def body(self):
141144
def set_namespace(self, namespace: str):
142145
self.root.metadata.namespace = namespace
143146

147+
def remove_namespace(self):
148+
self.root.metadata.pop("namespace")
149+
144150

145151
class WorkloadTypes(StrEnum):
146152
DEPLOYMENT = auto()
147153
STATEFULSET = auto()
148154
DAEMONSET = auto()
149155
JOB = auto()
150156
CRONJOB = auto()
157+
CLOUD_RUN_SERVICE = auto()
151158

152159

153160
class RestartPolicy(StrEnum):
@@ -156,6 +163,12 @@ class RestartPolicy(StrEnum):
156163
NEVER = "Never"
157164

158165

166+
class ConcurrentPolicy(StrEnum):
167+
ALLOW = "Allow"
168+
FORBID = "Forbid"
169+
REPLACE = "Replace"
170+
171+
159172
class ImagePullPolicy(StrEnum):
160173
ALWAYS = "Always"
161174
IF_NOT_PRESENT = "IfNotPresent"
@@ -286,7 +299,9 @@ class ServiceAccountConfigSpec(KubernetesResourceSpec):
286299
namespace: Optional[str] = None
287300
rendered_name: Optional[str] = None
288301
name: Optional[str] = None
289-
roles: Optional[List[Dict[str, Any]]] = None
302+
roles: Optional[
303+
list[dict[str, list[str]]] | dict[str, dict[str, list[str]] | list[str]]
304+
] = None
290305

291306

292307
class ContainerSpec(kgenlib.BaseModel):
@@ -298,7 +313,7 @@ class ContainerSpec(kgenlib.BaseModel):
298313
healthcheck: Optional[HealthCheckConfigSpec] = None
299314
image: str = None
300315
image_pull_policy: Optional[ImagePullPolicy] = ImagePullPolicy.IF_NOT_PRESENT
301-
lifecycle: dict = {}
316+
lifecycle: Optional[dict] = None
302317
pod_annotations: dict = {}
303318
pod_labels: dict = {}
304319
ports: Dict[str, ContainerPortSpec] = {}
@@ -308,6 +323,10 @@ class ContainerSpec(kgenlib.BaseModel):
308323
volume_mounts: dict = {}
309324

310325

326+
class InitContainerSpec(ContainerSpec):
327+
sidecar: Optional[bool] = None
328+
329+
311330
class ServiceTypes(StrEnum):
312331
EXTERNAL_NAME = "ExternalName"
313332
CLUSTER_IP = "ClusterIP"
@@ -322,7 +341,7 @@ class SessionAffinity(StrEnum):
322341

323342
class RoleBindingConfigSpec(KubernetesResourceSpec):
324343
roleRef: Optional[Dict[str, Any]] = None
325-
subject: Optional[List[Dict[str, Any]]] = None
344+
subjects: Optional[List[Dict[str, Any]]] = None
326345

327346

328347
class ServiceConfigSpec(KubernetesResourceSpec):
@@ -355,8 +374,9 @@ class NetworkPolicySpec(KubernetesResourceSpec):
355374

356375
class WorkloadConfigSpec(KubernetesResourceSpec, ContainerSpec):
357376
type: Optional[WorkloadTypes] = WorkloadTypes.DEPLOYMENT
377+
namespace: str | None = None
358378
schedule: Optional[str] = None
359-
additional_containers: Optional[Dict[str, ContainerSpec]] = {}
379+
additional_containers: Optional[Dict[str, Union[ContainerSpec, None]]] = {}
360380
additional_services: Optional[Dict[str, ServiceConfigSpec]] = {}
361381
annotations: dict = {}
362382
application: Optional[str] = None
@@ -371,7 +391,7 @@ class WorkloadConfigSpec(KubernetesResourceSpec, ContainerSpec):
371391
host_pid: Optional[bool] = None
372392
hpa: dict = {}
373393
image_pull_secrets: list = []
374-
init_containers: Optional[Dict[str, ContainerSpec]] = {}
394+
init_containers: Optional[Dict[str, Union[InitContainerSpec, None]]] = {}
375395
istio_policy: dict = {}
376396
keda_scaled_object: dict = {}
377397
labels: Dict[str, str] = {}
@@ -399,16 +419,25 @@ class WorkloadConfigSpec(KubernetesResourceSpec, ContainerSpec):
399419
workload_security_context: dict = {}
400420

401421

422+
class CloudRunServiceConfigSpec(WorkloadConfigSpec):
423+
type: Optional[WorkloadTypes] = WorkloadTypes.CLOUD_RUN_SERVICE
424+
425+
402426
class DeploymentConfigSpec(WorkloadConfigSpec):
403427
type: Optional[WorkloadTypes] = WorkloadTypes.DEPLOYMENT
404428
update_strategy: Optional[dict] = {}
405429
strategy: Optional[dict] = {}
406430

407431

432+
class PodManagementPolicy(StrEnum):
433+
ORDERED_READY = "OrderedReady"
434+
PARALLEL = "Parallel"
435+
436+
408437
class StatefulSetConfigSpec(WorkloadConfigSpec):
409438
type: WorkloadTypes = WorkloadTypes.STATEFULSET
439+
pod_management_policy: str = PodManagementPolicy.ORDERED_READY
410440
update_strategy: dict = {}
411-
strategy: dict = {}
412441

413442

414443
class DaemonSetConfigSpec(WorkloadConfigSpec):
@@ -426,3 +455,94 @@ class JobConfigSpec(WorkloadConfigSpec):
426455
class CronJobConfigSpec(JobConfigSpec):
427456
type: WorkloadTypes = WorkloadTypes.CRONJOB
428457
schedule: str
458+
concurrency_policy: Optional[ConcurrentPolicy] = ConcurrentPolicy.ALLOW
459+
460+
461+
ContainerProbeSpecTypes = Annotated[
462+
Union[ContainerEXECProbeSpec, ContainerHTTPProbeSpec, ContainerTCPProbeSpec],
463+
Field(discriminator="type"),
464+
]
465+
466+
467+
class ContainerProbes(kgenlib.BaseModel):
468+
model_config = ConfigDict(
469+
alias_generator=to_camel,
470+
populate_by_name=True,
471+
from_attributes=True,
472+
extra="ignore",
473+
)
474+
type: ProbeTypes = Field(exclude=True)
475+
initial_delay_seconds: int = 0
476+
period_seconds: int = 10
477+
timeout_seconds: int = 1
478+
success_threshold: int = 1
479+
failure_threshold: int = 3
480+
481+
# Define a class method for creating probes from data
482+
@classmethod
483+
def from_spec(cls, spec: ContainerProbeSpecTypes) -> Union["ContainerProbes", None]:
484+
probe = None
485+
if not spec or not spec.enabled:
486+
return probe
487+
probe_type = spec.type
488+
if probe_type == ProbeTypes.TCP:
489+
probe = TCPProbe.model_validate(spec)
490+
elif probe_type == ProbeTypes.HTTP:
491+
probe = HTTPProbe.model_validate(spec)
492+
elif probe_type == ProbeTypes.EXEC:
493+
probe = ExecProbe.model_validate(spec)
494+
else:
495+
raise ValueError(f"Invalid probe type: {probe_type}")
496+
return probe.model_dump(by_alias=True)
497+
498+
499+
class HttpGet(kgenlib.BaseModel):
500+
model_config = ConfigDict(
501+
alias_generator=to_camel,
502+
populate_by_name=True,
503+
from_attributes=True,
504+
)
505+
path: str = "/"
506+
port: str | int = 80
507+
httpHeaders: Optional[List[dict]] = None
508+
scheme: Optional[ProbeSchemeSpec] = ProbeSchemeSpec.HTTP
509+
510+
511+
class HTTPProbe(ContainerProbes):
512+
httpGet: Optional[HttpGet] = None
513+
port: str | int = Field(exclude=True)
514+
path: str = Field(exclude=True)
515+
scheme: Optional[ProbeSchemeSpec] = Field(exclude=True)
516+
httpHeaders: Optional[List[dict]] = Field(exclude=True)
517+
518+
# Use a validator to handle the 'port' mapping
519+
@model_validator(mode="after")
520+
def map_port_to_http_config(self):
521+
self.httpGet = HttpGet.model_validate(self)
522+
return self
523+
524+
525+
class TCPProbe(ContainerProbes):
526+
port: str | int = Field(exclude=True)
527+
tcpSocket: Optional[dict] = None
528+
529+
# Use a validator to handle the 'port' mapping
530+
@model_validator(mode="after")
531+
def map_port_to_tcp_socket(self):
532+
self.tcpSocket = {"port": self.port}
533+
return self
534+
535+
536+
class ExecProbe(ContainerProbes):
537+
command: List = Field(exclude=True)
538+
exec: Optional[dict] = None
539+
540+
@model_validator(mode="after")
541+
def map_command_to_exec_socket(self):
542+
self.exec = {"command": self.command}
543+
return self
544+
545+
546+
ContainerProbeTypes = Annotated[
547+
Union[HTTPProbe, TCPProbe, ExecProbe], Field(discriminator="type")
548+
]

kubernetes/files.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import logging
2+
3+
from .common import kgenlib
4+
5+
logger = logging.getLogger(__name__)
6+
7+
8+
@kgenlib.register_generator(path="generators.kubernetes.raw")
9+
class RawManifestFilesGenerator(kgenlib.BaseStore):
10+
def body(self):
11+
for file in self.config.files:
12+
self.add(kgenlib.BaseStore.from_yaml_file(file))
13+
if self.config.filename:
14+
[
15+
setattr(content, "filename", self.config.filename)
16+
for content in self.content_list
17+
]

0 commit comments

Comments
 (0)