--- - name: Merge public membership environment overrides tags: [membership_validate] no_log: true ansible.builtin.set_fact: www_membership_environment: "{{ www_membership_environment | combine(www_membership_environment_overrides) }}" - name: Validate membership deployment settings tags: [membership_validate] ansible.builtin.assert: that: - www_membership_image | length > 0 - "www_membership_image is search('(:[^/]+$|@sha256:[a-f0-9]{64}$)')" - www_membership_environment is mapping - www_membership_tiers is mapping - www_membership_service_groups is mapping - www_membership_environment.MEMBERSHIP_PUBLIC_URL == 'https://' ~ www_hostname - www_membership_environment.MEMBERSHIP_ALLOW_HTTP | string == '0' - www_membership_environment.MEMBERSHIP_TRUST_PROXY | string == '1' - www_membership_environment.MEMBERSHIP_TIERS_FILE == '/etc/membership/tiers/tiers.json' - www_membership_environment.MEMBERSHIP_SERVICE_GROUPS_FILE == '/etc/membership/tiers/service-groups.json' - "'MEMBERSHIP_SERVICE_GROUPS_JSON' not in www_membership_environment" - "'MEMBERSHIP_SQLITE_PATH' not in www_membership_environment" - "'MEMBERSHIP_TIERS_JSON' not in www_membership_environment" - www_membership_replicas | int >= 1 - www_membership_worker_interval | int >= 10 - www_membership_environment.LDAP_PASSWORD_SCHEME == '{ARGON2}' - www_membership_environment.LDAP_BIND_DN | length > 0 - www_membership_environment.EMAIL_HOST | length > 0 - www_membership_environment.EMAIL_USE_TLS | string == '1' - www_membership_environment.DEX_ISSUER is match('^https://[^/@]+') - www_membership_environment.LDAP_URL is match('^ldaps?://[^/@]+') - (www_membership_environment.LDAP_ALLOW_INSECURE | default('0') | string) in ['0', '1'] - 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) - not www_membership_stripe_enabled or www_membership_environment.STRIPE_PORTAL_CONFIGURATION | length > 0 - 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' - not www_membership_postgres_managed or www_membership_environment.POSTGRES_PORT | string == '5432' fail_msg: Supply a versioned membership image and configure verified LDAP, SMTP, Dex, database and payment settings (see role README). - name: Validate service group level mappings tags: [membership_validate] ansible.builtin.assert: that: - item.key | string is match('^(0|[1-9][0-9]*)$') - item.key | string in (['0'] + (www_membership_tiers.values() | map(attribute='level') | map('string') | list)) - item.value is sequence and item.value is not string and item.value is not mapping - item.value | select('string') | list | length == item.value | length fail_msg: Service groups must map explicit configured levels (including 0) to lists of LDAP group DNs. loop: "{{ www_membership_service_groups | dict2items }}" - name: Validate optional CA file mount paths tags: [membership_validate] ansible.builtin.assert: that: - item.value | length == 0 or (www_membership_ca_secret_name | length > 0 and item.value is match('^/etc/membership/ca/[A-Za-z0-9._-]+$')) fail_msg: CA paths must refer to filenames in the configured Secret mounted at /etc/membership/ca/. loop: "{{ www_membership_environment | dict2items | selectattr('key', 'in', ['LDAP_CA_CERT_FILE', 'SHKEEPER_CA_CERT_FILE', 'PGSSLROOTCERT', 'REQUESTS_CA_BUNDLE']) | list }}" - name: Define secret-only settings tags: [membership_validate] 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_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 tags: [membership_validate] ansible.builtin.assert: that: - www_membership_environment.keys() | intersect(_www_secret_keys) | length == 0 fail_msg: Put credentials in the Kubernetes Secret, never www_membership_environment. no_log: true - name: Validate optional Vault-provided secret data tags: [membership_validate] ansible.builtin.assert: that: - www_membership_secret_data is mapping - www_membership_secret_data.keys() | difference(_www_secret_keys) | length == 0 - _www_required_secret_keys | difference(www_membership_secret_data.keys()) | length == 0 - www_membership_secret_data.MEMBERSHIP_SECRET_KEY | length >= 32 fail_msg: Provide the documented secret keys using Vault or a pre-existing Kubernetes Secret. when: www_membership_secret_data | length > 0 no_log: true - name: Create membership Secret only when explicitly supplied kubernetes.core.k8s: state: present definition: apiVersion: v1 kind: Secret metadata: name: "{{ www_membership_secret_name }}" namespace: "{{ www_namespace }}" type: Opaque data: "{{ dict(www_membership_secret_data.keys() | zip(www_membership_secret_data.values() | map('b64encode'))) }}" when: www_membership_secret_data | length > 0 no_log: true diff: false - name: Read deployed Secret for validation and rollout tracking kubernetes.core.k8s_info: api_version: v1 kind: Secret name: "{{ www_membership_secret_name }}" namespace: "{{ www_namespace }}" register: _www_membership_secret no_log: true when: not ansible_check_mode - name: Validate deployed secret keys without exposing values ansible.builtin.assert: that: - _www_membership_secret.resources | length == 1 - _www_required_secret_keys | difference(_www_membership_secret.resources[0].data.keys()) | length == 0 - _www_membership_secret.resources[0].data.keys() | difference(_www_secret_keys) | length == 0 - (_www_membership_secret.resources[0].data.MEMBERSHIP_SECRET_KEY | b64decode | length) >= 32 fail_msg: The membership Secret is missing or contains invalid keys. See the role README. no_log: true when: not ansible_check_mode - name: Validate mandatory secret values are nonempty ansible.builtin.assert: that: - _www_membership_secret.resources[0].data[item] | b64decode | length > 0 loop: "{{ _www_required_secret_keys }}" no_log: true when: not ansible_check_mode - name: Read optional CA Secret revision kubernetes.core.k8s_info: api_version: v1 kind: Secret name: "{{ www_membership_ca_secret_name }}" namespace: "{{ www_namespace }}" register: _www_membership_ca no_log: true when: - not ansible_check_mode - www_membership_ca_secret_name | length > 0 - name: Require the configured CA Secret to exist ansible.builtin.assert: that: _www_membership_ca.resources | length == 1 when: - not ansible_check_mode - www_membership_ca_secret_name | length > 0 no_log: true - name: Compute configuration revision without secret contents # resourceVersion changes trigger a rollout without exposing secret values. ansible.builtin.set_fact: _www_membership_revision: "{{ {'image': www_membership_image, 'environment': www_membership_environment, 'tiers': www_membership_tiers, 'service_groups': www_membership_service_groups, 'secret_name': www_membership_secret_name, 'secret_version': (_www_membership_secret.resources[0].metadata.resourceVersion if not ansible_check_mode else 'check'), 'ca_name': www_membership_ca_secret_name, 'ca_version': (_www_membership_ca.resources[0].metadata.resourceVersion if www_membership_ca_secret_name | length > 0 and not ansible_check_mode else ''), 'rollout': www_rollout_revision} | to_json(sort_keys=True) | hash('sha256') }}" no_log: true - name: Deploy public membership configuration and network isolation kubernetes.core.k8s: state: present definition: "{{ lookup('ansible.builtin.template', 'membership-config.yml.j2') | from_yaml_all | list }}" - name: Deploy persistent membership PostgreSQL kubernetes.core.k8s: state: present definition: "{{ lookup('ansible.builtin.template', 'membership-postgres.yml.j2') | from_yaml_all | list }}" when: www_membership_postgres_managed | bool - name: Wait for membership PostgreSQL kubernetes.core.k8s_info: api_version: apps/v1 kind: StatefulSet name: membership-db namespace: "{{ www_namespace }}" register: _www_postgres until: _www_postgres.resources | length == 1 and (_www_postgres.resources[0].status.readyReplicas | default(0) | int) == 1 retries: "{{ (www_wait_timeout | int / 5) | int }}" delay: 5 when: - www_membership_postgres_managed | bool - not ansible_check_mode - name: Apply versioned database migration Job and wait for completion kubernetes.core.k8s: state: present definition: "{{ lookup('ansible.builtin.template', 'membership-migrate.yml.j2') | from_yaml }}" wait: "{{ not ansible_check_mode }}" wait_timeout: "{{ www_wait_timeout }}" wait_condition: type: Complete status: "True" - name: Deploy membership web service and reconciliation worker kubernetes.core.k8s: state: present definition: "{{ lookup('ansible.builtin.template', 'membership-workloads.yml.j2') | from_yaml_all | list }}" - name: Wait for membership web and worker rollouts kubernetes.core.k8s_info: api_version: apps/v1 kind: Deployment name: "{{ item }}" namespace: "{{ www_namespace }}" wait: true wait_timeout: "{{ www_wait_timeout }}" wait_condition: type: Available status: "True" loop: [membership, membership-worker] when: not ansible_check_mode