AI overhaul
This commit is contained in:
Executable
+8
@@ -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"]
|
||||
Executable
+14
@@ -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
|
||||
Executable
+53
@@ -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"
|
||||
Executable
+11
@@ -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
|
||||
Executable
+89
@@ -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")
|
||||
Reference in New Issue
Block a user