Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions .github/workflows/validate.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,28 @@ jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- uses: actions/checkout@v7
with:
repository: justgithubaccount/app-poly-gitops-k8s
ref: main
path: downstream-k8s

- uses: azure/setup-helm@v4
with:
version: v3.19.0

- name: Install validation tools
run: |
python3 -m pip install --disable-pip-version-check PyYAML==6.0.3
curl --fail --location --silent --show-error \
https://github.com/yannh/kubeconform/releases/download/v0.7.0/kubeconform-linux-amd64.tar.gz \
| tar -xz kubeconform
sudo mv kubeconform /usr/local/bin/kubeconform

- name: Chart contract
run: bash tests/test_chart.sh

- name: Render standalone chart
run: helm template chat-api . --namespace smoke >/dev/null
- name: Current GitOps consumer layering
run: bash tests/test_downstream.sh downstream-k8s
2 changes: 1 addition & 1 deletion Chart.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
apiVersion: v2
name: chat-api
description: ai-app (mind)
description: Helm chart for the FastAPI Chat API service
type: application
version: 0.4.1
appVersion: "1.1.2"
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ Real model requests require a secret selected with
`postgresSecretRefName` or explicit `extraEnv` entries.
Use an external database when data must survive pod replacement.

`serviceAccount.create=true` creates and selects one ServiceAccount. Set
`serviceAccount.name` to override its name. With `serviceAccount.create=false`,
`serviceAccount.name` is required and must identify an existing account.

`createInternalSecret=true` creates a chart-managed Secret for the non-sensitive
provider URL, model and optional attribution headers, and attaches it to the
Deployment. It does not store a provider key. `postgres.externalService.enabled`
only creates the stable `postgres-external` DNS name for an external host; it
cannot construct credentials or a database name, so `DATABASE_URL` must still be
provided through `postgresSecretRefName` or `extraEnv`.

## Getting started

Requirements: Helm 3 and access to a Kubernetes cluster for installation.
Expand Down Expand Up @@ -65,6 +76,10 @@ helm upgrade --install chat-api . \
| `openrouter.defaultModel` | `anthropic/claude-opus-4` | Model identifier |
| `openrouterSecretRefName` | empty | Existing provider Secret |
| `postgresSecretRefName` | empty | Existing database Secret |
| `serviceAccount.create` | `true` | Create a ServiceAccount |
| `serviceAccount.name` | empty | Created-account override, or required existing account name |
| `createInternalSecret` | `false` | Create and consume non-sensitive provider configuration |
| `postgres.externalService.enabled` | `false` | Create an ExternalName Service; does not set `DATABASE_URL` |
| `ingress.enabled` | `false` | Render an Ingress |
| `extraEnv` | `[]` | Additional container environment entries |

Expand All @@ -77,11 +92,13 @@ Ingress options, and the complete value set.
- `values.yaml` — default values
- `templates/` — Kubernetes templates
- `tests/test_chart.sh` — lint and rendered-contract checks
- `tests/test_downstream.sh` — current GitOps values and chart-pin compatibility

Run the repository check with:

~~~bash
bash tests/test_chart.sh
bash tests/test_downstream.sh ../app-poly-gitops-k8s
~~~

## Related repositories
Expand Down
8 changes: 8 additions & 0 deletions templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@
{{- default .Release.Namespace .Values.namespace -}}
{{- end }}

{{- define "chat-api.serviceAccountName" -}}
{{- if .Values.serviceAccount.create -}}
{{- default (include "chat-api.fullname" .) .Values.serviceAccount.name -}}
{{- else -}}
{{- required "serviceAccount.name is required when serviceAccount.create=false" .Values.serviceAccount.name -}}
{{- end -}}
{{- end }}

