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
31 changes: 31 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Repository Guidelines

This guide explains how to contribute to the Spin Helm recipes with minimal friction. Keep changes small, tested, and documented so others can reproduce them.

## Project Structure & Module Organization
- `nats/`: Helm values and kustomize post-renderer for the NATS chart; `values.yaml` for defaults and `tls-acme-values.yaml` for certificate setup. `kustomize/` patches StatefulSets/Deployments with `SPIN_UID`/`SPIN_GID`.
- `tls-acme/`: Standalone Helm chart (`Chart.yaml`, `templates/`, `values.yaml`) for ACME-driven TLS issuance and renewals on Spin.
- `scripts/`: Utility scripts (e.g., `scripts/mdl.sh` for markdownlint autofix).
- Repo configs: `.pre-commit-config.yaml`, `.yamllint.yml`, `.markdownlint.rb` define lint rules; align contributions to them.

## Build, Test, and Development Commands
- `pre-commit run --all-files` to run markdownlint, yamllint, and trailing whitespace checks locally.
- `yamllint .` to validate YAML with the repo config (line length 120, strict indentation).
- `./scripts/mdl.sh` to autofix Markdown formatting before review.
- `helm lint tls-acme` to sanity-check the TLS chart; add `-f <values>` as needed.
- For NATS overlays, render and patch locally: `cd nats/kustomize && helm template nats/nats -f ../values.yaml --post-renderer ./kustomize.sh`.

## Coding Style & Naming Conventions
- YAML: 2-space indentation, descriptive keys, keep `*-values.yaml` file names and placeholder tokens (`RUN_AS_USER_PLACEHOLDER`, `FS_GROUP_PLACEHOLDER`) intact for scripted substitution.
- Markdown: respect `.markdownlint.rb` (ordered lists, generous line length). Prefer short headings and actionable steps.
- Shell: keep scripts POSIX/Bash compatible, check for required env vars (`SPIN_UID`, `SPIN_GID`) before use.

## Testing Guidelines
- Chart changes: run `helm lint` and, when altering templates, `helm template ... | kubectl kustomize ./nats/kustomize` to verify patches apply cleanly.
Comment thread
dingp marked this conversation as resolved.
- Functional checks occur on Spin: follow the per-directory READMEs to install/upgrade releases, then validate via `kubectl` execs (e.g., `nats rtt`) or ingress reachability.
- Add brief notes in PRs on what was verified (commands + namespace/cluster used), especially for TLS issuance runs.

## Commit & Pull Request Guidelines
- Commits: imperative mood with scope when helpful (e.g., `nats: adjust websocket ingress whitelist`); group related changes.
- PRs: include a summary of changes, linked issues (if any), values files touched, commands run, and relevant logs or screenshots for install/upgrade attempts.
- Avoid committing secrets or cluster-specific kubeconfigs; keep sensitive values in local overrides and document placeholders instead.
63 changes: 32 additions & 31 deletions tls-acme/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,10 @@ This helm chart takes consideration of two different usage cases. The installati

```bash
cd tls-acme
./prepare-values.sh
helm lint .
cp prepare-values.yaml.example prepare-values.yaml
# edit prepare-values.yaml
python3 prepare-values.py
helm lint . -f values.yaml
helm install -n <namespace> -f values.yaml acmecron .
```

Expand Down Expand Up @@ -101,20 +103,27 @@ tls-acme

#### Customize values for chart installation

Edit `values_template.yaml` by setting the placeholder values (including
`useCase` for case1 vs case2), then run `./prepare-values.sh` to generate
`values.yaml`.

- `<uid>`
- `<gid>`
- `<domain>`
- `<email>`
- `<port>`
- `<ingress_name>`
- `<cluster>` (can be `development.svc.spin.nersc.org` or `production.svc.spin.nersc.org`)
- `existing-websrv`
- `pvc-existing-webroot`
- change `webServer.existing` field from `false` to `true`
Copy `prepare-values.yaml.example` to `prepare-values.yaml`, update the values there,
and then run `python3 prepare-values.py` to generate `values.yaml` from `values_template.yaml`.

- `nersc_user_id`
- `nersc_user_group`
- `user_domains` as a YAML list of user-facing domains
- `email`
- `service_port`
- `ingress_name`
- `cluster` (can be `development.svc.spin.nersc.org` or `production.svc.spin.nersc.org`)
- `use_case` (`case1` for an existing web server, `case2` to create one)
- `webserver_existing` only if you want to override the value derived from `use_case`

