Compare commits
3
Commits
ab6dbac3fd
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f59d2b72fb
|
||
|
|
eea6389a5a
|
||
|
|
d694ae5650
|
@@ -54,6 +54,7 @@ Defaults deliberately refuse incomplete membership deployments. Configure:
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `www_namespace` | Namespace, default `www` |
|
| `www_namespace` | Namespace, default `www` |
|
||||||
| `www_hostname` | Canonical HTTPS hostname, default **www.eom.dev** |
|
| `www_hostname` | Canonical HTTPS hostname, default **www.eom.dev** |
|
||||||
|
| `www_redirect_hostnames` | Hostnames 301-redirected to `www_hostname`, default `[]` |
|
||||||
| `www_membership_enabled` | Deploy membership/proxy, default true |
|
| `www_membership_enabled` | Deploy membership/proxy, default true |
|
||||||
| `www_membership_image` | Required versioned application image/digest |
|
| `www_membership_image` | Required versioned application image/digest |
|
||||||
| `www_membership_secret_name` | Existing Secret, default `membership` |
|
| `www_membership_secret_name` | Existing Secret, default `membership` |
|
||||||
@@ -239,7 +240,10 @@ app defaults to eom.dev. This role sets `MEMBERSHIP_PUBLIC_URL` to
|
|||||||
|
|
||||||
To serve the apex instead, set `www_hostname: eom.dev` and align DNS,
|
To serve the apex instead, set `www_hostname: eom.dev` and align DNS,
|
||||||
certificates, callbacks and redirects. Membership requires one canonical HTTPS
|
certificates, callbacks and redirects. Membership requires one canonical HTTPS
|
||||||
origin for sessions, CSRF, email verification and OIDC.
|
origin for sessions, CSRF, email verification and OIDC. To send apex visitors
|
||||||
|
to the canonical origin without serving it there, set
|
||||||
|
`www_redirect_hostnames: [eom.dev]`; other ingresses' more specific apex paths
|
||||||
|
(such as `/.well-known/matrix/*`) keep working.
|
||||||
|
|
||||||
## Upgrades and operations
|
## Upgrades and operations
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ www_repo_url: https://git.example.com/organization/www.git
|
|||||||
www_namespace: www
|
www_namespace: www
|
||||||
www_hostname: www.example.com
|
www_hostname: www.example.com
|
||||||
www_ingress_class: nginx
|
www_ingress_class: nginx
|
||||||
|
# Extra hostnames 301-redirected to https://{{ www_hostname }} (e.g. [eom.dev]).
|
||||||
|
www_redirect_hostnames: []
|
||||||
www_cluster_issuer: letsencrypt-prod
|
www_cluster_issuer: letsencrypt-prod
|
||||||
# Set to your published website image, including any required metrics exporter.
|
# Set to your published website image, including any required metrics exporter.
|
||||||
www_httpd_image: registry.example.com/organization/www:release
|
www_httpd_image: registry.example.com/organization/www:release
|
||||||
@@ -131,6 +133,11 @@ www_membership_environment:
|
|||||||
SHKEEPER_URL: ""
|
SHKEEPER_URL: ""
|
||||||
SHKEEPER_CURRENCIES: ""
|
SHKEEPER_CURRENCIES: ""
|
||||||
SHKEEPER_CA_CERT_FILE: ""
|
SHKEEPER_CA_CERT_FILE: ""
|
||||||
|
# Newsletter opt-in at signup and on the profile. Enabled only when the
|
||||||
|
# LISTMONK_API_TOKEN Secret key is also present.
|
||||||
|
LISTMONK_URL: ""
|
||||||
|
LISTMONK_API_USER: ""
|
||||||
|
LISTMONK_LIST_NAME: eom.dev
|
||||||
|
|
||||||
# Existing LIVE Stripe prices. Level 0 is free and is not configured as a paid tier.
|
# Existing LIVE Stripe prices. Level 0 is free and is not configured as a paid tier.
|
||||||
# Provision the dedicated paid-group DNs separately.
|
# Provision the dedicated paid-group DNs separately.
|
||||||
@@ -204,3 +211,9 @@ www_membership_tiers:
|
|||||||
- Exclusive polls on the direction of the platform
|
- Exclusive polls on the direction of the platform
|
||||||
- Optional Diamond credit on the supporters page
|
- Optional Diamond credit on the supporters page
|
||||||
- Your biggest boost for independent public projects
|
- Your biggest boost for independent public projects
|
||||||
|
|
||||||
|
# Create any missing tier/service groups (with the placeholder member) in the
|
||||||
|
# OpenLDAP pod. Existing groups and their members are left untouched.
|
||||||
|
www_membership_ldap_ensure_groups: false
|
||||||
|
www_membership_ldap_namespace: openldap
|
||||||
|
www_membership_ldap_pod_selector: app=openldap
|
||||||
|
|||||||
+28
-1
@@ -34,6 +34,7 @@
|
|||||||
- not www_membership_shkeeper_enabled or (www_membership_environment.SHKEEPER_URL is match('^https://[^/@]+') and www_membership_environment.SHKEEPER_CURRENCIES | length > 0)
|
- not www_membership_shkeeper_enabled or (www_membership_environment.SHKEEPER_URL is match('^https://[^/@]+') and www_membership_environment.SHKEEPER_CURRENCIES | length > 0)
|
||||||
- www_membership_shkeeper_enabled or (www_membership_environment.SHKEEPER_URL | length == 0 and www_membership_environment.SHKEEPER_CURRENCIES | length == 0)
|
- www_membership_shkeeper_enabled or (www_membership_environment.SHKEEPER_URL | length == 0 and www_membership_environment.SHKEEPER_CURRENCIES | length == 0)
|
||||||
- not www_membership_stripe_enabled or www_membership_environment.STRIPE_PORTAL_CONFIGURATION | length > 0
|
- not www_membership_stripe_enabled or www_membership_environment.STRIPE_PORTAL_CONFIGURATION | length > 0
|
||||||
|
- (www_membership_environment.LISTMONK_URL | default('')) | length == 0 or www_membership_environment.LISTMONK_URL is match('^https://[^/@]+$')
|
||||||
- not www_membership_postgres_managed or www_membership_environment.POSTGRES_HOST == 'membership-db'
|
- not www_membership_postgres_managed or www_membership_environment.POSTGRES_HOST == 'membership-db'
|
||||||
- www_membership_postgres_managed or www_membership_environment.POSTGRES_SSLMODE == 'verify-full'
|
- www_membership_postgres_managed or www_membership_environment.POSTGRES_SSLMODE == 'verify-full'
|
||||||
- not www_membership_postgres_managed or www_membership_environment.POSTGRES_PORT | string == '5432'
|
- not www_membership_postgres_managed or www_membership_environment.POSTGRES_PORT | string == '5432'
|
||||||
@@ -61,7 +62,7 @@
|
|||||||
- name: Define secret-only settings
|
- name: Define secret-only settings
|
||||||
tags: [membership_validate]
|
tags: [membership_validate]
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
_www_secret_keys: [MEMBERSHIP_SECRET_KEY, POSTGRES_PASSWORD, DEX_CLIENT_SECRET, LDAP_BIND_PASSWORD, EMAIL_HOST_PASSWORD, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, SHKEEPER_API_KEY]
|
_www_secret_keys: [MEMBERSHIP_SECRET_KEY, POSTGRES_PASSWORD, DEX_CLIENT_SECRET, LDAP_BIND_PASSWORD, EMAIL_HOST_PASSWORD, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, SHKEEPER_API_KEY, LISTMONK_API_TOKEN]
|
||||||
_www_required_secret_keys: "{{ ['MEMBERSHIP_SECRET_KEY', 'POSTGRES_PASSWORD', 'DEX_CLIENT_SECRET', 'LDAP_BIND_PASSWORD'] + (['STRIPE_SECRET_KEY', 'STRIPE_WEBHOOK_SECRET'] if www_membership_stripe_enabled else []) + (['SHKEEPER_API_KEY'] if www_membership_shkeeper_enabled else []) }}"
|
_www_required_secret_keys: "{{ ['MEMBERSHIP_SECRET_KEY', 'POSTGRES_PASSWORD', 'DEX_CLIENT_SECRET', 'LDAP_BIND_PASSWORD'] + (['STRIPE_SECRET_KEY', 'STRIPE_WEBHOOK_SECRET'] if www_membership_stripe_enabled else []) + (['SHKEEPER_API_KEY'] if www_membership_shkeeper_enabled else []) }}"
|
||||||
|
|
||||||
- name: Prevent secrets in public ConfigMaps
|
- name: Prevent secrets in public ConfigMaps
|
||||||
@@ -207,3 +208,29 @@
|
|||||||
status: "True"
|
status: "True"
|
||||||
loop: [membership, membership-worker]
|
loop: [membership, membership-worker]
|
||||||
when: not ansible_check_mode
|
when: not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Find the OpenLDAP pod
|
||||||
|
kubernetes.core.k8s_info:
|
||||||
|
kind: Pod
|
||||||
|
namespace: "{{ www_membership_ldap_namespace }}"
|
||||||
|
label_selectors: ["{{ www_membership_ldap_pod_selector }}"]
|
||||||
|
field_selectors: [status.phase=Running]
|
||||||
|
register: www_ldap_pods
|
||||||
|
when: www_membership_ldap_ensure_groups | bool
|
||||||
|
|
||||||
|
- name: Ensure membership tier and service groups exist in LDAP
|
||||||
|
kubernetes.core.k8s_exec:
|
||||||
|
namespace: "{{ www_membership_ldap_namespace }}"
|
||||||
|
pod: "{{ www_ldap_pods.resources[0].metadata.name }}"
|
||||||
|
command: >-
|
||||||
|
sh -c 'printf "%s\n" "dn: {{ item }}" "objectClass: groupOfUniqueNames"
|
||||||
|
"cn: {{ (item | split(","))[0] | regex_replace("^cn=", "") }}"
|
||||||
|
"uniqueMember: {{ (www_membership_environment | combine(www_membership_environment_overrides)).LDAP_GROUP_PLACEHOLDER_DN }}"
|
||||||
|
| ldapadd -x -H ldap://localhost -D "cn=admin,dc=eom,dc=dev" -w "$LDAP_ADMIN_PASSWORD"; rc=$?;
|
||||||
|
if [ $rc -eq 0 ]; then echo GROUP_CREATED; elif [ $rc -ne 68 ]; then exit $rc; fi'
|
||||||
|
loop: >-
|
||||||
|
{{ ((www_membership_tiers.values() | map(attribute='group_dn') | list)
|
||||||
|
+ (www_membership_service_groups.values() | flatten)) | unique }}
|
||||||
|
register: www_ldap_groups
|
||||||
|
changed_when: "'GROUP_CREATED' in (www_ldap_groups.stdout | default(''))"
|
||||||
|
when: www_membership_ldap_ensure_groups | bool
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
{% if www_membership_enabled %}
|
{% if www_membership_enabled %}
|
||||||
ProxyRequests Off
|
ProxyRequests Off
|
||||||
ProxyPreserveHost On
|
ProxyPreserveHost On
|
||||||
|
# Service sign-up buttons link to /members; the app lives under /members/.
|
||||||
|
# Absolute target: TLS ends at ingress, so a relative one becomes http://...:443
|
||||||
|
RedirectMatch 301 "^/members$" "https://{{ www_hostname }}/members/"
|
||||||
ProxyPass "/members/" "http://membership:8000/members/" connectiontimeout=5 timeout=120
|
ProxyPass "/members/" "http://membership:8000/members/" connectiontimeout=5 timeout=120
|
||||||
ProxyPassReverse "/members/" "http://membership:8000/members/"
|
ProxyPassReverse "/members/" "http://membership:8000/members/"
|
||||||
<Location "/members/">
|
<Location "/members/">
|
||||||
|
|||||||
@@ -171,3 +171,39 @@ spec:
|
|||||||
tls:
|
tls:
|
||||||
- hosts: [{{ www_hostname }}]
|
- hosts: [{{ www_hostname }}]
|
||||||
secretName: www
|
secretName: www
|
||||||
|
{% if www_redirect_hostnames | length > 0 %}
|
||||||
|
---
|
||||||
|
# Send browsers on alias hostnames (e.g. the apex, whose /.well-known/matrix/*
|
||||||
|
# paths are owned by another ingress) to the canonical origin. Exact/longer
|
||||||
|
# paths on the same host in other ingresses still take precedence over "/".
|
||||||
|
# A snippet is used because the controller's annotation validation rejects
|
||||||
|
# "$request_uri" in permanent-redirect.
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: www-redirect
|
||||||
|
namespace: {{ www_namespace }}
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: {{ www_cluster_issuer | to_json }}
|
||||||
|
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||||
|
nginx.ingress.kubernetes.io/enable-access-log: "false"
|
||||||
|
nginx.ingress.kubernetes.io/configuration-snippet: {{ ("return 301 https://" ~ www_hostname ~ "$request_uri;") | to_json }}
|
||||||
|
spec:
|
||||||
|
ingressClassName: {{ www_ingress_class }}
|
||||||
|
rules:
|
||||||
|
{% for host in www_redirect_hostnames %}
|
||||||
|
- host: {{ host }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- pathType: Prefix
|
||||||
|
path: /
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: httpd
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
{% endfor %}
|
||||||
|
tls:
|
||||||
|
- hosts: {{ www_redirect_hostnames | to_json }}
|
||||||
|
secretName: www-redirect
|
||||||
|
{% endif %}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
- name: Supply nonsecret fixture settings
|
- name: Supply nonsecret fixture settings
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
www_membership_image: registry.example.org/www-membership:release-test
|
www_membership_image: registry.example.org/www-membership:release-test
|
||||||
|
www_redirect_hostnames: [apex.example.com]
|
||||||
www_membership_service_groups:
|
www_membership_service_groups:
|
||||||
"0": ["cn=free-service,ou=groups,dc=example,dc=org"]
|
"0": ["cn=free-service,ou=groups,dc=example,dc=org"]
|
||||||
"2": ["cn=paid-service,ou=groups,dc=example,dc=org", "cn=second-service,ou=groups,dc=example,dc=org"]
|
"2": ["cn=paid-service,ou=groups,dc=example,dc=org", "cn=second-service,ou=groups,dc=example,dc=org"]
|
||||||
|
|||||||
@@ -92,6 +92,9 @@ ingress = find(all_objects, "Ingress", "www")
|
|||||||
assert ingress["metadata"]["annotations"]["nginx.ingress.kubernetes.io/enable-access-log"] == "false"
|
assert ingress["metadata"]["annotations"]["nginx.ingress.kubernetes.io/enable-access-log"] == "false"
|
||||||
assert ingress["metadata"]["annotations"]["nginx.ingress.kubernetes.io/proxy-body-size"] == "2m"
|
assert ingress["metadata"]["annotations"]["nginx.ingress.kubernetes.io/proxy-body-size"] == "2m"
|
||||||
assert ingress["spec"]["rules"][0]["http"]["paths"][0]["backend"]["service"]["name"] == "httpd"
|
assert ingress["spec"]["rules"][0]["http"]["paths"][0]["backend"]["service"]["name"] == "httpd"
|
||||||
|
redirect = find(all_objects, "Ingress", "www-redirect")
|
||||||
|
assert redirect["spec"]["rules"][0]["host"] == "apex.example.com"
|
||||||
|
assert redirect["metadata"]["annotations"]["nginx.ingress.kubernetes.io/configuration-snippet"] == "return 301 " + config["MEMBERSHIP_PUBLIC_URL"] + "$request_uri;"
|
||||||
for obj in docs("external-membership-workloads"):
|
for obj in docs("external-membership-workloads"):
|
||||||
if obj["kind"] == "Deployment":
|
if obj["kind"] == "Deployment":
|
||||||
pod_checks(obj["spec"]["template"]["spec"], external=True)
|
pod_checks(obj["spec"]["template"]["spec"], external=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user