{{- define "chat-api.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
Expand Down
8 changes: 6 additions & 2 deletions templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ spec:
annotations:
openrouter.model: {{ .Values.openrouter.defaultModel | quote }}
spec:
serviceAccountName: {{ include "chat-api.fullname" . }}
serviceAccountName: {{ include "chat-api.serviceAccountName" . }}
containers:
- name: chat-api
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
Expand Down Expand Up @@ -69,8 +69,12 @@ spec:
{{- toYaml . | nindent 12 }}
{{- end }}

{{- if or .Values.openrouterSecretRefName .Values.postgresSecretRefName }}
{{- if or .Values.createInternalSecret .Values.openrouterSecretRefName .Values.postgresSecretRefName }}
envFrom:
{{- if .Values.createInternalSecret }}
- secretRef:
name: {{ include "chat-api.fullname" . }}-config
{{- end }}
{{- if .Values.openrouterSecretRefName }}
- secretRef:
name: {{ .Values.openrouterSecretRefName }}
Expand Down
2 changes: 1 addition & 1 deletion templates/ingress.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ metadata:
spec:
ingressClassName: {{ .Values.ingress.className | default "nginx" }}
rules:
- host: {{ .Values.ingress.host | quote }}
- host: {{ required "ingress.host is required when ingress.enabled=true" .Values.ingress.host | quote }}
http:
paths:
- path: {{ .Values.ingress.path | default "/" | quote }}
Expand Down
2 changes: 1 addition & 1 deletion templates/pg-service.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ metadata:
component: database
spec:
type: ExternalName
externalName: {{ .Values.postgres.externalService.host }}
externalName: {{ required "postgres.externalService.host is required when postgres.externalService.enabled=true" .Values.postgres.externalService.host }}
ports:
- port: {{ .Values.postgres.externalService.port }}
targetPort: {{ .Values.postgres.externalService.port }}
Expand Down
2 changes: 1 addition & 1 deletion templates/serviceaccount.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "chat-api.fullname" . }}
name: {{ include "chat-api.serviceAccountName" . }}
namespace: {{ include "chat-api.namespace" . }}
labels:
{{- include "chat-api.labels" . | nindent 4 }}
Expand Down
100 changes: 95 additions & 5 deletions tests/test_chart.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,104 @@
set -euo pipefail

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
tmp_dir="$(mktemp -d)"
trap 'rm -rf "${tmp_dir}"' EXIT

render() {
helm template chat-api "${repo_root}" --namespace smoke "$@"
}

assert_yaml_resources() {
python3 - "$1" <<'PY'
import pathlib
import sys

import yaml

documents = list(yaml.safe_load_all(pathlib.Path(sys.argv[1]).read_text()))
resources = [document for document in documents if document is not None]
assert resources, "render did not contain Kubernetes resources"
for resource in resources:
assert isinstance(resource, dict), resource
assert resource.get("apiVersion"), resource
assert resource.get("kind"), resource
assert isinstance(resource.get("metadata"), dict), resource
assert resource["metadata"].get("name"), resource
PY
}

helm lint "${repo_root}"
rendered="$(helm template chat-api "${repo_root}" --namespace smoke)"

grep -q 'namespace: smoke' <<<"${rendered}"
grep -q 'image: "ghcr.io/justgithubaccount/app-poly-gitops-fastapi:1.1.2"' <<<"${rendered}"
! grep -q 'secretRef:' <<<"${rendered}"
! grep -q 'command:' <<<"${rendered}"
default_render="${tmp_dir}/default.yaml"
render > "${default_render}"
assert_yaml_resources "${default_render}"
test "$(sha256sum "${default_render}" | cut -d' ' -f1)" = \
"eccee8f126eec4287d1ccf664414f73632b70a6dfa46357b8c31f7d54e96637f"
grep -q 'namespace: smoke' "${default_render}"
grep -q 'image: "ghcr.io/justgithubaccount/app-poly-gitops-fastapi:1.1.2"' "${default_render}"
! grep -q 'secretRef:' "${default_render}"
! grep -q 'command:' "${default_render}"
! grep -q '^kind: Ingress$' "${default_render}"

created_sa="${tmp_dir}/created-sa.yaml"
render --set serviceAccount.name=existing-compatible-name > "${created_sa}"
test "$(grep -c '^kind: ServiceAccount$' "${created_sa}")" -eq 1
grep -q 'name: existing-compatible-name' "${created_sa}"
grep -q 'serviceAccountName: existing-compatible-name' "${created_sa}"

existing_sa="${tmp_dir}/existing-sa.yaml"
render --set serviceAccount.create=false --set serviceAccount.name=precreated-chat-api > "${existing_sa}"
! grep -q '^kind: ServiceAccount$' "${existing_sa}"
grep -q 'serviceAccountName: precreated-chat-api' "${existing_sa}"