For multiple CNAMEs, list all of them in `user_domains`, for example:

```yaml
user_domains:
- app.example.org
- www.example.org
- api.example.org
```

#### Install the chart

Expand All @@ -130,8 +139,8 @@ helm install -n <namespace> -f values.yaml acmecron .
The results of this installation are:

1. A self-generated TLS certificate saved into a secret named `tls-cert`;
2. A new ingress in the namespace, with rules for each of the domains, including the default Spin domain, pointing to the existing web server and its http port; the ingress will also use the self-generated certificate for all the domains;
3. A cronjob which runs every two months to reuqest/renew a TLS certificate, and repalce the self-generated TLS certificate with it. The requested certificate will include all the listed domains in the ingress.
2. A new ingress in the namespace, with rules for each of the domains, including the default Spin domain, pointing to the existing web server and its http port; the default Spin domain is intentionally not listed in the certificate SANs, so accessing it directly will show a certificate warning.
3. A cronjob which runs every two months to reuqest/renew a TLS certificate, and repalce the self-generated TLS certificate with it. The requested certificate will include only the user-facing domains listed in `ingress.userDomains`; the internal Spin hostname is added to the ingress automatically but excluded from the certificate.

#### Post installation setup (1)

Expand Down Expand Up @@ -164,21 +173,13 @@ This is applicatable to the usage cases like:

#### Installation and inspection

Similar as _Case 1_ above, but change the following in `values_template.yaml`,
set `useCase` to `case2`, and then re-run `./prepare-values.sh`:

- `<uid>`
- `<gid>`
- `<domain>`
- `<email>`
- `<port>` to `8080`
- `<ingress_name>`
- `<cluster>` (can be `development.svc.spin.nersc.org` or `production.svc.spin.nersc.org`)
Similar as _Case 1_ above, but set `use_case` to `case2` in `prepare-values.yaml`,
update the other config values as needed, and then re-run `python3 prepare-values.py`.

Different than _Case 1_, this installation of the chart will result in:

1. A deployment of a simple web server, running on port 8080 internally;
2. A new ingress in the namespace, pointing all of the domains, including the default Spin domain, to the newly created web server and its port 8080.
2. A new ingress in the namespace, pointing all of the domains, including the default Spin domain, to the newly created web server and its port 8080. The default Spin domain remains outside the certificate SANs on purpose, so it shows a certificate warning if accessed directly.

#### Post installation setup (2)

Expand All @@ -190,8 +191,8 @@ During, the future cronjob runs, your modified ingress will be saved first, chan

### Upgrade or uninstall the chart

If you made changes to `values_template.yaml`, re-run `./prepare-values.sh` and
upgrade the installed chart by:
If you made changes to `prepare-values.yaml` or `values_template.yaml`, re-run
`python3 prepare-values.py` and upgrade the installed chart by:

```bash
./prepare-values.sh
Expand Down
106 changes: 106 additions & 0 deletions tls-acme/prepare-values.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
from __future__ import annotations

import argparse
from pathlib import Path
import sys

import yaml
from jinja2 import Environment, FileSystemLoader, StrictUndefined


def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description='Render values.yaml from a YAML config file and Jinja template.'
)
parser.add_argument(
'config',
nargs='?',
default='prepare-values.yaml',
help='Path to the YAML config file. Default: prepare-values.yaml',
)
parser.add_argument(
'--template',
default='values_template.yaml',
help='Path to the Jinja values template. Default: values_template.yaml',
)
parser.add_argument(
'--output',
default='values.yaml',
help='Path to the rendered values file. Default: values.yaml',
)
return parser


def load_yaml(path: Path) -> dict:
with path.open('r', encoding='utf-8') as fh:
data = yaml.safe_load(fh) or {}
if not isinstance(data, dict):
raise ValueError(f'{path} must contain a YAML mapping at the top level.')
return data


def derive_fields(config: dict) -> dict:
rendered = dict(config)

if 'webserver_existing' not in rendered:
use_case = rendered.get('use_case')
if use_case == 'case1':
rendered['webserver_existing'] = True
elif use_case == 'case2':
rendered['webserver_existing'] = False
else:
raise ValueError(
'Set webserver_existing explicitly, or set use_case to case1 or case2.'
)

user_domains = rendered.get('user_domains')
if not isinstance(user_domains, list) or not user_domains:
raise ValueError('user_domains must be a non-empty YAML list.')
if not all(isinstance(domain, str) and domain for domain in user_domains):
raise ValueError('Each entry in user_domains must be a non-empty string.')

return rendered


def render(template_path: Path, context: dict) -> str:
env = Environment(
loader=FileSystemLoader(str(template_path.parent)),
undefined=StrictUndefined,
trim_blocks=True,
lstrip_blocks=True,
)
template = env.get_template(template_path.name)
return template.render(**context)


def main() -> int:
parser = build_parser()
args = parser.parse_args()

config_path = Path(args.config).resolve()
template_path = Path(args.template).resolve()
output_path = Path(args.output).resolve()

if not config_path.exists():
print(
f'Error: config file not found: {config_path}\n'
f'Hint: copy {config_path.parent / "prepare-values.yaml.example"} '
f'to {config_path.parent / "prepare-values.yaml"} and edit it.',
file=sys.stderr,
)
return 1

try:
config = derive_fields(load_yaml(config_path))
rendered = render(template_path, config)
except Exception as exc:
print(f'Error: {exc}', file=sys.stderr)
return 1

output_path.write_text(rendered, encoding='utf-8')
return 0


if __name__ == '__main__':
raise SystemExit(main())
40 changes: 0 additions & 40 deletions tls-acme/prepare-values.sh

This file was deleted.

13 changes: 13 additions & 0 deletions tls-acme/prepare-values.yaml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copy this file to prepare-values.yaml and update the values for your deployment.
nersc_user_id: 12345
nersc_user_group: 67890
service_port: 8080
ingress_name: my-ingress
email: user@email.com
cluster: production
use_case: case2
# Optional override. If omitted, prepare-values.py derives it from use_case.
# webserver_existing: false
user_domains:
- app1.example1.com
- app1.example2.com
2 changes: 1 addition & 1 deletion tls-acme/templates/cronjob.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ spec:
- name: EMAIL
value: {{ .Values.email }}
- name: DOMAIN
value: {{ .Values.domain }}
value: {{ join ":" .Values.ingress.userDomains | quote }}
- name: KUBECONFIG
value: /kube/{{ .Values.kubeconfig.secretName }}
- name: CERT_SECRET_NAME
Expand Down
47 changes: 17 additions & 30 deletions tls-acme/templates/ingress.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := .Values.ingress.name -}}
{{- $ingressName := .Values.ingress.name -}}
{{- $depName := (printf "%s-websrv" (include "spin-acme.fullname" .)) -}}
{{- if .Values.webServer.existing -}}
{{- $depName = .Values.webServer.deploymentName -}}
{{- end}}
{{- $svcPort := .Values.service.port -}}
{{- $userDomains := .Values.ingress.userDomains -}}
{{- $clusterDomain := .Values.cluster -}}
{{- if not (contains ".svc.spin.nersc.org" $clusterDomain) -}}
{{- $clusterDomain = printf "%s.svc.spin.nersc.org" $clusterDomain -}}
{{- end -}}
{{- $spinDomain := printf "%s.%s.%s" $ingressName .Release.Namespace $clusterDomain -}}
{{- $allIngressDomains := concat $userDomains (list $spinDomain) -}}
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
Expand All @@ -19,7 +26,7 @@ apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
name: {{ $fullName }}
name: {{ $ingressName }}
labels:
{{- include "spin-acme.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
Expand All @@ -30,26 +37,22 @@ spec:
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
{{- if $userDomains }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
{{- range $userDomains }}
- {{ . | quote }}
{{- end }}
- {{ $fullName }}.{{ $.Release.Namespace }}.{{ $.Values.cluster }}
secretName: {{ $.Values.cert.secretName }}
{{- end }}
secretName: {{ .Values.cert.secretName }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
{{- range $allIngressDomains }}
- host: {{ . | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
pathType: {{ .pathType }}
- path: {{ $.Values.ingress.path }}
{{- if and $.Values.ingress.pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
pathType: {{ $.Values.ingress.pathType }}
{{- end }}
backend:
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
Expand All @@ -61,21 +64,5 @@ spec:
serviceName: {{ $depName }}
servicePort: {{ $svcPort }}
{{- end }}
{{- end }}
{{- end }}
- host: {{ $fullName }}.{{ .Release.Namespace }}.{{ .Values.cluster }}
http:
paths:
- path: /
pathType: Prefix
backend:
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
service:
name: {{ $depName }}
port:
number: {{ $svcPort }}
{{- else }}
serviceName: {{ $depName }}
servicePort: {{ $svcPort }}
{{- end }}
{{- end }}
Loading