hyperi_rustlib/deployment/generate/
helm.rs1#![allow(clippy::format_push_string)]
10
11use std::path::Path;
12
13use crate::deployment::contract::DeploymentContract;
14use crate::deployment::error::DeploymentError;
15
16use super::common::{safe_template_lookup, to_camel_suffix, write_file};
17
18pub fn generate_chart(
35 contract: &DeploymentContract,
36 output_dir: impl AsRef<Path>,
37 identity: Option<&crate::deployment::ContractIdentity>,
38) -> Result<(), DeploymentError> {
39 let dir = output_dir.as_ref();
40 let templates_dir = dir.join("templates");
41
42 std::fs::create_dir_all(&templates_dir).map_err(|e| DeploymentError::CreateDir {
44 path: templates_dir.display().to_string(),
45 source: e,
46 })?;
47
48 write_file(dir.join("Chart.yaml"), &gen_chart_yaml(contract, identity))?;
50 write_file(dir.join("values.yaml"), &gen_values_yaml(contract))?;
51 write_file(
52 templates_dir.join("_helpers.tpl"),
53 &gen_helpers_tpl(contract),
54 )?;
55 write_file(
56 templates_dir.join("deployment.yaml"),
57 &gen_deployment_yaml(contract),
58 )?;
59 write_file(
60 templates_dir.join("service.yaml"),
61 &gen_service_yaml(contract),
62 )?;
63 write_file(
64 templates_dir.join("serviceaccount.yaml"),
65 &gen_serviceaccount_yaml(contract),
66 )?;
67 write_file(
68 templates_dir.join("configmap.yaml"),
69 &gen_configmap_yaml(contract),
70 )?;
71 write_file(
72 templates_dir.join("secret.yaml"),
73 &gen_secret_yaml(contract),
74 )?;
75 write_file(templates_dir.join("hpa.yaml"), &gen_hpa_yaml(contract))?;
76
77 if contract.keda.is_some() {
78 write_file(
79 templates_dir.join("keda-scaledobject.yaml"),
80 &gen_keda_scaledobject_yaml(contract),
81 )?;
82 write_file(
83 templates_dir.join("keda-triggerauth.yaml"),
84 &gen_keda_triggerauth_yaml(contract),
85 )?;
86 }
87
88 write_file(templates_dir.join("NOTES.txt"), &gen_notes_txt(contract))?;
89
90 Ok(())
91}
92
93fn gen_chart_yaml(
98 c: &DeploymentContract,
99 identity: Option<&crate::deployment::ContractIdentity>,
100) -> String {
101 let identity_block = identity
103 .map(|id| format!("\nannotations:\n{ann}\n", ann = id.as_yaml_annotations(2)))
104 .unwrap_or_default();
105
106 format!(
107 "apiVersion: v2\n\
108 name: {name}\n\
109 description: {desc}\n\
110 type: application\n\
111 version: 0.1.0\n\
112 appVersion: \"1.0.0\"\n\
113 {identity_block}\n\
114 keywords:\n\
115 \x20 - hyperi\n\
116 \x20 - dfe\n\
117 \n\
118 maintainers:\n\
119 \x20 - name: HyperI\n\
120 \x20 url: https://github.com/hyperi-io\n",
121 name = c.app_name,
122 desc = if c.description.is_empty() {
123 &c.app_name
124 } else {
125 &c.description
126 },
127 identity_block = identity_block,
128 )
129}
130
131#[allow(clippy::too_many_lines)]
132fn gen_values_yaml(c: &DeploymentContract) -> String {
133 let mut out = String::with_capacity(2048);
134
135 out.push_str(&format!(
137 "# {app} Helm chart values\n\
138 #\n\
139 # Generated by hyperi-rustlib deployment module.\n\
140 # Contract points validated by cargo test.\n\
141 \n",
142 app = c.app_name,
143 ));
144
145 out.push_str(&format!(
147 "# -- Number of replicas (ignored when KEDA is enabled)\n\
148 replicaCount: 1\n\
149 \n\
150 image:\n\
151 \x20 repository: {registry}/{app}\n\
152 \x20 # -- Defaults to Chart appVersion\n\
153 \x20 tag: \"\"\n\
154 \x20 pullPolicy: IfNotPresent\n\
155 \n\
156 imagePullSecrets: []\n\
157 nameOverride: \"\"\n\
158 fullnameOverride: \"\"\n\
159 \n",
160 registry = c.image_registry,
161 app = c.app_name,
162 ));
163
164 out.push_str(
166 "serviceAccount:\n\
167 \x20 create: true\n\
168 \x20 annotations: {}\n\
169 \x20 # -- If not set, name is generated from fullname\n\
170 \x20 name: \"\"\n\
171 \n",
172 );
173
174 out.push_str(&format!(
176 "# -- Pod annotations (Prometheus scrape config included by default)\n\
177 podAnnotations:\n\
178 \x20 prometheus.io/scrape: \"true\"\n\
179 \x20 prometheus.io/port: \"{port}\"\n\
180 \x20 prometheus.io/path: \"{metrics_path}\"\n\
181 \n\
182 podLabels: {{}}\n\
183 \n",
184 port = c.metrics_port,
185 metrics_path = c.health.metrics_path,
186 ));
187
188 out.push_str(
190 "resources:\n\
191 \x20 requests:\n\
192 \x20 cpu: 250m\n\
193 \x20 memory: 256Mi\n\
194 \x20 limits:\n\
195 \x20 cpu: \"2\"\n\
196 \x20 memory: 1Gi\n\
197 \n",
198 );
199
200 out.push_str(&format!(
202 "# -- Metrics and health endpoint service\n\
203 service:\n\
204 \x20 type: ClusterIP\n\
205 \x20 port: {port}\n\
206 \n",
207 port = c.metrics_port,
208 ));
209
210 out.push_str(&format!(
212 "# -- Application configuration (mounted as {})\n",
213 c.config_mount_path
214 ));
215 if let Some(ref config) = c.default_config {
216 out.push_str("config:\n");
217 if let Ok(yaml) = serde_yaml_ng::to_string(config) {
219 for line in yaml.lines() {
220 if line == "---" {
221 continue;
222 }
223 out.push_str(&format!(" {line}\n"));
224 }
225 }
226 } else {
227 out.push_str("config: {}\n");
228 }
229 out.push('\n');
230
231 for group in &c.secrets {
233 out.push_str(&format!(
234 "# -- {} credentials\n\
235 {}:\n\
236 \x20 existingSecret: \"\"\n\
237 \x20 secretKeys:\n",
238 group.group_name, group.group_name,
239 ));
240 for env in &group.env_vars {
241 out.push_str(&format!(" {}: {}\n", env.key_name, env.secret_key));
242 }
243 for env in &group.env_vars {
244 out.push_str(&format!(" {}: \"\"\n", env.key_name));
245 }
246 out.push('\n');
247 }
248
249 if let Some(ref keda) = c.keda {
256 out.push_str(&format!(
257 "# -- KEDA autoscaling (requires KEDA operator installed)\n\
258 keda:\n\
259 \x20 enabled: true\n\
260 \x20 minReplicaCount: {min}\n\
261 \x20 maxReplicaCount: {max}\n\
262 \x20 pollingInterval: {poll}\n\
263 \x20 cooldownPeriod: {cool}\n\
264 \x20 kafka:\n\
265 \x20 # -- Scale when consumer group lag exceeds this per partition\n\
266 \x20 lagThreshold: \"{lag}\"\n\
267 \x20 # -- Wake from zero replicas when lag exceeds this\n\
268 \x20 activationLagThreshold: \"{activation}\"\n\
269 \x20 # -- Override topic (default: first topic from config)\n\
270 \x20 topic: \"\"\n\
271 \x20 # -- Override consumer group (default: from config)\n\
272 \x20 consumerGroup: \"\"\n\
273 \x20 cpu:\n\
274 \x20 enabled: {cpu_enabled}\n\
275 \x20 # -- CPU utilisation percentage threshold\n\
276 \x20 threshold: \"{cpu_threshold}\"\n\
277 \n",
278 min = keda.min_replicas,
279 max = keda.max_replicas,
280 poll = keda.polling_interval,
281 cool = keda.cooldown_period,
282 lag = keda.kafka_lag_threshold,
283 activation = keda.activation_lag_threshold,
284 cpu_enabled = keda.cpu_enabled,
285 cpu_threshold = keda.cpu_threshold,
286 ));
287 } else {
288 out.push_str(
289 "# -- KEDA autoscaling disabled by contract; HPA fallback below.\n\
290 keda:\n\
291 \x20 enabled: false\n\
292 \n",
293 );
294 }
295
296 out.push_str(
298 "# -- Standard HPA fallback (when KEDA is not installed)\n\
299 # Mutually exclusive with keda.enabled\n\
300 autoscaling:\n\
301 \x20 enabled: false\n\
302 \x20 minReplicas: 1\n\
303 \x20 maxReplicas: 10\n\
304 \x20 targetCPUUtilizationPercentage: 80\n\
305 \n\
306 nodeSelector: {}\n\
307 tolerations: []\n\
308 affinity: {}\n",
309 );
310
311 out
312}
313
314fn gen_helpers_tpl(c: &DeploymentContract) -> String {
315 let app = &c.app_name;
316 let mut out = String::with_capacity(2048);
317
318 out.push_str(&format!(
320 r#"{{{{/*
321Expand the name of the chart.
322*/}}}}
323{{{{- define "{app}.name" -}}}}
324{{{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}}}
325{{{{- end }}}}
326
327{{{{/*
328Create a default fully qualified app name.
329Truncated at 63 chars because some K8s name fields are limited.
330*/}}}}
331{{{{- define "{app}.fullname" -}}}}
332{{{{- if .Values.fullnameOverride }}}}
333{{{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}}}
334{{{{- else }}}}
335{{{{- $name := default .Chart.Name .Values.nameOverride }}}}
336{{{{- if contains $name .Release.Name }}}}
337{{{{- .Release.Name | trunc 63 | trimSuffix "-" }}}}
338{{{{- else }}}}
339{{{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}}}
340{{{{- end }}}}
341{{{{- end }}}}
342{{{{- end }}}}
343
344{{{{/*
345Create chart name and version as used by the chart label.
346*/}}}}
347{{{{- define "{app}.chart" -}}}}
348{{{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}}}
349{{{{- end }}}}
350
351{{{{/*
352Common labels.
353*/}}}}
354{{{{- define "{app}.labels" -}}}}
355helm.sh/chart: {{{{ include "{app}.chart" . }}}}
356{{{{ include "{app}.selectorLabels" . }}}}
357{{{{- if .Chart.AppVersion }}}}
358app.kubernetes.io/version: {{{{ .Chart.AppVersion | quote }}}}
359{{{{- end }}}}
360app.kubernetes.io/managed-by: {{{{ .Release.Service }}}}
361{{{{- end }}}}
362
363{{{{/*
364Selector labels.
365*/}}}}
366{{{{- define "{app}.selectorLabels" -}}}}
367app.kubernetes.io/name: {{{{ include "{app}.name" . }}}}
368app.kubernetes.io/instance: {{{{ .Release.Name }}}}
369{{{{- end }}}}
370
371{{{{/*
372Service account name.
373*/}}}}
374{{{{- define "{app}.serviceAccountName" -}}}}
375{{{{- if .Values.serviceAccount.create }}}}
376{{{{- default (include "{app}.fullname" .) .Values.serviceAccount.name }}}}
377{{{{- else }}}}
378{{{{- default "default" .Values.serviceAccount.name }}}}
379{{{{- end }}}}
380{{{{- end }}}}
381"#,
382 ));
383
384 for group in &c.secrets {
386 let helper_name = format!("{}SecretName", to_camel_suffix(&group.group_name));
387 out.push_str(&format!(
388 r#"
389{{{{/*
390{group} secret name -- use existing or generate from fullname.
391*/}}}}
392{{{{- define "{app}.{helper}" -}}}}
393{{{{- if .Values.{group}.existingSecret }}}}
394{{{{- .Values.{group}.existingSecret }}}}
395{{{{- else }}}}
396{{{{- printf "%s-{group}" (include "{app}.fullname" .) }}}}
397{{{{- end }}}}
398{{{{- end }}}}
399"#,
400 app = app,
401 group = group.group_name,
402 helper = helper_name,
403 ));
404 }
405
406 out
407}
408
409fn gen_deployment_yaml(c: &DeploymentContract) -> String {
410 let app = &c.app_name;
411 let mut out = String::with_capacity(4096);
412
413 out.push_str(&format!(
415 r#"apiVersion: apps/v1
416kind: Deployment
417metadata:
418 name: {{{{ include "{app}.fullname" . }}}}
419 labels:
420 {{{{- include "{app}.labels" . | nindent 4 }}}}
421spec:
422 {{{{- if not (or .Values.keda.enabled .Values.autoscaling.enabled) }}}}
423 replicas: {{{{ .Values.replicaCount }}}}
424 {{{{- end }}}}
425 selector:
426 matchLabels:
427 {{{{- include "{app}.selectorLabels" . | nindent 6 }}}}
428 template:
429 metadata:
430 annotations:
431 checksum/config: {{{{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}}}
432 {{{{- with .Values.podAnnotations }}}}
433 {{{{- toYaml . | nindent 8 }}}}
434 {{{{- end }}}}
435 labels:
436 {{{{- include "{app}.labels" . | nindent 8 }}}}
437 {{{{- with .Values.podLabels }}}}
438 {{{{- toYaml . | nindent 8 }}}}
439 {{{{- end }}}}
440 spec:
441 {{{{- with .Values.imagePullSecrets }}}}
442 imagePullSecrets:
443 {{{{- toYaml . | nindent 8 }}}}
444 {{{{- end }}}}
445 serviceAccountName: {{{{ include "{app}.serviceAccountName" . }}}}
446 containers:
447 - name: {{{{ .Chart.Name }}}}
448 image: "{{{{ .Values.image.repository }}}}:{{{{ .Values.image.tag | default .Chart.AppVersion }}}}"
449 imagePullPolicy: {{{{ .Values.image.pullPolicy }}}}
450"#,
451 ));
452
453 if !c.entrypoint_args.is_empty() {
455 out.push_str(" args:\n");
456 for arg in &c.entrypoint_args {
457 out.push_str(&format!(" - \"{arg}\"\n"));
458 }
459 }
460
461 out.push_str(
463 " ports:\n\
464 \x20 - name: metrics\n\
465 \x20 containerPort: {{ .Values.service.port }}\n\
466 \x20 protocol: TCP\n",
467 );
468 for port in &c.extra_ports {
469 out.push_str(&format!(
470 " - name: {name}\n\
471 \x20 containerPort: {port}\n\
472 \x20 protocol: {proto}\n",
473 name = port.name,
474 port = port.port,
475 proto = port.protocol,
476 ));
477 }
478
479 if !c.secrets.is_empty() {
481 out.push_str(" env:\n");
482 for group in &c.secrets {
483 let helper_name = format!("{}SecretName", to_camel_suffix(&group.group_name));
484 out.push_str(&format!(
485 " # {} credentials via Secret (figment env cascade overrides file config)\n",
486 group.group_name
487 ));
488 for env in &group.env_vars {
489 let key_lookup = safe_template_lookup(
491 &format!(".Values.{}.secretKeys", group.group_name),
492 &env.key_name,
493 );
494 out.push_str(&format!(
495 " - name: {env_var}\n\
496 \x20 valueFrom:\n\
497 \x20 secretKeyRef:\n\
498 \x20 name: {{{{ include \"{app}.{helper}\" . }}}}\n\
499 \x20 key: {{{{ {key_lookup} }}}}\n",
500 env_var = env.env_var,
501 app = app,
502 helper = helper_name,
503 ));
504 }
505 }
506 }
507
508 out.push_str(&format!(
510 " livenessProbe:\n\
511 \x20 httpGet:\n\
512 \x20 path: {liveness}\n\
513 \x20 port: metrics\n\
514 \x20 initialDelaySeconds: 10\n\
515 \x20 periodSeconds: 10\n\
516 \x20 failureThreshold: 3\n\
517 \x20 readinessProbe:\n\
518 \x20 httpGet:\n\
519 \x20 path: {readiness}\n\
520 \x20 port: metrics\n\
521 \x20 initialDelaySeconds: 5\n\
522 \x20 periodSeconds: 5\n\
523 \x20 failureThreshold: 2\n\
524 \x20 startupProbe:\n\
525 \x20 httpGet:\n\
526 \x20 path: {liveness}\n\
527 \x20 port: metrics\n\
528 \x20 failureThreshold: 30\n\
529 \x20 periodSeconds: 5\n",
530 liveness = c.health.liveness_path,
531 readiness = c.health.readiness_path,
532 ));
533
534 out.push_str(&format!(
536 " volumeMounts:\n\
537 \x20 - name: config\n\
538 \x20 mountPath: {config_dir}\n\
539 \x20 readOnly: true\n",
540 config_dir = c.config_dir(),
541 ));
542
543 out.push_str(
545 " {{- with .Values.resources }}\n\
546 \x20 resources:\n\
547 \x20 {{- toYaml . | nindent 12 }}\n\
548 \x20 {{- end }}\n",
549 );
550
551 out.push_str(&format!(
553 " volumes:\n\
554 \x20 - name: config\n\
555 \x20 configMap:\n\
556 \x20 name: {{{{ include \"{app}.fullname\" . }}}}-config\n",
557 ));
558
559 out.push_str(
561 " {{- with .Values.nodeSelector }}\n\
562 \x20 nodeSelector:\n\
563 \x20 {{- toYaml . | nindent 8 }}\n\
564 \x20 {{- end }}\n\
565 \x20 {{- with .Values.affinity }}\n\
566 \x20 affinity:\n\
567 \x20 {{- toYaml . | nindent 8 }}\n\
568 \x20 {{- end }}\n\
569 \x20 {{- with .Values.tolerations }}\n\
570 \x20 tolerations:\n\
571 \x20 {{- toYaml . | nindent 8 }}\n\
572 \x20 {{- end }}\n",
573 );
574
575 out
576}
577
578fn gen_service_yaml(c: &DeploymentContract) -> String {
579 let app = &c.app_name;
580 let mut out = format!(
581 r#"apiVersion: v1
582kind: Service
583metadata:
584 name: {{{{ include "{app}.fullname" . }}}}
585 labels:
586 {{{{- include "{app}.labels" . | nindent 4 }}}}
587spec:
588 type: {{{{ .Values.service.type }}}}
589 ports:
590 - port: {{{{ .Values.service.port }}}}
591 targetPort: metrics
592 protocol: TCP
593 name: metrics
594"#,
595 );
596
597 for port in &c.extra_ports {
599 out.push_str(&format!(
600 " - port: {port}\n\
601 \x20 targetPort: {port}\n\
602 \x20 protocol: {proto}\n\
603 \x20 name: {name}\n",
604 port = port.port,
605 proto = port.protocol,
606 name = port.name,
607 ));
608 }
609
610 out.push_str(&format!(
611 " selector:\n\
612 \x20 {{{{- include \"{app}.selectorLabels\" . | nindent 4 }}}}\n",
613 ));
614
615 out
616}
617
618fn gen_serviceaccount_yaml(c: &DeploymentContract) -> String {
619 let app = &c.app_name;
620 format!(
621 r#"{{{{- if .Values.serviceAccount.create -}}}}
622apiVersion: v1
623kind: ServiceAccount
624metadata:
625 name: {{{{ include "{app}.serviceAccountName" . }}}}
626 labels:
627 {{{{- include "{app}.labels" . | nindent 4 }}}}
628 {{{{- with .Values.serviceAccount.annotations }}}}
629 annotations:
630 {{{{- toYaml . | nindent 4 }}}}
631 {{{{- end }}}}
632automountServiceAccountToken: false
633{{{{- end }}}}
634"#,
635 )
636}
637
638fn gen_configmap_yaml(c: &DeploymentContract) -> String {
639 let app = &c.app_name;
640
641 let mut out = format!(
642 r#"apiVersion: v1
643kind: ConfigMap
644metadata:
645 name: {{{{ include "{app}.fullname" . }}}}-config
646 labels:
647 {{{{- include "{app}.labels" . | nindent 4 }}}}
648data:
649 {filename}: |
650 {{{{- toYaml .Values.config | nindent 4 }}}}
651"#,
652 app = app,
653 filename = c.config_filename(),
654 );
655
656 let _ = &mut out; out
658}
659
660fn gen_secret_yaml(c: &DeploymentContract) -> String {
661 let app = &c.app_name;
662 let mut out = String::new();
663 let mut first = true;
664
665 for group in &c.secrets {
666 if !first {
667 out.push_str("---\n");
668 }
669 first = false;
670
671 let helper_name = format!("{}SecretName", to_camel_suffix(&group.group_name));
672
673 out.push_str(&format!(
674 "{{{{- if not .Values.{group}.existingSecret }}}}\n\
675 apiVersion: v1\n\
676 kind: Secret\n\
677 metadata:\n\
678 \x20 name: {{{{ include \"{app}.{helper}\" . }}}}\n\
679 \x20 labels:\n\
680 \x20 {{{{- include \"{app}.labels\" . | nindent 4 }}}}\n\
681 type: Opaque\n\
682 data:\n",
683 group = group.group_name,
684 app = app,
685 helper = helper_name,
686 ));
687
688 for env in &group.env_vars {
689 let key_lookup = safe_template_lookup(
693 &format!(".Values.{}.secretKeys", group.group_name),
694 &env.key_name,
695 );
696 let val_lookup =
697 safe_template_lookup(&format!(".Values.{}", group.group_name), &env.key_name);
698 out.push_str(&format!(
699 " {{{{ {key_lookup} }}}}: {{{{ {val_lookup} | b64enc | quote }}}}\n"
700 ));
701 }
702
703 out.push_str("{{- end }}\n");
704 }
705
706 if c.secrets.is_empty() {
707 out.push_str("# No secrets defined in deployment contract\n");
708 }
709
710 out
711}
712
713fn gen_hpa_yaml(c: &DeploymentContract) -> String {
714 let app = &c.app_name;
715 format!(
716 r#"{{{{- if and .Values.autoscaling.enabled (not .Values.keda.enabled) }}}}
717# Standard HPA fallback -- use when KEDA operator is not installed.
718# Mutually exclusive with keda.enabled (KEDA creates its own HPA).
719apiVersion: autoscaling/v2
720kind: HorizontalPodAutoscaler
721metadata:
722 name: {{{{ include "{app}.fullname" . }}}}
723 labels:
724 {{{{- include "{app}.labels" . | nindent 4 }}}}
725spec:
726 scaleTargetRef:
727 apiVersion: apps/v1
728 kind: Deployment
729 name: {{{{ include "{app}.fullname" . }}}}
730 minReplicas: {{{{ .Values.autoscaling.minReplicas }}}}
731 maxReplicas: {{{{ .Values.autoscaling.maxReplicas }}}}
732 metrics:
733 - type: Resource
734 resource:
735 name: cpu
736 target:
737 type: Utilization
738 averageUtilization: {{{{ .Values.autoscaling.targetCPUUtilizationPercentage }}}}
739{{{{- end }}}}
740"#,
741 )
742}
743
744fn gen_keda_scaledobject_yaml(c: &DeploymentContract) -> String {
745 let app = &c.app_name;
746
747 let has_kafka_secret = c.secrets.iter().any(|g| g.group_name == "kafka");
749
750 let auth_ref = if has_kafka_secret {
751 format!(
752 " authenticationRef:\n\
753 \x20 name: {{{{ include \"{app}.fullname\" . }}}}-kafka-auth\n"
754 )
755 } else {
756 String::new()
757 };
758
759 format!(
760 r#"{{{{- if .Values.keda.enabled }}}}
761apiVersion: keda.sh/v1alpha1
762kind: ScaledObject
763metadata:
764 name: {{{{ include "{app}.fullname" . }}}}
765 labels:
766 {{{{- include "{app}.labels" . | nindent 4 }}}}
767spec:
768 scaleTargetRef:
769 name: {{{{ include "{app}.fullname" . }}}}
770 minReplicaCount: {{{{ .Values.keda.minReplicaCount }}}}
771 maxReplicaCount: {{{{ .Values.keda.maxReplicaCount }}}}
772 pollingInterval: {{{{ .Values.keda.pollingInterval }}}}
773 cooldownPeriod: {{{{ .Values.keda.cooldownPeriod }}}}
774 triggers:
775 # Kafka consumer group lag (primary scaler)
776 - type: kafka
777{auth_ref} metadata:
778 bootstrapServers: {{{{ .Values.config.kafka.brokers | quote }}}}
779 consumerGroup: {{{{ .Values.keda.kafka.consumerGroup | default .Values.config.kafka.group_id | quote }}}}
780 {{{{- /* `default (index X 0)` would eagerly evaluate `index nil 0` and fail
781 lint when no topics are set. Use explicit conditional instead. */}}}}
782 {{{{- if .Values.keda.kafka.topic }}}}
783 topic: {{{{ .Values.keda.kafka.topic | quote }}}}
784 {{{{- else if .Values.config.kafka.topics }}}}
785 topic: {{{{ (index .Values.config.kafka.topics 0) | quote }}}}
786 {{{{- else }}}}
787 topic: ""
788 {{{{- end }}}}
789 lagThreshold: {{{{ .Values.keda.kafka.lagThreshold | quote }}}}
790 activationLagThreshold: {{{{ .Values.keda.kafka.activationLagThreshold | quote }}}}
791 saslType: scram_sha512
792 tls: disable
793 {{{{- if .Values.keda.cpu.enabled }}}}
794 # CPU utilisation (secondary scaler)
795 - type: cpu
796 metricType: Utilization
797 metadata:
798 value: {{{{ .Values.keda.cpu.threshold | quote }}}}
799 {{{{- end }}}}
800{{{{- end }}}}
801"#,
802 )
803}
804
805fn gen_keda_triggerauth_yaml(c: &DeploymentContract) -> String {
806 let app = &c.app_name;
807
808 let kafka_group = c.secrets.iter().find(|g| g.group_name == "kafka");
810
811 if kafka_group.is_none() {
812 return "# No kafka secret group -- KEDA TriggerAuthentication not generated\n".to_string();
813 }
814
815 let helper_name = format!("{}SecretName", to_camel_suffix("kafka"));
816
817 format!(
818 r#"{{{{- if .Values.keda.enabled }}}}
819apiVersion: keda.sh/v1alpha1
820kind: TriggerAuthentication
821metadata:
822 name: {{{{ include "{app}.fullname" . }}}}-kafka-auth
823 labels:
824 {{{{- include "{app}.labels" . | nindent 4 }}}}
825spec:
826 secretTargetRef:
827 - parameter: sasl
828 name: {{{{ include "{app}.{helper_name}" . }}}}
829 key: {{{{ .Values.kafka.secretKeys.username }}}}
830 - parameter: password
831 name: {{{{ include "{app}.{helper_name}" . }}}}
832 key: {{{{ .Values.kafka.secretKeys.password }}}}
833{{{{- end }}}}
834"#,
835 )
836}
837
838fn gen_notes_txt(c: &DeploymentContract) -> String {
839 let app = &c.app_name;
840
841 format!(
842 r#"{app} has been deployed.
843
8441. Get the metrics/health endpoint:
845 kubectl port-forward svc/{{{{ include "{app}.fullname" . }}}} {{{{ .Values.service.port }}}}:{{{{ .Values.service.port }}}}
846 curl http://localhost:{{{{ .Values.service.port }}}}{liveness}
847 curl http://localhost:{{{{ .Values.service.port }}}}{metrics}
848
849{{{{- if .Values.keda.enabled }}}}
850
8512. Check KEDA autoscaling status:
852 kubectl get scaledobject {{{{ include "{app}.fullname" . }}}}
853 kubectl get hpa
854{{{{- end }}}}
855
8563. View logs:
857 kubectl logs -l app.kubernetes.io/name={{{{ include "{app}.name" . }}}} -f
858"#,
859 app = app,
860 liveness = c.health.liveness_path,
861 metrics = c.health.metrics_path,
862 )
863}