if render --set serviceAccount.create=false --set-string serviceAccount.name= \
> "${tmp_dir}/invalid-sa.yaml" 2> "${tmp_dir}/invalid-sa.err"; then
echo "serviceAccount.create=false without serviceAccount.name unexpectedly rendered" >&2
exit 1
fi
grep -q 'serviceAccount.name is required' "${tmp_dir}/invalid-sa.err"

internal_secret="${tmp_dir}/internal-secret.yaml"
render --set createInternalSecret=true > "${internal_secret}"
grep -q '^kind: Secret$' "${internal_secret}"
grep -A3 'envFrom:' "${internal_secret}" | grep -q 'name: chat-api-config'

external_postgres="${tmp_dir}/external-postgres.yaml"
render \
--set postgres.externalService.enabled=true \
--set postgres.externalService.host=db.example.internal \
> "${external_postgres}"
grep -q '^kind: Service$' "${external_postgres}"
grep -q 'externalName: db.example.internal' "${external_postgres}"
! grep -q 'name: DATABASE_URL' "${external_postgres}"
if render --set postgres.externalService.enabled=true \
> "${tmp_dir}/invalid-postgres.yaml" 2> "${tmp_dir}/invalid-postgres.err"; then
echo "external Postgres service without a host unexpectedly rendered" >&2
exit 1
fi
grep -q 'postgres.externalService.host is required' "${tmp_dir}/invalid-postgres.err"

ingress="${tmp_dir}/ingress.yaml"
render \
--set namespace=application \
--set ingress.enabled=true \
--set ingress.host=chat.example.test \
> "${ingress}"
grep -q '^kind: Ingress$' "${ingress}"
grep -A5 '^kind: Ingress$' "${ingress}" | grep -q 'namespace: application'
grep -q 'host: "chat.example.test"' "${ingress}"
if render --set ingress.enabled=true \
> "${tmp_dir}/invalid-ingress.yaml" 2> "${tmp_dir}/invalid-ingress.err"; then
echo "enabled ingress without a host unexpectedly rendered" >&2
exit 1
fi
grep -q 'ingress.host is required' "${tmp_dir}/invalid-ingress.err"

for manifest in "${default_render}" "${created_sa}" "${existing_sa}" \
"${internal_secret}" "${external_postgres}" "${ingress}"; do
assert_yaml_resources "${manifest}"
kubeconform -strict -summary -ignore-missing-schemas "${manifest}"
done

workflow="${repo_root}/.github/workflows/validate.yaml"
test -f "${workflow}"
Expand Down
17 changes: 17 additions & 0 deletions tests/test_downstream.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
k8s_repo="${1:?usage: tests/test_downstream.sh /path/to/app-poly-gitops-k8s}"
application="${k8s_repo}/tenants/product-team/apps/chat/base/application.yaml"
values="${k8s_repo}/tenants/product-team/apps/chat/overlays/dev/values.yaml"

test -f "${application}"
test -f "${values}"
grep -q 'repoURL: https://github.com/justgithubaccount/app-poly-gitops-helm' "${application}"
grep -q 'targetRevision: v0.4.1' "${application}"

rendered="$(helm template chat-api "${repo_root}" --namespace product-team -f "${values}")"
grep -q 'namespace: product-team' <<<"${rendered}"
grep -q 'image: "ghcr.io/justgithubaccount/app-poly-gitops-fastapi:1.1.2"' <<<"${rendered}"
grep -q 'serviceAccountName: chat-api' <<<"${rendered}"
5 changes: 5 additions & 0 deletions values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ livenessProbe:
serviceAccount:
create: true
annotations: {}
# Required when create=false. When create=true, empty uses the chart fullname.
name: ""

env:
Expand All @@ -54,6 +55,8 @@ postgresSecretRefName: ""

postgres:
externalService:
# Creates only an ExternalName Service. Supply DATABASE_URL separately with
# postgresSecretRefName or extraEnv; credentials are intentionally not built here.
enabled: false
host: ""
port: 5432
Expand All @@ -69,4 +72,6 @@ ingress:
# Additional container environment entries, including Secret key mappings.
extraEnv: []

# Creates and consumes a chart-managed Secret for non-sensitive provider config.
# Provider credentials still belong in openrouterSecretRefName or extraEnv.
createInternalSecret: false
Loading