AI overhaul

This commit is contained in:
2026-10-23 12:49:53 -04:00
parent b040ca26c3
commit b4db1a0182
21 changed files with 1226 additions and 189 deletions
+8
View File
@@ -0,0 +1,8 @@
FROM python:3.13-slim-trixie
RUN pip install --no-cache-dir 'ansible-core>=2.18,<2.19' 'kubernetes>=28,<34' PyYAML 'kubernetes-validate>=1.31,<1.35'
COPY requirements.yml /tmp/requirements.yml
RUN ansible-galaxy collection install -r /tmp/requirements.yml
WORKDIR /roles/ansible-role-www
COPY . ./
ENV ANSIBLE_ROLES_PATH=/roles ANSIBLE_LOCAL_TEMP=/tmp/ansible-local
CMD ["sh", "-ec", "ansible-playbook -i localhost, tests/test.yml --syntax-check; ansible-playbook -i localhost, tests/syntax.yml --syntax-check; ansible-playbook -i localhost, tests/syntax.yml --tags membership_validate; ansible-playbook -i localhost, tests/render.yml; python tests/validate_manifests.py"]
+14
View File
@@ -0,0 +1,14 @@
---
# Credentials must come from Vault or an existing Kubernetes Secret.
www_hostname: www.eom.dev
www_membership_image: registry.example.org/www-membership:2026-10-23
www_repo_version: refs/heads/main
www_membership_secret_name: www-membership-credentials
www_membership_shkeeper_enabled: true
www_membership_environment_overrides:
LDAP_BIND_DN: cn=www-membership,ou=system,dc=eom,dc=dev
LDAP_PASSWORD_SCHEME: "{ARGON2}"
EMAIL_HOST: smtp.eom.dev
STRIPE_PORTAL_CONFIGURATION: bpc_REPLACE
SHKEEPER_URL: https://shkeeper.example.org
SHKEEPER_CURRENCIES: BTC,XMR
+53
View File
@@ -0,0 +1,53 @@
---
- name: Render membership manifests without contacting a cluster
hosts: localhost
connection: local
gather_facts: false
vars:
role_path: /roles/ansible-role-www
tasks:
- name: Load role defaults
ansible.builtin.include_vars: ../defaults/main.yml
- name: Supply nonsecret fixture settings
ansible.builtin.set_fact:
www_membership_image: registry.example.org/www-membership:release-test
_www_membership_revision: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
www_membership_environment: "{{ www_membership_environment | combine({'MEMBERSHIP_PUBLIC_URL': 'https://' ~ www_hostname, 'LDAP_BIND_DN': 'cn=membership,dc=example,dc=org', 'LDAP_PASSWORD_SCHEME': '{ARGON2}', 'EMAIL_HOST': 'smtp.example.org', 'STRIPE_PORTAL_CONFIGURATION': 'bpc_test'}) }}"
- name: Render Apache proxy fixture
ansible.builtin.set_fact:
_www_proxy_config: "{{ lookup('ansible.builtin.template', role_path ~ '/templates/membership-httpd.conf.j2') }}"
- name: Write Apache fixture for container syntax tests
ansible.builtin.template:
src: ../templates/membership-httpd.conf.j2
dest: /tmp/www-test-membership.conf
mode: "0600"
- name: Render standard deployment variants
ansible.builtin.template:
src: "../templates/{{ item }}.yml.j2"
dest: "/tmp/www-test-{{ item }}.yml"
mode: "0600"
loop: [website, membership-config, membership-postgres, membership-migrate, membership-workloads]
- name: Enable optional external database and private CA fixture
ansible.builtin.set_fact:
www_membership_postgres_managed: false
www_membership_ca_secret_name: private-ca
www_image_pull_secrets: [{name: registry-pull}]
www_membership_environment: "{{ www_membership_environment | combine({'POSTGRES_HOST': 'postgres.example.org', 'POSTGRES_SSLMODE': 'verify-full', 'PGSSLROOTCERT': '/etc/membership/ca/db.pem', 'LDAP_CA_CERT_FILE': '/etc/membership/ca/ldap.pem', 'SHKEEPER_URL': 'https://crypto.example.org', 'SHKEEPER_CURRENCIES': 'BTC,XMR', 'SHKEEPER_CA_CERT_FILE': '/etc/membership/ca/shkeeper.pem'}) }}"
- name: Render external provider variants
ansible.builtin.template:
src: "../templates/{{ item }}.yml.j2"
dest: "/tmp/www-test-external-{{ item }}.yml"
mode: "0600"
loop: [membership-config, membership-workloads, membership-migrate]
- name: Render disabled membership proxy variant
ansible.builtin.set_fact:
www_membership_enabled: false
www_onion_enabled: false
- name: Render disabled Apache fixture
ansible.builtin.set_fact:
_www_proxy_config: "{{ lookup('ansible.builtin.template', role_path ~ '/templates/membership-httpd.conf.j2') }}"
- name: Render disabled website
ansible.builtin.template:
src: ../templates/website.yml.j2
dest: /tmp/www-test-disabled-website.yml
mode: "0600"
+11
View File
@@ -0,0 +1,11 @@
---
- name: Parse all deployment tasks without contacting a cluster
hosts: localhost
connection: local
gather_facts: false
vars_files:
- ../defaults/main.yml
- example-vars.yml
tasks:
- name: Parse membership deployment tasks
ansible.builtin.import_tasks: ../tasks/membership.yml
+89
View File
@@ -0,0 +1,89 @@
"""Offline invariants for manifests rendered by real Ansible/Jinja."""
import json
from pathlib import Path
import yaml
from kubernetes_validate import validate
def docs(name):
return [doc for doc in yaml.safe_load_all(Path(f"/tmp/www-test-{name}.yml").read_text()) if doc]
def find(objects, kind, name):
return next(obj for obj in objects if obj["kind"] == kind and obj["metadata"]["name"] == name)
def pod_checks(pod, external=False):
assert pod["automountServiceAccountToken"] is False
assert pod["securityContext"]["runAsNonRoot"] is True
container = pod["containers"][0]
assert container["securityContext"]["readOnlyRootFilesystem"] is True
assert container["securityContext"]["capabilities"]["drop"] == ["ALL"]
assert container["envFrom"] == [{"configMapRef": {"name": "membership-config"}}, {"secretRef": {"name": "membership"}}]
assert not container.get("env")
assert container["resources"]["limits"]["memory"]
assert any(mount["mountPath"] == "/etc/membership/tiers" and mount["readOnly"] for mount in container["volumeMounts"])
if external:
assert pod["imagePullSecrets"] == [{"name": "registry-pull"}]
assert any(volume.get("secret", {}).get("secretName") == "private-ca" for volume in pod["volumes"])
assert any(mount["mountPath"] == "/etc/membership/ca" and mount["readOnly"] for mount in container["volumeMounts"])
all_objects = []
for name in ["website", "membership-config", "membership-postgres", "membership-migrate", "membership-workloads"]:
all_objects.extend(docs(name))
for path in Path('/tmp').glob('www-test-*.yml'):
for obj in yaml.safe_load_all(path.read_text()):
if obj and obj['kind'] != 'OnionService':
validate(obj, desired_version='1.31.0', strict=True)
assert all(obj["metadata"]["namespace"] == "www" for obj in all_objects)
assert len({(obj["kind"], obj["metadata"]["name"]) for obj in all_objects}) == len(all_objects)
config = find(all_objects, "ConfigMap", "membership-config")["data"]
assert all(isinstance(value, str) for value in config.values())
assert config["MEMBERSHIP_PUBLIC_URL"] == "https://www.eom.dev"
assert "MEMBERSHIP_SECRET_KEY" not in config and "SHKEEPER_API_KEY" not in config
tiers = json.loads(find(all_objects, "ConfigMap", "membership-tiers")["data"]["tiers.json"])
assert len(tiers) == 5 and tiers["level-4"]["usd_amount"] == "64.00"
for name in ["membership", "membership-worker"]:
deployment = find(all_objects, "Deployment", name)
pod_checks(deployment["spec"]["template"]["spec"])
web = find(all_objects, "Deployment", "membership")["spec"]["template"]["spec"]["containers"][0]
assert "assert r.status==200" in web["readinessProbe"]["exec"]["command"][-1]
worker = find(all_objects, "Deployment", "membership-worker")
assert worker["spec"]["replicas"] == 1 and worker["spec"]["strategy"]["type"] == "Recreate"
assert "sync_memberships" in worker["spec"]["template"]["spec"]["containers"][0]["command"]
job = next(obj for obj in all_objects if obj["kind"] == "Job")
assert job["metadata"]["name"] == "membership-migrate-" + "a" * 16
pod_checks(job["spec"]["template"]["spec"])
assert job["spec"]["template"]["spec"]["containers"][0]["command"] == ["python", "manage.py", "migrate", "--noinput"]
db = find(all_objects, "StatefulSet", "membership-db")
assert db["spec"]["volumeClaimTemplates"][0]["spec"]["accessModes"] == ["ReadWriteOnce"]
assert "persistentVolumeClaimRetentionPolicy" not in db["spec"]
assert find(all_objects, "Service", "membership-db")["spec"]["type"] == "ClusterIP"
assert find(all_objects, "Service", "membership")["spec"]["type"] == "ClusterIP"
policy = find(all_objects, "NetworkPolicy", "membership-web")
assert policy["spec"]["ingress"][0]["from"] == [{"podSelector": {"matchLabels": {"app": "httpd"}}}]
apache = find(all_objects, "ConfigMap", "httpd")["data"]
assert "http://membership:8000/members/" in apache["membership.conf"]
assert "X-Forwarded-Proto" in apache["membership.conf"]
assert "Require all denied" in apache["membership.conf"]
assert "env=!membership_sensitive" in apache["httpd.conf"]
assert "LoadModule proxy_http_module" in apache["httpd.conf"]
source = find(all_objects, "Deployment", "httpd")["spec"]["template"]["spec"]["initContainers"][0]
assert "rm -rf" not in source["command"][-1]
assert 'cp -a /tmp/www/htdocs/. /data/' in source["command"][-1]
ingress = find(all_objects, "Ingress", "www")
assert ingress["metadata"]["annotations"]["nginx.ingress.kubernetes.io/enable-access-log"] == "false"
assert ingress["spec"]["rules"][0]["http"]["paths"][0]["backend"]["service"]["name"] == "httpd"
for obj in docs("external-membership-workloads"):
if obj["kind"] == "Deployment":
pod_checks(obj["spec"]["template"]["spec"], external=True)
external_config = docs("external-membership-config")
assert not any(obj["kind"] == "NetworkPolicy" and obj["metadata"]["name"] == "membership-db" for obj in external_config)
assert find(external_config, "ConfigMap", "membership-config")["data"]["POSTGRES_SSLMODE"] == "verify-full"
disabled = docs("disabled-website")
assert not any(obj["kind"] == "OnionService" for obj in disabled)
assert "ProxyPass" not in find(disabled, "ConfigMap", "httpd")["data"]["membership.conf"]
print(f"PASS: {len(all_objects)} Kubernetes objects; default, external/CA/SHKeeper and disabled variants")