diff --git a/podinfo/config/def.k b/podinfo/config/def.k new file mode 100644 index 00000000..1072ea85 --- /dev/null +++ b/podinfo/config/def.k @@ -0,0 +1,154 @@ +import regex +import k8s.api.core.v1 as corev1 +import k8s.apimachinery.pkg.apis.meta.v1 as metav1 + +gLabels = {} +gAnnotations = gLabels +some = lambda x: any -> bool { + x not in [None, Undefined] +} +schema Config: + """ + Config defines the schema and defaults for the values. + + Attributes + ---------- + ui: UIConfig + Podinfo optional UI setting. + moduleVersion: str + + kubeVersion: str + replicas: int, default is 1 + The number of pods replicas. By default, the number of replicas is 1. + image: str + resources: ResourceRequirements + securityContext: corev1.SecurityContext + SecurityContext holds security configuration that will be applied to a container. Some fields are present in both SecurityContext and PodSecurityContext. + When both are set, the values in SecurityContext take precedence. + podAnnotations: {str:str} + podSecurityContext: corev1.PodSecurityContext + imagePullSecrets: [corev1.LocalObjectReference] + tolerations: [corev1.Toleration] + topologySpreadConstraints: [corev1.TopologySpreadConstraint] + affinity: corev1.Affinity + service: Service + autoscaling: Autoscaling + ingress: Ingress + monitoring: Monitoring + caching: Caching + test: TestJob + + Examples + -------- + + """ + name?: str = option("name") or "podinfo" + namespace?: str = option("namespace") or Undefined + version?: str = option("version") or "v0.1.0" + metadata: metav1.ObjectMeta = metav1.ObjectMeta { + name = name + namespace = namespace + labels: { + "app.kubernetes.io/name" = name + "app.kubernetes.io/version" = version + "app.kubernetes.io/managed-by" = "kcl" + } + } + ui: UIConfig = UIConfig {} + moduleVersion?: str = option("moduleVersion") or Undefined + kubeVersion?: str = option("kubeVersion") or Undefined + replicas: int = option("replicas") or 1 + image: str = option("image") or Undefined + imagePullPolicy?: str = option("imagePullPolicy") or Undefined + resources: ResourceRequirements = ResourceRequirements { + requests: ResourceRequirement { + cpu = "100m" + memory = "320Mi" + } + } + securityContext?: corev1.SecurityContext + podAnnotations?: {str:str} + podSecurityContext?: corev1.PodSecurityContext + imagePullSecrets?: [corev1.LocalObjectReference] + tolerations?: [corev1.Toleration] + topologySpreadConstraints?: [corev1.TopologySpreadConstraint] + affinity: corev1.Affinity = corev1.Affinity {nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms: [{matchExpressions: [{ + key: "kubernetes.io/os" + operator: "In" + values: ["linux"] + }]}]} + service: Service = Service {} + autoscaling: Autoscaling = Autoscaling {minReplicas = replicas} + ingress: Ingress = Ingress {} + monitoring: Monitoring = Monitoring {} + caching: Caching = Caching {} + test?: TestJob + + check: + replicas > 0 + +schema UIConfig: + color: str = "#34577c" + message?: str + backend?: str + +schema ResourceRequirements: + limits?: ResourceRequirement + requests?: ResourceRequirement + + check: + int(requests.cpu.split("m")[0]) <= int(limits.cpu.split("m")[0]) if limits?.cpu and requests?.cpu + +schema ResourceRequirement: + cpu?: str + memory?: str + + check: + regex.match(cpu, "^[1-9]\\d*m$") if cpu + regex.match(memory, "^[1-9]\\d*(Mi|Gi)$") if memory + +schema Service: + port: int = 80 + annotations?: {str:str} = gAnnotations + labels?: {str:str} = gLabels + + check: + 0 < port <= 65535 + +schema Autoscaling: + enabled: bool = False + cpu: int = 99 + memory: str = "" + minReplicas: int + maxReplicas: int = minReplicas + + check: + 0 <= cpu <= 100 + minReplicas <= maxReplicas if some(minReplicas) and some(maxReplicas) + +schema Ingress: + enabled: bool = False + tls: bool = False + host: str = "podinfo.local" + className?: str + annotations?: {str:str} = gAnnotations + labels?: {str:str} = gLabels + +schema Monitoring: + enabled: bool = False + interval: int = 15 + + check: + 5 <= interval <= 3600 + +schema Caching: + enabled: bool = False + redisURL?: str + + check: + regex.match(redisURL, "^tcp://.*$") if redisURL + +schema TestJob: + image: str + imagePullPolicy?: str + diff --git a/podinfo/config/values.k b/podinfo/config/values.k new file mode 100644 index 00000000..8ad1cdca --- /dev/null +++ b/podinfo/config/values.k @@ -0,0 +1 @@ +values: Config = option("values") or Config {image: "ghcr.io/stefanprodan/podinfo"} diff --git a/podinfo/kcl.mod b/podinfo/kcl.mod new file mode 100644 index 00000000..64ac0dc2 --- /dev/null +++ b/podinfo/kcl.mod @@ -0,0 +1,7 @@ +[package] +name = "podinfo" +edition = "0.0.1" +version = "0.0.1" + +[dependencies] +k8s = "1.29" diff --git a/podinfo/kcl.mod.lock b/podinfo/kcl.mod.lock new file mode 100644 index 00000000..38b108d2 --- /dev/null +++ b/podinfo/kcl.mod.lock @@ -0,0 +1,9 @@ +[dependencies] + [dependencies.k8s] + name = "k8s" + full_name = "k8s_1.29" + version = "1.29" + sum = "L8gHVh822FwrQGsibx6qHRqisIekluCkMpkUO+tULXE=" + reg = "ghcr.io" + repo = "kcl-lang/k8s" + oci_tag = "1.29" diff --git a/podinfo/main.k b/podinfo/main.k new file mode 100644 index 00000000..8646d8f8 --- /dev/null +++ b/podinfo/main.k @@ -0,0 +1,12 @@ +import templates +import manifests + +manifests.yaml_stream([ + templates.deployment + templates.service + templates.igress + templates.serviceAccount + templates.serviceMonitor + templates.hpa + templates.testJob +]) diff --git a/podinfo/templates/deployment.k b/podinfo/templates/deployment.k new file mode 100644 index 00000000..b41e1962 --- /dev/null +++ b/podinfo/templates/deployment.k @@ -0,0 +1,115 @@ +import k8s.api.apps.v1 as appsv1 +import k8s.api.core.v1 as corev1 +import config + +values = config.values +deployment = appsv1.Deployment { + metadata: values.metadata + spec: appsv1.DeploymentSpec { + replicas = values.replicas if values.autoscaling.enabled else Undefined + strategy = { + type = "RollingUpdate" + rollingUpdate.maxUnavailable = "50%" + } + selector.matchLabels: values.metadata.labels + template.metadata: { + labels = values.metadata.labels + if values.podAnnotations: + annotations: values.podAnnotations + + if values.monitoring.enabled: + annotations: { + "prometheus.io/scrape": "true" + "prometheus.io/port": "9797" + } + + } + template.spec: corev1.PodSpec { + serviceAccountName = metadata.name + containers = [corev1.Container { + name: values.metadata.name + image: values.image + imagePullPolicy: values.imagePullPolicy + ports: [ + { + name: "http" + containerPort: 9898 + protocol: "TCP" + } + { + name: "http-metrics" + containerPort: 9797 + protocol: "TCP" + } + ] + livenessProbe: { + httpGet: { + path: "/healthz" + port: "http" + } + } + readinessProbe: { + httpGet: { + path: "/readyz" + port: "http" + } + } + if values.resources: + resources = corev1.ResourceRequirements { + if values.resources.limits: + limits.cpu = values.resources.limits?.cpu + limits.memory = values.resources.limits?.memory + + requests.cpu = values.resources.requests?.cpu + requests.memory = values.resources.requests?.memory + } + + if values.securityContext: + securityContext = values.securityContext + + env: [ + corev1.EnvVar {name = "PODINFO_UI_COLOR", value = values.ui.color} + if values.ui?.message: + corev1.EnvVar {name = "PODINFO_UI_MESSAGE", value = values.ui.message} + + if values.ui?.backend: + corev1.EnvVar {name = "PODINFO_BACKEND_URL", value = values.ui.backend} + + ] + command: [ + "./podinfo" + "--level=info" + "--port=9898" + "--port-metrics=9797" + if values.caching.enabled: + "--cache-server=${values.caching.redisURL}" + + ] + volumeMounts: [{ + name: "data" + mountPath: "/data" + }] + }] + if values.podSecurityContext: + securityContext: values.podSecurityContext + + if values.topologySpreadConstraints: + topologySpreadConstraints: values.topologySpreadConstraints + + if values.affinity: + affinity: values.affinity + + if values.tolerations: + tolerations: values.tolerations + + if values.imagePullSecrets: + imagePullSecrets: values.imagePullSecrets + + volumes: [{ + name: "data" + emptyDir: {} + }] + } + } +} + diff --git a/podinfo/templates/hpa.k b/podinfo/templates/hpa.k new file mode 100644 index 00000000..216571d6 --- /dev/null +++ b/podinfo/templates/hpa.k @@ -0,0 +1,43 @@ +import k8s.api.autoscaling.v2 as autoscaling + +hpa = autoscaling.HorizontalPodAutoscaler { + apiVersion: "autoscaling/v2" + kind: "HorizontalPodAutoscaler" + metadata: values.metadata + spec: { + scaleTargetRef: { + apiVersion: "apps/v1" + kind: "Deployment" + name: values.metadata.name + } + minReplicas: values.autoscaling.minReplicas + maxReplicas: values.autoscaling.maxReplicas + metrics: [ + if values.autoscaling.cpu > 0: + autoscaling.MetricSpec { + type: "Resource" + resource: { + name: "cpu" + target: { + type: "Utilization" + averageUtilization: values.autoscaling.cpu + } + } + } + + if values.autoscaling.memory: + autoscaling.MetricSpec { + type: "Resource" + resource: { + name: "memory" + target: { + type: "AverageValue" + averageValue: values.autoscaling.memory + } + } + } + + ] + } +} + diff --git a/podinfo/templates/ingress.k b/podinfo/templates/ingress.k new file mode 100644 index 00000000..59a9ec6d --- /dev/null +++ b/podinfo/templates/ingress.k @@ -0,0 +1,36 @@ +import k8s.api.networking.v1 as netv1 + +igress = netv1.Ingress { + metadata: values.metadata + metadata: { + if values.ingress.labels: + labels: values.ingress.labels + + if values.ingress.annotations: + annotations: values.ingress.annotations + + } + spec: netv1.IngressSpec { + rules: [{ + host: values.ingress.host + http: {paths: [{ + pathType: "Prefix" + path: "/" + backend.service: { + name: values.metadata.name + port.name: "http" + } + }]} + }] + if values.ingress.tls: + tls: [{ + hosts: [values.ingress.host] + secretName: "${values.metadata.name}-cert" + }] + + if values.ingress.className: + ingressClassName: values.ingress.className + + } +} + diff --git a/podinfo/templates/job.k b/podinfo/templates/job.k new file mode 100644 index 00000000..0829762f --- /dev/null +++ b/podinfo/templates/job.k @@ -0,0 +1,41 @@ +import k8s.api.batch.v1 as batchv1 +import k8s.api.core.v1 as corev1 + +testJob = batchv1.Job { + metadata: values.metadata + spec: batchv1.JobSpec { + template: corev1.PodTemplateSpec { + spec: { + containers: [{ + name: "curl" + image: values.test?.image or Undefined + imagePullPolicy: values.test?.imagePullPolicy or Undefined + command: [ + "curl" + "-v" + "-m" + "5" + "${values.metadata.name}:${values.service.port}" + ] + }] + restartPolicy: "Never" + if values.podSecurityContext: + securityContext: values.podSecurityContext + + if values.topologySpreadConstraints: + topologySpreadConstraints: values.topologySpreadConstraints + + if values.affinity: + affinity: values.affinity + + if values.tolerations: + tolerations: values.tolerations + + if values.imagePullSecrets: + imagePullSecrets: values.imagePullSecrets + + } + } + backoffLimit: 1 + } +} if values.test else Undefined diff --git a/podinfo/templates/service.k b/podinfo/templates/service.k new file mode 100644 index 00000000..f940e39b --- /dev/null +++ b/podinfo/templates/service.k @@ -0,0 +1,34 @@ +import k8s.api.core.v1 as corev1 + +service = corev1.Service { + metadata: values.metadata + metadata: { + if values.service.labels: + labels: values.service.labels + + if values.service.annotations: + annotations: values.service.annotations + + } + spec: corev1.ServiceSpec { + type: "ClusterIP" + selector: values.metadata.labels + ports: [ + { + name: "http" + port: values.service.port + targetPort: name + protocol: "TCP" + } + if values.monitoring?.enabled: + { + name: "http-metrics" + port: 9797 + targetPort: "http-metrics" + protocol: "TCP" + } + + ] + } +} + diff --git a/podinfo/templates/service_account.k b/podinfo/templates/service_account.k new file mode 100644 index 00000000..5aa60d17 --- /dev/null +++ b/podinfo/templates/service_account.k @@ -0,0 +1,4 @@ +import k8s.api.core.v1 as corev1 + +serviceAccount = corev1.ServiceAccount {metadata: values.metadata} + diff --git a/podinfo/templates/service_monitor.k b/podinfo/templates/service_monitor.k new file mode 100644 index 00000000..7d2335ef --- /dev/null +++ b/podinfo/templates/service_monitor.k @@ -0,0 +1,14 @@ +serviceMonitor = { + apiVersion: "monitoring.coreos.com/v1" + kind: "ServiceMonitor" + metadata: values.metadata + spec: { + endpoints: [{ + path: "/metrics" + port: "http-metrics" + interval: "${values.monitoring.interval}s" + }] + namespaceSelector.matchNames: [values.metadata.namespace] + selector.matchLabels: values.metadata.labels + } +} if values.monitoring.enabled else Undefined