From 26d4e9b77130f36a23bc57f518ca5ef5a47c1a41 Mon Sep 17 00:00:00 2001 From: Greg Hellings Date: Mon, 26 May 2025 23:37:24 -0500 Subject: [PATCH] Configure Dendrite on kubernetes Configure a matrix database Configure Dendrite into Kubernetes Update vps configuration to new settings --- home/modules/baseline/xonsh_footer.xsh | 12 +- hosts/genesis/net/hosts | 6 +- hosts/linode/matrix.nix | 12 +- manifests/databases/kustomization.yaml | 1 + manifests/databases/postgres-cluster.yaml | 7 + manifests/databases/postgres-matrix.yaml | 9 + manifests/databases/postgres-pgadmin.yaml | 2 +- manifests/kustomization.yaml | 1 + manifests/matrix/dendrite-config.yaml | 189 ++++++++++++++++++++ manifests/matrix/deployment.yaml | 29 +++ manifests/matrix/ingress.yaml | 21 +++ manifests/matrix/kustomization.yaml | 7 + manifests/matrix/service.yaml | 14 ++ manifests/namespaces/kustomization.yaml | 1 + manifests/namespaces/matrix.yaml | 4 + manifests/secrets/kustomization.yaml | 1 + manifests/secrets/postgres-user-matrix.yaml | 33 ++++ modules/nix-conf.nix | 4 +- modules/nixos/kubernetes.nix | 3 +- secrets/dendrite_key.age | Bin 2219 -> 2323 bytes 20 files changed, 338 insertions(+), 18 deletions(-) create mode 100644 manifests/databases/postgres-matrix.yaml create mode 100644 manifests/matrix/dendrite-config.yaml create mode 100644 manifests/matrix/deployment.yaml create mode 100644 manifests/matrix/ingress.yaml create mode 100644 manifests/matrix/kustomization.yaml create mode 100644 manifests/matrix/service.yaml create mode 100644 manifests/namespaces/matrix.yaml create mode 100644 manifests/secrets/postgres-user-matrix.yaml diff --git a/home/modules/baseline/xonsh_footer.xsh b/home/modules/baseline/xonsh_footer.xsh index 6663435..b6e393d 100644 --- a/home/modules/baseline/xonsh_footer.xsh +++ b/home/modules/baseline/xonsh_footer.xsh @@ -7,14 +7,10 @@ def bw_unlock(): current environment variables. Also returns the code for them.""" if "BW_SESSION" in ${...}: return $BW_SESSION - result = $(bw unlock) - while "BW_SESSION" not in result: - result = $(bw unlock) - lines = result.split("\n") - l = [k for k in lines if 'BW_SESSION="' in k][0] - left, right = l.split("=", 1) - token = right[1:-1] - $BW_SESSION = token + result = !(bw unlock --raw) + while result.returncode != 0: + result = !(bw unlock --raw) + $BW_SESSION = result.output.strip() return token def vpn(con, bwname): diff --git a/hosts/genesis/net/hosts b/hosts/genesis/net/hosts index e37869f..9041681 100644 --- a/hosts/genesis/net/hosts +++ b/hosts/genesis/net/hosts @@ -9,7 +9,7 @@ 10.42.1.3 printer.thehellings.lan 10.42.1.4 chronicles chronicles.thehellings.lan nas.thehellings.lan s3.thehellings.lan 10.42.1.5 genesis genesis.thehellings.lan dns dns.thehellings.lan smart smart.thehellings.lan speedtest.thehellings.lan nixcache.thehellings.lan gitcache.thehellings.lan -10.42.1.6 isaiah isaiah.thehellings.lan minio-01.thehellings.lan pgadmin.thehellings.lan +10.42.1.6 isaiah isaiah.thehellings.lan minio-01.thehellings.lan 10.42.1.7 hosea hosea.thehellings.lan 10.42.1.8 jeremiah jeremiah.thehellings.lan minio-02.thehellings.lan 10.42.1.9 ivr ivr.thehellings.lan @@ -32,8 +32,8 @@ 100.88.91.27 genesis.home smart.home zwave.home nixcache.home gitcache.home dashy.home uptime.home speed.home 100.91.131.66 gitlab.home gitlab.shire-zebra.ts.net gitlab.thehellings.lan registry.thehellings.lan git.thehellings.lan 100.68.203.1 hosea.home hosea.shire-zebra.ts.net -100.84.183.79 isaiah.home isaiah.shire-zebra.ts.net -100.102.186.39 jeremiah.home jeremiah.shire-zebra.ts.net +100.84.183.79 isaiah.home isaiah.shire-zebra.ts.net pgadmin.kubernetes +100.102.186.39 jeremiah.home jeremiah.shire-zebra.ts.net matrix.kubernetes 100.90.74.19 jude.home 100.115.57.8 linode.home 100.65.5.38 matrix.home matrix.shire-zebra.ts.net diff --git a/hosts/linode/matrix.nix b/hosts/linode/matrix.nix index 41dc922..d91fde8 100644 --- a/hosts/linode/matrix.nix +++ b/hosts/linode/matrix.nix @@ -14,7 +14,6 @@ in "${domain}" = { enableACME = true; forceSSL = true; - # This is needed so that servers contacting hellings.com can find # the actual application server at matrix.thehellings.com locations."= /.well-known/matrix/server".extraConfig = @@ -51,11 +50,20 @@ in enableACME = true; forceSSL = true; + extraConfig = '' + error_log /var/log/nginx/debug.log debug; + ''; + # Not the appropriate place for the chat client locations = (builtins.listToAttrs ( builtins.map - (val: lib.nameValuePair "/_${val}" { proxyPass = "http://matrix.shire-zebra.ts.net:8448"; }) + ( + val: + lib.nameValuePair "/_${val}" { + proxyPass = "http://matrix.kubernetes"; + } + ) [ "matrix" "synapse" diff --git a/manifests/databases/kustomization.yaml b/manifests/databases/kustomization.yaml index f06faf5..f6f06b6 100644 --- a/manifests/databases/kustomization.yaml +++ b/manifests/databases/kustomization.yaml @@ -4,3 +4,4 @@ resources: - postgres-cluster.yaml - postgres-gitlab.yaml - postgres-pgadmin.yaml + - postgres-matrix.yaml diff --git a/manifests/databases/postgres-cluster.yaml b/manifests/databases/postgres-cluster.yaml index 8ea4898..23c61ca 100644 --- a/manifests/databases/postgres-cluster.yaml +++ b/manifests/databases/postgres-cluster.yaml @@ -24,3 +24,10 @@ spec: superuser: true passwordSecret: name: postgres-user-pgadmin + - name: matrix + ensure: present + comment: Matrix DB user + login: true + superuser: false + passwordSecret: + name: postgres-user-matrix diff --git a/manifests/databases/postgres-matrix.yaml b/manifests/databases/postgres-matrix.yaml new file mode 100644 index 0000000..ac52c97 --- /dev/null +++ b/manifests/databases/postgres-matrix.yaml @@ -0,0 +1,9 @@ +apiVersion: postgresql.cnpg.io/v1 +kind: Database +metadata: + name: database-matrix +spec: + name: matrix + owner: matrix + cluster: + name: postgres diff --git a/manifests/databases/postgres-pgadmin.yaml b/manifests/databases/postgres-pgadmin.yaml index ab82d78..0a4ce1f 100644 --- a/manifests/databases/postgres-pgadmin.yaml +++ b/manifests/databases/postgres-pgadmin.yaml @@ -95,7 +95,7 @@ metadata: ingressClassName: traefik spec: rules: - - host: pgadmin.thehellings.lan + - host: pgadmin.kubernetes http: paths: - path: / diff --git a/manifests/kustomization.yaml b/manifests/kustomization.yaml index 0f54c5c..ad3955c 100644 --- a/manifests/kustomization.yaml +++ b/manifests/kustomization.yaml @@ -3,3 +3,4 @@ resources: - bitwarden - secrets - databases + - matrix diff --git a/manifests/matrix/dendrite-config.yaml b/manifests/matrix/dendrite-config.yaml new file mode 100644 index 0000000..50d3b49 --- /dev/null +++ b/manifests/matrix/dendrite-config.yaml @@ -0,0 +1,189 @@ +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: dendrite-config +spec: + data: + - &secret + secretKey: username + sourceRef: + storeRef: + name: bitwarden-login + kind: ClusterSecretStore + remoteRef: &remoteRef + key: 36d1046b-727e-4e09-a391-b2e90171d3d0 + property: username + - <<: *secret + secretKey: password + remoteRef: + <<: *remoteRef + property: password + - secretKey: matrix_pem + sourceRef: + storeRef: + name: bitwarden-notes + kind: ClusterSecretStore + remoteRef: + key: 6de3500c-7c6e-4419-b4f5-b2ea0018ff28 + target: + name: dendrite-config + deletionPolicy: Delete + template: + engineVersion: v2 + data: + matrix.pem: "{{ .matrix_pem }}" + dendrite.yaml: | + version: 2 + global: + server_name: thehellings.com + key_id: ed25519:auto + private_key: /etc/dendrite/matrix.pem + database: + connection_string: "postgres://{{ .username | urlquery }}:{{ .password | urlquery }}@postgres-rw.db.svc.cluster.local/matrix?sslmode=disable" + max_open_conns: 90 + max_idle_conns: 2 + conn_max_lifetime: -1 + well_known_server_name: "http://matrix.thehellings.com" + well_known_client_name: "http://matrix.thehellings.com" + well_known_sliding_sync_proxy: "" + disable_federation: false + presence: + enable_inbound: false + enable_outbound: false + trusted_third_party_id_servers: + - matrix.org + - vector.im + jetstream: + storage_path: /var/dendrite + addresses: [] + topic_prefix: Dendrite + in_memory: false + disable_tls_validation: true + credentials_path: "" + metrics: + enabled: false + basic_auth: + username: metrics + password: metrics + sentry: + enabled: false + dsn: "" + environment: "" + dns_cache: + enabled: false + cache_size: 256 + cache_lifetime: 5m0s + server_notices: + enabled: true + local_part: _server + display_name: Server Alert + avatar_url: "" + room_name: Server Alert + report_stats: + enabled: false + endpoint: https://panopticon.matrix.org/push + cache: + max_size_estimated: 1073741824 + max_age: 1h0m0s + app_service_api: + disable_tls_validation: false + legacy_auth: false + legacy_paths: false + config_files: [] + client_api: + registration_disabled: true + registration_requires_token: false + registration_shared_secret: "" + guests_disabled: false + enable_registration_captcha: false + recaptcha_api_js_url: "" + recaptcha_sitekey_class: "" + recaptcha_form_field: "" + recaptcha_public_key: "" + recaptcha_private_key: "" + recaptcha_bypass_secret: "" + recaptcha_siteverify_api: "" + turn: + turn_user_lifetime: "" + turn_uris: [] + turn_shared_secret: "" + turn_username: "" + turn_password: "" + rate_limiting: + enabled: true + threshold: 5 + cooloff_ms: 500 + exempt_user_ids: [] + federation_api: + send_max_retries: 16 + enable_relays: false + p2p_retries_until_assumed_offline: 1 + disable_tls_validation: false + disable_http_keepalives: false + key_perspectives: + - server_name: matrix.org + keys: + - key_id: ed25519:auto + public_key: Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw + - key_id: ed25519:a_RXGa + public_key: l8Hft5qXKn1vfHrg3p4+W8gELQVo8N13JkluMfmn2sQ + prefer_direct_fetch: false + deny_networks: + - 127.0.0.1/8 + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + - 100.64.0.0/10 + - 169.254.0.0/16 + - ::1/128 + - fe80::/64 + - fc00::/7 + allow_networks: + - 0.0.0.0/0 + key_server: {} + media_api: + base_path: /var/dendrite/media + max_file_size_bytes: 10485760 + dynamic_thumbnails: false + max_thumbnail_generators: 10 + thumbnail_sizes: + - width: 32 + height: 32 + method: crop + - width: 96 + height: 96 + method: crop + - width: 640 + height: 480 + method: scale + room_server: + default_room_version: "10" + sync_api: + real_ip_header: "" + search: + enabled: false + index_path: /var/dendrite/searchindex + in_memory: false + language: en + user_api: + bcrypt_cost: 10 + openid_token_lifetime_ms: 3600000 + push_gateway_disable_tls_validation: false + auto_join_rooms: [] + worker_count: 8 + relay_api: {} + mscs: + mscs: [] + tracing: + enabled: false + jaeger: + serviceName: "" + disabled: false + rpc_metrics: false + traceid_128bit: false + tags: [] + sampler: null + reporter: null + headers: null + baggage_restrictions: null + throttler: null diff --git a/manifests/matrix/deployment.yaml b/manifests/matrix/deployment.yaml new file mode 100644 index 0000000..f951193 --- /dev/null +++ b/manifests/matrix/deployment.yaml @@ -0,0 +1,29 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dendrite + labels: + app: dendrite +spec: + replicas: 1 + selector: + matchLabels: + app: dendrite + template: + metadata: + labels: + app: dendrite + spec: + containers: + - name: dendrite + image: ghcr.io/element-hq/dendrite-monolith:latest + ports: + - containerPort: 8008 + name: http + volumeMounts: + - name: config-volume + mountPath: /etc/dendrite + volumes: + - name: config-volume + secret: + secretName: dendrite-config diff --git a/manifests/matrix/ingress.yaml b/manifests/matrix/ingress.yaml new file mode 100644 index 0000000..450bc05 --- /dev/null +++ b/manifests/matrix/ingress.yaml @@ -0,0 +1,21 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: dendrite-ingress + annotations: + ingressClassName: traefik +spec: + rules: + - &host + host: matrix.kubernetes + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: dendrite + port: + number: 8008 + - <<: *host + host: matrix.thehellings.com diff --git a/manifests/matrix/kustomization.yaml b/manifests/matrix/kustomization.yaml new file mode 100644 index 0000000..34b5e68 --- /dev/null +++ b/manifests/matrix/kustomization.yaml @@ -0,0 +1,7 @@ +namespace: matrix + +resources: + - dendrite-config.yaml + - deployment.yaml + - service.yaml + - ingress.yaml diff --git a/manifests/matrix/service.yaml b/manifests/matrix/service.yaml new file mode 100644 index 0000000..94a6076 --- /dev/null +++ b/manifests/matrix/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: dendrite + labels: + app: dendrite +spec: + selector: + app: dendrite + ports: + - port: 8008 + targetPort: http + protocol: TCP + type: ClusterIP diff --git a/manifests/namespaces/kustomization.yaml b/manifests/namespaces/kustomization.yaml index 4a33a16..5631712 100644 --- a/manifests/namespaces/kustomization.yaml +++ b/manifests/namespaces/kustomization.yaml @@ -2,3 +2,4 @@ resources: - external-secrets.yaml - bitwarden.yaml - db.yaml + - matrix.yaml diff --git a/manifests/namespaces/matrix.yaml b/manifests/namespaces/matrix.yaml new file mode 100644 index 0000000..b88de94 --- /dev/null +++ b/manifests/namespaces/matrix.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: matrix diff --git a/manifests/secrets/kustomization.yaml b/manifests/secrets/kustomization.yaml index 84079cc..3ea41b4 100644 --- a/manifests/secrets/kustomization.yaml +++ b/manifests/secrets/kustomization.yaml @@ -1,3 +1,4 @@ resources: - postgres-user-gitlab.yaml - postgres-user-pgadmin.yaml + - postgres-user-matrix.yaml diff --git a/manifests/secrets/postgres-user-matrix.yaml b/manifests/secrets/postgres-user-matrix.yaml new file mode 100644 index 0000000..dff00b2 --- /dev/null +++ b/manifests/secrets/postgres-user-matrix.yaml @@ -0,0 +1,33 @@ +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: postgres-user-matrix + namespace: db +spec: + target: + name: postgres-user-matrix + deletionPolicy: Delete + template: + type: Opaque + data: + username: |- + {{ .username }} + password: |- + {{ .password }} + data: + - secretKey: username + sourceRef: + storeRef: + name: bitwarden-login + kind: ClusterSecretStore + remoteRef: + key: 36d1046b-727e-4e09-a391-b2e90171d3d0 + property: username + - secretKey: password + sourceRef: + storeRef: + name: bitwarden-login + kind: ClusterSecretStore + remoteRef: + key: 36d1046b-727e-4e09-a391-b2e90171d3d0 + property: password diff --git a/modules/nix-conf.nix b/modules/nix-conf.nix index c94c061..4086fbb 100644 --- a/modules/nix-conf.nix +++ b/modules/nix-conf.nix @@ -30,7 +30,6 @@ ]; # For home and for work machines substituters = (lib.optionals cache [ - "http://nas.thehellings.lan:9000/binary-cache/" "http://nas.home:9000/binary-cache/" ]) ++ [ @@ -41,8 +40,7 @@ "https://cache.nixos.org" ]; trusted-public-keys = [ - "nix.thehellings.lan:0qWYHn3gGllXChhAaaxKlNZtRy6yG/XJs1RFSqV3nW8=" - "nix.home:0qWYHn3gGllXChhAaaxKlNZtRy6yG/XJs1RFSqV3nW8=" + "nas.home:0qWYHn3gGllXChhAaaxKlNZtRy6yG/XJs1RFSqV3nW8=" "ai.cachix.org-1:N9dzRK+alWwoKXQlnn0H6aUx0lU/mspIoz8hMvGvbbc=" "nixpkgs-python.cachix.org-1:hxjI7pFxTyuTHn2NkvWCrAUcNZLNS3ZAvfYNuYifcEU=" "greg-hellings.cachix.org-1:y01Jl/L5evlhxdnUW6n56AiI1k8g1wxWhTxJCe7XSco=" diff --git a/modules/nixos/kubernetes.nix b/modules/nixos/kubernetes.nix index 15ac302..23e14ad 100644 --- a/modules/nixos/kubernetes.nix +++ b/modules/nixos/kubernetes.nix @@ -21,8 +21,9 @@ in config = lib.mkIf cfg.enable { age.secrets = { - kubernetesToken.file = ../../secrets/kubernetes/kubernetesToken.age; bw_secret.file = ../../secrets/kubernetes/bw_secret.age; + dendrite_key.file = ../../secrets/dendrite_key.age; + kubernetesToken.file = ../../secrets/kubernetes/kubernetesToken.age; }; environment.systemPackages = [ diff --git a/secrets/dendrite_key.age b/secrets/dendrite_key.age index ede84b9d41dac35c87559c7d1d48c0e37aa581b6..b1088ffb56de4fec564f45cf6fc09ff31e3e09f4 100644 GIT binary patch literal 2323 zcmZXU+3Vy60mnVoibE|e^+~oe1zR7&ubE^fGZ|deB$G@glVoyCCdqg}?o94GlX(y- zQdrhjx~)QGm$v%gLm!H(;I2@rP%9LvU>6sDtW~5-TLr6l^r3WhpMSr<;KTPcJm<(s ztfI@RU79?S79Q9@y`wMn%hEq`Y#2eHX@5MbjiBupA-9`vRfn|Y$#ld(D!H%*?cApW zlIlpv*SOBb3MNnjvZtA4ik93?fy*v3%|kiMp=~l^gw4zzH1j5?u*lsb7&A{#($bEM z*KOYoP=VW;q>6MF=#9zY%HOl*9GofRIVdyxN~&9H(Q<)r^}=|@jXjBjjku2DiBE$y zKN01@6vR`K8@URb1iP_9t?=?BEndZf>-B|cAS`zw#39QA!KBW3%N4o`xVD5!fgR$Y zwI|xcoIqo}hvV4>MeQjv8IFyB$ti1|bVX`W3riZ5%0!?s(;>M~4VHl4T{OaLUszJ9-OD?id3#N#&SLP*0Ww zsq_Gq>41Xp9uu zv@~#)Hbs>gsq3a9h)G7K$WY=72^a-F+2i$U;5W;FjPo!MluXb(L>L0RKjuM{+7d=# z1cA+p;_8$(O!QsI>+?!hV>`z4A~(+PP$arWz;{s1?pbLlwHYq9)q#qv1?KfMnaQ@( zqF$9I0(!OoHGO(QMl z6*S@Lt#BA-8ADi-fQ}d7!B2Z|&Udz_2zIg9#kQ7pYkL7&hPNAgn_cUz2#h!DF|+rY zQ#4dbR7C9CW{mn{BgrZ;inRu|=ZhK?K+_7wj7izSP}PV{tI7^h_WT6hRZxM*YrGg5 zyuQptvu;%?W{8cMj5A4>9e|t7T;tT<2B2Cly?$o^IIvyrYi|RLAYF23t2#_vkDQUE zfES!=w-Qn$2}BQiYP{U*;DB5;^0F8u`bDQ-Dv5y91jm*|=l|_}Rpmnr|(VCaF2nf4%w&FmJ zWm8yKH=D9&m}#-L7@Wk6we3g3U`bW7oG;9lT!oT4M#sA`zuUl^c*@CYMT4~1F9p4u zdo2ycFqJgwd|4J9PC1NeS_Q}$ka!%P)JA28#ufWS>+w#zgoMT!qLoYuTx1q7OQ4mMOnfb)V69qMY~bQt zZ)oR3C$;d@UoByGiH0<)4kC+Yj6!Z&C&8(G!!d%J&QIkQ+Cp=<*X=|XN2YvOijq~W*3^Ly7X<)eQ^O)D zO`8%xcgo2$iM7n@iz}*vOrF=!!vLH{ie33MvQS5)0_stR!Gm5Ick4ZQwj*>>1;xDGrE7zm`x2wM}( zm~HGJ_SzTc4LjNIXM%2bkyIE0>a3b8bT;r3|`PPK~Vrr;XF-XbxF(1 zk#=Y0;Lx|x9(4a14OpEkU;O%e4-C!GPp;kj;F5J#Sa{GnP=a-M4A3XEg zcW>VR_xRH zQ{MGoJb2;Jvy0nz|NP-+a^>oU&;5q`$9eOiHzt31{>+Uxdas;&>g*Gje)Z#D|5m#B z_Ftd8Z~yM`y{jGj+-JXY?Q7rq$j7!9FZ|m%_rOcI_svJL+dup8iCfz1FY<3b{_dY{ z{PPd5P`}GBy+ZUmq23Kyqb9@~JAHV#= z8yCO#zWJjkgDXIL&kwGC_>JqI{Nj~Y{~Uh*vEwh?cX!`@>%DuhUzV?46YpwYTYm5~ F_J2v|7{34j literal 2219 zcmZXU$;Zk{y*h`hsdNO=XCVMVolF3Z=J(J~@$)3qFlXWuiA-2|&)PrqL zdeVXliu zW`n{pOBiF3Ie;Z~Gzjxx+X$ecy!tl4uLmosmU&VVCJ*D9fNg;SGk z>wxWDTCLT3nyvfXJ#3wsk4qKlBvL$pxp-#a<2mzrA@JfZp-{0xTz8wT%%R2^D+I@j zPGVZ?g6VDsxXe|=J!g#&5_KlVAGf1*$nMg7u`rMG8uTRu8hRMg0xs!979zuB!`pDQ z$E6*O>Hw{@ReQKbFtlnX!5l~bhKMNN5T;R{4yw-Ztm2~uadGIy*|B5U%;tQ$OACFN z_ULuuKx#KD6+bw~ZL<{|+mDIM+1j-`gpY?@2`Eppnu0uKD?%m;K~4AJG4++JD%fRY z3360;mnnlJwi&z1a6RIbApTw2RUMj^UXSyPi>=IdfXZMZU~ePgY^zmEa|t;gK|Id_ zVjIeOZk<+fNzffjp9N6|Kx)~RJE`QZksFo;zLl3?KQcUm?cDZq&l1|m(@nO{_8cRy z8Ev~f;KdWq98VnMc%pa$PN@_)S90OYh5_dSS~&+PCmT<#u`A(vt#VPfTS~JPJQ}^& ziijEvxrQSy4ROyBhMYsfSf{~)$61C8{6-sRKpxNy7Nr7LppHcGo)?T*PVfPAW#|ra2QEF(m-rAQ5X}bxUVBU)Jm>KBxVj>*UKl8tF7HW~@yG%DHIK8i}IV2=lG- z5Rnrr(tC%k(<0it4hw*n0Wg%+W^!?h=$M#ngDTWK5wbf8CGc}%;M-pFTknM7xxi&( zfA=>CbbBdVA4$p;2g8>PF&w3SDw7g9%Defd?QGh%Jn2w`G=|lFbz0qOTOURbu1mW= zt3zvv0oo=4?#kJ~WN1kN#)gwxyFA@TRDNVh=uS9gCGnL&(e0NS_LON zdz{h>0dBfNRWfen9`h-1Abo@a5Fhr?zydrS9Os3>4u+Zww;fP{o3l0G!rg8ASk72u z-7DgaBD}4Df;TMk%tp(uSt=okor|>>n5vL^s2=ZadS0=*NtIHc2hwplxw0gW8n0na zK;?;VfFhVFqm!vc90vD(8#Er|jdc;hG`>HXmYUF2?%y~bBD_%p)5S?<8AaQ?bVyl( zHHKT!S72k6mUZ(xoxyjAaq)%)PDZpb93nnR0MSNMw}9~J9&LBNO{uZId0{Y6X6s4= z+3x_Jp*Bm5R;p!^HR)XY&^peu9@ka|tv28Vnoau=AZ+b-;OJ}V z(i`bB8QZFv*V zE;E1NB0L$Mzf(o*AOV2mj42Yp%r&~}B$}LIhhAF?v57-V(lskPtL_MCTXI)7sf(aR zGW4;lTqosNk3|`+d~G68(13R%ixgvsY7)XKQ_Bc>PXsxDE~845MN?+Xyn689!J4`x z=Kc)%Q`YWp=0?4==RwyX<>*FXRfc_zJICoz&HlMv74N?B`LBHX3;yvn_oJWriT>u3 z_TQiU?FfG2bI<*#dgpf^`}xE6*Zrs7`0)>3>en*$_<8h+cb(TC|M7+Ap68$Xn)T=( zPyCA?eAt?&Nx(Mu0M^q&s=ul>z0J*|FQ zd+YeipS}3XQ?*0ZuZe$tW&@?~J^kvxxo^DvMdc6jFWyhT_rsSz{Qh5R?H%aNXFvYJ V|ESNt_qSKS55MyJhxlLR{{xI9*xdjC