diff --git a/charts/openab/templates/_helpers.tpl b/charts/openab/templates/_helpers.tpl index 770d557..408d0db 100644 --- a/charts/openab/templates/_helpers.tpl +++ b/charts/openab/templates/_helpers.tpl @@ -51,9 +51,65 @@ app.kubernetes.io/component: {{ .agent }} {{- end }} {{- end }} -{{/* Resolve imagePullPolicy: global default (per-agent image string has no pullPolicy) */}} +{{/* Resolve imagePullPolicy: per-agent override or global default */}} {{- define "openab.agentImagePullPolicy" -}} -{{- .ctx.Values.image.pullPolicy }} +{{- default .ctx.Values.image.pullPolicy .cfg.imagePullPolicy }} +{{- end }} + +{{/* Resolve imagePullSecrets: per-agent override (if explicitly set, including empty list) or global default */}} +{{- define "openab.agentImagePullSecrets" -}} +{{- $pullSecrets := .ctx.Values.imagePullSecrets -}} +{{- if hasKey .cfg "imagePullSecrets" -}} +{{- $pullSecrets = .cfg.imagePullSecrets -}} +{{- end }} +{{- range $pullSecrets }} +{{- if kindIs "map" . }} +- name: {{ .name | quote }} +{{- else }} +- name: {{ . | quote }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Resolve serviceAccountName: +- If serviceAccount.create is true: use serviceAccount.name or fallback to +- Else: use serviceAccountName (for referencing externally-created SAs), or empty (namespace default) +*/}} +{{- define "openab.agentServiceAccountName" -}} +{{- if (.cfg.serviceAccount).create -}} +{{- default (include "openab.agentFullname" .) .cfg.serviceAccount.name -}} +{{- else -}} +{{- default "" .cfg.serviceAccountName -}} +{{- end -}} +{{- end }} + +{{/* +Pod annotations: global baseline + per-agent override, with reserved +chart-managed annotations (checksum/config) merged last so users cannot +clobber them and produce duplicate YAML keys. +*/}} +{{- define "openab.agentPodAnnotations" -}} +{{- $reserved := dict "checksum/config" (.cfg | toJson | sha256sum) -}} +{{- $annotations := mergeOverwrite (dict) + (.ctx.Values.podAnnotations | default (dict)) + (.cfg.podAnnotations | default (dict)) + $reserved -}} +{{- toYaml $annotations }} +{{- end }} + +{{/* +Pod labels: global baseline + per-agent override, with reserved selector +labels merged last so users cannot hijack them. Hijacking would produce +duplicate YAML keys AND break Deployment→Pod selector matching. +*/}} +{{- define "openab.agentPodLabels" -}} +{{- $reserved := include "openab.selectorLabels" . | fromYaml -}} +{{- $labels := mergeOverwrite (dict) + (.ctx.Values.podLabels | default (dict)) + (.cfg.podLabels | default (dict)) + $reserved -}} +{{- toYaml $labels }} {{- end }} {{/* Agent enabled: default true unless explicitly set to false */}} @@ -65,3 +121,8 @@ app.kubernetes.io/component: {{ .agent }} {{- define "openab.persistenceEnabled" -}} {{- if and . .persistence (eq (.persistence.enabled | toString) "false") }}false{{ else }}true{{ end }} {{- end }} + +{{/* Discord adapter enabled: default true unless explicitly set to false; returns false when discord config is absent */}} +{{- define "openab.discordEnabled" -}} +{{- if and . .discord (ne (.discord.enabled | toString) "false") }}true{{ else }}false{{ end }} +{{- end }} diff --git a/charts/openab/templates/clusterrole.yaml b/charts/openab/templates/clusterrole.yaml new file mode 100644 index 0000000..c895a6e --- /dev/null +++ b/charts/openab/templates/clusterrole.yaml @@ -0,0 +1,20 @@ +{{- range $name, $cfg := .Values.agents }} +{{- if ne (include "openab.agentEnabled" $cfg) "false" }} +{{- if ($cfg.rbac).createClusterRole }} +{{- $d := dict "ctx" $ "agent" $name "cfg" $cfg }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "openab.agentFullname" $d }} + labels: + {{- include "openab.labels" $d | nindent 4 }} +{{- with $cfg.rbac.clusterRules }} +rules: + {{- toYaml . | nindent 2 }} +{{- else }} +rules: [] +{{- end }} +{{- end }} +{{- end }} +{{- end }} diff --git a/charts/openab/templates/clusterrolebinding.yaml b/charts/openab/templates/clusterrolebinding.yaml new file mode 100644 index 0000000..d52decb --- /dev/null +++ b/charts/openab/templates/clusterrolebinding.yaml @@ -0,0 +1,22 @@ +{{- range $name, $cfg := .Values.agents }} +{{- if ne (include "openab.agentEnabled" $cfg) "false" }} +{{- if ($cfg.rbac).createClusterRole }} +{{- $d := dict "ctx" $ "agent" $name "cfg" $cfg }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "openab.agentFullname" $d }} + labels: + {{- include "openab.labels" $d | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "openab.agentFullname" $d }} +subjects: + - kind: ServiceAccount + name: {{ include "openab.agentServiceAccountName" $d }} + namespace: {{ $.Release.Namespace }} +{{- end }} +{{- end }} +{{- end }} diff --git a/charts/openab/templates/configmap.yaml b/charts/openab/templates/configmap.yaml index 84dcb1d..7900042 100644 --- a/charts/openab/templates/configmap.yaml +++ b/charts/openab/templates/configmap.yaml @@ -10,7 +10,7 @@ metadata: {{- include "openab.labels" $d | nindent 4 }} data: config.toml: | - {{- if ($cfg.discord).enabled }} + {{- if ne (include "openab.discordEnabled" $cfg) "false" }} [discord] bot_token = "${DISCORD_BOT_TOKEN}" {{- range $cfg.discord.allowedChannels }} @@ -81,8 +81,16 @@ data: command = "{{ $cfg.command }}" args = {{ if $cfg.args }}{{ $cfg.args | toJson }}{{ else }}[]{{ end }} working_dir = "{{ $cfg.workingDir | default "/home/agent" }}" - {{- if $cfg.env }} - env = { {{ $first := true }}{{ range $k, $v := $cfg.env }}{{ if not $first }}, {{ end }}{{ $k }} = "{{ $v }}"{{ $first = false }}{{ end }} } + {{- $stringEnv := dict }} + {{- range $k, $v := $cfg.env }} + {{- if kindIs "slice" $v }} + {{- fail (printf "env.%s is a list — env values must be strings or maps (valueFrom)" $k) }} + {{- else if not (kindIs "map" $v) }} + {{- $_ := set $stringEnv $k $v }} + {{- end }} + {{- end }} + {{- if $stringEnv }} + env = { {{ $first := true }}{{ range $k, $v := $stringEnv }}{{ if not $first }}, {{ end }}{{ $k }} = {{ $v | toJson }}{{ $first = false }}{{ end }} } {{- end }} [pool] diff --git a/charts/openab/templates/deployment.yaml b/charts/openab/templates/deployment.yaml index 2352ba5..c1bf7da 100644 --- a/charts/openab/templates/deployment.yaml +++ b/charts/openab/templates/deployment.yaml @@ -21,14 +21,27 @@ spec: template: metadata: annotations: - checksum/config: {{ $cfg | toJson | sha256sum }} + {{- include "openab.agentPodAnnotations" $d | nindent 8 }} labels: - {{- include "openab.selectorLabels" $d | nindent 8 }} + {{- include "openab.agentPodLabels" $d | nindent 8 }} spec: + {{- $imagePullSecrets := include "openab.agentImagePullSecrets" $d | trim }} + {{- if $imagePullSecrets }} + imagePullSecrets: + {{- $imagePullSecrets | nindent 8 }} + {{- end }} + {{- $serviceAccountName := include "openab.agentServiceAccountName" $d | trim }} + {{- if $serviceAccountName }} + serviceAccountName: {{ $serviceAccountName }} + {{- end }} {{- with $.Values.podSecurityContext }} securityContext: {{- toYaml . | nindent 8 }} {{- end }} + {{- with $cfg.initContainers }} + initContainers: + {{- toYaml . | nindent 8 }} + {{- end }} containers: - name: openab image: {{ include "openab.agentImage" $d | quote }} @@ -38,7 +51,7 @@ spec: {{- toYaml . | nindent 12 }} {{- end }} env: - {{- if and (ne (toString ($cfg.discord).enabled) "false") ($cfg.discord).botToken }} + {{- if and (ne (include "openab.discordEnabled" $cfg) "false") ($cfg.discord).botToken }} - name: DISCORD_BOT_TOKEN valueFrom: secretKeyRef: @@ -69,8 +82,15 @@ spec: - name: HOME value: {{ $cfg.workingDir | default "/home/agent" }} {{- range $k, $v := $cfg.env }} + {{- if kindIs "slice" $v }} + {{- fail (printf "env.%s is a list — env values must be strings or maps (valueFrom)" $k) }} + {{- end }} - name: {{ $k }} + {{- if kindIs "map" $v }} + {{- toYaml $v | nindent 14 }} + {{- else }} value: {{ $v | quote }} + {{- end }} {{- end }} {{- with $cfg.envFrom }} envFrom: @@ -80,6 +100,22 @@ spec: resources: {{- toYaml . | nindent 12 }} {{- end }} + {{- with $cfg.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with $cfg.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with $cfg.startupProbe }} + startupProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with $cfg.lifecycle }} + lifecycle: + {{- toYaml . | nindent 12 }} + {{- end }} volumeMounts: - name: config mountPath: /etc/openab @@ -99,6 +135,12 @@ spec: mountPath: {{ $cfg.workingDir | default "/home/agent" }}/GEMINI.md subPath: AGENTS.md {{- end }} + {{- with $cfg.extraVolumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with $cfg.extraContainers }} + {{- toYaml . | nindent 8 }} + {{- end }} {{- with $cfg.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} @@ -120,5 +162,8 @@ spec: persistentVolumeClaim: claimName: {{ include "openab.agentFullname" $d }} {{- end }} + {{- with $cfg.extraVolumes }} + {{- toYaml . | nindent 8 }} + {{- end }} {{- end }} {{- end }} diff --git a/charts/openab/templates/pdb.yaml b/charts/openab/templates/pdb.yaml new file mode 100644 index 0000000..f44f825 --- /dev/null +++ b/charts/openab/templates/pdb.yaml @@ -0,0 +1,29 @@ +{{- range $name, $cfg := .Values.agents }} +{{- if ne (include "openab.agentEnabled" $cfg) "false" }} +{{- if ($cfg.podDisruptionBudget).enabled }} +{{- $d := dict "ctx" $ "agent" $name "cfg" $cfg }} +{{- $pdb := $cfg.podDisruptionBudget }} +{{- if and (hasKey $pdb "minAvailable") (hasKey $pdb "maxUnavailable") (ne $pdb.minAvailable nil) (ne $pdb.maxUnavailable nil) }} +{{- fail (printf "agents.%s.podDisruptionBudget: cannot set both minAvailable and maxUnavailable" $name) }} +{{- end }} +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "openab.agentFullname" $d }} + labels: + {{- include "openab.labels" $d | nindent 4 }} +spec: + {{- if ne ($pdb.minAvailable | toString) "" }} + minAvailable: {{ $pdb.minAvailable }} + {{- else if ne ($pdb.maxUnavailable | toString) "" }} + maxUnavailable: {{ $pdb.maxUnavailable }} + {{- else }} + {{- fail (printf "agents.%s.podDisruptionBudget: must set either minAvailable or maxUnavailable" $name) }} + {{- end }} + selector: + matchLabels: + {{- include "openab.selectorLabels" $d | nindent 6 }} +{{- end }} +{{- end }} +{{- end }} diff --git a/charts/openab/templates/role.yaml b/charts/openab/templates/role.yaml new file mode 100644 index 0000000..8f4441c --- /dev/null +++ b/charts/openab/templates/role.yaml @@ -0,0 +1,20 @@ +{{- range $name, $cfg := .Values.agents }} +{{- if ne (include "openab.agentEnabled" $cfg) "false" }} +{{- if ($cfg.rbac).create }} +{{- $d := dict "ctx" $ "agent" $name "cfg" $cfg }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "openab.agentFullname" $d }} + labels: + {{- include "openab.labels" $d | nindent 4 }} +{{- with $cfg.rbac.rules }} +rules: + {{- toYaml . | nindent 2 }} +{{- else }} +rules: [] +{{- end }} +{{- end }} +{{- end }} +{{- end }} diff --git a/charts/openab/templates/rolebinding.yaml b/charts/openab/templates/rolebinding.yaml new file mode 100644 index 0000000..b6a4780 --- /dev/null +++ b/charts/openab/templates/rolebinding.yaml @@ -0,0 +1,22 @@ +{{- range $name, $cfg := .Values.agents }} +{{- if ne (include "openab.agentEnabled" $cfg) "false" }} +{{- if ($cfg.rbac).create }} +{{- $d := dict "ctx" $ "agent" $name "cfg" $cfg }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "openab.agentFullname" $d }} + labels: + {{- include "openab.labels" $d | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "openab.agentFullname" $d }} +subjects: + - kind: ServiceAccount + name: {{ include "openab.agentServiceAccountName" $d }} + namespace: {{ $.Release.Namespace }} +{{- end }} +{{- end }} +{{- end }} diff --git a/charts/openab/templates/secret.yaml b/charts/openab/templates/secret.yaml index 7af14ae..1f18472 100644 --- a/charts/openab/templates/secret.yaml +++ b/charts/openab/templates/secret.yaml @@ -1,6 +1,6 @@ {{- range $name, $cfg := .Values.agents }} {{- if ne (include "openab.agentEnabled" $cfg) "false" }} -{{- $hasDiscord := and (ne (toString ($cfg.discord).enabled) "false") ($cfg.discord).botToken }} +{{- $hasDiscord := and (ne (include "openab.discordEnabled" $cfg) "false") ($cfg.discord).botToken }} {{- $hasSlack := and ($cfg.slack).enabled (or ($cfg.slack).botToken ($cfg.slack).appToken) }} {{- $hasStt := and ($cfg.stt).enabled ($cfg.stt).apiKey }} {{- if or $hasDiscord $hasSlack $hasStt }} diff --git a/charts/openab/templates/serviceaccount.yaml b/charts/openab/templates/serviceaccount.yaml new file mode 100644 index 0000000..329c8b2 --- /dev/null +++ b/charts/openab/templates/serviceaccount.yaml @@ -0,0 +1,23 @@ +{{- range $name, $cfg := .Values.agents }} +{{- if ne (include "openab.agentEnabled" $cfg) "false" }} +{{- if ($cfg.serviceAccount).create }} +{{- $d := dict "ctx" $ "agent" $name "cfg" $cfg }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "openab.agentServiceAccountName" $d }} + labels: + {{- include "openab.labels" $d | nindent 4 }} + {{- with $cfg.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- if hasKey $cfg.serviceAccount "automountServiceAccountToken" }} +automountServiceAccountToken: {{ $cfg.serviceAccount.automountServiceAccountToken }} +{{- else }} +automountServiceAccountToken: true +{{- end }} +{{- end }} +{{- end }} +{{- end }} diff --git a/charts/openab/tests/deployment_test.yaml b/charts/openab/tests/deployment_test.yaml new file mode 100644 index 0000000..c07c8b4 --- /dev/null +++ b/charts/openab/tests/deployment_test.yaml @@ -0,0 +1,219 @@ +suite: pod extensibility controls +templates: + - templates/deployment.yaml +tests: + # -- imagePullSecrets -- + + - it: renders global imagePullSecrets (string shorthand) + set: + imagePullSecrets: + - regcred + asserts: + - contains: + path: spec.template.spec.imagePullSecrets + content: + name: "regcred" + + - it: renders global imagePullSecrets (K8s native object format) + set: + imagePullSecrets: + - name: regcred + asserts: + - contains: + path: spec.template.spec.imagePullSecrets + content: + name: "regcred" + + - it: per-agent imagePullSecrets=[] opts out of global + set: + imagePullSecrets: + - global-secret + agents.kiro.imagePullSecrets: [] + asserts: + - isNull: + path: spec.template.spec.imagePullSecrets + + # -- imagePullPolicy -- + + - it: per-agent imagePullPolicy overrides global default + set: + image.pullPolicy: IfNotPresent + agents.kiro.imagePullPolicy: Always + asserts: + - equal: + path: spec.template.spec.containers[0].imagePullPolicy + value: Always + + # -- initContainers -- + + - it: renders initContainers + set: + agents.kiro.initContainers: + - name: setup + image: busybox:1.36 + command: ["sh", "-c", "echo setup"] + asserts: + - equal: + path: spec.template.spec.initContainers[0].name + value: setup + - equal: + path: spec.template.spec.initContainers[0].image + value: busybox:1.36 + + # -- extraContainers -- + + - it: renders extraContainers into pod spec + set: + agents.kiro.extraContainers: + - name: logtail + image: busybox:1.36 + asserts: + - equal: + path: spec.template.spec.containers[1].name + value: logtail + - equal: + path: spec.template.spec.containers[1].image + value: busybox:1.36 + + # -- extraVolumes / extraVolumeMounts -- + + - it: renders extra volumes and mounts + set: + agents.kiro.extraVolumes: + - name: scratch + emptyDir: {} + agents.kiro.extraVolumeMounts: + - name: scratch + mountPath: /scratch + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: scratch + emptyDir: {} + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: scratch + mountPath: /scratch + + # -- probes, lifecycle, serviceAccountName -- + + - it: renders serviceAccountName + set: + agents.kiro.serviceAccountName: openab-agent + asserts: + - equal: + path: spec.template.spec.serviceAccountName + value: openab-agent + + - it: renders livenessProbe + set: + agents.kiro.livenessProbe: + httpGet: + path: /healthz + port: 8080 + asserts: + - equal: + path: spec.template.spec.containers[0].livenessProbe.httpGet.path + value: /healthz + + - it: renders readinessProbe + set: + agents.kiro.readinessProbe: + httpGet: + path: /readyz + port: 8080 + asserts: + - equal: + path: spec.template.spec.containers[0].readinessProbe.httpGet.path + value: /readyz + + - it: renders startupProbe + set: + agents.kiro.startupProbe: + exec: + command: ["pgrep", "openab"] + asserts: + - equal: + path: spec.template.spec.containers[0].startupProbe.exec.command + value: ["pgrep", "openab"] + + - it: renders lifecycle hooks + set: + agents.kiro.lifecycle: + preStop: + exec: + command: ["sh", "-c", "sleep 1"] + asserts: + - equal: + path: spec.template.spec.containers[0].lifecycle.preStop.exec.command + value: ["sh", "-c", "sleep 1"] + + # -- podAnnotations / podLabels -- + + - it: renders global and per-agent pod annotations + set: + podAnnotations: + team: platform + agents.kiro.podAnnotations: + trace: enabled + asserts: + - equal: + path: spec.template.metadata.annotations.team + value: platform + - equal: + path: spec.template.metadata.annotations.trace + value: enabled + + - it: renders global and per-agent pod labels + set: + podLabels: + tier: agents + agents.kiro.podLabels: + agent: kiro + asserts: + - equal: + path: spec.template.metadata.labels.tier + value: agents + - equal: + path: spec.template.metadata.labels.agent + value: kiro + + # -- reserved key protection -- + + - it: podLabels cannot hijack reserved selector labels + set: + agents.kiro.podLabels: + app.kubernetes.io/name: hacked + app.kubernetes.io/component: evil + asserts: + - equal: + path: spec.template.metadata.labels["app.kubernetes.io/name"] + value: openab + - equal: + path: spec.template.metadata.labels["app.kubernetes.io/component"] + value: kiro + + - it: podAnnotations cannot hijack checksum/config + set: + agents.kiro.podAnnotations: + checksum/config: pwned + asserts: + - notEqual: + path: spec.template.metadata.annotations["checksum/config"] + value: pwned + - matchRegex: + path: spec.template.metadata.annotations["checksum/config"] + pattern: "^[a-f0-9]{64}$" + + - it: per-agent podLabels override global for same non-reserved key + set: + podLabels: + tier: GLOBAL + agents.kiro.podLabels: + tier: PERAGENT + asserts: + - equal: + path: spec.template.metadata.labels.tier + value: PERAGENT diff --git a/charts/openab/tests/env_test.yaml b/charts/openab/tests/env_test.yaml new file mode 100644 index 0000000..934a85e --- /dev/null +++ b/charts/openab/tests/env_test.yaml @@ -0,0 +1,83 @@ +suite: env polymorphic rendering +templates: + - templates/deployment.yaml +tests: + - it: renders simple string env as value + set: + agents.kiro.env: + MY_VAR: hello + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: MY_VAR + value: "hello" + + - it: renders valueFrom env (fieldRef) + set: + agents.kiro.env: + MY_POD_NAME: + valueFrom: + fieldRef: + fieldPath: metadata.name + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: MY_POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + + - it: renders mixed string and valueFrom env + set: + agents.kiro.env: + SIMPLE_VAR: simple + POD_NAME: + valueFrom: + fieldRef: + fieldPath: metadata.name + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: SIMPLE_VAR + value: "simple" + - contains: + path: spec.template.spec.containers[0].env + content: + name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + + - it: rejects list-typed env values + set: + agents.kiro.env: + BAD: + - item1 + - item2 + asserts: + - failedTemplate: + errorPattern: "env.BAD is a list" + +--- +suite: env configmap filtering +templates: + - templates/configmap.yaml +tests: + - it: filters map-typed env from config.toml + set: + agents.kiro.env: + KEEP_THIS: hello + SKIP_THIS: + valueFrom: + fieldRef: + fieldPath: metadata.name + asserts: + - matchRegex: + path: data["config.toml"] + pattern: 'KEEP_THIS = "hello"' + - notMatchRegex: + path: data["config.toml"] + pattern: "SKIP_THIS" diff --git a/charts/openab/tests/pdb_test.yaml b/charts/openab/tests/pdb_test.yaml new file mode 100644 index 0000000..07bc2e2 --- /dev/null +++ b/charts/openab/tests/pdb_test.yaml @@ -0,0 +1,56 @@ +suite: PodDisruptionBudget +templates: + - templates/pdb.yaml +tests: + - it: does not create PDB by default + asserts: + - hasDocuments: + count: 0 + + - it: creates PDB with minAvailable when enabled + set: + agents.kiro.podDisruptionBudget.enabled: true + agents.kiro.podDisruptionBudget.minAvailable: 1 + asserts: + - isKind: + of: PodDisruptionBudget + - equal: + path: spec.minAvailable + value: 1 + - equal: + path: spec.selector.matchLabels["app.kubernetes.io/component"] + value: kiro + + - it: creates PDB with maxUnavailable + set: + agents.kiro.podDisruptionBudget.enabled: true + agents.kiro.podDisruptionBudget.maxUnavailable: 1 + asserts: + - equal: + path: spec.maxUnavailable + value: 1 + + - it: supports percentage values + set: + agents.kiro.podDisruptionBudget.enabled: true + agents.kiro.podDisruptionBudget.minAvailable: 50% + asserts: + - equal: + path: spec.minAvailable + value: 50% + + - it: fails when both minAvailable and maxUnavailable are set + set: + agents.kiro.podDisruptionBudget.enabled: true + agents.kiro.podDisruptionBudget.minAvailable: 1 + agents.kiro.podDisruptionBudget.maxUnavailable: 1 + asserts: + - failedTemplate: + errorPattern: "cannot set both minAvailable and maxUnavailable" + + - it: fails when neither minAvailable nor maxUnavailable are set + set: + agents.kiro.podDisruptionBudget.enabled: true + asserts: + - failedTemplate: + errorPattern: "must set either minAvailable or maxUnavailable" diff --git a/charts/openab/tests/rbac_test.yaml b/charts/openab/tests/rbac_test.yaml new file mode 100644 index 0000000..9de0817 --- /dev/null +++ b/charts/openab/tests/rbac_test.yaml @@ -0,0 +1,140 @@ +suite: RBAC (Role + ClusterRole) +release: + name: test +templates: + - templates/role.yaml + - templates/rolebinding.yaml + - templates/clusterrole.yaml + - templates/clusterrolebinding.yaml +tests: + - it: creates no RBAC resources by default + asserts: + - hasDocuments: + count: 0 + + - it: creates Role when rbac.create=true + template: templates/role.yaml + set: + agents.kiro.rbac.create: true + asserts: + - hasDocuments: + count: 1 + - isKind: + of: Role + + - it: creates RoleBinding when rbac.create=true + template: templates/rolebinding.yaml + set: + agents.kiro.rbac.create: true + asserts: + - hasDocuments: + count: 1 + - isKind: + of: RoleBinding + + - it: Role renders rules correctly + template: templates/role.yaml + set: + agents.kiro.rbac.create: true + agents.kiro.rbac.rules: + - apiGroups: [""] + resources: ["configmaps"] + verbs: ["get", "list"] + asserts: + - isKind: + of: Role + - equal: + path: metadata.name + value: test-openab-kiro + - equal: + path: rules[0].apiGroups + value: [""] + - equal: + path: rules[0].resources + value: ["configmaps"] + - equal: + path: rules[0].verbs + value: ["get", "list"] + + - it: Role with empty rules still renders + template: templates/role.yaml + set: + agents.kiro.rbac.create: true + asserts: + - isKind: + of: Role + - equal: + path: rules + value: [] + + - it: RoleBinding references the Role and ServiceAccount + template: templates/rolebinding.yaml + set: + agents.kiro.serviceAccount.create: true + agents.kiro.rbac.create: true + asserts: + - isKind: + of: RoleBinding + - equal: + path: roleRef.kind + value: Role + - equal: + path: roleRef.name + value: test-openab-kiro + - equal: + path: subjects[0].kind + value: ServiceAccount + - equal: + path: subjects[0].name + value: test-openab-kiro + + - it: creates ClusterRole when rbac.createClusterRole=true + template: templates/clusterrole.yaml + set: + agents.kiro.rbac.createClusterRole: true + asserts: + - hasDocuments: + count: 1 + - isKind: + of: ClusterRole + + - it: creates ClusterRoleBinding when rbac.createClusterRole=true + template: templates/clusterrolebinding.yaml + set: + agents.kiro.rbac.createClusterRole: true + asserts: + - hasDocuments: + count: 1 + - isKind: + of: ClusterRoleBinding + + - it: ClusterRole renders clusterRules correctly + template: templates/clusterrole.yaml + set: + agents.kiro.rbac.createClusterRole: true + agents.kiro.rbac.clusterRules: + - apiGroups: [""] + resources: ["nodes"] + verbs: ["get"] + asserts: + - isKind: + of: ClusterRole + - equal: + path: rules[0].resources + value: ["nodes"] + + - it: rbac.create=false does not create Role + template: templates/role.yaml + set: + agents.kiro.rbac.createClusterRole: true + asserts: + - hasDocuments: + count: 0 + + - it: rbac.createClusterRole=false does not create ClusterRole + template: templates/clusterrole.yaml + set: + agents.kiro.rbac.create: true + asserts: + - hasDocuments: + count: 0 diff --git a/charts/openab/tests/serviceaccount_test.yaml b/charts/openab/tests/serviceaccount_test.yaml new file mode 100644 index 0000000..5d39110 --- /dev/null +++ b/charts/openab/tests/serviceaccount_test.yaml @@ -0,0 +1,87 @@ +suite: ServiceAccount creation +release: + name: test +templates: + - templates/serviceaccount.yaml + - templates/deployment.yaml +tests: + - it: does not create SA by default + template: templates/serviceaccount.yaml + asserts: + - hasDocuments: + count: 0 + + - it: creates SA when serviceAccount.create=true + template: templates/serviceaccount.yaml + set: + agents.kiro.serviceAccount.create: true + asserts: + - hasDocuments: + count: 1 + - isKind: + of: ServiceAccount + - equal: + path: metadata.name + value: test-openab-kiro + - equal: + path: automountServiceAccountToken + value: true + + - it: uses custom SA name when set + template: templates/serviceaccount.yaml + set: + agents.kiro.serviceAccount.create: true + agents.kiro.serviceAccount.name: my-custom-sa + asserts: + - equal: + path: metadata.name + value: my-custom-sa + + - it: renders IRSA annotations + template: templates/serviceaccount.yaml + set: + agents.kiro.serviceAccount.create: true + agents.kiro.serviceAccount.annotations: + eks.amazonaws.com/role-arn: arn:aws:iam::123:role/my-role + asserts: + - equal: + path: metadata.annotations["eks.amazonaws.com/role-arn"] + value: arn:aws:iam::123:role/my-role + + - it: supports disabling automountServiceAccountToken + template: templates/serviceaccount.yaml + set: + agents.kiro.serviceAccount.create: true + agents.kiro.serviceAccount.automountServiceAccountToken: false + asserts: + - equal: + path: automountServiceAccountToken + value: false + + - it: deployment uses created SA name + template: templates/deployment.yaml + set: + agents.kiro.serviceAccount.create: true + asserts: + - equal: + path: spec.template.spec.serviceAccountName + value: test-openab-kiro + + - it: deployment still supports external serviceAccountName for backward compat + template: templates/deployment.yaml + set: + agents.kiro.serviceAccountName: external-sa + asserts: + - equal: + path: spec.template.spec.serviceAccountName + value: external-sa + + - it: serviceAccount.create=true overrides external serviceAccountName + template: templates/deployment.yaml + set: + agents.kiro.serviceAccountName: external-sa + agents.kiro.serviceAccount.create: true + asserts: + - equal: + path: spec.template.spec.serviceAccountName + value: test-openab-kiro diff --git a/charts/openab/values.yaml b/charts/openab/values.yaml index 1608b78..22de070 100644 --- a/charts/openab/values.yaml +++ b/charts/openab/values.yaml @@ -4,6 +4,14 @@ image: tag: "" pullPolicy: IfNotPresent +# Both formats supported: +# - name: regcred # K8s native format +# - my-secret # shorthand string format +imagePullSecrets: [] + +podAnnotations: {} +podLabels: {} + podSecurityContext: runAsNonRoot: true runAsUser: 1000 @@ -35,7 +43,14 @@ agents: # # trustedBotIds: [] # empty = any bot (mode permitting) # trustedBotIds: [] # workingDir: /home/agent - # env: {} + # env: + # # simple key-value + # MY_VAR: "hello" + # # valueFrom (fieldRef, secretKeyRef, configMapKeyRef, etc.) + # MY_POD_NAME: + # valueFrom: + # fieldRef: + # fieldPath: metadata.name # envFrom: [] # pool: # maxSessions: 10 @@ -110,6 +125,9 @@ agents: # size: 1Gi # image: "ghcr.io/openabdev/openab-cursor:latest" image: "" + imagePullPolicy: "" + # imagePullSecrets: unset inherits global; set to [] to opt out. + # imagePullSecrets: [] command: kiro-cli args: - acp @@ -164,6 +182,40 @@ agents: # The PVC file is NOT deleted but becomes invisible to the agent. Remove agentsMd to restore. agentsMd: "" resources: {} + # -- Pod extensibility + serviceAccountName: "" + podAnnotations: {} + podLabels: {} + livenessProbe: {} # {} = disabled + readinessProbe: {} # {} = disabled + startupProbe: {} # {} = disabled + lifecycle: {} # {} = disabled + initContainers: [] + extraContainers: [] + extraVolumes: [] + extraVolumeMounts: [] + # -- ServiceAccount, RBAC, PodDisruptionBudget + # serviceAccountName (above) still supported for referencing externally-created SAs. + # When serviceAccount.create is true, the chart creates a new SA named + # (or serviceAccount.name if set), overriding serviceAccountName. + serviceAccount: + create: false + name: "" # empty = use + automountServiceAccountToken: true # set false if agent does not need K8s API access + annotations: {} # e.g. eks.amazonaws.com/role-arn for IRSA + rbac: + # namespaced Role + RoleBinding + create: false + rules: [] + # cluster-scope ClusterRole + ClusterRoleBinding + createClusterRole: false + clusterRules: [] + # ⚠️ PDB with minAvailable: 1 combined with the hardcoded replicas: 1 will prevent + # node drains. Use maxUnavailable: 1 instead if you need drain support. + podDisruptionBudget: + enabled: false + minAvailable: null + maxUnavailable: null nodeSelector: {} tolerations: [] affinity: {}