From 2fa8685a97788ba173d8124d4d1941943256e3c4 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Mon, 7 Oct 2024 10:04:28 -0400 Subject: [PATCH 01/65] Update installer docs, comments, pw --- deploy/ansible/group_vars/nuc/main.yml | 10 ++-- deploy/ansible/host_vars/localhost/main.yml | 2 +- deploy/scripts/install-combine.sh | 51 ++++++++---------- .../images/03_Enter_WiFi_Passphrase.png | Bin 35602 -> 110100 bytes .../nuc_instructions_for_use.md | 2 +- installer/README.md | 6 +-- installer/make-combine-installer.sh | 10 ++-- 7 files changed, 37 insertions(+), 44 deletions(-) diff --git a/deploy/ansible/group_vars/nuc/main.yml b/deploy/ansible/group_vars/nuc/main.yml index f9084dc30b..be45ba5490 100644 --- a/deploy/ansible/group_vars/nuc/main.yml +++ b/deploy/ansible/group_vars/nuc/main.yml @@ -1,5 +1,5 @@ --- -################################################# +################################################ # Group specific configuration items # # Group: nuc @@ -27,7 +27,7 @@ k8s_user: sillsdev install_ip_viewer: yes install_combinectl: yes -####################################### +################################################ # Ingress configuration ingress_namespace: ingress-nginx @@ -48,10 +48,10 @@ eth_optional: yes ################################################ has_wifi: yes ap_domain: thecombine.app -ap_ssid: "{{ansible_hostname}}_ap" -ap_passphrase: "Combine2020" +ap_ssid: "{{ ansible_hostname }}_ap" +ap_passphrase: "thecombine_pw" ap_gateway: "10.10.10.1" -ap_hostname: "{{ansible_hostname}}" +ap_hostname: "{{ ansible_hostname }}" ################################################ # hardware monitoring settings diff --git a/deploy/ansible/host_vars/localhost/main.yml b/deploy/ansible/host_vars/localhost/main.yml index cf6b4bcc85..d1d6186be3 100644 --- a/deploy/ansible/host_vars/localhost/main.yml +++ b/deploy/ansible/host_vars/localhost/main.yml @@ -43,7 +43,7 @@ eth_optional: yes has_wifi: yes ap_domain: thecombine.app ap_ssid: "thecombine_ap" -ap_passphrase: "Combine2020" +ap_passphrase: "thecombine_pw" ap_gateway: "10.10.10.1" ap_hostname: "local" test_wifi: false diff --git a/deploy/scripts/install-combine.sh b/deploy/scripts/install-combine.sh index 2a969c4f39..b7bb29b170 100755 --- a/deploy/scripts/install-combine.sh +++ b/deploy/scripts/install-combine.sh @@ -1,19 +1,18 @@ #! /usr/bin/env bash set -eo pipefail -################################################################################ +######################################################################################### # -# install-combine.sh is intended to install the Combine on an Ubuntu-based Linux -# Laptop. Its usage is defined in the readme file that accompanies the packaged -# installer, ./installer/README.md (or ./installer/README.pdf). +# install-combine.sh is intended to install the Combine on an Ubuntu-based Linux machine. +# It should only be executed directly by developers. For general users, it is packaged in +# a stand-alone installer (see ./installer/README.md or ./installer/README.pdf). # -# Note that 2 additional options are available that are not documented in the -# readme file. These are intended to only be used for debugging and under the -# guidance of a support engineer. They are: +# The options for this script and for the packaged installer are the same. Note that 2 +# additional debugging options are available that aren't documented in the readme file: # - single-step - run the next "step" in the installation process and stop. -# - start-at - start at the step named and run to -# completion. -################################################################################# +# - start-at - start at the step named and run to completion. +# +######################################################################################### # Warning and Error reporting functions warning () { @@ -34,7 +33,7 @@ set-combine-env () { # Collect values from user read -p "Enter AWS_ACCESS_KEY_ID: " AWS_ACCESS_KEY_ID read -p "Enter AWS_SECRET_ACCESS_KEY: " AWS_SECRET_ACCESS_KEY - # write collected values and static values to config file + # Write collected values and static values to config file cat <<.EOF > ${CONFIG_DIR}/env export COMBINE_JWT_SECRET_KEY="${COMBINE_JWT_SECRET_KEY}" export AWS_DEFAULT_REGION="us-east-1" @@ -46,8 +45,7 @@ set-combine-env () { source ${CONFIG_DIR}/env } -# Create the virtual environment needed by the Python installation -# scripts +# Create the virtual environment needed by the Python installation scripts create-python-venv () { cd $DEPLOY_DIR # Install required packages @@ -64,8 +62,7 @@ create-python-venv () { python -m piptools sync requirements.txt } -# Install Kubernetes engine and other supporting -# software +# Install Kubernetes engine and other supporting software install-kubernetes () { # Let the user know what to expect cat << .EOM @@ -105,8 +102,7 @@ set-k3s-env () { fi } -# Install the public charts used by The Combine, specifically, cert-manager -# and nginx-ingress-controller +# Install the public charts used by The Combine install-base-charts () { set-k3s-env ##### @@ -150,8 +146,7 @@ wait-for-combine () { # Wait for all combine deployments to be up while true ; do combine_status=`kubectl -n thecombine get deployments` - # assert the The Combine is up; if any components are not up, - # set it to false + # Assert the The Combine is up; if any components are not up, set it to false combine_up=true for deployment in frontend backend database maintenance ; do deployment_status=$(echo ${combine_status} | grep "${deployment}" | sed "s/^.*\([0-9]\)\/1.*/\1/") @@ -168,8 +163,7 @@ wait-for-combine () { done } -# Set the next value for STATE and record it in -# the STATE_FILE +# Set the next value for STATE and record it in the STATE_FILE next-state () { STATE=$1 if [[ "${STATE}" == "Done" && -f "${STATE_FILE}" ]] ; then @@ -179,8 +173,7 @@ next-state () { fi } -# Verify that the required network devices have been setup -# for Kubernetes cluster +# Verify that the required network devices have been setup for Kubernetes cluster wait-for-k8s-interfaces () { echo "Waiting for k8s interfaces" for interface in $@ ; do @@ -278,9 +271,8 @@ while [ "$STATE" != "Done" ] ; do if [[ -z $RESTART || $RESTART =~ ^[yY].* ]] ; then sudo reboot else - # We don't call next-state because we don't want the $STATE_FILE - # removed - we want the install script to resume with the recorded - # state. + # We don't call next-state because we don't want the $STATE_FILE removed; + # we want the install script to resume with the recorded state. STATE=Done fi fi @@ -304,18 +296,19 @@ while [ "$STATE" != "Done" ] ; do ;; Wait-for-combine) # Wait until all the combine deployments are up - echo "Waiting for The Combine components to download." + echo "Waiting for The Combine components to download and setup." echo "This may take some time depending on your Internet connection." echo "Press Ctrl-C to interrupt." wait-for-combine + echo "The Combine was successfully setup!" next-state "Shutdown-combine" ;; Shutdown-combine) # If not being installed as a server, if [[ $IS_SERVER != 1 ]] ; then - # Shut down the combine services + # Shut down The Combine services combinectl stop - # Disable combine services from starting at boot time + # Disable The Combine services from starting at boot time sudo systemctl disable create_ap sudo systemctl disable k3s fi diff --git a/docs/nuc_instructions_for_use/images/03_Enter_WiFi_Passphrase.png b/docs/nuc_instructions_for_use/images/03_Enter_WiFi_Passphrase.png index 9d2986005c0a2b690ad03f3a5aa73356fde56eb3..041664b6eb02393c52dbcd62c9b8361d0a2bb63a 100644 GIT binary patch literal 110100 zcmV)jK%u{hP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGmbN~PnbOGLGA9w%&|D{PpK~#8N%>8N9 zEy;Bqh~=B-`PDq3P(T%?0x=LEH~<7kiKN&PB~nAXMY-3K-0ge}>tszqcR)6lBA<6~Ds@l#SlI&o~$Jyr}+NZK$}&7em#y3}lp3kbS3 zG{j<{i}El8J(8MZOX^<3ny*jxj|Y8#vO8PAb2AukmQEu{4ow{U9YydwDlW6txg!@K zU)nSQe%_mdD?u|~E>1P2{kqw`4ro!<=SgI?3~PCSa)sxqbqt1>s~=a^N1D5BS?R`! zJ#jTi^~Dx7o!<6h>A?hR&urtxY^;0xw9d9iNN9pAW{PDN8J^ked#Y9}cpPi%LG~C= zj0>_hm$-2IH;d)I=l5C`Pn-9l?(M$)y+X`U296Ly0*l9op!T_O{8wbbe`{|8$lQC{EXR???&HkcLBt0?5$>f!& z@;K_0Hc{;Lum1`xsj`1D9)P9j5G{M!E09|SYNQrfMZYYQ$7S8vV>ts=`eY%DT}@uk zw;ZKn>~r)-y%9yvE3$_O&xwfqv0XA5L4|SLxp(yWLcq6xMoJ#EM7t}Bv5%6sv>EGs z1+X`pfWSwyF)^x*Xj)86RrCmcEmpM!j<1>_g^thhr&`>zF*>Kp0acY}r$OhwKpH7Ond?9MP z?L%vk%LZwUPf){0ol(S*;r0}^8+jwtFlpE0&uM8D56(6LeTbKW`B0diZdwgJn>RKE zzHifE;W1nD6PT-l@)=iw{;X7TgtwIrkY_pbXnHbwPQaY62gbx$v|HP{F-E<(T}Ivy;#q*Q9l72Y(Z=N5Jhte^c%o;Y zgCE`6xHY^Er+LB!PFJ?)=ms>XBhVH9M}SJ)DV5>U>Z3=)gi3^#aIi{_$Y$dXp0a|oQDdDp!M~9AIngro&^zlLJ{Cpv--T1v`+SI>X@@M)5-~6G-E)>aTd^J0WvN1qh3q^nikQ*bgECoZ9W4gS^B(D zx49UtK?BW1H-jxElS`}$inQJJ(RPZRcKx;2hpX3L8?H#Xe64KJzINla;Wa-nLr}0S zz#P1-Ww*XHi17e5;LH4dWVw6WL`{b?&AGg6MD`|QLoCZ$Xfma$n2X&&yGdJOdc%m_ zS9AiJdF$muPDf)FjOl694j#(N`KU1cnpzUCPhylxS`SUj$FWXLDJx$j7NK%p%k;Z& zzU26Cp#v#ZLSiOw@-c@c*$J`$v86h5V*?l@Jgs^WAImznOPU>2=`>LX@#~x|@c{CS zJK0CRP0m>DLcbWXA%br9$`~If{c(8A==NNj9F*M{&+F-d<9*VudbUkt z1Bp*OQmY#VHi$?9G8-@vtdIGzGj3AJI!BR9I}imjgm_|hzb(MOcrD8KaRhSR7fT}L z%#+9DFY?A6^?cuqvvxO!Vn@G|{TGj+Ijxu{;Jbob+ulf6EG1O8Hf(Fqe#AyI+LlK( z*|s9fH)0K^my;s;dR*9heS{dx2@8jM9MsT6H-x7gMP{eXI4RW{-ML~A5&ap`jyeFB zyqoB&cIjezV~TdRfeyFI+g5o!ou~oK^$EKEuI+kO^3@C1 zheuz&IQ-FLr-!Fry{wDQ?V6C+1~OX?uu2BZ1IYA<_N`&$7R#tNVtz3R#QuP&;*s&U zifOhZ=5hdx3)5y9u5 zU^Ty}*8_QTeG$S5UDHsG+qr%sC=~j~SeEA4gE9@o(4BW8I=pUqR)wA%e5Y;P2(dS= zdBuVdukbd)wADqw)1HJzj@NW?4TRIoVG`5rhj7}J8+Fi*LM~1bbt9;=1k+V|>?_8{ z9Dun#gr9{VM#HpIR2<5ycDZYe_d$zsXi8h_fuk^+e+>XUHF73nj}SjxjYdT1zak5q z>8Fdu1PUf&GWhvWmpoK%eJD1>dOqRp)Q@eCOasWc5zM0t@l)RCrI2A|hX1=3mglA7!%)I3ZPYE5;qWKKK{^8ma|gSceW2_DipV$S-vyvv=2y z;T?A#9R9?^w+=t^u3Ltmc-x8LL-!vY-gW1p;VrlB9}e!>F+6kX%J9;;Yo^a$h^4-! znC2HT`pbp=b~#zVc41_F_F^!CjGZHncT+&uYxnvAf-i;wdLU+w8BC}f#QOu^P%Te*=<*+rX_VlOrj5!5a+-@>v*F)M_)QWwiK?d>3|9K{@ zaSJ4MY-&p0zXl_J%*p9a=Ag}wx+W~mnV8Jgz!6d|j8befA|(sv>=Tg$=5MI1 z#Mszg0nQbskKt}ATNFYG2_!XAK!CPuDwGm98N7MI>BIoqXA^8&B8%|_V4Z1BZ$w|FyMX3O zkIe&Y)yfKawsDI|C!i<;!6Q5^XD0&QQ$5fn?QD?;aS!N|yB zrEjzVB_@`!&lZul75F$`=PVg8Hgk2Fx0Clx16tZkiZ+Lg zD-1QuLys~+LQ?&RB{BNQLn-r8^CmPuE3HSsb~dNvXK@bDtyq;c4L+D#F(V`g&W_bukedX8x!b zi-w1Ofyj&g44zZ#mY+6fG&244J;H%`u`FQ?hcIs0f1Nm(GneKgTcpoEm79@0%6PZe zNvlA+*rEr>Y#x}$1G{(VV)7k#931XHxleO>^ref#SD!hrTYOFum%ZP8*P-FU})?!nB4?P_w9tQw^h-sSO$C%g__!pkcuL%82u;d3K2koUem(PUSiCrUG z(Hrmg63cy7Ml%jrQ91+cj(PQD+%)D#7V}u1hc43B`e^HnyXlsVm`j%18qUpt`Sx;J zfzFUebjHwGKuzS-(8+6~bT=Z;AZBQ~dVH{SFyqLw1x4`%67)aZk86r3F^1G zGjzJEL*v@F^27-H-d)>=yJWF=`{CWg4wv1!xC2(%h(lkTk19K;p_o0=Kqi*Y6>c6?A6tSPb>Lk)-=X%|^5XtR;@!W3)8 zIG`ICJyPVM9zU|J^I-(_#X42UC5%(`oWJH>Y~v#ebvsZpIaW9qwJ$)431CO@+zvE< z*{T%dv|(_4r~U}~Y@4M}^$ZgZ4M{(BpG0_@>s!sNkOM!9LEZ9D&BkR2SCkGY zx3&Z@pgnR&1iSZ5xP*3)qP#Bq7;ufRde*FF^8wlyi=j44KzWu+X^y`L<7fe#rmK4J zHMXki@o1&O2ea;N0Qj*GkxP(g{@Q28H8lGsXa~Sjzd!>07{h+qchzMnW(C?6W3CDx-q?qx*Mhj%P1jAD%vSMHiLNp1GozwuPc+Fc0kBt_n9^!{B|AF=4c6i`!+i zSpq(Gy-q>Kk2!e`dyjD$m+&^!GY&B43uvg*8BxqiE%((?oj{N2Sx7(gT^VLbYm=!O z*skjIdkEM6>ip+0H>S*I?MdHepGb*;w#kc3b=Vd_-L*Bw`vJDSJ#v)=P>y&)AO(2W9zdHI@V^ z#rmo6vF2m{HC7&)ynKuyF-^xpWMsm8jMXNNWfudI0AgxNAsBf@Js-OBI91vL3f(U7R;{DjIU`lg3ZiWjdEtt5a( zlfu}^dBkC$g3ffXC-P#qC`4nhdwxNp#jES)^ zFM?*xKX7~-)!=VhA+(=Eu{CEV$CsF*f{+q6+D5QlUiR^s$345Y4fmbcJG|wVeZ%qn zyN2DefJDEzjD6z3uHlx0yNBbl#VvvWEFzD}qVs`U4-EH9%n_+$(aD!9V64r7`S~1U zzH8xiq6U3EyN7F!6Y1a$uS<4xvqPtft$SOFYh z;UK*^WPSJaGrQSpUcJ}^jG5hk84y=CDL6m=i5Zc`3x7_x!jYCzoyB5D0(5}9gqzH- zc>{X7#h%BkK~FcsTn@f?WbJ4(*nE`bSw%whKr5qXdgW+`4d!47nf;No;J#culwpN)hRNE*y{Lh79IUA zf{$ae@zrm0m&HOs$rX<*9&ESv8zOdW->%_<_a7ZT^yZ^_`=8qm?->s5-8o#nA&U}O ztl*0_w;kFu+2z}E{v#Tj9oMaIVxu)k0a?%ko^%HO|x z$FO6^HrXGAJDxR|*Bv?L)46iC*jD&9Z4MfPO39=x0jdq=(i=J8byxzJZ{ude3ON;R z-M0$B$0o+GtVBZ(ajGq)$<1t$J8Appk@+-C9;%+%6~3M!;Wl}Bz!nRU*?q1VjN9~w zx>XUM<^jO&Vz`;`$@2y9^$nr%JY4 z**p?|&5wn!e2L1_Mzd*n-dFh^>TrdV>}`Z^$1M@tgxHyL-H(PeuQ@=+s*SE0*)EOH zHlP`8fO4(Q*oe%w7Bx1}*1o0GT>w9MX79y#dz{~h$OnA8R|m9hHq=qp>`$}dalE7~ zTWYAy?7Fc!;OC>7QJ*OpC!l=uTfoI(k1Os8*TLPi9GDVvw4LA$8#g0=>um>y|M^Gm z8GiEZCv{bnHLLCqtxa!}z+9w)&+p;TmuQSRnJIDEj%rlb!g^rmTX*@2TqcNkyH+M~Zhf zFh`yHF3~VaC?M<5#|Y4@Zd787Z#QItwAJKoKu?2+e86*@zc~&G7{ngC0jh1O408Nb=^+-p;j;N zRecG-i(|^LI8Nr{pmI(#_LGjuu-KoW(ULlv6J04Z#W6NHNxg=e)*3V#2D4$os5Lxz zJ?1-h=&&ujY55woJgil*P`vlp-r+BN@Xp}}-+Xj9ymzN2g|m`i6&P10r8Ie{8X-=bvF}Y0bBS%j?9PpzF~9qF?auC z#J=IqqkHx2G0rZ(?e>F8#xE6Jyjp%w9kD8f1JNIh+O~R=i?0)jZDQj5m;;+?B01sU zf#wEqGR|YPO}&hXf|(9szDP+!yfF!&Nw;CFKBs2xdsrkGz%%YdH|7xG?7f`S9vyS- zbY}|4@qTDqHN&J|L6@I7f~_}31sz?&4A#cX@S3r*9_G?>8ds)kHf$QVG<;j@CN?`i zJy?L(AsRi`herr0F@e)Xk2l&|InQ@>C0V_z8Q^ndohIs4O05)daR7K7kDh%F8^;J= zL*h;lsFv3g4rn?y+PyzPgNEnL3#XBn}E!J{V9`41hxG`bLxkmC}boe1z{PQOASjeBgcx@23-Usw&AHGEwkbm^(nc>&J_R{eC z-#I0VO4&Eup_-S=$sc?`%q9AP>So0_3DxW2alCk3S~-F)+suV-0_JAEy|%9d(ylr_ zX51*~BC1@Xr|>qQ#}iEYcS!e#0A{J<8PlCZZ?5nSAyiI75_q3&sZTq=xHty1wQ4a6 z^G)CddthwSxs=!zG`VEjYw{3I)hV0mwsx6&K^-d)$q5>r8W`Z?!HIglpPq(Ch?k~D zHaQr_d_Z-NWI^K<#5O=1;Oz-G&p<=9MzcQ+6Pxc(z^04go2*)TaYQuxzI*WFNC0os zcQbCMpW4Od<%^J%V2`N01igGsRxuY(^u`fE9yq-*vUBI2g@#(Z*N@12y}Z30$b7WT zwQF5UkW^@h#r4KqIj!GQsn8vRg-<|joTq%0&KxMlxvK^7d} zdGVrNBK_(!=Y~gLzBGL2rHfj=B@4&r&RmhGZF;5>m%Op?{KoU=l?PurGwJ&(b>xIa zBNmYOX857KyM|j2?H=wqws&~b$$i89C-=*C|M2d+4-FrA>v8=I=fhIR!suOh9@H=U z9No7|Z&UPL$wk(=p`SUB$C=3>9MFVPa+Src+Dn_4-pjTuh82>7SLYak;-R-}sloXA z*Mu8O;aCpF_icRH3BQ|u*U`PhH)ZZW@yM&gA3Sy9hHMUrp3mD^k6g5? zDflMn`Ln#)=9~gO4$;%k;JA}9+15c#k*A2ZFcz))F=POVoUD&c(Q0Vg6WL-YLz2F0 z_hP8r7!=z;X6u0S*P9ObqmMBaY#T9&sjsp1g@=e*hK{+5Nlq=)+w?7#ETraHHU*LE zoU{GLu{w0)BxYeO6M#RN*8s0Mt1rH_-IGVXZVHKJjR`?Mx2Q)SDa|;fj;7aBzqK1K z5P1RW_6eANsm+6W)X4O!Nho2#<_kwq8-fjWoWd4IeqvtzO!l5bLdZUFdi8ryv5>lK3i4`@AL738V4=<%;spy z)9zfZjMe_JEzK6@pg-YIaXd;!OE)OmCgYd3=Cu^->(X!KmmF$Nwkh`7C}^6_-f$a| zAhrdZM+N7=f#Y#w(&wGEOK>hkf$U=8QfH(Q*rb05BKG1bCW5;Fg_ug}O_&@)d2Zx_}`^n*jb61DI_u1!$|McUJ z4Nt##aTxp;H?F@n+;wvQ@IU>byM}-3`)<{rU3^*=m;dAoFAV?co3H4C<@}ZFcA4AG zV#xzOSBcBjxWy0_Iryv}KG%n{XINC?%qT9IW1)0S7Nhub3C@t>3qbgcCNjoC={W1s08E0d0 zlXk@7a{jkpyr7>+M6OUcuxF=Uo`2Jc{j#vWtP9^u;=`gBXXfyk&-+j8({Ib;QuPaG zujoP?UyuO@3&K4zsJGuP^CI&DytUgMuwTE`a^=^Rh7i z@Po&+ufO-1=ZF92?>sr2xpaNly>lEZXJE7lFpmN(*6}U=w@EHI!-=tDzj9vj?fq{& zcV7S4K;9V7J}Efc{GjYtoQ=fTFy?pOS$=vGbBjDNcUTBNsh{V(uJ({c0>8fqi;1EB ziC3Lje8dW_&5uI+LA%F~iFKe7)L6&?{jV8clV^Q!aQ+71i4X2Nz5s7(auN7`iO8Wq zqhnh|X_VecA|cUlg5kHe&Q+;4P)c<8etaLk%@}JpIzC2FL!v+G1DS@|IG@@i&!O4R z#b1Xl57>4WlR%Rn>x9_U9PNfCH^3@lALU1zqUKm$uL{hU_C=+L5Zi47Dr{TZ)V%m0 z?Iy&y5ivi{tC}nHlq!3Po=Lov*XK|0|GQC*D)D0*x{Ffw&`W(pOLZsm5zAHBrwg*(vUn>C$ueGy3L3W^-mMF``;PC^dxPRLrr4iY0Pfh16*$H#5fX^v$6uyR z@|CgOdT7t^=Ra`!@XJ4Vm;9k|LUZ}BjQ=O4{^7SA(?3M8n8c!g@6H{1Y#_&f?<4o< z_SK8mhIh)s7VV#V_pQT^yydw5!GigO{wwFNY3vnzW(Po}mw~C-yo6cVKBOsbO0wM=(Z}W3!P)ce0 zM{`KAzSZpG)nH)T51xK+0|J+0R(j+{D9PaK636*(P=yVxetAAx13WZH(NG^-0JlDe zfvLMORecn`LBXKLHkUSX0jgnjMHq**i>b7m{(SrxINGy9RE{?a)NX}+k;}0IXyair ze05Bb*;5;evquivFg3Y&PQoWNHk1r7_k2L=lYEd;PdBrp37a1a5P8w>L9S`l_azjD zkrM%3ayLPYVI3QkrFj52Ci1dE_&T6z%UDabAjYf#V>l1QqD+ZP7L$a|fNcqKrt0xB zHx>x^=|mJPi0(MDR~I-a`1~EtQsJ|VSa9L;GtPGT#UwtLhY7+05w{Y0|C^5JBIKhF z9oI$C`|mrfXD9JRkkc2h58rNDquZ@q9)KO>3H zapH5C&!4?AT)b93&Df5{k5P(lTE^IRcCE^T{Kt%8(TCq1|M_>{I=o*NJy;;Y{)pu9 zrKiv8*-^|3&iL(-1r&TZdxc=Nxa>cC-V^h59GBq5|KzJ> zpCYZYdn`ZORMb49BZB_8HK57kpgA5;Y)+QII5%~9oVQ86_D3wm02#*v{*^lDzsbSX zSI_Sp5ZjB->>a*8CL6!AH(_iM*D?8^^5f^aRj1RyL z7u{&boB-t+k=!}-`ioGSakG9s8qJ{ZecS~qClz&3+jyRz)tkNqleTSpaQu`S^*nqI z6JFBpX;nKoww(YrnH%j3P0tZK=d$5>j(}4OR9_9;3>xnQqXbFF16VzNZfZm5gN zx0l7_HCYT|L5UZrJH`GMv0>qef=jfxC*?!89?;KB;*2BiH;FHSJo@s*;rTOHbOsb_}&;SZl3KJl$r zbslgj`7w#XmuaxL92bV=c;UwhaMl#J3&PoCEF`fwe?k_)vd#?OfDe#)cwHs|y7=Xz zyJXRiabiFGzy9DE9slo0E~n)W2z-_mU!J;0#)8ZB-+k$lE)Ma>#(VBLq;`CC0cVu| z>)&~D`1_xIUO#h-GucP>?$AF-?ve#H7MU26bw5HI{XG5dYk6?c8F>!S=*ahjJR*l} zZx0-8&iH}J*djlPN$tKZ18pbQwmr~~2?{!BH(G?ZML%M!3MB4=afh&0&QPE>`lc|B z79|&G-L)0I9en}XcjUuXS{1SFQS&G2$ZRWMRb7c+VfH4c@+vsx5}7^BV>3R$+Pd*v z^5RwijYih{taKM!vZZU1i^1D_iznqcp0tCd)d6aZXrdnd0ByeC8fP(c3p&-cy1=5Q z$oi&k=39dHE5&O-#%rbL)gCMiRR!h>J130B( zTZb55xq{wdFyY99>UbeTW~;CmP&^IwunXsNk>dQg=4@3%bsCv|Jy5#J^-*861-KD5 z&My7j!zYJddjIWuS-O`$4e17-H~c>F|Kj`Z z&`Y(r-4H$>iTgx8FAJhy`^rnhfA{-O5C4@cF#bnbDE-zqUe<-zsf*W#bC<8_na(p( z$M&o%=cHg^hReKtaI8`=ZZ?V7ZsrMPBw#wu%fc0lT9lX1T^p`R4!C8~cV4=n-(bhh z!EqnVJv*@oEB{(hFWA|onQxT^D?YdSjyn$M@1x^PEf#KAG;;i3d-mM$k3avs`f&Dl z{9Nkm<#VQ2uIs|@Klssm^)s!wJdckF{QB2k)-#;=qd~85PjYk+7gS#tE%%IYLA%d0@|${+ogb^$>B02DRQ6J=??0j! zqk2{+nl>8F5OtVOwEJAlrxqJim91^lP%g?6gj4%S_*KgQsw<3a5M>hC3e^{#nu9JT z-xxp=>oc5<#6k^Urofp+oTbE@{M%)LQ%X7GhJ_(6!Q$){E^p$?Be*FxE??qI(|hhd zJp9;0C-ibHE}i4-*15|!^vfu?7v(?x!V7wq>Q}z_f__{4YtLQKOToK$Y*P(V+_c(dey{#bBh?ju;Z2=ay77NMaQFPm*L;h1YI;&6Evm)C)9yLV`gxIB-ubr?6! z{$cUdVqGp}4CS+w@03Lq&WPg79WGBj)DFvLF6zxt$Gd@%-# z(FbljFnr)Xe5ByGo-xD%9cK}7wimf#(K(*TL?z~2fR_Mp&qy9mpT4YLGQzjTaW?hc z4;&rdeg6?XHuml+{o^)I7(f38#TLf~&Q|`$*Iyp~NOHzPc8}~gnJQhN{@!<99lkC3 zVexpc=;Ew5ZZp*^CiMvwb7Y%DACQlyPxraB0<-%j19(zyqRTe~%sa~?+I+h=SH1D*^Dgvd&29`Z znOANz%=rX!{q&ISSGD=J6^|O%fuBF)VlB`j6Ly$8UK_0$jSk%Md_9oPB&DLk1!Y)F zdNw=5HYiO7S{5ZZ!}iRnE4lzd!RHupHcmgQ=n5`nVhjJbWRaoeg^T(*KAfS#t$qIO z58g5S{JU?_g(p6H_q6EZ^N|1LC!QSs+uwg$zm$T72EKHGOTSpa;>_ArspA{lxK#Rm zcOO!Xzw|?Q4ga%`+%x>;A1UR}f8Y*1Yl@t4CNv#mg|}HfQkyX66n*@|yx>bDI7^D( zFz58yuFx~HSm^0lLN%JkWS)Jy)S+LFt#Z#!d^Qu8zAww79$%C}f9wYU7nk65!KL+b zKTmuv5?>Vh-~HSJ!!Q5foqC_i|3c>aZ^&HZM&50pT1u|7l}nB&Pw7-Rz*`DaL!ydSZL$8z~z5ED+;3& zj6;7f+`8H!HY`37KK8F-Wdk6#f%%&vKzJw*;Z}%8-sy1fORQqqFulospWmFS6Qzgk@%W9 zN>E+K^Q8iHE74cm=o?$-1DY{;zpJMl7C4h}`QxG%gR2&Z0Sj{9Pcsg)(eB#-yRR{3 zS6F+Jrr9IkRY_4W9^>-c+Ghvm16AQ;YaJB_-zG=v&d$7HsyU$TID~3g0erqS){w<7 z0Wn&n9`ykFkstkNu0o>c0UfW!{IeML&AQ*FAU|-ZqkXe;vq|x1&Z<4a+cLig{5^j3 z0c?6Z@g#%TZ~yM?x+NbPCR+eqHQ-Z?!*I}ULxgWl~=O-+~FII3q&vADzn8fTHg!T9mzA{;}= ztCRx#fpF&1wc(;1TV>(zAbiXl7MQqo5pG0`S<9++Lkn24Uw@AV}O!gy&;)X02-j=4eQOpZtaolALV?PyzrO=8$bjB&Q~ zNoKwnwh2^M1gIbTsn0ET61SVc$8HeskC*0s(AgJ{smz~X@%hq25|?SIX{&u0>Kxpp zP7lbNxyR#S6Re5t&iAHc1AmOE>x{0xAWq|in#9h?RfMZGk#7EKdK?7hV|t?x&v{{?#{M9ZpFfyLOga*&qj;k-}wb{PGSyHCNO(=WVr_)mWBf#J`6;12z{zz1bf3J!iD2a8DDss@)sfiSWW&5MMf4pkmFepYII z1~pz=38KSxk(%kV{!kK~@|@$Ma)&O^%3mVtm4dny83Pu3_!12kdq4cZF}=+az7+LO zWv>4ZzxJKs_rHBgZ)jc?kt(nO$`^(3Ep>cU0iSvOVc8G(D8qmAyH5=t`#WDB{+Ius zZw&wU-+W9z>xwgvIP0iWuEW!Ts1i`lMoI+*FdytV1C9J}<`Sr5Q zKxs2v9i!D1Xv~!aoarJV{-en-z~pcI|R3mA+YeeODj* zATQw(K4#-(LJvq7Z{+yKG%jC{<-+i7T;6sz)^VGhCtkU%U(&$YMVwj07b%YJ+o`vI z`E{Jx{K|{N?|%DL{U-FYr!VUnC@hxnE#ZgnKB)H{{8R6_O)riA^xIDkA9?Wj@PYe| z>dm_Gtz~@95;sKu?Bi$jjOai4!i)Nu%O_vCq;sH)QpLioP2jn`xGFviMq}ZJ&oN?Q zgj@088}Yd3B+eS*Hbc0Se?u1704`_ai!%6v41rtYeCet4!^^U$!p8-0AJ6Z<@34Mp z3E$er=UI=*Jb&;_hxL{_h`sN`UcC*`;e9)Xr)5$18JVZANO|tmC7nB5D%Xz>*cOY# zX0&oJVU%&CSghg?3@qaD&GWh9N~!WCChKhi^f?Pr>3+5`v9C#Y4X0w&C^`roEshoNoDIf( zQ=s0G?%I9>uMR00|I(B|%FAgx<^d5el!!8M$s}N%6;CV3*wzI=yqQ7(uk&oQ0s&MwLqLEn8iD`Dbw*_Q|%uL;d?iGPE zu0(XgU1@fRuRByRsR$pdVBGf+mof3Vy;`)rZ7kBp7f^7S^Oi%q^cFN9e(Q1lCh~2E z_UJdVaXA}jvHtm&UL3yl{CT~V56*PqXZ>)WL|g*LJskfRKYrivmwxcBdWjYb4lJs0 zY5Wf#Jw5#PBd_RXbbRysfB(AuW8t=eNH3ihfB6 zSOikj2Tdue61022 z)Z3wqn8-L%8sh@0j}`c&VE#Clb0DXVDFQ|m(}a8j7|0>K%?e{9tpS$mo&Y!vLXVAL zi((UP0x{;S9Mki2Mfrx)1AJ0k_$UyI!RFi|9YQ~xHz{Rq5-oK?&v}AOaav)l>EgjAI;cK$T>lUL!(tu(E{8WnH#CU82TViHJ66Wjv3nXKf(W zwrvCEYFnQ_G~!sAwsE1$94gG`;r;M6vCal~I}73KJs77RA81>(BI^&7dbUbw2i z>HVAEcxCuQ+2R|(_`KmYStMXF^?mmo(uE@~pW^1_@4ox+aN@vj{fWRwp1&}B>bs|g z-}%<5;rG7%>hPH-&g!jbaF0TKp#)$2IVF0yRZsacN6BV$AaH&!;S3(`q50GAIH3!k zM_;-)Jo?H-T`1!7e^_MWwn+H(p7-5*XxO=ZyDkKgD;8k5H4@G|;xmT$20G>jKQD-1 z&cR0k$~U3Q-van)#81k&Uy()h)z@V4cKtP7d_x~+1#u}G_hZDFypKFs?t6*PoMKLJ z_7Y#b!M!bU_7Io4Klr92g)4pGtRQZlj*kmK2Olvg`mgB>#3|&g;2_2slpB@{P*2=ST#R%|Iy+86Z>_n7&AVfiH|$r(ltKvfNzz1_CJHBa- zdBqDUW|&43=IA}YIft)k+`xDrbDF`{8MY$j@)mu^>L^!CMfUV-vcv4Y-f zfse~Rmp0LLoY~ixfK9kSbL=EoE4OLC8B}704*DWZ`;GJI{1Mp4F<0NDZ_TlAlkme} z1)FMHgSF%Zp1P5cAGeP|J+=d;H!ZJ7*l{O#s4I)HtpgSgxDO&0OCOX41@1rjE?M+k zmIcb^o;*AJ%dfqpH`{*x>{VU#?UIESzOaEs6)tDvi#1qO+;@Dxez^r_BC#;~&2PLs z{EM%=q?fUAD;Zoyef;H%dTSbdlN#yo+KDfG@HRZYM9lb_qX3w4@L9uK4(`##8qUJv z*JrSxeD2Iuy)28xB!0;TH|@S#7FjrJhhMtEH{XS>egL;u!Yz8R5W{Vipog=G{K-h1 ziNiPJ@$Giph6k67ai$L6kjHI!@RNi0pWHvZ>&}DvF@oEV?A6-_;U?tYeBpv!hCVzT zmjyX~+Z>BYEI{#_;@G0#5;OY7WpT_SE>~lLhyA$Ct;l&8!czXq8T89MxV(-gPMboh~G&`(_GqWZR`@x;En4C$>`b_^`TSe1iHpYEWYWye+=KX-u#& zhY8H)^)%k#kj6Mq6NnhJf6Ot{{7E zO#}>Hr`A#J@I4Za%JljYZ1l(R+8kIaLc97sX90|7EZZJ1_59j&wyuWRu=U45P+988&Nu zLXj1oU=4%=TJt1SgGLnv>Hvj@$1B&$ujTyw_uZ{mrrr;!;gRMW&IT!EQqkc z!ew9l;tMW;1cT4b;S3cPFifp{W}xg)-F33X zZD??5wS@&Jrnwi;?rjF>Nnh~b^M3d>8vL{&E*0Z*oA}JA~}v$4?JWyn0DxqKIaHN z7GwBL>z!DbN>a!iHwecUrEu#eTpGq=5(`HxT9JcZ_6M;JuiTCYi~8qe@%CMrV|?Be zzqVu3YVeIxhZgd?UB-dM?A^!8#}I6q^xOEj2PftNz`n!+8K0BHPeFd{$L<|){_{Wo z?I(5akSor{VxcZW*BtQEjJg093C(>>$bq?Gn}IPsKVY0H2)V4fh3yY^vi#;ch=U_p4PU!cl`90)NN4?Z&E^68~Pd$d^_@6(gd zK{_q0ADVsu&SQXbJ6cr-Zl_;CyM@bQ@;64MMl|od0TNV-ldx^++h>19^;WLtNx%GhF)(5wj!Hv&xrV@)YT*Ah0 zQh(~vQ@RkvBC34$u6(HjmrHT~Mw|u3f~A~|EWewL1r^SQVaPtq>6qz--Pa?;2e-J; zf5t4f)}u_V+GqU~R)Gr%HX>@Y1|V&DF|MHwCR@XmW1W=8;%Vial6Qz5Yf>!ca7i4C zT`+y92SV{1-ml8y8K0NL0uTqMLbS>-5Umw8gM-gy;t!3FK6pZok9+mo`~Rzd_=tY~ z6=z0Wb_)Fq4o1@`4G-%>K~BVyA23d(dR@TBdb`JrKIfejG_{_^7?<7Du?wn>*XBX;l7u$lfE{SZrRG)9k@i|NGNqkXQwo8iE&rkU6b zNxtc#B}0fsYZ8^^38-D_8r12l;dT%?+O|5wj8ZF=TQ(_Tmrk~U*!ljJEzYX1m(yH7 zfupY<(nk|woNhk=bqra)y3Z{L{^|>{RhM>YZ`=1VE+*;q05h@cV60wY1{F{IV!H9# zYr}q7*x|E(xMYc&hvOUB=VjrD+y4A_zxVX;rKiv8x4Uu48=tqs=l6b07Lh-$_nkbb ziLILaoe19Bsgx^qq@V4?%1AK-Oi+Y@~^guXr#?9oP zI(>QgC!c>_|3KhE-&j&4&UK{?iQ6xKSloGJuYRWTKm74~^;_-#(Lea+@M+msdWO{j za%IlUFqwgfHrFXaJkFR*Ldc;5ZAa1xU|mXewL+fKfP-o2BT?hO+lYC$)#YfbF67xhVt z2CtFz>Cv+TD$~&-QcZgZJ{>8{)>b%9d81FDM(3fvN8|t*k61gF351_X#@J1C$z7vC zQ?k}CF}S#3i*5Pbmrt5PTu1)lHi0v%KAhn%dR z5Z{HwHt)_jSXoB2YSp*46TrB*G>Mx`V^R16Z$6^8b-|ZWzWCI+;Zxr|J$(K73%W4G zuXGw8S$6^_QFr1}@AI0mS zDkfdT;fqSRXC&sXl=9`J;*%m8t#av~ew>wMVPEAN*=0egHu!vTHtpNm%17U_#hk;2 zeo7HN+16rtNLBs#@=n!6U2MMWfPjrenl*oHVbD0nV=Fct1|C*njyLs1-O~2Q3;kv5 zKRz)ED(H_Td1gc)po$n@clf2lJ@h|1PZ5xhFt=V=43tH%~ z+aLCYy1bwD(U!(1rql*_d#z6ogvFr{j<__JM8lKU;ViEF4Bf&3n)X-rH#JA?_Ba^p zN&jricH(nrd^5Zeqi%ynW{r-a%2_3wt|+J6*2ws{^?;3K6L`%@tn*o=+6tDAfpOo7 zx^6x2`lJ-vy2VY)o*%w!hf zMJE}`1H;J6I1cinfY0mQad_|W@BipM!=L;9+lPCO?;F1J;)UUF{Qfh;C%*NHeqIh| zDRK4@UpT>+b8uT6+*BKvIKTGnx#918`Z>MtB)B+(iPsvOT|BS{Kc9G57n1mL2fnO> zZ$cm0w?ofJ;&w#H2={9I&Px}D|LnJ)&>NxSj2RXtHD4Wu8;c)DHCv^bhq7_(0D7v3 z!9`3ctP}_SqL}JWEmOokBeEQ(d14WVeS=#w9mZvD;g-L0ra9n*C4NfrHGn4n}Wp13A#AR;u-NdQjG|vKWH(Pa?Pxqf7N+?p}2*!HU zT(TT`0C#0yjs9FrlAA|OZ%w}9)9#`#iJ8o27UONMy);HY-!YfjeL)mXBhxv*9n76grM{DQDQ zE(q22_SMNPAZqF$+}0z>D}#pm*i1{najJ`c-$&5Uj|R{md=*~D>XUwghBnDzyujN% znz0e`{8VbuA1NcZcmO=op6E|})^5h(pmM3PR28l*I%5bPOt(_ZW@e!9Egq=3n2l`( zd4U6X-Xv#%x~FmaCWH!XwQqaqX~OK9W1KfScpU|Jqpiv{Ih1WWo6--*fBm{(EuheD8oOQWq}Y7+ySg zMQ@w)#i!5fZ-0N|x%2uBaf}wXqrt+}%|<~Th93`|KxU^++7q3Cv>&f82+i5~pO6Jj zA5MRjZXh*U4xToV$#j=c1o4PV$%trjh;M2{ndKHAG3=VAMg94PV6z$_c2zN4Qk3xj?s3EXye6OE!tLuc_|gMDWp9+e(}-m?J*`n4YO@U zqu;?eI5hGcdhod^-Np2Dw*qK1dJ#^4P2L`}x`t+)eO@`M1WJ#ykY{hsJa;MIOoLoM z4j|Kwi2ab}&Cyn+GI9YHGeEQ0s&NJ65dC=-gjk~XgIMFN(hYi(d<|YJ`eJhxLcb|Y zH$C2be6N4w@Pk0wp7e`g6`&{CYR6L4M7xLm4zwhC&#^sv2^tGN{8ZrApFO8Hsm5hc zp6SAkvGL^#{0ty&DTB+>PrPzTZ%&N`A?_!LvyxbJ;!-$16NxW`=nb%C;fWN!B)ot8 zh3AL=_a8nx{N2ys3m`A+mvwL}oM%p5*4qb_MWxGEhf#8{K|83^$^ur>+NlZwwJDYC zz-(ZCuLl$NiwB$4pTXI)-5Jr$8H)f1=9>755{rM=W#NeZgZ=a(Zp4mXwTWWvw!-zD zB1^{^1geI48QdL+smc!qkHs{D0sj6)t839^XX^x^Og81 z#VspFTJ+26=$t8s0QRj9alg>p{c^h5>m&cHM{`T3$biAfxc5I z?!d2@9Gv3!(kR+yLkj+j7FxyTQ1znj2>un60WR#HJ@)gP&8`A!cYTfG3;x73fuDEH zkqz52@0tVPIa1h1?a%C$N5V=}on%`zBfP!G*#aiV4-=u?C5ySzDmf9}9(8+2v@C31 z0dc)N)?xAojYl180FRdn%Tg0v%7-IDF|KYjxWH8vzCYbp2DK;s>G7WEH8xiVbgYTi z3`X`sTP#uDtHCqQkavOG65@Qau=*OgY4yL6F6k za}?!rb;%ba5EiNyt~yrt;8<0+5nchO-_#q(AMH!R+Iguetd87dF|*WZ5eqMj|pC1(Jat@U@e?>I2rb!4ypY#)C81fOrj zLK0^qG0|9*e)ug%hmSsZOmDc2&s5@8H24|C|KSgw9e(rcJ9BwBfAQLI{?c{* z);PXw)Bmhr%$pz|e&f^$_geJR$E|?}2q_wh2QzkeV#m%fwRNs4HW$HJO%kUK?rS%yYmr~YMc`=qKZ z?jxiMHh7UMZMb4;YV$E6PJLOR=Sh7jH|Ru(Ie4y{3k9muE+*(E!pxhozGyDfxZ29} zGN_(ym#~78g1IpFIe;5EaV!%($EQt;w84#K76?KyfbL_weh;yqtWR>J%)UmeFO~o^ zr5P~c|oQ3ZLhw4w-#vYylTZbXfqym zAkmv(4QCFPgV#YWx3Bz~0!7}u0mLCW1Rd%M-E z3GsgH9#PMDx^XcL&1=ySb%4y)%OgV;zXZhiINx5QH^*5MACZlBPA8k&08$$1$Xzqf z;AH*v>;`QHeKv9MR|F1qa13{kV`9ng5&M|~b?5SIF?yLlN;bA6XB}r^S48`yCxO)_ z^M!V_^{|#lEbw*zX+@hkoj!0$!1!1a=&>V(+b;N4B9C*lIUms+uRtfL8Hb12QlGNsG)&Vn@(4abja4b2+U(MmlngY8f5kcsz7K#tfLw0_XhVq_YzKQac^^xJ*YwwQ?jth?cmD<2K(X1#jqf@xr#Dn$4d9mh*GKLa4aXQdve(KRiZ9_Q|fHJC2 zK~DKQcRYI_eB`d*2KNO>v>Fq6f?9PpMVVin9*b=mXi2fn2pVWa@PuUW+~{{519jX6 z-`4Q4U28l(Vl1HYV$Xbl>$(e_pX@kH01gX*zI|yBY~%!u%EK9+;Du z)2}&b!|7Qcz8`SZRw&i(OXZMcpe@>g8VjHEJ3TzVSk&$dj+dTpgM<@M^&M4?B>gw` zZ;k{dU3mltMGdHyf7ih$C%;o} zV2*_%&O-jNcb**n_`7b=&tIbcGw&)}+yMRk_Z`tQr9d_>BbePBX$^-Ur^kJx`Z3hO z`Is0_U19_M6j5&tvOM2NyurFUL}+d$Z*tenQ{=3PS;}S3AN9dqF}4Y1Ma%>~CdSX{ z(YHyTp1WpFJ+gjaW0^zJ7fqX{(Zx~@|9_79#=B-5fiwGJJ~zP<=R>nMf1=k%`dTZP z*kv0iBf5^2hgKz5Mo{tLY`taTz z!viPx4R;^eBN5w%CtkfgeBr5c`inRK&T``OmOuTDlX@$l69;w;ubjW8H!}b3OP7Yz zm#*tgvhlgh55DQ>@M902&;=(htK*V4a=82G-r*3&XMAM5njjQCNPfI>rs^?rH&&IVXymyFm9!qFMT0!Tn4y(T#7;MRtK6s1Gggf zi8f1&v9{@Y&-5=Qdc6*dp^n2)-I=XLHBII$w!J_$CZgE&FJ=~28#q5p9YgF}keCz* zE1Q6+MMaeCeGryX^-H_#gIu|b*_hhFh`QgR&$YYVxBbjbUE~&$x1l|g1Hlk2j3uP4 zHfNt4Q!trL$F7Fb_E4Xc#%H!3lsod%vnUbDF<$Lc>b_spS$Z%(`qX|^S4{e7ZD(&l z-RNhZuE48|OsB&5a~$}4n3WE3b6gLOEh~hMQMTR&xIW9-tU^<&D-%?Gi%&@bCOjUa zW#dsO=J$xE7%OenMVZT`bT9(k$TyD8hc*hzt8O9=uJo(@2R)9pdA`7;RHrAKIxilP z3pzHOf3ywy{BVXEL%3=e)}I!@$Dl5ybZrju1SNkn7`JM%EhYKPq=!>nizcKra0U_I z;KsrZpO5_3ix>4aKlmIZbKk#v=Wxf7y}Dq84Yvcr=OA&Jy8!o;+%dfUj)QtB9QPo6 z_Vnf9j~+WCdKZSPH(ncdOFuZndH1nB!<|R>=*`dZtzdi`7zLlhyzls4y_|mAAuL)& zb#{o+IY)+%r3DULoLR-qzMCRBYmqwqZ7xAen|QDZi~c!ODq{hQok)P=d4P~SkM&6V zH;S=yq=Tu!FX)uHtX5Rq))1NvqEFjdd^S(S+{Dav-ocuA?sB9*bcqS7c}Mu1eBEL4 zM4=As(bv&PL2jngL6_>r)|GM&KLn3dKO^-(pKT{|Hv@8K<%2$<`#1)o9c}&?ka~1) zf^=-b7CC&;2dp|(S&`HVN`YHC_`VfrN1GuXn}g1`)u}PjPq3uBDJGvWPV^_79yZ7g z)JR`3{7KAQm)WaSVQ8gJeJSzkM>9=K;KYo_5T7N^RE)rl8SvQ@o zWija^f!Ll&nopeO$su)ZV_Ph{c5EN^%R&%~N-QQHdH%xiSy?FJ8{N9X79D&6=h%T= z`YSmI#|_PKlku1B%p`Kc?>S?!iL;XU*~+I*mBl2!5e^)bSS(ui;>W8KARYX!G!~io zX~1@zgyqiUClGrL?Qy*wztU62hcl#D&}wPw+trUt^iy~(BI89{#sQo?O8OT<)P1`l zOP%}NQ!3yq9IKf=DnvA-LOHB!jlq8@*q^xN6Xq3j+5F|EU?x_YW7bej-Ulu=*jl}T z>!+DBt80@%^i4zrut@=hG>>XCUZyeGU*sg*$@o3L1CGDavP6lf#etzL^rIk$F@TRp zVVlLUC@%z~7b^8wh+v!SPmhp83)vKXKJc|Ji-1K~v*dEOq| znLa_Tavaa+1}PQ=xhMkfHx8?KRW=q~-YEqOn)cr+cv=hz=}jQ^+SeS{`O*6D2IElyP}hQ4^%MYY zD|TiQE%r#ShuJU)PCUSNY1o#|VnGY{3p}((7J2eUkAyE>yP-FwE?(fiu2IPV>fa5KKJTJB* z#&L~QX9L_p0)T?WGtQ#E>&`>NM;|&q{Or4K8GioVw@ShGmf>d~J~{l@LnrjII~Kw^ zr6qg=ggh!>&wPoUWAd)fJdQxJ#zBlG&zK@o~25-|pD;Z!-P*O&=+s#oT~ z8mcA5$I%AB?%Ui-1^@Jnw%3aNJILy-MhhYn?JQbsac7}jO|4JQOv-`^i$VOunL4dI z4ODTm5R1Hv*Ipa0$rfkobhT?KV!??sPdm0_F?nM+d-=w2UJ8C{51++^4VTXGD5Jny~d z(C{8???wHve#87>S!`l4iE%W$WD#?@1%SGi9uCkpQ0f>jDLVGxZ?*|0t}15R2yY*A zLz;W!_`2)pKK;4QAAZZR;T^h|#7{ALO_m7bbh{Nt9iJzSIc$zzXN1?z`ib$8BYsWS z^OAZU8-Y68aU0?L3|!MJFX~!dKqR^LBp&@nBXcR+?vMaDDbrC!K2Y&oqV*t-!OTechG1rp~p2l`kd=mQ2w=-o9{~Os-S+Ux%n=2`T`nTM(7|?mqMMtjCDSMLz_%z2*cRw zG-90AZZOl;mw{y1#k}U-Kl3f_u{g!kd$HlVv&kp)A{scXNKSV_9?xL?s4kE z_2F|*l>1TQH@gvw7}?lZ#=OQBZGLbir&|0GVsnBsst0%P9NsSr#vi!FiF8lZTy}$&QfAKVHz*h$Wm1Zcj7Z4&24OIX}~}!8t>X# z806bMkQ2~^1Ws+TA49E5rOR=3g>ToO1ZKzaBL&ad`8Ol#dJgKYt?^N#*s@&-yY>lR zmCrQhkP0ixn5+TlaUcao?}*7YK?cS~t^=ZUJQiP7qMqg;+sY08fZ8MbXPnpdb&tpc zwC!V2?&(@0DiS;tA*)LK%0xb4MAB7;_Mvm`*_>oJ$m{1(U&jk{U9HC z;ex7j{e<7)#&2!oc0u12&Zi$gGkoK@3wkLW8rTsh5A7Nry!C)C03Ut%((t(_&+1Pt z;<9-8dBknXy&^G}uD+%dhM!-=V(sx)E)8FM_Pj3MepeQn&z-qiFQID+CES6H#yA7v zfofB>ecN^!^X}n8Z$2`->BRow+~w=TH=n2pJ)+92I@;tFd1Re1Oyc-~Bd<*%_>ePpVm;;e&n62ht zHi}-Kv$^CxSgWU#UPaZI>4%8@W&P8Z=C;9Hsmbv@C(K#J(Z?uo+`D6c;x{lQ@9%MeHNH$M3>~?ZMGYt*CBliR2mfA(xS|<5we1X4N3~jV zqYrg7v$+eOg5J5C6PN0QnM!I$FRs-LJ>R%D?!)z7rno=p zzxc{a`jLiuwl@Piq1;#*F-auhG~)oq{OTsgE%vQJrJ7=)&kC>MeXvI^FxMx*7~{op zpxKO73^l})BG&ZV;lzDJWc#Ye1(YxTh9Wk~%*94zeG_%g9A^`4g7HhSlU#()5OsC( z$n>*u!Jp;=8`|71AZmMEqR|(m{TiY#YE_7z;WraQWO*yxYb*SWCwg50j>}hGqJ;V2 z!l>_Wp&uV#IR-8c^fk71sgCNFR@E6_j}0*n7`rS0C$j<3-b1G0;qvf{9%3>-UXLzo zXS>sDHuLDaO5#%Z7=p3pYv$YeV2}IJsO$QV+$`2?HHTzEI*UmGVj54@Lx$RG1>W#c z*gqCqdd8?qmIaCa_O>)(h98&5E0lv1zSvB-C!}P4^zYgMfXd@OdkN~&Hgc7W?Mq9Z{lXOBcP$7y?F6w>5%SyQHS}( z+~862PnUSxgz&xoslC{oA+{EDJrHMaBI)!TDF;d__MFKrS@V z@2!V+iQW-CGmJ$s?h}f|8t(o1o_h`r_nz3NJd6WplMjg<3t%if^#=4$o*AA$b47nD z^TFE=4B!8z!@8iv9~tNa+f$dW4NnR8i%*@?j{sCiBKR1@+wV9qy!qsQ{q1%vR53@$ z5g%#5H~Vqhr+bd=(O*Ks=TUVri52pe;Nx`flxhWRMDVd$DHad=!tvH3#!Q6!aL1Bv z^pOhGMZK?8-uSFxy!vL-4THn_p3&}tc@x^v3Oyg!Pj2LSPG(z+G~bm7v1m=AvpfOV zXY&>x>2o zvJysx>Ara7#&G%i4LwtM%c0%6V8hMXu?RgQi!{WZx_G^u6_tLj-oUMta7n%VLJ`iQ z;>_cBWr6w3=_{I#7SYC;O^g9Ii$_j4D~bDno|A70y7c=+NJ?lF2o7MM6&iZSBb_c$AiUr%~S z#)G-Qyx_J{IMa#3-*?AC77JB;i3^`Y)rGR2u`P?}_0SzrstO*=nT_y1L1-q1m5?|M z#e>&i-P;1N^Gd}^V5>ilgD`&82x#okU^|-|mieg24MYN@d zijlA(KhyH?{%pF{U-m|YkID3D>+v$bhqG(X3$Mp@FrmH_74+3qSn+_$MBDRljib%&98_mQaU9 z3kywL_QeA5oGh}ixWjLp<0mEYD?qrEjV~Ob;4_=byL?>>zU+jL1MHF<@VQo`^3CTj z4$q#I#iZm?hQ4jsy<@vB{%{%j_<>#eSyp_32>sz#hj3dSEMO1Iq7(~cd>-~oPn{n= z`Q1~)H=Zvyp2u0k`(-RYaQ_iKQ~IqJE)Ji2^33p=$IlL*$C=1iFYB4gH_Lc%NgHP! z@$GwjmK2M~ciwqOzsU81EQhXd$>p&7aTLK!hRJ2V);~ec~#*0DYPvJS(`uvw- z1v-D^`LV8m#>#f#1*;1`UJvTlyd2awW1lOrBefaSOkv*vgYV-8_HauCj?uCL&h=wv&3VhS=gc<}} z)3Kp^kp-7uaY^`(WbyMwSq$RdmiNkn=Z79Rrk|z6JrQw65SN*;2*bh-XX3CZ!)4t& zk6BRK6{7- zBz|@hXD6|M#pPgpK?RFBEW}QW9iPvvqlm-7oN?YbAKX?H19V;#{jbP^7vESvCH>7W5L1~i#z1-O<80j2P{0#oxiGQ6Y+%^@Nt8CoQ>SO3-=Y>u8UZFE)!=&pO(e& z)3TVxTtFWSM|`OXXE(8U)Em}Mb_w!hnW7vE=4&V}W}@i~N^^m%;+O#~jdlEr%;uo| zl&V78e7UF7IwX(gv9b;-!#o*>_6#rwejXZFUMM;*u28odOEAWo4P2~h8mO5`M;mkD zIBF^G21=C&7LSH+KImk8&xM_RD#dtcPXHTuPAfn?G1Tie7;WY12m@uSIZ4&$9{W{} zi_s0+0&1YDMIIsHb-X^IHraYD!f7^dl4|!wjq1u!-f#`nadJ0}1ue`)neR47og=a9 zmOf}np|ygP#u$+K%vSWKggP^R456Pg#ck}Ur%vT)rIvzrzge*{)31L4D(sX;Y zAZle`|8tn*WPLck&q<$SaA*uiQHaeiU;4V~d#-|2&o#7Msg)g3fU1x2x!2>aXxI)M z!B`f-%4>iW;uZ)t-FSb75c53&XT;CU8gLx4)l!Omsm*REFmr_pE$5vZMb~>#aVa!Y^4pLk~#}{~TODAkG2lxUE?g@GqelJ}Xl-Fg&jLYpfD~f#Z8|k;on6P*R z9}7av1HPoQQ|4m#&TV=&6kR_l<3YjMR-}f7DaNCVWV>9BcAMrn2-M<`8glc5X_y5w zg_p1QFb-v`15(#!v{x7*^>qNQ z&wB+bwHs%7c(Cp4-0h`|i!(%1#L3BZ@4e{iG+i=fkgW6wC9xYxOw z?CEBh<1XpIPfnPhaUU&1mK-))M6H^=dGeWU7Cy5Gr+75ubm2^DX;^pL_V0;g{Zj`|xMK{|+m+Yx|=Q9@jIFxVI*>un5K3Nqjc3 zEH#}lZaGx`QW0aFOE(yr6w!_cd4gFuG;ui|pL4`#9?R{F3LuUA z@Z0IQl#MU@{LH&#Oc;lhKPO}Q)9<}aFM;DMEAABuKH~5(04zqaNW@vlS1(*0uGcq` za?Ie%J2(rB#XI`dCq4JI<%z;t1%j&Yx0euO0lv^WpKe^cf-$m{$w8}FBF{lw6EA{lp9V?;>Y7AppB0H4 zsBaurYF(XW({>h7Nd-RMBifPMELXO3-Fjed!9|SfNomGzwg~pGej>IVCR~ri=i5FX z518R&0YX3AN4jqWN{gIke6D3Y#NrE`+MO1w(O{glKo00ddqNlrbhK&)-8fzC#|pvz zBYJkgG>Wbh^SWyg#$39yF+~`+7UekJ+!)NKbM8;YHR0u7MU#T{hg`iKbt#OhV}XIi zI7H#lP{z^_1e07`6*>5Sr5kx{CFJ-po3|J8sAs;(xPw*~$c1ADoD4+Nx1?u&jHME3 zqdr|I20Jt9N8{j#sXw(5ndTh7V40CXUr9ENT;kL3dCi0@0&FY}0eqq4Sy>Ey;iyd&Iel4E$Cp>|6M*R!CZd-{JL&&3Ecz70@6Z$lM#Wc^9gjuOSjviZ{Ge%^(~YX zoOOf^TZ|EBPcf#GGNxM&?b5T404`nQbDdZ?VsWT3@_;j%xCD+xGDORPUitzs#_O_> z#chv9n6nFJ7vb%5oRz+ewv<|h52h{)U_v$#t7R2l8wl-s9AxmmIi82{pgRd!t0B<5 zjNc4+0}T(&XA802m<6q@zt#PFUMHq%Ie&IwtUXxB%ko|W#`DR_Es*L3o984slxBzl zq?_@LwfJi)wpF_dSiF;|oRPc$7wlo?vdkF4*RYDU{dBsi;NonJ-fRk@BXd(uI5-$- zZMt2=acm$sQhSquch61cA#!`VQ%vMBU+cq1Y$YaOeyE#_Y0adykTnH6uwZTC!F7x$ zq<;Ze>}bv}x>X)LXro?R2KEpCXsyB2APuNH(HzWM?dD@?;Po?D0BA~$BB^NMvTyNg zQ~u$yEPl@Mk7QB#X(_mDdr}rtw;tM~U)aHyRlvi|+wqypU-{w-!{7VtbHhLU-1Dxy zF#H4A{)5jxKm5kmUm3pk+<9G4;>$gpt}!D^yw1bu0qWo8<-b}A&?>QNFHQp*jbVg_ z$U_NLps5RLSt#PN|G)V1i^JcS+;uFUe?f|jMe6@Zw*O4V`^oQ|(qFQ{;?pFgH|dYL zJMDt{(jl+Bj9tf(6Op* z4CSv#?WRG?8ZjZRM*D^%b+nl#F-@fKlq){(XWMM5j)#m(L|e@v;j;}+0LJ3SGhs+< z{ROZ@A;tn|dX315dYQh@x1!a7yNUFDu_&ow3B;}t`&=%F&)_(y zc?I1^`cGT>*o6gipQ4GH-^X~B(SOos-M7{Q?IlR*1#Fu>I5n%_Gi$Mx2Sw?}Av7Dz z`ApJ!QLg4a6V&vLFexQZ{u@zSl*d@H59)p>|0)5P%Qrw%3E`8rfn(DBXB=46&KMNk zSjFX1=f!wcM>!ma!8o-Rr6Tg7PaV{KA?Ww9q0RA6Am*4u*~cTl8CwSveGMsd z2I<}y?Y!A0m`kQjd^1GdO?1djV&>9S%*0lB-yE+6<{T#ld14Uz6^o;5vf#rPUmkn; zqW-Mp`ODY!EF&&$mm7V*rk8b*JZ_nT&k5qoD<WCB9JO3l@?1iOQGHmv3|9GnD89 zXEgDdLiFtqAXSA;9#GH^&H(DqR!SVs8sgj3s1wkE9?q6dM^A0>IH$AfBl^UdOMI>p zxAei!9e(whb25et`dLzJS%2jDi+UeSoK=MmzSx7y-S`YCzO9bbbWHfK9lc&Z6N)pL z3bhkWBfurSG3H`@A}BZ04*`v+v#h)3Q)LsBiJid~$D_wp63c0tg%Psc-0`ZY9A}T5 zh{Z9Fm}kaZY`ZBo$Y*Ql=x?dpY;G6k+xvB_Qo4P^*KyYU+6}I*`bM~7Zpo;xpEJYh zy3%fDgnDVkkc<~>J*h3j1iOnFAa*)KrLX8E#JO(PJU138oetjciF244(b!aTXfqU^anWV1eNY|F?%Sg zivpkXYax6aoPaeiR2_X-^fIkV7)NsAC1xz*@NMlYxI8O%L}77PF6Z9R`$6JoA@S|( zBm2u4ODycLsKe(evBd%k3s7C8iVp6brxRj62MPdv(CMg&P z7KP{=U);ge<1>(0fa3N<_>vKBD+GJV4R~F-{B<1Yj6R@g7?86R+kjD6Q?60u0er0m zIGd7@cgcOC2b*>_?T1|bOHzY9M}Qn!@mtVBeUA7cP#S2OLh z^?J3h0O+E?HQ_+p>Bl*mYsbDKcL_ch%`fDZ^({r;(8yJr`2mfmM&VDOu~zpm{RZsE zmx$=k`h*EVgkpje=k* z`%Nmo7~shq=Hl(AG_Kpdk-lW)}(3-$$? zURMoPJlF;&Kwl%_5KathkmxsgTMjz)cxYgK1Rx&&%Ar-pqz%`{1dw6z=iDx6|1)CQTrmnG{AsDu5$Tw{HhBUyqMIm_U|D zu+4SbP&2Izo=0)m%&xO5CG8jX@)g7h_Ce4*#UaRAFqmX*m|jxF;t1z&$51HUPOH28&qO(U;yDNXCS5YHl)r z_efs&0uL76SMg;V*<$Xn*v9-|kvd&WW?-Tyc)(K}NO$arCEY07Kn!Xl3GE4~xEuRI zP8Erk$7;zf^xe=F@^~YW&R?*_HgeThPKRC$Hnpyev8{;n1HV-jO(O>>F(;3nud!!i zO5$d^Q8eR=h{0$Vn?`Au(r#!m#sPiJ_SeB=K7vMdmqkO0U`BJoEyihy{mU4p3z;cK z{j$|Is&k<;)7M4Y&3qq;kFSQ+VH;aU%Kz3r=GaM=JOih%&Kb5+)Ep&3EK{h6{s!iZ zF^0ypwEq^|YoTLa>WgzA+c+K5JX*u#sZn7L(1V7iLz`{06qnML#Uwh(GVqbUF5Kn? zBgCRP84=ns6WUNTNQ#HF)kIE+k97DouksgSdE$aZqH8z#k(20y7tEzC_C~{#1;8h?O`jB;O*7%m-xwiUlRk4xYYLeo+RCO?;agi&K2r2Yn+33%I+F?X$CjWq#}Y zC~V6O?CM9od^zT&vsd+=l2`!Z2I=?~JNm(AL~)59XDo4p_J{ArFXSB1FBsu+JILnDj19lagIg(K@#!zm&jb6c?6(qOahCH0wu^OW>ZbvP0kTzD zYO7NUk=Y_l7tqeEo}o&)m@bS1zEu62-(*Z(Sg_}D>`S2;Tn;XY0$&H{k9M!&>mH3g z#1r6neT@lvjSRR}Z{SGO^11-q%**=#ZJW(})e^@@twJlPx@F@5lRiHQ#v>+bN0eY! zidiAC<6>!a6lytxQsb83IH)xd)rY>?oUsV1|Aey>Lc7w!Li*?&lOGq9)Erwu#oS31|(FT0=*}^L;&{2{dFjkD!MU12SsV zN{gJ)vJ4-SP7*AQrz-jd=W841NU%3sHQ=5cj0S69Z=y zahV*ywu8kNzA23}pl_ELd}|zwN!%I;{bHepvyZY4)631rWf6%OEGCgJ`oR~2un5G@ zDq?|$U%dt@Pa--*4tK*VAW3*hg*bZNLOx#KcDrxOcRoMnU!F((g|+ZbW- z`qtaZyr3`Sif?k`x4Cgf@~X^Z`BwKfUCiPvDeg_lImDSvo3lNYo}P-fwkm+}H}xz%ilD(XXXeq_ArBRq$NtgR$7+S>9+x-PoSPS}u4P zE5SJAg6$@K3REs=a!fEqIke-LC$@ms?axPt(0P|RwUM0wA=DVtP6w;8mkWl zu?@sw5v*g|k+1MvuR(afN7NO6odck!mE}V&w!Tc@jI5eoIV`vcp|Naq>q1iisX$i0 zEcw?0I?}y%S$44ervU_@R+{Pr-e*Iqpby(QjSQZz^?89Zw^p>C@R46)&Y-lwg|0`| zW*&IRUMgTCr}AAPn&0N0f=p~#Z)bnhlG@mCAU|~)=);yJAZG44T9t?9#Z90wR#(jB z0vqkvW?XVYKMwR_LAOg5SN9y-t7ij`?BA)2LM+U1MiFQGaG4sHvp@61nc+)MpViym zVDW=X$*8|{?wV@h>>c6}he8cp+I~?uScKviVX!DfEG~EBOyl0&+hws@7L)p`I1+<1 zcvuu-@rhs60T+u%+(aCgn~%r>7z6S5%0rFv)}5B&NKb0N?t ze$fUCLY!qiE(=L~(Z&PQi!-x0>&bO|HIoLI(cjwDOn$>ac+8uh_+P@xvl z7u1(^zoRqwJQtu0;l~V`*k;ZWWc}0}>O(=$!f~&?dvnyw`X>iI_EG!n#Cvy`!#HCP0W!r-{z`CX7}#E=DmuF7PVV z>@OGMHR9#Y@@DQ7gK#|#P`AgX_rRZkdCc)5hwV*Zu|D6f-dsPyhYjWyuiFiJnjW5$ z=z2-bd+vB}uw7Opoo>`zLf!WkRNAFpwN-b_ZDbUvLVd7UBaFVSW0UcC)Hz}r@dFi*beLwKUSP$hHpShS~fmGS@GUOMy7;LH?D890YbU;!~e1_PgKnElB zNl3z71&d>N9hBD-Shp?Apkr}PPI#HnQeO>;p_JEY(ATUXWs`$?D{@nY@F#jcc9LH} z)=w`7r==e8S4$St>zf?-ix3+6Uy*5~w)!yIfTegK*zxET+P2pwG|XuxdLHV{bOiAY zJI%ibqn1}TkLhtyIOR1f(o3^EYNQLU`Oz4J;rG4y*T0I;Y`|PL;jq04EVjwFr#IJ6 z@YU342YPxDo;T4gmAXhn-tl;-o!i!sjj`^>rDMvW#E4R(Q5w~z4`XiDjf*MU!VIz1 zM~s{`pw3n>&9hA=o2U7hV(jJQ=We@>v+ZvM*lXF8_FQq$tXO>Ko|`4)ea>G;NxPIl zsk7A}*ube1ljI@&RF87ihzEh=p|0lQgU5T(D*sBYn2c3*4U|`7ZB(jOYYHK@ZB&%F z+y}rzZSpiv{I7O;7N7yHu#F9BaUrQF?f4`BiY3I@_+g=S8YyUwJAzRx2?udLWCA!D zzXaEZZMvBCb)1+p&DRwovjM(If8B2cNWKV+f)^*V6nK5=^+3PlXw&Kq60RnITzx4? z6*qx4{ZU6_3SgOiQ;I%nI*n3i+u41;F{aTqJ54D!*oeT@vAEK-m1{&ouNX}MD~d$5 zs5;X)a2r0jX2o;6d4974nk*1UR{baODnU?AHxj7-%Rd7Aa(o(*eMl*K{9}HM(JK6m z0ZgaiI(LSK+XFPbjuc8cO0Iip^wW6@e#5$w;UAa&$K zd&!SwnoBDTPDCDqaV?L{bKJKRrk$)pUCnF-S!^$lx%{@6d&JzV3EEy_pJ;nqsbk;a zEEz5{mlE4@5Ga$7me^{%{2Td<9pgcj{{!ls_0n}g~dA<}M3fkB%V-=}`n~5c`v-}R5KlTb%^y>$|G!fbH z&Y0QIUao7#uz(dqtjM^uacsKv#3ZOOWuk&OSL+V41Tik?F1!Y<#wwkKT#_-8Q5ONT z{p7yJ8oOY7PIiJq-YCQ%Z-rJ{3*m32qqUe$`Q~7LKF>EoR|OXaANnb5AH(`IA-vb= z_6^B(#Mb3FUZ20T+U6>ZG1F}5BB#Ct&nE7jO9{39Io+F?`)&I^_Hq5gPLtC<@gQ~B zYCFPuG!CASbpbG;dxLQlw2O`A7;E|sdfbW?{khFZPXy!428lK{a1G_ficgac8a;WK z%w??z%-6&aO=H9wJ%!k3xrA^+SPK}8K%Q~<;ys230?^5U$Cy6pk}!`d(LD1R6i~H_ zFk;8JtRt_pLcf<}7eU_!Gs(n{jRW2U8b0bz0R1|V^+6!EUUxo*uz76;Q0u0-CjZD~ z8}RX&J&Ub%d+-{7*NJ0hj3W2|ufK$*A48gp>&M5e$vgBhK9_E??+C^!MIM6GDrl@d z4>`x|GU~zJ>m$VARai|v|4c8U$5DHEZKrOW*S5_usR55I3e^-_jZ$+lG2CLkz4jf* zRo~e&TLW)qU%-EvWzTcZShqPV@F%tqj}5PhZ3 z>6u*`qq5C%qV4A;5~!c$sgI7{^lj*kKiWra7hF+%@pS_dc4pFosR5Bd?xB0|bpd`a z#@Ug}geC?J@5OHwo9Ou*SO9KYU>J2;D}1aJ4V4E^q^(pO%!dB!JVZ`_a;-iA9M0MB zdO9>kpg>Eu?_>8XeY*Vd|up*=ccjNo|prdZ9gUfWsMO&2QRWya6~tW2_fP$#p&`ckL8e5IX^m3qM!xk{VMA!>tzLOa`8 ztk`HqzuA${h4IkM?7pPtoUX+;{i|>&f>|l!WI&E-o|(-$flFe>CqsGJt4$3xu3_Xx z-H}ru?O>`+&C@%*R@5Qz7qEpkm>2@!XNeeX?6qNOXHv41V5@VHfQf>`vKFdcstP~o zO>{E&e3rE8_>IT*EX-fEV;c|~wEVc{;A0sF6YSonZQBApeIG+4rV8WZ%{sne&!fkk z>#T+LiLEbn{89j`gQ;x#SOT))H?agdn}PBLx!&lXz|UihWr(wbizfOhVN>1ux&#-? z3@GoqX~Fm|K>(kWGETgXnvG!H+ah}TO1td)=nzoPxj3C8jKqy>v;E}Zr%>a1eA7ao ze&6);>GQl2Tl^Ww>*WA5HOTe0rWn5nj;JxE89=K$Rt>eE(iD^$^C!*G}4g30qc_hQikas!J+5 zD^jX#i1W?h%F)w8eU*Tt=3||O#;)uqMeTe8^kST?Qmj(j5zeR_-V*~UZEc;s?IJI}N7>Va)(zM;r8dwFE= zq;;(#%pxkmCSM}{5gGzsn z6i~HEnRGTnby2(=_$cO@AhDHs7#&3-T?Xcw5yq)wFAbYdJBDC37m&@1OUZJ|VY0gK zOFveh!RRi>%o`0zF6rv{P*wm^N_Twdt9_zTgp0k@V=nNGVP*e=LOtJZV+)tjg{xIX zY$LQ>=B`GnnY?YfQ2!i(@SG^h&U-q}?)z$FIQL(lNN%s&1=*?@KW@o7M-Q z?%N)jk8S#5JA>J4-{i6GZF6v(7^3xJ#O&w{NwW;K*jHgPKN+H)Z!g7WX!ZE!n_*;{ ze>4c#prLUlm^=Oqshk;@V^R;UjTQuyt8+Yn_If^kisA!Hfse++E; z_I;vm?O6_C$6NOL8s~)+<)(3{SCZDtF;LJ@9gNz*b5WmbXFy&xH*8p4hy$owH02zKTwNJv4f*-e>|rm-zzVYP&QWI32k4xP!^-n~s`UqXU0KYo0>BWV)yVmiq@cXm{c2$ z0NTNc1<2yG?ed9!2js4xoV5#ZkyZJ@#!70y1xaTh@!h;Py*&x$6^BIkBr=+?Ve*p(Q zPqGf_L=Hx+&0qtnDQad2{Zug4`B_c%qmKEqJQo42P5DHU6W!ItN-r)!GY%goK(qR` zf}p4H{X|`{t4r)@mm#hm7bJIdb&N5lJ_r~G_*idufVt9w21CqTx(=ER$NK;}fuOC3 ze4r6@aB+=<@Hh`W#OI{P8eg5~kW-luYjof>m74IQeiIXHSq>3Bj<>4`4`PEk>vIC( zXh`eFU*VGoGt{ojrp21EFh4_~bY8mEC=ZQJ8$XLq_Z16%p0Uv9NJxQb&T5GLbvkKw zT-wU{CesCi9iXqtVJ)0qjH`M~j48Nr^n9~9xITe~J4!1eC)F1ltTC5aSU*j7630+C zL__HTh(q&Sb;-k4_62n|2eI8)H(eVNSz z_RJS;(q40%uYN8FHsSIWB!4C8U<3UwR`jg6B(12l#&9uuPAW`nLVPDZe{x6>%{apnoOyVy3s&*jWv*X)wj5COvS(M3Vfey`(w-se$An5 zYQ^WqdJj#$1(UIOJ8ayEVfnIF1 zXHc&2NUgDDqhObnW7`={dd7^1ItCtFKygiDt1f+{culK!xxm;}f%kcU7k$339-=;B zG0pR6NiWk1+@gntNogxWTCyLAl;}#OAKpzwldL z+aEdkd`pkco^xO)&l6fQ`w8Y%uhpup#pTnDE9mPMR2_$EOMDYF9BIh^@C6*nfLZw7 z>g5V-SqQ18nMWP~P3qF0#R||rs42ze)`L=fAF0sT5B-s%W22FED~OA!R9oPt5WDI# z$heKB*%aa+)pHxZAaYnG%R6TII79Mf-i#Flflizcv%0XOVfy3 zXtAFRrr|lNhf1)Qy0xQEAyJ+KWHH)-aaqV&%DejK;A4f4m>On2E1WkxE)_txs!e@? zRPpADe?3$W!(gC{<^rRjFHsQuIJ1G$n5kPufs$R#(Yur{4?3=Y-4#qF z7g6*mfV7H;x1l~B^JSC5^x{-JF??S_5Hw$791 z6PeA!`%Qm>9*6x#6b_#xO|BDoZSk9Yoz`9#0DM~H0oxj6(6*J;fR^d$78Qf5){e-n zbw>haCWksdnJ&<1IUS8@g0ziDlnIn;eAJs*)3fSit9{8f_6N|O`67JoEWNOXXSN1- zGZ>FAP*^m^0p8-1$IzdqZ5q{OLTIZSHo|;SCk43+DA@%#)7X_5=k(x1vBXo|0C04= z{CK=hg4Ap=+#i)>dEg%r+vLt2LRGi4nk{`i47I4elqHa#_1|bFeqgQ-<}fXiuWTdG zht3X2XzQjNn<$KrsC>&)b;MRm9@iU>2RV$5G3>VhPPEaP8qDuFaA_%dd7D-mZrj#z zNwdV4GB<{zIikklpL42)`Lquf_^d=N_@gbN@tb~zh9-^t)1;Lsw>#u@ND z-q3YHc^*zrF}71x9rs*+UXNRk5W8v~*Kh>XO4D02M?H?WqmRggZDo!rVyv{)w6W*$2N)E*mBkyi%MC5D@1oZ+XS%g6$|uSSTE0DRxXT? zNz&;;I2u)njZR8igJ^lZTJPQtKNvDP-jTqYugmi@H$fWMHQcDh#r}f-z zbb{**2lbcw@&4>uXFy@#ahc2M;QThz(+j7x`P1o+Sj9Dx(^e$)=~F5g8qE0S!W>q= z9SG)gDaaS5Ey$=;%B~n{O<_5e$z0}GMN&-io?v#uzWohr2b9_XKwBgq3V~l_Xg5XOI{T6JG5`4BQ(vm#71~w^2usa*5iBZ6kLD z!fc{7K7q{_<$Xx6%Nk<@KMxZ@q9+V{jE#s|9-SC3tB8HtW@$crZgakh>J;?s9^mwR zgzwmEW|-0H9-|3*TyKw@rM3rsgmLstgVqE~I+;D%d_OTh!gC{*Vu3YzF65KFd3N7) zNrl_$^Et|HqeQwFgtRud6G-S7>}qMV+F;O=zCos z2YcZ7ebd_~b-o#t(R@Dw9XE)spHfYN6BE1K z{+J!>`dHfpV`=vN$38P(jNb&zsfFx8(Hi?=#dna|rWkVsKkANtz}tZ_wykIIIuX5? z484Bl`rnFg&Q4q8unpv&nIu4TWRbAa!;(LPi_yNUJ{gg18-3Q}mhkqTuNmMQfUK{Z z0k!5dqwkIEe=S0vwaLngd7e-Z8^=Ohv=iP>PLB)B`8I@qX3fI&8ISW-h%qZQCVg$C zZz74E8_VK2*W_aJ+oP(T`(W+95X|M)mGd#3zE-5Lk*r;b8PAT|*2gpuN2LVPirgwZ zx6@ZWzl%+Wvr(TjxEYzrJp3C3UmjM!ObBXB+1w;#^meRsOEk^y6u~iSWT0!4F;0=` z_xqfp$Az7Rkh=U&_l>8`pgY!?_?w%b?-4K+A?KJU{Jq^j{+!~f!_})qK7F)jZ6A|m zWPZz5BkE`}KH+rzNL#s}Rq7M9-h~r#EiQn<8&|vmF-;r3gIbL zc%a6(xpvHJuJ6nSL=A;-JT1ney`&TU9+@5x+k}^@)MBh>tAG$jWLz#VXshkmx1h$? zJjr}@eMX1oAX;@mlOkJ~A==nEz>3Ad(@`GBO`m|q=-*Gl3Py?y|TncGKN z6@MJtn3LGce+A5wX&@EoaLI|W85%u7)?ZJz*Y+5Ft&?+*#SnR{8Es;t4r4sjrGz$i zY>ycGEZY&$AV*Hp7g753dL_O$Zm$*R3VrG0Tz@eH5dKU^jhWLWMtxNBvG?@UZn-F> zYR!W>>DUC`KYwO&7EosozNgr0k2eQD6O4h)+lkC3aAu9H?KT%YPO|Dq_7OTG8l6cm zD-q*-JNPDJ%#isINP8zXJ94`aF6iBux%@gYJ+0VJkJo!P5O8-XWX=^=0d)CI``3Ui*TdT~sK;PxyNxRdRh^YhrW7w8Z<=4 z77x*JxZ(&t-;DB23ewQ0OeA?EzR55ghjng|Cv% zoQbg+JBjTUeJ?!4J2wZD-ef>1`aIFVJa}p?N`5{fCV9+oJ?1ncJT;4(n+t#;Yr9

p}e>vwx;m4>2DvMaEu9g0pBrt(lWRxOpJ_Xab5>qZBlx>Z z$jRcxs)MADb<3~r18XlqD5DzqrmNvDUgz1h!K;%!Dkway+MVBn8r+W+O`}DVhd|pM z{g88Iq0-Ewz;bD=&JQUqrrC^t1H}BRjt4oOTX=Y^5vV{9G&-b|rBY?J0N2YJ#y5@? zh#KsBf}|sWeD)dot+^Plv`ak&?U~=D(9DTc`KGBw{~17wg=6jvNdX?v<3)eu`2GO# zGq(nw6G30>BS@?PL)zUV+2EK{j1D!~e&fD^jm4vF8ri^ouU2^|rPvVIYPtRt8s*w0 zK10kYVjQDDMR1n3sf_U3fV@tzkeLrN&u%1R62U=5WgxV|~6O zN@wh|aSvjF3f3RK@(AlM6d^z4A|C+C$hqIhmEKXR!B9!uHajcS}X z<0{2C9;BrR8tlUd>Hadi*H^o2HPraJ50XMO`E@uAFj9l(20rIP)$!2uBZzH3KCh`` zlA>;&;jviHLm{fo+6{^k`MHr4Npw}U%cjCpH78W0!0vpi+vH+fhthL!ShqlZvvRfu z-Yzob&&w&j<^nf1+Aeh^2c4$OK(jY()iroeUgjQy2?)0U zXUgl_3Gq$E5`eUdSfJFoeu<0|`!P%Hz3TU6xG)^Of zgS)30vA}C|1=NixxpoXur+b1Z@FLri3YuVW9H~2|puL3Eb0hG%M1(Q<7l8&@f7RB@ zUF_W&rLc{2ut+N^^rP>4T>2998!T zTI~oaVPoR76}$FkdeyFVY5=hI!dPD#aiSbcEERQ31=EEMxQ?#? z2PN25*NUg0gnkOQMWK`h?bItkjOxk*>aM?(5BNR_?MSvs4vNg|V`|?8C0`c=AGk9y z5$S?bV7#ZWorST_2(veHWf~;|sl-_Nv0Z}2rHf_LS%aDj!K%qN zxx{>l(3jW4_)&P=#zSY+2d7BsoGB44iAyF#xl^--X_Wc3UnNe-u(PY$r42CJyp7l@4g6ck_CXQh}!5V;;D&KeHBZ=%%8b0LH8r{S2rJu?|qda5aA zxTy=qIsKk*E+)eUd>yanZ`zkb8rspT=B)1+Gjrth^wqwJ&Q{UuADHv4L5N=?SC8LF z&Yl&KGt)s0H_qI%nV3Ac>cbQ>T~B<}jryywNlc$R4j&xHo0H$l;TQE>cVh}>@Z1=; zh?xM&caIx~j50%C#8zV0%(LmrTU2>mb1YH!X2vrE=8~c_rV`UZ7*je#vrXk^bPe@6 zP^n`~>C3lEebqcyL~cKJS$30tS71g?#~2do=C|fp)y`O!u!d7Gc?q8hC70u9#Qt^~=U10JMA=3n=vDYmfhYp_*vOAq z%Qc>j3y(E&GCyd0Eos;Js8KE8pZL56(1~pjV+fZMDOJuYngc4np9y&ek(7=m~9iJ`eO6uUY~1oj3Aw~8ea`%Q}ydZuqBx5 zI(Fl+iC&LOFvjzkwv9DCLniQxi{>m{1Zk0CcIx| zuuTo$Uvc3f>SG=RRYNz1rcwJLe1ENrV*%)x_8sIDZT};uv5T#$27ZQABVgoAN#p$L zW}=67-4`?j#C-**#Wa8Z8*}Y(*YJ8A*u$Ag zZdq|^L8QPLCp{+tiR{LRqfw;Eb+8F{tB^i8#x3W2Gn2FeXq$B<~A9M4gdP|yyNLv}E=pF)z$iy_qsRyTR6DXzQzv<`ThUiI4j&-3DFj*YU zrPrP3xvpn|W?VOe+ULzoHDXuCyMb(>&zniJ!i%h(*O`uIpVol$$G5b_*e*)ox^U?< zC6;8`9M(?A6yw+|x2(@~34iY({ZI3V&U4#F3)OK2P~Bb(>{-c>@O(dj zne_mzV~_9w4x~2oVD+BNM2g1XfaYX=AzC|?V(n;VTpf!V$11l`pMA2Zt!xUMp7g0`n8f;!;L{k3t5tcxe{HLwY*7&JHRx9p9)f-b zYM`(WmXd%qOMQycPhrcW*8+-LZB?h*ToJo=p(NQFz*n>VGvmm`hCXU-1&p&UG96u9 zdArt2->Bj_%YO=LWdKFTpe==tK?)VvCNQl%)IJH<)H$@dYT!fImD%DL%Igs-rQIOd zCU34DrZJf-z^Y}ROUK5oxkb28y3;&EoAZGwVNr@aXwPDxOab}UODBdXv7=4L3fm}H z0fuT7p+*Ow&(9NVECpV;jVDUq@VR4Ei&P#2L0G_Y~=db$+x*->;h4WS_wd~3Ak#lvpqpiPmsk_n|ID= zV8ki66Vprr>b@;dz3&l1S6xif@{QlQ-y?*eIgX@k zeR#V2U{kv}i%0`CW%Ev0y#7NckvbFF1GGHKJ`y3Cu{GW5GcZ@pg*>`hXIsE|X@>|S z;!T67^@i-4L0kex{bJ80LX6e?xKyZ#0SXgit-1;jokg13{UkoZ6Y?)NRFhOPKQQ(c zJFJe0hf;pg)gses(aWbmoI%yDG^{+LJAH5KSCtHkM%a*MHtNA8Vj0G62v$a5w!g*!}I52#iz%D zH6vi>vC^F@B?Xztb>s6;bBS*eZBm#2IyKGwz;3+yEcJ0W1L&|A(1rFF`k;+wrovp= zj+pqMk!X8esbi?wvxg@5Qoje>l~Ov;HvsmfC<_hz^O!DsD+iPH>UBXG`dEvyn1H5Nb@1kn{ zuf?b^)nXUEZ-Dc~ZAIm=5&|8nhN}apYZIYT=vR(9#&%!5Kw}isv!k)yVswC0`}c6M z6W>jO(vY3xDDB1+{D?_&&3IDAc_0Y}vj+Hpsg%s$YbQD?B!x3u(KH0clcH4b_9;xd zU|a{v_d(>lmUYp~Nj>(Z#)_z;X$!~mVjGy+IA=IUM%^q<{Z`l1 z3>sr`BMQlD5=wVAa%hiy^jFH*DQ(2a(Uz4FQIk++OKnhdlj8e`G2}#$>vb}y93D`> zaXjry2_s19gjCcp@3eZ%N4ad9Fq$Y;r((2B#>F6q@^gyb8VCic(ZR<{z3FuYKyN~5 z$aFYB?>Ad7Bx1se2DT|U3AhP3t?AxdfX^#~&vgw4aD3fw-Us;hCJ9^IB&68u8y_~k z<3)X?S|E+u?!UVK*DIvR~zqdlM5my)}@g z7X{~1CO6|a9aI~NZx}C?vIYmKO?mKxCf}KwXNQmV9DsTifq*7CF^2}?tK1B91bwxe zjjo=)+l=FjdcabwZ7U`QL)2d4d4Dg~bEV#dM*W~$=U>`%X(h%P@IqO6fbcxE9z$lt zMr{pAn(An^lw!Ofqwrj9IH%{WLDa9>rVG2|?Yk^g*O9AMaIEyze#}ldVfNCTv8n#z zoh)2jyaIM_lQtJYsvG%$kD+cv-FM?$lr)_tW1BuKZ#O2tD9gucP?yA;eVMy}I)-u6 zMb!6L(-&ZgQ>vYVvh_m({JtWGP%8Q28{M8*&celgPDbnxh`EgYqB1?J2TXEHI7F`B z3g~ey|1+*u)#qc3AW*KjTE$T#!pmfwrW=FO95-^H6Lr~UA8TTW8e29uH8$AR@Cl_l ze&l#vBE|rjwsNA~;uv1to)_anUqFpFp0?$IVNpUG78GNF8t;Rg2=K`1|az2uox?t3@6c9M8;pMv4*#4KEPID zmf$rP^s~c6(TP*DJm4tjv!p{+&_^y&+p=wZt{y@ZjctLu7TK|7deaY1uj#YraQ5!J zzd?v8;FYYKuc4Qd?rW{+KgH}P$_p~NW>Gi?4PMDz1=HF@65PmNz2A85ax-vxCe!5G zwchsddUn>kr?-fFAsMl`E)as|G|V?1H&b#P_11El!3w=C^=UEPSlBnvjXHJlvXujI z6C^qQo8AYLAy{`}diy67dM4w3ZRFXu0rA=5xLWx$~M!?a_IHl=t$j% zF7@T@OB@fS#2BfQ8ruMMvwpHZw3A@38fDXhM(WOylp_ypMqR1Id^6o3Ga?!+7Mtpt z1Q{Aq2Tl(F-JO|)SxTK@Yj+WR4ZRmPr)OyT=awa)atUcqY=Dnv-SJ{fssVU=m~FZ$ z1%ENWK3sq8_2KFbDc4^cu3Wz%#cyGMeYhs|>%tkkg*F;29%ci%24tKbVhpwkUrghR zW!w^IN1LIEopQ>HtNDz3L+0w*jWQSbU0AhF4r7bH!Ii$et=kh&LM5_^x@fTUIA7UP*oCYc^x=A3Z+ z$X3LGqqLKdeBg2GA|WDDg|RU<#zJIxBFz5>Udzf@!D%+iWZaZ zQ-GSzJee1A2rm)z3C5=T+{b%(e=C>AL=Lc|4LE(|jVkgib0}`{-;f97tixdJlD7Cc z-jSTQC;P#Ga6C7;Kk@v;f->@JH-=1(9QsPz2imiERV?FRDD`n4>!w1@V)N!UhY_%Q z8+pJqhq8t8wC_^M<~8dR=-E9|j@W3%Nyt<`kf6CVACugGj&76VL^qIt3C9q03C(xk$3BitaVpUX zR`^Kor@a&7!xX|(>RcSrB)x^x{mtefmo;bLB`2IH?8Qyx! z{^7{Jox`48+w}!mqiu`7@?X7hZFubEi^J!iJUcvd`ii*t7T6n8&ko?sfh@+P{~bHF z4?FSdZ}xTAlyA@@Ut)S;;zv#xFPJ-baFJc+YTxc1!v`=|x9%TaI(Kz=?3GKyQ>QKu zXD?kZ8Px~t02?-CJI+|h!Evg+P#w$}{8dw*DOxRvdPL=zGw9X04$uwylHI3o);qV$ z0^8=OjWhiUX}e>YZ?N{3T;S;$s2^k%uggrJi_*}?3N~Fl{xI4v`}3}&dx!f@>>KVm zws&|@_UWg-dun(^j`f{#TsAtWB0g#Xj#lhOjCS7!OoRJSQ8`knb7YS(-lIxLh4buW z(^&fFbO~_D(>N?jhT-tuoicxWhqv8+V0eqXhTyf~j~+WcJbUKKaN)`geJ)|H!N=3Q z{4v&csd7runxN@O;Mvk@n+lll`gQN|y~CSM>>qA9v}^e0^B0HDJ#l8ZbnS+|#%LGH z9+7DQHI)h4oML>5!-FlR(MrU6i%9^UXwGZ^yVoAs5KuST%4b1GW9&a4PeJBWQ?LV3 z7tnaI!xgn67(d6$I9b0Ldb%;b(pL1L6g-Gu&&D|(eH!b1>M(d+ZwJ=$qdp*Q@AjD5 zDPYw(^h7<#0X%PgLR0iK1>CQV84a<+wPV)@|KeX$W2N+ZvZ%Glj*1!{eBh=>QP1Y= z^$7CF;v6)jGZxLo>QT4mNqZ$4#14~iaB1o;c@zDm_uW4H>G$3~eB{An!~5?$I=thK zgTp(eJbc%|;oVXmyzRhn>!CfmczE>XOT!CiWihD|QQNm&-Q|m{us}N`Z$d}+@6{3mbsD!{ejj1K2Ysn&C|tTjkQbW=-{56 z`sR&=f$l%Y0jdTN#@pl#j_)VvR2%JHt8zuf>eaLW>{lFv@4EBg@S_hMAAauPTZV(u z{(}6mgvI188EDO)K$9gywD)*~`Pt97z>v25FU@gm4Pf(js~4>hKvQ%D+FEe`AzwU) z-+sHy;aiUnKmCrA@(0&;`J?mFaO&bU{df%iIK_e!#|xfY7?VET4K)R=P**puAKj$$ zKpS$wYt(!1Jv{u>+fNKX{OAgopVL&@Vv2(%IIkAZKtizkc?L& zvw3i~hVMBGn2Te3ZmuGh`i}i&{GKhwMr5|Nh_QamOMUx&qpsjbGsbwZtBI~{tKIu! zJ;YAh0@azbOQoBO8)K>17GS*wyDbY5IT_R^4;dHr*p`R*rMb3M+OnAZWw)55`U2ui z#CC&BcaFaj=rfb9+Tv*P%Ep%DQDf$E4)sLlx-6D%lNaD0dCM_9GlMr#{3ZV{zWn0w z+mF05{NbZ#hEG0vdid0%r-n~Ic6#{o)8~dKUcEeAy82ojT17V}U}RpG71~~TgZbHq zPY%B*i?S>7uKxHdmvrIAvw({TytQFVz-O)QJHBuD)^_tn@sL`$a zrfk=M+-70n;yE^t*sihqfJZ4}Gjj#wLb$ezPitCl3S16_TcCY}j_CIq9%y=B<4^Ji zqDB`{&%l1-KJ+#p7kNM;JH}9h#gS3hHZ{$syi#=)LDILP379l1n!asY5e>dC?~{7X zI{+p%XFd%W9jQi3(G;{9c`-NduR*r;KDIrFa6Zb@R8wa$l$(~T*Jrc=oLG)6>NeDutf^B0ES`o_z{-}=;5v$>DGO z{?o(X|J?J#@8c4nylLQUl*UT17Oj`hRF+cStWk%*Fv??DbPU7$<;~?6-+#Ma9s?J4 zo|*B031zD5xh0{(ExuA1SL!|@LUWK6EzU~*%)__HLh0miOzN;9UIC5}@nt1!$DG=} zU_-ls+l6qUl-N=jPxw9V>x!r)aU3som%e#;5xwU=LYT9mmR`l9}UbzbaPkiYHrgTs$M zbVAQszVE)n!=XJp>vNsCngFUmRlk&OBh-!pj+F)^U?W@T;VJ@(_ETJn;+G=FruJ)g zqWa;h^gdVAAR8E6)E!t_lB$pu+U`DH!kIs z%Y@gZjTdaZ32Tu$U&yb)t{N62TYUopN9@3Md9#7(nsoM>EZWMOOz{EI)OhVfV(|GZ zl5%!5f>_PXcshk*M_)2X-69sRZU`X|g2m?e=2b3- z!&bh8fEl*{siTSw`trHqo5WX5NAoZ`vMvA25CAI1&T%70?5CnxbW1+vjoY73!ua$_ z2@S;fIK-Gu_q*`X7y7Bkg4pm!0k+p~NZ-~^EyB?`L$Bz^x`u`ORJ}H#%nc#zSLaY{ zD3}-6aI7G|ax9cFa=v8>NM3c2FL% ztJXxFeQK;2U}G6!$C=hz&@SO+ERq+r(53{o_4vR%iW8ji8|sAzpkDLVgARR^Er2@c z@RNra<}n9S%GmIrl?LEwALe44u>~cT^F*$K@ly46ufsx4EK2}~8WUK&chx>ox1w-d zEqH*<^y6_u9#M5&@dUMDTbc@!AJH@fu`Os!Ud;Dpr2;|Ds2bKrJ?UoTTMS5bpLn|m z#ZU<4$^$2W@Eis;wgk?vvZdOB4iw5-Y3v@g4d)2I&%tT-t5Qov_0e=FU%;s(ffri~ z?JZy{I?Q2`ri1q7M`G<1NnR03S*fR7(`?4aenuNEG2`=2fAypH4)40_pkB89*WY?| z__eRRq@M-cwSzGuAbD`_4!w+oOP+Y6!sX4~JGTw{cJ0th*jOauMfu{@8^g=zuMSVX zdRciV5A7a~?cb%B7XRXh?i_yTfuqAe{o;$-AKrX$`3q-95rZkb<b`bTDt&-f?)3><8I5s=IBtcBk?_%e45i)iL4Sf_)_g$KOlmu1UEvoVs{@ zxFY);IYwaNfVsct*dCo%TuR61FEP*3r5-C!pSm@`0fG76Eq;vazLWcgqtYkF z25InLy>NAS{_Is97siQU!-+-A9vL4#vxs@Y$5oIc=7V#6M#hWd4}HRi%fM&FhW&}p z5#sp-J)iGvP41Bp33Epzd#%sa+#hs-lB zW8*l(^BnVvk3ziv-otvV;tPDg^^I5b7>5>)RrHH5v*C+77p`2_{o;YoTi!0Z_fR1qRcB^=kUCS|Mnxhhm-Q0L|>SblF93u-#6rS>GZ{Gx^MBCgpUQijAKs5isu*R zqOc3_yu@*g&*8%OoaBvT89d~zJbB2Dq3F|N-67gD4WbtdY$d*d#ATsTcano`3Amda z7W%Ayt4F#@#ZH!>Nm$hcClWOH4qZ&;$G|i}f$I}>ZBnmnMzKn{V}ewdPYAD4!DR#RZPmfTuoxy|gn80eF-&5lxozKHEw=`OP2ZLaXLC@M zdu9bnNLl47N)jWq-O)qvLSOuARaxW1s@PemuEYlk^v6lY2jt%_FS;M|i^)e`8GcO` z2)dY*7kZxqs#s)TG4LnfbK7vgEDG_WjyDA?MDU`2^58E0ChR47BjUGX|L&)s(+|V{ z;G2&QKl`Wuhw=QHua{f|C)V)(P)e~09F zK;P)FNJP%Kg!Y-oPY-|dlg|ualtmrhkluawLFHpnbadY?9VhH}$pQ$AO1zQ%?zc}3 z|LjXI=$jqh;PLv893HxT|L`GMME=;@jt}p-=kRd5yb0h%A73<*_j~YFAO6yV_v7;mco==0C(n>BPXKky?je5UP}fAFs1Ew}Esact8C$jh>L z`a{_lfBVzV%Hrw9@WZn6tYcpz$Ir-O`&Z=; z1uk;2fWS9@|JOhDX8qRfU;nMghu@JuUeF)*1@_lZOCLY?@U1$?Z#;K?_|0#;tP9#7 zeel@uk++upb3)$mF=sf$F$R2d7{|e{eCfsEGf$kBKU~Tp1UY{At;dF+dDktvc>1pV zVer5$`}O$MMV;jO^=HrPA3*rr@K>e&pMLz&;gRPq42R@+`~Lfm3_teJab0L)v4M|y zAYXi`0}BlN(S+U7Jf|cd9CLs6{dZ_A7T}1*_^<%QqT(NZ?s?r8m@h0g@Ua2hCE(A0 z;Ev&)cOKNSVKIZnChnf_+uwXe=MrCR!I?_*iN(;W6aBkCI9lV3|S=0)ApP! z#IZR3rYwT6z{3LSy)t)SlR3rZXe@$oY~YVP%sUo~m|J|g4EyiD{KOObS!Ns)KP|`8 zM`bK8$U+(mIecm9z7zZPdHAKL&ki5|+RGaMbML-o_=87J4}V9F7aZSzNscL8=01L) zEY2`~j2V}~AAjYd=6lDHy*j7=^b0TOA8vRpz*ORO9{Ul`5xmwsblZL%+sD8D^6<}P zKVP_dU7y3sHU7P_n8ZBz#UwtigFjeqmFF~kc>TfME3i<;!uI#R{i^=)fN!GXQattp zju9M_(8cQ#_WS=$UYEWs`{IDSw*2xB-Z}gQ*;jug&vEQW>=!K30SQsO_nt$;&%gUN z{VXnywO{?pi^ISCs{GLv;}|7GuA;4ZtWI#G1i`;pWmsXRzQAe8_?9swi)O%Ne! z^Y8#ZK)VI|{*3f(1+UWHBkk`rdc8;ewWhljye;Z!I-bjgJ}uC#(5)m22_lJNB98(g zA%vG3Jp)rhXMj_)E>1l2evR3B7tokxjli=0+%t5hG_7K~-zozHCdLzL4T^sn4=@$(N z3tpk|;&Zn0?8Ciu=8gNGzDx(4hiT_XY(B8zfP;e}qC@>`N(7L&y^s!M*a>6i2ZHNTX4`t?3kT_4h|$p5-@v& z10H5&&ZP0gK@-UeUi^;P4kX_%rlb`IJ0z^=Gsfl~I@lu?j1Oib*REbg{XE16R#{)f z1mfm_1&<;5!NVi3qGSpE#;ghG7?0a2X3UNv7R(r-KX?#fKNZZ-Vcd}XBeBN;_Ix_% z<3N|jnPe~{cM7d1=wkiCHWDa|5oUnV_gn9k?FId~kK#qJ&_l9;_&-NU9uf%j4e{dP zr5~pggy+!7%kB43jQeF~7jeP<_fTT=%4eU>I!Gpw{GyNl%LQj}VuIPhZ`}6?&j@3^ zVEtm|8i^x@3CWz!;2{n?`0;%--k7z*lg5V+2iC*7x#Pj5@W6h|O35o_?xOa!U%QWz z-lO5^l)QW&CD};SU<--Pjg(~GO4|UETO`Ib6#NyVHN@W!^bv^&PMYxjc*-g3c~%i) z1l!m~FiZcBv`t`~F_uVx5Fcj5v7T`PhU6cyp^kArn`E%ff{t~8^^B7-Z11li-KS9B zF%EY;@F@F%9+G;*j6@#thzn0TW81in5SV_U#>jwBc}-`L*p9l^eiR-cI*TTThI_4}TdvPb|OzARYA z2tr2nTPXSc!rqpTq2`N8N{_NhY$!OOA(B+%(uyTk%YKbav_z&jQEzCMz6&ASD796m zD(;sp5WhKOL%M`QWQ2T@@v?Ti>$3x%%8v0D;rp1zj+O9ZG{#E8!DY@Pd>Qb?bWl2T^LjqeV3r352qYUD>EMfl z2M!E4@L(T^1QRnUNUEMm2M|2ji-R|w;{5NQzbm}&Z*SvE*Vy4dpU%#4(1YE3_8kac zy8R)ZJ-dNqk;vcx{(;Zz376AZKW4Bn3x)&r^PjRg{Nzivvk$aC`h~mslJlRD{m*}M zA19kg#xVPUnMEX_IACCw^&C2woJ#S*=g!;rh4=s6p74J1ff*BA>V1fkGz`!{sp#^rs??p^e>t?Zu@ zUy2EF^0CLPN>s5vkjx@sK|JSBqKc#d2dzK))NS17Yj3%qXNYmT0Fst7x2)xa7*FJ* z@0f-8>6f0(@6Ck&KmFWY;s1Nx?L5xt?-oiVpGtP2iv#=B)W5&q{QxJMnEAt{yg#|& z&hR^bwTo|6_#&-eYzs&dkc40+>oYX&xIBnt{W~aGxr7ebNX8+9qyd+}G3JoLrRDG5 zaSD$eX5+4;vBk{TYo2=sC%srF@A|~8;s2x<@xaH&zH|@w5tq#IIxEDwh2r`KC2x>L zB8(&oysdNsvj4!5@E2eDR`~r-euHO@i0-=EMnkCP01 zFR&fqHWSDoIYq)nUFB^aN!N3>Zwfy^{l|$Kw#zTj_aB$U|Lk*j^DHqEQJ_e4sA`)E zq2WUWcz+&c{UL*Cre1^USbj?ubkA$aDwrMhYWhGTp^^fQ%ZOT>0GeD3UEAIi%P3L7 z!c?p<6)THsfheQkYW^sY;|9+=2R&9SlxBIgxbvb8$FyCvOCa&Q!I}e4tHtJ>d06^@ ztdEJCmWH|(Yl}*Y1=x1JmH{ZIg&}@&0Z<)1sIF;;6vLnc2k6DBLj7r<76sD}BuOpv zLdS(6S>j-T9XjrX`=J+{5q{~P>={ZW`jz-+8t+fYM7 z^WY-~!(N`*!R*gbo&my4(A^XllIwl@5Am4dAcrOHlfvxSSMHPp7A|$&eD}kA*$=Z$ z=qnOEj45UZk(fX4DY)dfCj2G!2bVh07xd}t)VIH(Bm;fI{bxvKkN`c44$gXsw6={Z z8A$;yO@4_ESf8at9hcd$4shE75_crhNOmw|!v}6kzHs1w_F0?w9x;p$k}M?6cAP&% zF(OvXRNEQ;Y##Ep-zeq%rf$_tYX#(!&V={6CwL1B@SL zpiVzol3bh!tfP|xByPB$5&gmpG?KRSPCtp00nBD0LBff{U2#V2j_4EH2W}03A`arX ze2Vo9-$<}wA2B0Q#CCy$|2^0yh{lN_0X~tSWBnk>e$Dev=WPIQG5G77@8u*6_evsJ z#&+x12DUl$83_&gv4YkKk}<4zFFFAm zfFO+age^>IGI4wisTemCh(odcj_L} zyPLKntWzYI7*m{J;hsip>lk0WhU*z;oD|NbZ3d<>t9v8KLmx>S#+qFsz_>&efPZXb z*rssB0w-}eIk<(^!+*Wu4&Ep1B|Dfo$9EGauSoR%>-Dm|@4_}l-yxhFVVlF{Y|Nfx zcJ#Tl{lX7UYC+?xFLcs^m~jgbPVjI^9=CGEt2sEOg1DVx<_LY}Z ztf(=WkVue?jgG$xhGf9jA|DO|`V1FuvIjbq)CR`b8%AkueFIoMV zMHXnugRX^8gla*=#Iv?kx2j~I%E?GvYTd)q)!!J|A=ua}P*d71fVX8TdV>jZDppv? zM$+Rc8cZnawlNE$KdYboXe=fsm8A->H2$y}qqpX5=@@wc|ivtf1 z_DBj>Qh^*|z(EoROgwymgDH}|bI#Zp&Y^=ZW^izj#LOKIDoD0)X_!-E{8Q1HQY2?M zplu-A@PTB(uVy{t$1KPS95g8&B#by%ty{f{j-)GDeux-KvL@Oz&-oWwVo;%DY;eI;w=^RSzkg)L#F6kke#eJB# z?8pgO+*ahQH-lTm!V<|c`isN@PxP*#q#S)gVvBWy^#ogp6Aw^f#s{-a2>WgtBP0r_ zpF;;i#Dx3)@Sw;)QXk~fNcx+BjdGLNMw+t z;)IN6bSc4q8quFZCllwuJ5>QohCt zA9h>tiV7gM9ZXrZ63Nfk-&Ep zb!)0JE3yi7O<~!Ttf?J?xVJT^Hp#5ZFGIJhY%gpUN<@ z-cXVuaGeTltBUnAFWANyJ1VELaf$a!r!Zao30BV{_NjxKYz>g}`s#~H%X|%XtOXTK zH8_WT1+ROqaI+2Y$M&Vhgx(_H?9^gN?q~5?GXyvrwrRI_+V}Q&$%@?;R1$8m8$?2c#0|4~@BYNC;opAj>*2Sr{5oIi zeb>i#^JQEdRBv*<;HP=F zuI1ztGYbFPtIrGn?hVfh|DMW!po02u|LFPQ=U(wte)t3l5)M|d1#{SGBMHS!BW8Rw zrWUYMM_4E|NXisJrjQ6=rUx^KxEB+%EjXy71F(hH&EUyq#DN1k?%mu-OY|M%fSF1> z@bue1@=Wdv`t8*pN*7oU>zYL#JK4=U=|janIVURIgZ0v?>Lb`LWe%!-aB*| z2WdQAjDs>}-;iV$-xZQMbnqz8faCr*0Ar3zx)^UHxY#x^{%G`j{oe z)g=g_P_#N5~ur2&9mCI@T|K-chPCqJuS$o{KxSiG; zwhbgXw|whizC4Uq_f$_dDu5Vw1Ir{t0mr^rpz9bz+_Q=MFfqG_*=RsjlZgaUBE$0C ziCmbEE>s@xw#9b{w+>)d4{uYz*kdLc$v2X1><5rc(uaXUUM#O_vgp&fh@5O{vWS+8qXl( zWa#9zIN3SOaU)5@wsGo4xknnwCe{(IDr`G>4a?km?|%M%A~FU1nvtmjJWe>dh>pV6 zjoHFpO$VjVr$S{}o)l1gTR|i#sUn5j9!3vUwxgvFEt%-66BGPnDb2dBwW-3fSXO~# zFhgZi3^hfiTl+0p3+9Uu+vi#d8T$CClRoV<7q)`Vw6xHRNpTRKx(>aGlkkzWRwSke%a+BTbCpOmu-=B;K|DO z{hi#q_1;h4#!IxXx$$0}al++Pj+6e8IN)+8cJfGGkT7T;u;OswL_xBLq#&{gx&@S5 z0umkUjB(&a9f`)Xx8r9WH}UK8m^6CF_3^9&$jl7}J0IAX!<0+LzSV0!Xf zhx`gtSE!|i10PQB8>&*a(w9z{xPG3d(to-IMCt#t7lMhhI`Y{7xW8D zBwf!u121qt%83&$Rbu7?iJ`_Jqh(vLDDSevK@!OYl6}Od1R0OLa)fYU8PE5iHopI>4+GP7ZL{8TU%!el)akV8uxZ zW{!~nB5A+_Ft|4mPckDeY==nx^q`Oa;c_kCreMZH9pjQR#v1D!i4V3R9N^Jkj0NJs zxZFqjNJi0tI6Tr9DtMRz$pY@H!)z1o8O0@PBD3ud90}usIC1YO5=z|ufEjE&G=yaE zCFgDp&!w?=I;~^aXOmPABW7=r0ApqrP9RQ`RSs_V2X74bQi+*7C5VPf2 zf4F*qTOe=}ifs?ybF5398TPR8qFYGRaSP0M(YRx~zznFqRRG(l0{uom@URfZ9Jlgd zd&St}dx`G^whMf>usvX(gR20zf`B9#C(Jkz;(JPIJI422-|m2ig)k$I@3ej&;g5l4 z4x<2*8$5;DN#=U&De5uJ)=RM4ReN-##b?9XRM!vbSh$u&qAiIkU|1GiX>WJ zyft%m7YQA6vyMl@zLL}S+^`FB)1_lH}(JaTFxF0`o4I8R3S5>4kDN}3Ox%| zpw4#Y><{#YGz&gL$3nWm2M}FKG3y{IqNyZ_UHc$ZIXKnOjSRBrDt5X^K(W)tLplGi zkKP>qpC7t0{OkXAQ!4ME^1pug#_)?D{7Sgu+S~Xt?HXJjW3$e74oA>+&3qfH)~s3; zeFDaax4{7-Q!fO(t`N zNem~(fHiC%I5GIzx8%Va9%mZUe@!uHK|EOgZ+~_ZKhT3&PRvjiM%{R#i5X#Ba)uxd z$~ZvlEfTnQ4hcBV7}9!1*Kl9kIj3&qhm?@~%G3SjJ!brbFD==%>y&(Vu#`4*BkK;me>n!N55Pl3ZLm{+++t9p3qouhKRl>wvch+CF}X*2ynZ9ajlHc*7lh zdqw3}!v!u*>7o!-UdITqJZxUB*w z-Pj)C7NH}4ja!R_Yc^5n#X4{HQY~P|jAfH<{_b#Wwiq3@i?-?}eSs0pu(DKqKfR3t zeY~nOg41C!&`jYNP!y;;dr2f#czl$mP+qK-Olm5_6s>yXaiKsEOA(J~B^mIjj&>3i zk<`|BnMXjKp#+OaV;wDM3z?zYl%)ZX^}>nd=$*Q0r#K@mg^7w54{ML0x|wej3&6YgV9+ za~#~FipuH%Q|T5sIlxR4UP_MS?dEUo=W)Ig<80zVY$#X#<6b_XBl+a}-`HNZ$u;^Q z@9ZD9HeeP9$rX8Z%Gd`P9vP?<6iSV|EO2 z(!W7rNSajeJ;77TSjSkuNK!saV}}_m+&_s2K``@+oE$S|NT`vdV^DfJon>fdJf#coDum3RVa8 z9{Zy(gN_%S$6XbD8MR|+`p2EiEMI=3V`WR0NBO&8)=CMYR2W>FMNHh=|Oklki?z9n?_>SOm zG#*~TeWiHV2k-d1XvY@jY2@QtJt z$r#3mQ&IAVCHjNQ^cYXvXNqkI2XXjD9jj1)0AuV0O_CZU0C=G4|8vc5zD#{SC49K& z?z_)Dg&&l{c=dJ^Di;j|AzQ z(>C(#E$*{J;;dtj^?=Kz>uKGf!1mn_9gqh|h>vxL@jz@biU@2sQgAYY2VAheaUy_+ zrI1kSwuLUC?|70LmvfPzayRioC3blLH6cV4qhfnfWyCuC;ydx>JMg6bJ^W-izdZao zr}4WwF~f~*0m+`6!1MzmkkW)1Cv3lW^{xC?iHqpO0Qa!siFk~g#-L&09!uPQf_opa zZ_)jY?q_h>9qSZhh`6!c;#P>O=>z~LFL*lwUe$ydN?c9AHi&f#d4-NQ3aA`!W;1-- zLgh;tSr`>bs#AF~w4i z!P){;UJ=_$W?eNDp*)7S2P9?sM`2<#66J+PP3l~yk5N=*ybc2mLrB#^NRaHSj@oy! z71gecb_hl7Fr7XkQePs?usUEpEzw?KH#S*N39p33g2b+!Pviet6gc2jN4sBgaw;W(tu2*faXk@L`e(zQ2uj$hh>0#1RJz zBq86t?-4#&0_UHZf3^HtU*|Gbbox!{8x?(M@XXOP_Dbtm8Zj8pmbESSkUKuHdKBiY4^);ZoCTP4|h z^f1q);od_eGBD2D3$_nTEfX+C<+bp&Z%=n#p(ptW!53^ZFl+hP} zy3FvKuC7ZgP;e51cMsxSnxFs1gZxxDe)R`4Wq3dbV~A~`uoG?Idw~-J%&_w7i6~*g zIN*iex81!zeCgH)_`xEKDP~!5*&egBm<7daWUxKp33A+bhp_n)Emd&;9r}iuFC;pM z8FAuLI<`-|h#N0@NAiOQkZ_qE+Y-hH`k47dVuSk<5jLLE$4j@7n1jDWI_LssjxTw- zTw+FFAnS&U!QG90mIX#75<0;Byz;yAe8q!e#P{IPp~JlX;U8nUkJcBy^Y_yd`vc4f z;{*h8e|gVCJQkXi>DcSKD-e0um!OK17U<(CZM>iy+aP}B=9y=1T_V}-kQ$xNW`~LU%P#!Pv z(I@NdT2ZM-N}mm2BwvRKD8{0qUywXs+r0FHV_@`>RLzzaj~XqZ$8Qe{m?@b=@>V-j zTP_A-%rJr}k+f^lp^xYL(rsrSEp&AFqpO1h0?xv5utTD@iFTkkgV%!ljgU;>Y`sLq z7Ll59MKTWdIMCt213a~fgB<+hAc;g|5ACF%a_ai<&t7~M|C$J94)C&O9HgX~ zA+ta@AmV9cBp67HkO<>mu9GOS!1!*W#071Go6&r2buxr5 zOzk>onEk`6dGHc>Bwk2>@We456u~SX5;nYl63NYrp1Oq-s_#B;Dv1JgQNz1MRI`4lQ`gFW(PB0ctRVqIanguK{AWkNxZ)gNetc!0J|J0jX#nQ z+$)E7mg2HA);HGsNohM+!*;=Y=`&8Dbr$$CBob02ALwfut5k`QVbKKx+cpwpBp@I7 z`#X4c6EjUn^f2>;S3O~t774l7!-p8g=7;SU?~cTbCT6+tbTYp>hWd6Y)-{a}#s>Yu zc82ZzCh8xadVd}z^tcR*?G4`vT((CNgP;EU5!xQ`>nzwlkZdBkMvNFk{F)Gcaun}z z#d^cGgt0;Li|;E!MN);IPsGU#9yCEhg_%FB>vbfD7n37yyg~;)vCZjj$!=R*$GGB; z>#?K?Hw0A0i3EJ!OW$d%4MRh5ToP#z#zJczTM`iaZYxQvWf@SwkX^}TRm z>Oc%R0kgW0!H)ub=!Uzvj&1x!l&oR~6yt=GBfMN5uO-4SN#SG;W3ZXl9k%Pu8&>iC zmOt`>C-F~8o=p2k-Pa&ye7A7|gMHb~8+%q@o6O^CMOllzyHV?}8yz$MZQoSP2dm+gCiR#U9W0+y~$ zK{Ir2*7R#M0CcsbFtW)_MW150ko(kbevqq zI(CXUaN)p$GiqGs!u@M_iV-_q92D^@4lknvHGUxiPgf!lK*E57tjwMr58DFKk^B5}bi4i3;r4smZF5+IB!^pRj84kTeTOg!_1gNt0c#S5h6 zXAE($#Ecx0AdK_(QF4hSbNeal`2H}&k9Tz9V8yEuC9Z2M^gcq&T;ZqqfKPt)UVbp*lc6Lf@=ZOmXH5yP+az&2)IF*b-DKX-@AtoS9IG|v2@XIUq>clGnPJs57g_fbx? z;T!T8CnVZfN0=c-@{LQXxK#mPZumojgc&vXyYB1v@e80ac_*>YKFv;f)Xv(l}#3 zp-<0aJHYn_ucE>e>qwfh9`HTFNhR#yX>RN{ki_9_0Jv9l=bn8$M%d=rBYlWx`X0pZ z1Asc>z%2vVjxigKm!f}WxBRLWUdMz)77xc@onw7s+lC%qsfCkzyayJ)5`{Q%l8Uj! zrE$y(V+J3u`jLm?aF4cp-?5KE0*R9H4TL_VX!*YWEh64NoxjcHv;$d*;@TSFIM zR)wGJGy^6?Iu=zC5c~8A*merGX%u4_fwt1BeKk7bs(d&p|}i)9qcoo zgnNb7@qq@vQ+qBQT=AQ@*x{l)123|s#0qEGcrXM>01m*|k>V*>Bult#ikEF8nZiyM zJ6|Mru#Y&9czy1h`}jbG`;U;sVkeA*6leZ87~x(gB!~Y<2TjZt0654a!37_`*n)WQ zS{)=Oe{#bee9%K32@76hgIONz?D4<|k_zggB$GfMo)SdIcwtm9yN0nql7##4zJm^Y zIQZa&?0A7U4){neaKk=+XBd|*aoJeLRgyd;9XK#yh78Fa5&`~Y5|TwSfY~J+v~Yk$ zLWaxv&m}*wivt^yBE*hl1EKSJ&8v{Ak|3k6H~?b?5y>~+--t_tIM71{{X!CfqzFkc z5+NMu@sp1jYb27mw-G*Y07cS{b^RO~XB?1`s339r@1MJqW5WG+0A}zID^3I;gUjSt zFF3#=;Xpr8KXVf$%_IXLQM;ZJNo*%rCwP_3UP`R64PpIbb_YL=i2D}tBsV`)LUKqp z@ER5*PDq|{V8_f7e$yH6?A$>oE9g6Z-3C9uh-4MX6K1_OQNOWGAO`#@4`lE#3f?P; z?|>!(D4RB{;$Lt2d(wwb_(KwdZ3_>BAOYG;$pVrioIF8Bw<}&f6f2T7JOPe%j|2>t zJ@s`~xD=1=@~>~Ymw%NAzvzPR4%Pu)@q={+S!~<m2fSJe+Y|iYSB9`&AcqqZBvV+Am?6adxJV**Q1Xf7AO6sP%-$i{$2%7h)2DB_ zkJkwjG;H%ol<=MU+&A{|%qrpnuq~Xtc4Zo8Z0mS&K7MN*+bw3OF^<^2@Et~6xMviJ zH|~+Vu&{kT@uBWFMsC4W`dJP@fM2Jg5N7WKg*J99# zFWN0)a!4f4^D!BICtRy*nU-#lL?XYY0$Ng!jMOIBzWVx#4R55I4RcCj2#{* zK^@}_J&YU1Relcd5l+64_#jTPO;Ql#IxevVzP}8Yc`?g_1pbS+Jw$pko5`RB4x;D- zk{dkyfy;)NO@&p=-XTWBili3n6DJIK)CTr29y ziABFL1{gCMG9Fu8#zo?e?F`*VGKu5@znFyN4)OD)M@lH52YsClL!8)tkUZg6UhpJ1 z)(d8vFoTR_65HTIw4O0@j+s-)>NsN^VZ89r4Elpt#b612;mfv#S!(p3`$&l(l076U zu#J-{tOq1>hz*vp&T&EkJ#5dI_2PXF8N^JRY)f*B2u|8~TOzh@OBfT3DUx#}bTa$7 zo)XTJInjnJwj-PzVr-C5VfGaCowq;8Nf_cmGKmu`xh%h*#$0BN0h~NxoDl=$@R}~n zpd#tUcN(+MNJMcmgYP?NSO>Vh0ox&dp$Xq5B!K_y#oKu%ke`O9Z4ST5k5^YAxs2|* zkb&Yc!~P4&BwhrM#2<+tUd;o4xYYvVg;`wuG$@i;tV7JYW1V39ki27mg?&I?w|Kew zp|G1|u&=>vE!G?82M!%(`8R*pcD|B>OXhgH1Rk_Pzp!q7+!$L6<;T?;8*ypcPEA<& zsK_#6o9#+fKn;z-$HwsX0MJ}p#ekZbgy^s8OX2MU>Yzm@)YKT!XRMZ}DiDm_gqEzL z7J%9r(p5OW9d)hD6Q2d?!^GfvsKLd+c2KdlRJTS`I6DkOOD3$18jL=wDqpN~Z$o*< zGnqsb2u>5lCvq|={gi&-U}&NJR^Y$}iY7NWIO-Y##fdXu7h24s4=)03b&CbkDo*^O zF^$WG#(;>%0;q_{idQxh1yoYk8LvOC(Wfd$=veR~`e1*NroCeO5q7*(M$F*A6TQ<1 z^MoOMV*@~wuq$-cEP!nK7E+iVnCgMo(B$^~o{Abi4 zqA_48>yKFs*iasz`e~h@Kp(pFhkp%LE$2@dtgT3zK7DJ%PrrxH-TwXPa3h!RU@xpmI@nLt^-b2*<2x7Qb`Pg>MEiv!^#nSOpYyx z#7^HV8upuXAo8jrvC-+bqGk_JV-_u0pcNnJeR?$*-yO4B@$t>*BaO$9p7#T|7(`kDY6k_oRzd0!@la0%eZ@Z-k;Y3Q4?<+fSBzhMCp&Q|@u0xAjJx^}8Ra7hV1j^1 zY%+pbDhj{`+sjJQCzMEVz+sV%MqQSRPNXUEDGJ;CCn^Q3d|b-U?}(qS0!U(uMvXrc zR900hy26lZ0n+JgODvmxNHw;Fl%Rc8pR7tBvB|J)U7`=_GmR4`s;66zt zz_^_u-C9$D#$LpRcC05vzgAug3&)tW&T%R)!ZnjlqNxoa5);YtEQqdlIUXUUk`M_h z(*gL}p+eC#qm6|Y3M7lmo#z~a#ktK-8%?Gf)d&rH^# zD`HWB6bDrY{ua-=NLlOCGmyB{#c@Eq+NLZUzw&hFV?D957u;Zoc-HtsdsnzlgwEUBt zh-QV>`m4g*Rarx45`C!I*o_XX+m;N2fs|2BF6LsT1fP9iw7>}PsdEQH#f$QyD`?KRRPGqHo?d+lloAC3t&8_v6>pwMaU| z#1jTNdGUbB7uB;jFTVeTBFKk$yj`2G3cpK*Xaakem%J(7jAh#Gkv0!vc6mZ(*g2S9V}PE3XU zd@u%l+_m+=5>_sXXabrMqqU+@B!$v7-{Ix93LAsbXAw7%KxGPjwCt#kG$V~aYBO0z zhpNivrjyNnWT|bSvo0*yZZ;+&h^5pi?6MBfBNLJA@P}bU`0<5ErU|nB>`$tBAEPQ2 z7Yt{Tp|aq~y`n3^kG$|ne2G|}a{u_3?+G9K(mnh;+IT<-4k2IoLtB~*iHWNnB)#}i zi%XI0kdTOpQ6D$%5B_HtofUred!NepxZ>>=c<}2giXHcP;+|2AUB}f>EYZ(QC~Qb0 ztv~)GxC*aNt`TBI`^X?#;xn`rg;dt*7CNPYHb~c*%k7Gdj<1fAEFf3Yw2)?0oa9|mb|wY+uSrdOt$vMfP;wDXsa+`8^%{t z$H?bb0}Y|@*up5G#|&7JcKaLyUilpz?9hZ*GY6GfN1~9g`Wy#;00U-?Wfm;jb|w5K zSk}uo^owzsX>6fq6p|TpU~B}LE>V>TN*`SRX--|r9D*_q$rJm)!QRNW{$88ozv5fWFM z_9{5jerm8Av(TnJQF@H`yEalrc**dJd#?GJ6nSrmKRTldVJ}+FY~`J zDdAm&j}jPMQ;$3{4}syV`wj;LhYFKk0YgmNv@y|qH=ZM3k0rlrA+6my;hK0q?7|A4 z&>687Di`M3UWJ$ebl{z(d;4_q-oyK2;igEJ<4XtSA}tE#vBt?T34@oG0x<3Fnxms) zetd)v{9W7_CmFw(?zv`$^1o;E7Dzz_UO&YhJv0RWpqW^n;TzTBD#XGz9fdZDBuNFg zc&|)f?0UywS0WMW{UNoVkFBp6iu0K;3LB)7_{N#saGm_!v3i2vgH}!8Sr{X6cUPqb zI{HUPQ!*MuG--S{AhncF_|(kRpQM?kDVkwtEr8nWE#mjao{~(!(GXLp$#hrl?Ym=ED zj59eH_96umk#-5(KBAb-cU8iBO(dTDbyuWnGMY$gyI5tHn4RDC)=udVbs4bs zq|f=9#~CDc;XL&5=5)W@&Jc07Gq232h4rr3uEJ5M7kowTJd!LqaPT;Lsi^ zR(w&FG-Rq0J{@fFMA3N)zQ_!<d+LR~7Un*vIa8BasR}1WU8DG8`cUDAd zd62ahKgZ|(W;4HmnYdl%1kZW5nqgEE?$%Ih^0XSH99N%FEqMVkvW*UwW3Wq6q6%AyX!PgfvS12z#K(9gP$WyT zY6ya9K3nffR}pjKXp)(iLdP0V>oDS!miWL4p-YZrivg#pClB_gX9BxiD zr;$GcuYLE$G`?fDlN@K1wlHxJLzUqYs{2=OH{pFWz`+2;O|7z(Q8BI0cSJdqUhA zbsz-XScUXs6QR!Dy9o{!V!fAjbBVfom=j3ThG!=DH#;;fT1GjoD>Vzdn|Bx|nm1~$ zQP}7{Cdk~1(-I9-L>eQ(q3^c4y`*eY?|J$9nM;9`&>M6ySYEKNX2GUqXg{*jV^!rv zQ&6PLHCUf%Jrhz4>JY}GZ&7_h%24D7l77&y!KwCMC#iF%EP9b2z1OrXHh(^mB%m_o z5TF+^r@v^!$(5og*Jm)QYJCp6I-KzoU61sZh#|UIts3YuIAXhxKbBn%km<AqL(+gimbXu{~B5~3vH0zAww<#D({yZr?rQI&- zuCI6lDZ!mqQcj}a&Og9^;hJh<5%*a6z0UoN6xniHwB`~!t<-Cfikx9BxYbm@O8fHb zHDiETNPfVxpYMG8u47F7QY^0)a>AMDWE!s3xeO8;q`(>LY*5;{oov2ZL1gi9Xxs6G zRL&wE#jUYrSslsTUHfdh0k%W<=yZlI?pma0&{|yWb{lfW;X^loO=z)z=~VKlx-C^> z)qN7u3DMATaZydojYcC+l9?YFyx2AM~8{J}fDu_QdOGv6^?hXap!@QMxs=lAUx1o$~=BU7K!e%`TGM-d; zjS+cJy%(LxD+m~e)&!GUv@=u+Kk%?$!7&FR3D6H8$Zs*cfQauYk-+mTAyb1bFyn1M zTUM$g2|>nLWZzTnbYNROWAvU0tIsnz|6B3nWgL;e=SwXqEx^ zp6GPEf*?qW; z-7yGioNSL$2)E`59}F+hENP}_+G3aL^NUIoGf9E`PS#GKAy9T}3BRDpJFCpTS-+{l z9mDSGlQ_wDyR4d38^CifL-!@(>(SPXep?lHd$_;<^oay46 z@qh#l=Ef4Xh%3~pTBSQ*g<>wY8m+ne#yndvVlT)MHL!E1Ty`CbW=7`3J_Te3k%l`Q zje-F+QJ7HD5-WG)JswKyxuE7Eun4)YPaqXb69moHD7qwl@C*>L6Kqa@rk)m-#~^?? z)A?%H;hi?STGz`z%mL-->#Iu>-#9o{uR6k^`Zs{q4xci`rH$H z+qbMLty@_4i@>^P$9}t$nAtB1=0}ga3M($y;jE-HHM#m=>)wehCFsT2Tbj?RSavh2 z+v@|+Y?;Ds7n3Y=O@R5M#U4<}W#qJQgFg&>%e)^Z=E`ckRN}ZsWP`uz#v{wR)CwxY zq}pzp7KcK`OekJv3$vQ7thv}R;qQ?mHil~um;Zi^8%{GqeSUh$Eii9O2H*O*S1|60m4F{tRm$OXcS5p_@ z(-mkq6oG9e=kO#%%21M=$VTrW^7hS=5fumt3Dr}D5j(r66hG%S6un!tLM^|nEj2|soDm|mcCs8v}M>(c%&~#W(ycYrwgCd(6`GX|P(z_W|qj#-x#-2TBakjd^ ziglKsX^7RtpN_0)=XoDHR>-A_N_AS#Dkhe{Fvvggi)!;>OaZ|{72rEv^0p!uJC*fg zIVoeOsuY)5*HkISsrY?Q3lCzo})qSUYjkM1Zwx6?m%` zFDuhD^~4sYL7KV_5X~F7$M+2eT{Lbf+wH6v2`Do{Kj44&$_@@z5vy&S^vR_~S|g$ZHrrs$UXQ(o*wOhUikMh28Bj*J9)u6njR`?}9w`IFsZW zg0oqPh&z-LAT+WL%))m?po{H&rof6ZzUa=Gwq6=(KqDDm7pkr-o{(RO(FT}d~DhtM%M2y2!FV^NgC zDv8qIN>0Z_BrY^e zi} zJW(VjFl`Yd3k#UTH%*ztaB+>1zRA0Tl5(HmVY(f2a zS7p;?OL=F`r4gVdgf_dRvoemazsOrh{N9*fTFZ#W>WTn@ABlk; z6akAyF@wz%=n7ODRnSs3YVff&)^P`mQS|2}soQAIVfUTn)25D$6z@qz4^f;?I!0sf zLJYuUl1uov>>y^0kem`1Sh#uewY{ad04C%<&qhJ$f+&Gqnno!776J1L!5T};8nDy&Hw-9} zO*w=?Cz$-NF^<2A903MOINujqRoe}rS|;;(Dnnp%nGksmzI|G=5JIQm%H1Ps8ou8J zIyzZ+2c4?p_QKG%iUAN*x zxfRgVKmO-2;0E?7g9jGtt31v5lO7eW<$k8{$i1%fgFhK!X7cyCVd{;@o%ui|qM z7E!c;$l_wX!3Fmk%(sUIcm1*^zKdT?j@nWv@P|Q7D<=C{rmWniY&aBjF@srmQ4>uVvu$* zG&=Sz3t@k1xI^;({aqaZimWGkU5e<=Scez+f#R%1415a+Q@Qn{io#Um=qbZ+v zY8W6(j;sCGD$-&R)b)#jCpFtXG?NNh0Pdn)R5kJp{eBq#e>bAYXR4;4l?zdJH%iOl znmC+2F58T#{I||QFlbJkk^T&3CE0?h1?B1NJ2L6KYK2?euLc}#CI08~6smYMiqEbG z!t*hbqO9aF>qY={~Lkrkr0>s_!@r6+v}>$$6!??W1A3>qq&v0jn zTrES5B7saa;0YZS4nAU za)*abmRpqz#kW6=edh809-oDZu7rE9*kWNXOP};LYhZBQhOgWaD)(0fj1W(_S?!88 zcKgC$!mXvr#Vtsv=k`w>BM+8W4{hfq5UKBj(9Ygz{jFFj1FHk*yBSp9$<}#^CO|0^ zSsAG7!TPdaNon~%ecTRtVHecOi)7m{WS_c7lu1odJgG8fHicqVs>7&K1o3l-nv-x* z#Ny4!1h*8`O+pkp4a_2cmY1~C0wAiW|Ew#m!TJ673pImNQq$FPFsAoPORK@zYxK8J z?d}8XJ{;b4Ec$<~C-V=Sb9sP1r2nt}NaOo5b5#W|A64Or^OTvY6~;lmu({q^yT3zgqLw~tliZBV4@D-W?%iklFlHxRAr7r{F)1x z4k+n%V`Sb(+5A+(SmgjspE-Ydoj#$jS`YcG3Ba=pF}`ukDD%<%m=pJ&oCzjuc zey4JmSZ$ggxs)sA85Eq`{s4Ml?-3^u130=zqY+|GW^e)tymIG3hBk@^ILS*YSmHnhXXwyxS;E){nAKC}8- z0_Iq|L=1X(5u56o7!4kMMTjC})hFf`$x67?#z-N2&)Uy^ckJV>m7A&$JZ~x-|czcdS)SH|7?LKa|Fz!mN(B!3B z#5JC3RBI<)tjSdd7|&^CqaKbbqvpDlUnqP!amms9T+MBb4DZqyjVs%cSOmMU42+z1~ZC z!_s>|IiQfdEv&Q~h@L#m7W>T))V$dl@xoc~yz6HF{^Ps6#Da2CQr4Szt+^DQ??Q9P z0;~bMV_Vpp*Zb1H+yUBvcYPxQqsQwDMD0LN=ev$3*0aYKxW4@qlkVeqMufgi`@Y(D zwoAG%YmXEDDP~fiH>Wy9I)3bYkXO-fM$)@3 zx33*Q9Ff~qaAVIm&o((2)UP|w3yggNf!R+U(Ft~ZE@Odzm(woWvX7h*wQr7EE>$0v zd%PV1yR3#{=BdvgeY(xq*Ke{O%f1T&kK|R~UI!-YXg$+B?l-Y(ekBKKcZXx{6<~#1 zZV!qCd9zzhts64Q1m` z2%&J79I$^w2KCwMvPXWua4Bk7P9b|pqOQxNeqV%B8@b;Rvw+z%htqC%=U9)iEP*d@ zL3;x`t)++IWiaqJjRD;6(;5V_hkXSgOA-={9dtA{oyOfnvS3iA;vL1w6*8H z2-H5+JLCznw`;%5vxdeWIT!=g+n%&pkuIJD)w`QPFT4h%!Y&h@#9N+$ zmw#PWkhZ<)*KeNF!;IgN8R1cl$pT)7Mm9++YF9~(ZpXAkD{GPi4DZ!uJ6wp^WiR~G z!^W~|j|wC&wYRXGRsmvdXZL2~dB5X!V$40?Q$b5xF1mmzrJq14XYX%Kfrf1-_nX9C zpJxiF9)5h&9;7sz;?@rTiRz4${&EEe$?aa|2CgW-WG%n`aT(C` z?Q22$*z;Ri%p6MIlMUcrr9b1|h+y+}^JlTsWu`Hj^8>H;74r6~^}9}+!BTB(l*iWN z*9$~}S0<3Si)Y);z3tlSkNR$TKgz$Y2EA*z%F-;)3zgS8H9od&d*bB*pfTfwAAd@n z*&1VwHwn78%{SkVUC!}re_t+uW^Jth2~)Vix=B&OpEC>JnTfQ+taE;|_}$O(gr=V_ zqq78@gu8%}mm&@^&ULM;_JjFt@RG;xvl|mF^BcXPOROWU)z~LO2>kHSwgI0}#6fR2 z_%KNKEIW{d77JdmYK}pJUZOEzN2x2zGjMx3yl>J|13Q2!EirNVn%ePpsHHa|0eZ}Y zUMYNZagXe)L!f)xjtV`b-vN#MIlMiuTJXJ-yjB7RW=#i=pPn6t0*JB)2P>gEJ$&qa zZv!;Jx=gxwRrP^L9cQLwydC3!CR9s5fm;JF^w>D)q5w;p9N*e@naT3|Z)aWJXzqv? zb6w>Cr(}j0ECO)V{vb-S;ixsGp7&FB_6^d2vv^_RfL#>4P{%^tMJL)Wkry`Vv23Ua z!@!qQ(5o!JJzu9#=i|3pc;_y@!A?8Jwr$6BnY_K*H-AvD@xv|{PQ+cVsfWj*s+=HYsDNuq4hi2)OAvJ6OPou2jdAR8Uk@gWxT1#{ zc#X^kWs#%)au!TPHmKu_wH5hnT?4P(kCo%E-No2>IA%ayg~Hmk2UWxDf3h0%WiTek z^bkd-2F2d=2#g1uET+|YdRX{AHxbQ>kPdF)`zI?yf+A}>$M9E{#F`Q|H+(qU`c3`v zGEf>m#_;qYHe2w?>zsI~pLO#32$mQvM;9bzl@tQQ89& zURImVT-t#*V%~Ncar0LWU5SY_aYyPZ)nimz%7&*i2IrMs-eNNIk(neQl(t?=ar3}R zE*##=Tplu>25L8+fmcBa_+*%Ib>e-SG;I|;5ANe{I?KYjBA3?c2L zmqSpyiM-E&{4~%mkhKFFTwm|JMQ{VIyz6u8Cj_y|Z}3k+i=9-U z6%mq6Qmj>|!!GX5SGZl9fERO?OCsPZ_=LAp-s%UE10r76=fNi5Z?@N*WXNQDpjF(> zm&sOniqrbrTaa}iapUh4pMaYws~B72$-uF-zDkC*p}4@yJMdF%**SuCpx{)6Q^$k2 z9#&}AQaDlf2-P26XZR&EPp9X4uodZ07iiUfEK)>?R;agROGF4q}TkcIB)^lb0th*m=+Pv;5LC#)k1^cDW{-HMUY!_+1 zXl}|^RdY+>+S$J9fa5PjKziDlIFj_P!&qBbFp=jVFlrQ(n; zdC{SmB%wqD&+0!ytzK^WT-jG?Uf6c+ejCBIDE#Vo;)y3P85OniozY=NA*4Yk9DL~e9#5r(C0`%!)6b)e3DaqJ>w8Ke6G zCw4~1Q?6~#to(1mZA$oVc6zu1VE+tpX)7W5u7s#2-QqqBFEr1urN8qudR1oo$%R?j zC1&HFK+5MTN&so8IrYWe@nHnd36<43dE45=ds|Dt3sX7Y zBop=>Dgrg8h`vqKyn^~9pW_341KVD{`u556T&V&QJdC8HsHRkNyHQ7^n|k<&V*^QU ze#;}EhQ@rJso+fB)SA7Jz>IWOi)Y(o2tRAN@sFu~>tIQ@g3U&IihNb{sPM4B9x^e< zkDq9(5W@2ESF#TE3S-=vg4Y@&s)=kg^zF;-XcGsp3F2QAthMsXaRsDVPxiQ`uSyPT~9@uT-B#in3zHv7z{LL!3{2ZDv^s6q)`f|{G$?5!Y0YuX5 zSwt-)7NFAh`d7yg-^MV9q!Ooi&mZkEjSv*5>Z*A$0-x2Y$Hxb6v8p~EvaKt1!2`M@~yUJHKt}&?;VkxWJ(!M zE%C<_;C$?$=&eNB)LPP*?1H1(r6$5o=$c~Hli@VF2b1FjNnLH*h*gf_=pN~>=74v( zBONr8>qcE%uWdln&d6iFKEaVKV`1z~LNZbQzkG+pnYJ1ResbF#j`v1H!V?#tV((R) zI_V5NNb&JPk&GhuH(kzA+SqS?0RkOz7qb7k7={I-?cA_WfwJa#YV~otcW*pRZ%1)J zwN3#U7f~PWocwge{G%6{{X!`s0e`G=GbMeQspA3D2YJGQoKqU&Xbty&MTmu^M)qi( zy!l3}kbW)>WXdUVr9t1QIC}roi-^IEN38bATmzoOG_8Cey&0M8n-l&v2;?Ca99#Vx zZHgUp>A;AI=Pm6@TxT9Z`m}bB+TyCWOiVUBoNmV0i&ViH0MkQx`6^4*l81Y-wT|PZ zy&0Tr%2F=!s%x3#M(i@RA#`18yvB@1I)g9xh+IR=dB!KOS@|%rvEM-)&v+N+FLZCZ z>7iioZY}(~llR-MLLaqX-eC=RV2XL)*W*KNsM12y)k&5;`)*Bb?`eU}_{)ceghsDz zBD?<})d&beE>qrZgFhvdH|jV9-N3RB&ugRztpWB?N;Soj;3}m`pu*&4Z+dk)DjAA# z_cKJggp4@ga^ZC5%0u+FEY!tXw1cIPWFLlxx7&+{8oj|^TJBQq2+)w3u zjlu@dZKf@$1vV#Hn8R>!XIqrT0ULi9fH5*r<#<}KB1QMk;`7h-gi8;a0cB+L`==q8 zCW5TR>RffQp+j&iou7x147|eZMW~Nw0R0N7y`D?q_C!&rJ^3H-`f|s&ugh_rHbjpx zcG~aW&qDF0Q$xU^osub%LlFgrAq;_UUR&{WI*4SC*MPvCMpgtolHKPgF9ZC%evy}B zmui3QL)U?ixB23E7w19teYfm2BvT!|hFP1qEZ$MsumK**&KF*3t0c|63DKTc@K`r@+?KTs~9q>NRxd>X&AVR14D0I4{E|V==w6R z@vO}3$=FJlW@Ku#FWesO#!A)t4`mN*QJ9?$0nl{S?T2XEJ$*tpH+-XgXVYRGKSItf z77%d13rCdf+VKXCz2Xe5{ms<5^>U@)v*Z0P1MGPFYUyH!T; z!Qd0Jm`2B?*F$j4)KXH4Kj>uZdFneYi?$)U(fcS}py1`DNZGsiB=ZHzMSSho_HE5{ zp1cPmd=@`V0QVHcSIlSq^oX2=?6|?xbmKoF)Q_V}uRd+9AI zjB!0Pp{^Q%z|P$y1uFJp&sWlJ@x1$XM_3pz?FPP;Yy@llkp;zFAV2mo$K4 zFL|)ne0e84J5aTk{@0J|rSBtqfsUV5#`Q?Z#fcWhuQ+PLyJ&skKuMEacp(dIQPE0Xs1)t61p$Fvv1{OhedM$m2c z=W_(Imvm?Nix==@e$URZkH~d6I4*uDz2JSmc35F9Uf?4{2V5zE+(^()-fMaQ)_@I_ ze%iIE4yG6ReUdb!p^9kWTHvF)H}8Hf+SQa~0QlN>E{5g37}VpP9J4grUH5lCrHL$h z4dt6L#^%lLM>4yKDRi-gQ}b5M8^$}Z73*A6dT5Ag^kZf1l{f6C%TMUr(hpV>3a^T_M(l-#H*_zDe5?|M6BxdKlWv z-_s4G)}?TT_j^X*+M_kreOS)LYZ$maU20?LRlE&Kq_NG;7w~$~-J!@uSMZQ3@)!$d zP!uKE#Hx9z9s@G6^7!zY{P1w(`}lW^nup9?|JLH|*}OukXRs0|grAi4H(%$ab}h1R zf{s(5Kj5B>+vsxua^3MWE1LGpj+80U%lU5(gSRy8@aRka&!n|uz6`g2rGiYK<2@!lQEcS+kAzGg$vqizE7t2wNFCfDyJTVq*K|Un zfDl3?hEy^Y8RwA}4Yg7fzC!T2ahYnpck;Nn@tCX$5Bq?_jCo)COif8kqq(t43L_%U z-6USx$ol6HvW03*G$>@*rgqY_S!K9ESXxz4g8T<=c>>5j^m6L2*x8r>2Y)2hsjto z!U@=AtwH<%hscodvPl9-(M^Qp6NFzzz~A%Iw*mqbUEcKj;OIcF^*7f^_;DX@NaP(3 zR3Y-3>LPc(6dXtH_0mrk`hF?0NwWEtg#^5NNKQqLSyI?klzZ0%Rc!gCQe`6z9e_KH zUsNBCoM+o-^S&E2^Emh(lz`3y;d@*Unjrm$VUISo<#sDBej=~+QM3O1ub-0uU;d0+ zxF)SBh=;ROH6yy`-=W@<#qM$W$n$F=&?3_FeAOR}d#s7@I_)v<)dE0q(4fWVbf9pO z(wFdNJSCP-gIYe`zb`l~M6XL=3u~7jS}GBF2zd_DSbE(U9AHwvQ^g@b*|&y?1&UUA zB=sc*@oTKa@L<=R>;5R-`ZMEVF>vDIva#p_xRryf{oy_ENYr=nvYiQnu~u^aoDKd2 zncIJew|Ma}RrG<~Z-4rqDj5?*8`JCQ2e7sIdi)Ih6C&d#-UhEK{o86{6ddDd-}hKr zi=wj8PUB$6>h9e(bw%`{F1#N}#YKsrM`YQ{r^!MHeHma@iN6MEF*eM-5#|I9uMOQq zJBtN{T><&WB`@Jd&h|`^!Xt^4SDmpS0n+5>kCOY}3hf%l3RFcasDe34k^(wZ$y6Fm zj3xpz`RTEHk;9q1*E#es1JKrq7u?0i58A9xChTEj5U`rwH_CxL5qd&7qVHT4Ep?{+eqs#MYA_P zmNn^$6Su^c8?@+JRQ|r)^2F?6R)+%hoR3DLKl8o|i%-j|MW5+#Thtmjs58JLahwMClV@bNg`Na| zU)c?R?PWA~dA=|RwY7-EvFPZm(0L3kw@A_^`NyaiqQ8wlFcb^t#cBGX|Nd-079_;^ zPBHtBR0WMh;k@0C0>#CjwH0(y!ZF=*Q5k#|d}**45NlR!4_};Pd*FnH{5U_-a9a2WHYKtx)Cl{v zA($Ipp@~>D5}Di-&ZAqbOe2}ZE9MA#5i7J~MUieJ zbjnQ;=;(&){+~T6W`Ntoe6pTbKh&-SMr7hO1M0O*aH86Vnk(o^8d-{+GxH|9@Ln@N zodCm9?=nc3Vv%MrbQ|P3_>?!a^xqZOK(0ZwH)3zgo;B{70uz{8nDWe|)3(&1+qzxi zgB%R94Gc2W5c6#ikhW`Peh)lXlv^1f%cU9R$JmUY0v@;(Ja)Y5s^30hX zQ`^}3X|mvYuZIQEpoN*x3hrMOML`A=ac~x$H3Rqo-(`cQvbJzHC?1Q^NQt4B6!9B$ z`BYC^I zkv0Vm%}Tb}g09G2s8<2|RhoN~YB~LH9WX$QnFs*YN5kkJHP%~K)n8(GG-Pb8c+`lH zd$hM>i&)J_M*Rm#;2~37%2YxvIra{Vk--c@m!8JCaZ%ZeLPNz7$B*^@Ypd+0Xgex5 zyR9^=bg*|r#ueuI<{F?8q2x^ygUL{NlOIww>A%e=W1>uB&VkC6Q+3nahE{>2K6aEeS<952$|IDc<>8UwB6G?HZGu4Ynr*+FPvp1<4>jiapqh(ZHX{({1 zYR6}N%Kg`uT07`;QB)0HxS~;`#9J@d6#VU2O@2l;9T;piTu{6JbgNw=V8xhb_FASY zvGjxCroiic(^C}*k$ZOBs!uqzp)k3D z9&rAwg7hEMW}EFVH{H1pws3jVUK~tZYY+Q$#)}*B;P5162#@MqoTEuB!gGA-AsBTV9?xbCTMD%NuSq<(r4J==~dKT*Kj-BFU@Vk3hQB zb+-x5`*8g(N1tse>;1$bUimN=na+!@AeO>#*{42Nwx6hXBrE!KOtW7>RWxC?#3jRX zC3kex_emoPIH04tdo;RWDwiy`BV>DR zr%a_HVynp)bDL2`V~!#J)tc036L{KkR))d|2k`G*YqXicNxL}Jl35EVFFR&XD!9^G z8U^?+N>Aj9dwU`1csnEE??7+n9zrr_Ekdr{U9H}EirzjQad%6_TH9`hi|^a}RhELE zf>OT*jA@}P3Qs3NSFwto8V#*B307Ws3U*dC+k(!$g+x7@Q2=@qu&T+>PZl`&jDbd9 zs-gXPu$dom=arQvpCF9SSEx-qp8-L$IGaXWm}Lv~O?5BAfHnWRmhcxw!P;} z4sOF#xT>0k52i|;oh5v{I@Is$NodfZGYPzmLHF$gEWcUONYVTkC6BY|@oNG=Uh5#axE;z@`PqXtC~Wt_3D% za)eeM6EZi~e=WqX19C{j*Fh-J6*y>{l#uz^$3BgeDOt{W^nR_DbFC&cF)4^ zfOqW#rNpMJ-7dIslSm$Qu4TbczNsyArEcUTzV8;GSz*u zuv1}LwT}K|29>ri!{2-|{Z^MHf0!#5wpvriU)HbY4@R1Qma7baR zWuencYd-*zT$50{o=AF@&qsDEUP}~oz2JKxC}Bz&yPK64C(l2El-p~#;x(}~yq_10 zL{U|B?P@;{-QF&8kETEx1HHuO6;s~gv{bFx7V4z~ck>UYUw5c8?f7+Vd2CJ(5HWR!ALHv@Z6QQ@zko{Cp z!{-yKK;f;^;hxW%`KfoWUi(IF$#*ustS=y=&d&y3*y)sVq<`PWW^jO#=L(1LrIPk8N07Q|c zbEv=6#8kosDp@MIvD$K+LW4OVNC@&|+DyPPA~Erda6?P+v#&3YaBtS}CqXEQEeR$> zvR-(R(JUElkmIsW)*WS4RbhoWp;rzsBxF+M%7qm6-zwf}8l8 zGM?QdqDJ9dG1IN%GP?-0CJQUkt$O+uPxgiBACPxL#M+PvU9t+)b8FsWWc-(dKi&pC zYA4PNOr-3&dcE=swl2^2A*^}*Bbd9)wbODgLc`|dKKq!_*UF^L9Lhkh(4`yc)zICf zT3cwO>=)L}!irtVv7YErUBQR&e;~=1$3kglCjnw;@9v&InH#)ECDJMV8=plDh zmid0stbZCJCG}hNQf$EMM5Xo`GsLxF15s21*21R;13G|W+3+TFd(9k~fI#C}1Jjss zQ5#8Za8=36tW%08`bV7jB9UuMji03C24NB!qxqQ*Co=0=u$tl}eKv{Yc0!BLxMR2oS!Q@`&=2z6KzlF@$)$k(O?e8$e^GWaT{^I?&( z9o8_Q03_YIcFC5oFCL;-%Xy=F2J zPLfhObC{Mv9-vq|BB=%DKq|>p;RR`wNtRmv;Z=O%Bt^2`dyYXZ&5Z|%^;IsrV1`8W z<1Uf09nWSb=mtNe+Mg-oHXatp6;Do7FTJCX;}{730Z-HQ{Rf;2Yt>Ij;k&BDUe*x| z_G}BEKS4W4fBl7NQ7bfD1Srs1YUD-2At6R9U2W!qgU!)mtK6*@F5LJnPO%XdGlY&# zjPsB+v^DPkQ>;KFH#gEe57P>@0n&QOC(ru~7w4-aJ0Z)YG7;Okt~sk8!Mx3;}ImL_n096b<)J zM5^I~tLJw%$cN=1mTDpc&z^O1`1U(Od`kRB^K+ab%gXr*qCJa-1~<_Li>>e(R7WPO z1t~bch!~XYRBsHJmanG~Lkc)>CPP{z{ZbsDwJ=>vAF2YvMl!iRer?GPDP-tv6d~Lr zPmhIo$gBW^WH+&xiU&5Ha4VXj{M|SPmT|=I=r~C=(af9-szwtv;+77O8l{6@CH2?^ zg@xnk#sr{h*F-0>+mGE#f>^QJ%)V(-cNuM??_Qhgt;88oajj#=U~JS<-qDMl6DG+V z3aExO&LqZ%RuJ{=ij(Q7q*7ragh0lIHpds9M|Pi&M|y6L7qd4}kJt=wp!l@jT1cuo z#KxtsZjv@9>(@3S>+p_rQ#>6{6F`e`-TIhJNL^1G%Y`sL+kR>B0sC;q&1zeRx~DZu zo0A}2(s{12j5D}@mp!O*kDx$h-Ccb#jD?}wfvd{XDYSX_A}MIl>iKGS1iD1#O{>?z zX22oZAK!~ajgg_$%a8CJpBCU48}gIj^+lI6FF37$!x531cOO(EZlX+Epe_=$)6kz} z^4Ep@as$InlSVH2OoD@syAn_EnLJjHNRwVg;oN+_)HP9yO@^srR;~KhG3J%-Ys^el zhA+ek_`pZPZCux4=+^U3&b3XZj6>0mK7`fLPDX+US#{I|{&nhx50;S!>;lg$phMx> ze5SN1213Bf8o`Minsd_fu#Nbqx%lu1RWTkmSI|ofywQ0)xPYQD=3A}dA@2$ah5|F} zwCem>Mk73$u62%DMPFpu#hC#@@Efap&Irv>(NRRBeF4)0!%M+mBm}*VG);-Vl3otA z)^nTrn}PifUJdAFmM5s8^^$`tTP~=z$#NJPkWZn?0);(qQU* zEG({F*Q)6y^^&4gH_U!UsdA18-=yM$vIw}PC3Ge`QHsj=jU<2k)Tj1%^_&JfCoZey zt!=6~RDRLNOj3*yX?YIP<$uw1q^Fp>XV)|@$+(Q>w|)J>wsxs!K1@+13gf4b{p(zp zI()JE#kH@{OjSZ2hqg~?ta7}ij8J}-6wD33)QbHUsgqRo-3gC=ayb{K zomq2e7@9&lrgG#r<|hZ<#SYaHOuNE4xGf%4t;6Sh7R5_1=Cz`s61M6fyl_^%o7K-Z zo1Bk2W^`b}gBe|xv|aa<8bszY_gf^~P}t0wAtIoghyApxPLpBS>cH%?PWY6rMzTZv zEQh$QvjWoUVBDAK#{$TsUDl#J&e-P$t=U}|p&SFrck8)}8QJXE3EIZQMBQ`lk%R~#aCUxPnO-)~k-hXB& z5vDQZ!k&!dH?!8sV2^!TmyB|x=EYbbU|3g$(4F8F6K4svQ5Hr@fkIx`zxec3zOD@M z_RoqEKTe>MPL7U}iGB_+<+(2S8B<7^;Kwe=n6Fhao^8$81IgZRa4FRVw1VKa2d1kDah zlU-l-gUD>iBHsVZ#%CF2y0=_C&&|;frMeIH4~D%!k=S?KFh9Pjf)t;c zxVfx#%(&Z3LtxeGooOPGri#dqi+$0+p-_((R(JR5>o~2&%ES3re0Hm-i*G^Yj@z8R zsdd7tt&U-{_F>5|41Nbl;-AiF3D6l#T-l056(+?kV=6zc!Mn|>v0`gAG0iBhq-eGr zqSr^$8m6JFivuzfb!L&;yE3KY>X}*DV&s)|jtUnN^W}FNtI&m_nv41Ir3ZFsI^G`L ze}ov)!hDptX8d}rq?L4Sn}x*im_p)pI^#?|0WgK4Cykmeg2wn-n-mC5YRmQb1(g8; zHFB+%+Q%@pxFQJH&rqqC4c+G^x;w34CiHM@^Z3eNU8-$t(!XW-W8qYMVsz5HJhAg- z2QJbH5{Do}2oKmf%gD49eCN;&AcdL&FynYOWlL1YM7D2Mtstw!Og0XvHX<8T_Tq8edoT&JJ0JcC$zY1TzQYb@EIbS?VzDkrKmpqS> zE-mOww4&>!T9{9&W?#Mtgq@-Qr z%>?IHhb$R3VR|H42TN8&^{UHUK%?QsOX z#p}md&G9h~gU<+SFi!g7vg6G%vkc-4=!>CXlJBbQn3!4)TJ#7_Fpf8*OYm&Gj~M@? z(Ie}ucENfgwdffk@rxOq1sd&v?9*~@d;1NjKx;{`3FWBa86`9WoIQkQ!mGo_Ky~eh zB`*d8(fC*tL>twY0W8;s*Pq4SEV6bTjMkYIYS@o0{6^o$6GaRWHQHDdx;=dw+g-KQ zE87Zoj01-v_8G9!@7T11bvuv~Y0;3+s%#=kMQvh-t#uEjYed<)mChWXl~87hjMl-z z%BnY&L?j$Df5S@4+ERImTmG!9+7fhWs;>}<46z4@l*&tqf7B4JLzSrTo&=!QIbEnM zipJFOiMBJ!FT{;?2J`aRS)QsUnzvtpaV0Q1YR|*cAns$Z;C2+~jJsnb3V_cBQg1qsYDXg&6nm6o2c2q4y3;%%X3r)9Sy+w78& zTV%KA#XK=2of*cXi5}Ty#nuNeWZTP$Wo(}Tm7PVW2lI^8AP`#--<-i7li)IH<~?l@ z-2hr$sm4&~NcP&O`~nDw>PhFAtqv_%Tk(q|JRM6n^KjhUc+I*BAVOM_(#5Q;_) zz>c*fym|_*_OysDX~eSyp~X-<98u-44y2w1l&6tqPU#;-q5ZJ39?EB*C`=`!3fUy1 zC@dEXBRZ3axYB{Q2NAYGAG|arsT(`OlUdJ-`AmbLgX&lo%SS>*B_WMRpHKwLBspC} zf`Uaz^;ogIrf@cw;bd)J6U!45VHi!7ntDS<$tcNQgsofH@m3wRQG;yb^hbily0Ude zpCJ#GwS~|zO9aDa_;!LLC=hX60>@fl0Z30VamTD>_Dix!RcpR7BHS1RHxK! zTN=Xrnk8Cx#@JVxcCRzW$SAz*7~O8Xnc{h4`j#Qmv+2~)+NlZJ9+u;KmT{j2eKE}w z?{fOs3pLy`5VMTYOtLCGrgL6qgd?vqY7CvwEd42T8_s*}&E#wW7~^lups5{Qj?q!f z(dU}^V?A_p*&j0wUZzK<6+?|aQ@qjVm~ibry1*D^i>mxcBTj6tbyGz&JQN1_BSU>m zp)oFVfOMVD)a59gE_1^KN-ZWm`_V*Fp)KK+Iv!qE(GFU?irth~cVP9sOv?Hl!|JGv z^w4rjZ{VmIFA0dW*O$m^IwCKSo_dR-&2TKM>ru0#v3O2R60|P8tBF6cDs2>P+oJYT zp~2>!yh*tOoDmD~qE3Gz?QJRMx zcSTFq`zo4^SVqC=S>L)olL#tKUeRJHoq4Wv1rKl?@9-^ZgA|2TN5|I!ZO~OlVe1J- zsFaPZS_}$y6iZ%YJEg(Yw%_*-EK6-`QBYm?>&nRH)rl;$$xxxlOWILJ*|Ax|G1NRG z8j@{^J_=?(qG|`#7+S)LLa7wCYKa4RTPk2#RW>rZ-KC;32Fin;tS>>$Rg%?86z0%2 zzU`{mRJweojg+9~ElrV>36?vCo&jpjW7lV0U!am58DcOmGQ?YkkX5Vn=dB^0mZY_O zq*Nn#f>^3kwJx=;OqbT83f*#~mSgvN%8{g{W)T2A+o&OHXOCi2q3af-P>TStG9^c7 z!dITPqAj&xB}%zumj!A(qVdj0c~RclNjoRVGR3fS3{2v8^h6v5k9tD-E7*Qs{~`@d zBhOH4fXWzH75E|_Nsg=D9uRp7E7H-4g4z$VAFZQ7OS|R5E-g6*KqY0VwI-lW5=o}$ z3oV&p$4mez{wNqxuLsKQZjoKxQkMEc1lEe{QK8z5NX{cBTQN(dF(M9CxfNLot)>G# z1er=pT4eD^W5hbIFOd^i8HLj95`ccJ7J5|IxD(0p9U5eTv1R?D9v8^~x9fgH)anD$ z&fBIU+bAU!swbbit_50G4XLw_G}czW2GJD>wJ&vn(Z_=9Ngrx=AG_A41`qof*uL8hFw0)4y{ z8-GrcNewdE<4+8%)CAs$HSA?tZHuwG7BxG7m95H`un}SJZU9j3C?U!SnEjKMs1`ukdfxA`bMq*LoRVD+p$Amx$^LW;rrF`p`xWLt;=_86f$KIC%WQ0hP! z&DP8i_AM(1gEDwD=*lW3 z9(#;{l?BrF7a88x%$P-~;!|4PxC>NxHPO+bkf40U#LBF*l@?xYbPNX8#yt8%zyud> zBkD+<_~0w>Dg`1j%}iQh-6;K`5L8RC?N)+Owz8iR-ZD_PSEG;d;bod2!MuUhw3M!A zhys;_?y6NQ!j=uI!^X9^i$G*m4iMRFyFT8fhO|288gbdRUPJbi;XSLR+WDgZlETwy z`J|>Q$_%EDPu9e zWF3N2we=ZtjPf$B}_(*u*kwf8;gGUOVkVF!> zY2E5@^7>W$>K5C_Z%1hVyX&USnFZzASw8Ca`eK#sP&h-8s2yO|sO7sltTpe5^yw`p z?yN1E*3THbLo%Lk?Vl*9#iVh|Iz?Wx@4%t3@6ki)s_87f$kyuM2y8N`g6)GzNw4W- zilA%>R5GKcYo)q$8w1eN1ZpuTynfcLSyl+I76Vq)+SMz<_O0u}sVA+8)NGZ3W;`|I z#C3&9KDdp#=~30c-^B`FYdK*Ep=T;!=;TC}pV zrSRB6hjl?H+j_N27O?@S48tBwb4{RYCj#u~n*j$#pgb%WnkCqdQ~_Cky&7EX?&TE4*;W3N&l>_M~fFtCd(}uOONf&n5tYd7MH%=2ekoI^@yn3 zgqO86!%HMX2SFH{sZCu{fAfvYydNrt4QF?&FjJ$o7aX_ z#N-1*5}<-QmH^YK!E_Zeu<{i)!}HKqv~)bUv#2-)9R&5Ewyl`uIrS<{!7BwI0oAuq zy&=14MMVtAci1)g2#`5UeE(F=?w6F85E&}VP<|qm@bLqxZWI5F8);!hQZt<1w+ZmvEYl7BT%>uD3b`)YU z`>cDfV>z+ZpuW{^8EKaf)M$X$1yt6;%Zx?KuZNdwwSiW=H6NI5+)v5mZTCG|CX@7m zV+0b&GqFYy4y~gWa4bMFdDnvn!krHwnLIkvZ8Fu^ln{f$V%3@# z|KzVpvkNDm0J^1(i)-8@`q;y2Dc7&lwV8*`ZcSV)`E_q zA72m4&vmW!MO6nhhkPnZIory9OM8+Y2}+qESWUq=ec@S&yeHD6dUbkBR$=*l zc87UZq8m4$60PO22G+0ms*S5gQC@ec!Sm6!v>M;T>nsipsmRyq_>0b%kR(XY37cJ}-G&Wlls%wiv+oGd! z;BPE+^gBH^$Xv0k)}@}}rH#4kH*w=<3AN_+tZYpW5X(01Y(;pbeWhjjVbtv8F~(n} zm&gbCwoIKaJ@S#Nul!e&QgpSYD9!r>R7QDLU+rbz;$P+a?2dz4#$WcQb`|Pxte=_< z^<}MCd&*H=-|pJ$%GzR~y4Hq9Q-il9a{VT?6vqUybOSZLuEa0EL2~M zVu*%PiwUqg3e}khrLK(EaxhOUt-e~QjJH3FuR$L+$?Ow$PNBx{nN)f=D77|AxEXDG zEo6Q>rZG@EZGWCK6xn$6Mcf}3G>7|ZZH$S!eefB=7)>i->-j6$=qt{SV!eyuewC5c zJ=Za88z?Q7D18q>wM#3oA$G>a(?S`GwbhQY>k*Was;#coy+NWEaky5>nTtJR^aH7F>qP5WHlIm)ds539_!CVn zstXw{Y6lQbi7clO3jV}y<|t}*EV>#Q;8~xxc)Kk*6#JQJs!^h(X6Z4=b_J-Z1sNsO z+ct;B3Mj2h%W9SDp!8Muswlmeni=A?KT^vRc|a_xU9r(UY^gj-mMx*Qnrtrk%qUv| zl`Wwb1uA2GdB9g~+`W9)mr8bdXj`8sR#y2QJ{FZ#__}Q?)Sp6RM7pJ8X)4o|6RY*O zr3g>^kS%B{Q|fkRg}14IZP~}S`)r{yD6C7xLTNP~;B{NJYPz1LIyHJf)T_wmmcZNe z_N;6RHM_nIs4p!w89?c_Kz=M*vfii0VTtz^LN9uJe*7H1m0N6n3(-(f0P#yhLjE$kxDs9&w+;Zem^L8xsAYYRy z{q_6y{I*ZXWy`fbwRB5OiyEyBZ(r;J;ItzzIoW4Tqh-a?>phV_4_!|dT}sVjD-wbA zCH0yOL5*W+QXjHXhekP8uj{vk@zKgyU zL-#cA#qzth1tqN}A;|jT0sZPqXNt;8Ot~Kl8>dB=E*84Ayv+1*&?tv=YJByTdAe08 zYFhAF+rk7|FUKrH+bANFxmL$Rb!sRrUp>Yea^sBduG4qAhud;OY?jio9ZWjiJIe<$h$g15w0R@2!vbG z0Pzq(EKKiuy&Q+OogP8s$eOub>Zqpl(d&Sg#{hpy6IfYWS6Dd|wIXeAO`x)wr+gWS zTyi@{hL%G0GDJuIRHKYZkTtE1tSgdQQyQSw@YjOM3bHlPQB9=_P3kpRy2>iFEi{{! z2>NUaPkU18W*?;mk+grgtu}2DNKIpulDVp{5l9Xg}gY@Mb)FyRL{y5whTlkOXgaJrTL<;b&D3= zLgVr_Yfu?mJfyE#!uGo+Z{cHU$y>UW)nZ}&+dAN7E&6QMP?NDVZ>JWE@@1)%Jch0K zJsoXJ)3&Fpz9P$XeFMt(dH`rv7Et+8N9@XHsiU-3v9h*agYw}C{ppi$@j$PYvBb9+btU5MElPiQ#=<2ND8wCoG=&RAq?=;~V?i@Lr*?RRCo z{g#zdJ0~)>uRwo)E!sWnLruoh#@GS+VijmF)uLZo8}f>i`FfS5N((JrOYh3~82s+C zrO~AeW~FD;#-k2vt52uw%WwpBh+cgc*!R7!qiaPemQ$kv(zda*eU%(&cxxs4ZCYC0 zXQry9yzKW3bhQaqE!H7d_kZGGY<#u843E^}(28?x4703XXJy9Omk6ya z_R)aX_kB_Ryz6er{t&Fbx79++=NR8LnIXLvA8`!DEc=hT1+PD3uXZ0~fyO%TgPM%5 z1|mdl9MW2U9-`ajfzC5_RO*@~L?%n6m<-0q09Gbxxx}Sxm53VEKLtFCe<@Z5QaISj z0k0clmJ;|#L|=;6uC;lr`L6`pYDZ!1s;GIb_NiXe7 zO}FSSz4R$Ofu#dkS0HVTFWb+O(#p`bEVVygvKEU%W3q9HrD0fmEuVnW^HTpx9YxEI zjgw`lq5D-2k=4I8YW@M$QNGyAr3Nokle6|(=!*%w8sGXH?zN!h2e9@4@7ME(;APcb zt$3)e=PSL&tD&Y}LzaY#)7M|~Am5*Zk2-rHHd|MP+ zui5uHUPg5-R90biG7nU5twtFJ>lkxBCSK>xaLg|mMzw?ZY9McN7^)+OfQ)Pl%aYd$ zqJ-#-i>Y6x@Hfu+NQ{YXc9*ka2dojE%=4`Z9}+KH0H$%CUQVpw+iAeIC=-S=K=deSTWJMa6a^tWO`qqGMW)y}>aw z6KlP)QAiU>n^1ZvE8a6nhWRX{KNKW*`sbl^wbz2>s*e77Sf2hV#7ftbl}P}pLbiaV zzS!`m&_3$2CL<6Dkpirw29-;uD}*k-%HrYaH5rA~{u@ziX3zL!y3_662i#!C-Ftcafavqb?pTWT_Zl~H8b z`kt{yvv$0_41av7SrUkiEZ>r>`zax|N?EU$?U$a=QeD)AYh*I91Ab9G@pyu=G*I6k z8JWsU%e6HQrF6yHOlTh>yYRlO?rP#p3Pqu%hd-NOv^ zU-&I4p^uNkOM1J~r~a5a5!JH72wmqLkxIy3B;PX`VO!wUk{a3VW>b(+N?l_dyq@(T z_I>yY@hQ6xk;13M(vGHWFZ;89b$d1YnWj3bXYB!2Tj?sRkakJaw%BR+Ls05VE4Qt- zjR6wcwlY~0P)Vgq8-Ha~BBj>!=57UA%IiYxdrnPOBhhHAM2@Tli6YmmwbEj4Sr09U zHT;R5wP$M?Sx!*Oh&_S!M$n8Xlgb(zypElv?A8 zEw!L_x^h6v1|Zs9sI0YXi^BHX@|CKvJcX5s`*ow!9c|RTjhfvSps&Tkw*j^3b*!J3 zUn{Gn=EK6;@v_#AmKva^yqXQb(!D+Kw6vhI7GB=lsM)iA0hIyG>(2_S>*KA(59rS+ za@~}nSgbuSXYFVKhHQGMycTcM+fqLks#}BFnFZdS*O$2D`>Xb~)Zk-N`y#I`k!sn~ zx}|BtsIWTe`I1sZJkbX)sU&+*4vUr6iH98iy6EoGfErDG;j`q`WG(vQ0z_YmT9Rei z0$+o5AN7)kR!KoD@;3(Fn*J%YX6HmzN)_t{fBHG4`?p0$_L!@T2H2w$~qOsbEr0eRiee8B}zyR`BC)Ci6{Gg8fzCAJ4QA5K6qWv?}N(vG7Dyj zT~Ycdx3nR&B=d=(K;y!)6=QAeENt7$)@-)?D9Q*kTC|#cO|}M&rPfC+N6bFj(iJtD zqOY$tc}1ViKAuI3KW3fOWXAXe`t19qw`b{N{AFLcHV&m_tBrbbTsGA{FDkYYVr3HH z)%wh+cK1M*A-e+0_p)lEM9XictKF=xyA1J}>Ecm&9+2@owb7LpSf2XP$duR81!{3v zWNT$TL;9;It&Oo=vuTlgWMi#qiKIw(`E?#Fw#L9xQ5U_e-Sv^7ezcT+glhQ~CB2MI zdr`_{l+u03mb{G;-iwbX!&*`;ACJh`@2Ry=M`^W%E)PCX%3gM7mK80!#_Ic`_7zgk zR68DMf!gvh3)GI<5^p7xW@E^-b{vrL_PneePis?Q^=-Z86Es^aYBUS4=i~MmlC}0! zPGS3L3+Ud~P#l7BQVQ!6=<}&E3fu1EwE&qha;+YX)$c>iMvYdBL1ipY>sV^t+ft|> zEn`rN!@}BW*|hXp?0u-&ENR{Eu)^!2ZA*!~H?Ta|?28k*qRMn_3azkV9B9?bBsoI* z1ssnWO~bc;B8dZqx~W6>0xu=L{K2N{9}qA4H*7b@mZ5a}r|@k+Y2FS1ikAmK&k@QW z(6W{7bZwRP)j}z&9>k87l5wrs&l2UImGN;4U+f8|Yqvz!Dd1G9GEt~zKljNO{gZYL zR@O>s4a>tvLRdOc^NWJUApKCP5a@YblJXiYbXJlTDKlr?XF}ED%sW2n1l;T36arQRadnI+N$Y$ zsBEq8*^U&2r`2?4MXk4G+j`6`6dki6v}9Dj#V0s>eCm6t+(ughW#G) zmFOq-pGUZ_vgN8T3#EBmXxHQc@O%+3ZS?t8s0~Y3`wA=5@@-{&QFvX;Z`t*z^GdDR z^~E>9mgs|vwqvPb#fGzho?R##Gd3q%?_k@2x8L?c@tij7e^ zuA&}Owu&~sOlw!K3{T#=E}VMO+9;tYWqsdTZJqWTKnF+OzQa5!*K zc8=ByfN$*18;k$BdTZN_cWhY92AW zRx+Y&LCdn9ZsBF>_7$GjwcnLcibvO@XjyrY5Nc^BYb&%S^gdXL(rQ13EoXbU@92@G zBjNV@9}N%eKa`ZQz;PMd=9!zxUBtcv`;z@jhq)Y~6IS7M zVSPfx$Do)cUZs0`jSyttZ1!J6b^#pwHmq40*3lAp=)fWFD{rT*&Jt`)-fpXH(I?yD zDQfhZ51^(yWZ%QuZp8w$>{_2dsUsY_pTj-{+uB|G4um@&IKU?}c3oI~yUwkTF<7zj zZC4dK2*&TO%N;O2(Jozi9fhT99q?_y>Uw)#*P?4jK0O`qx*mOg3V_uyFUcgnNKf9n zo|4HmOs(|`$bHM7fUeBGFJEg6vP~oy4H;v)uDg!?Yt^#pA5frB|V+@O#mcMSn01 zNH#QiMWTV7NFFl}q4+TXtZdB}pmemP-q%OfQ5hi9fZ94KWlM_yDA*Dd(pD`iFYuIX zKSO1Nr+!M!K0UzlTXcnw3ES0vN+$QXWD>xt#dVw@2f{y4d6Z5%u?~SgzqR-QwX1wUWok5qwKqmiVfmhi{SJ;NPusR3Ja_xayhL(+ z?bq%LpZn&6oJeEa1-zdO<#_8Ql-n0i%~EY2q29LN zw;_2nC7b4jA$ls*7mJ`R;Q`6E^zgX$>kGz@k6_52pc`A)k4W^#sCH!8ma`~5)peAL zog!u#&!L^+OLlAt7oWG4%Zr}2g=p)UcQ__&`ib_2HLG|h`rYSl4bM1zBcDjf!Co!P zvc8)EtwtXP#sT|^Z+qbR@&)L2ye8*WQht2C($3&w$@;R2}AZus)TC*NY@3Z*K zvMsIghlMeItxSDAd8-y}P9JJov&Py7l^YY?EVA?UcCnZx1~0p)`1z7G{hzgE9q%K) zi@yICJY^Fn{75cQ*3-TL`-kmxq6yyFr>y5hOHcg9#L_3DyfJnZUZK?t zbXNGW7oEiiEF7$C9Dvx6zZ^?hlp|aZ%gL5iI&{!~t>LlB9&tlG(cUF5g~ zY6->CtsMn;DqBK&FKeWPZP#_X@HVoZc(IUHm#(N&yXcRrZQsN9Q8jI`u(Cb|FKeN^ z{CyBo&CEi^!p#xsrOl_U5-Ud zUu|1H>KQ^+8HKh>**Zi7ojz_WvLVzCy~{<*o{vAb%NWX-mMEFTC2m}9eA1S6;n`=M9G>;0lX=!b?pJfV z)J`Q*)fYrDi-Akx2M-_NL{*DEu)}gxaDW001v<(DU|a1-k^3@{@NPSK4bKW-RuyeL zI0XB06MTIgXUns}>g9Uhb^<_htVcVBb=0g4MPSjAt@%~jP)s#+bwIJyQeu`q3`I0c zA6u9$o{S+MW9-fn=QxyJ=e3Wo6uyhNoc-b*Tfz&@-W*Opd0n`J_7{Km_4~sOw>}Vd z-o7uF+x9W-GrR8R+h6dVMN$d>_&&=%K}&AyJ=z*Ib_LM2`v=i4ZGdO@8y;dK%UeCz zv#RPZ%Sk=SMmYuA&@cCcLRYYiRF4_dexIMk$1v}=WpOC`G-ebQgKjd(KJz)bE!Z@` z*5Q)2Qnje2AX^gTT1H7zJ6&0{yZY9raJ6P5%8-`u#E*scCTj=*QKBxTlo<^TIKqw*P>xFpjjT^b&~HOLc4dklQ3E@k6}0U02VR#e6dSMq zYe6(JYztqEgNJbM+5vtT0#7Jnw(q;o+sc=b*Q{DeNuS69xe9+8lL9y6ui#7M>-f^w zDxTroKm|)&2Ge_|0MysV%VS6i^SwqiMsxsLL$r1A(%JgetN5mO*v7$04=!s~uSl2K z5NkOKGFoNt0ioctRV+#?rH2yWmD{t`6ew39welnqD0R9}zFHH{8QQiqm9_9~FJIcw zmTYaAp5g6f$T(=Rux+X5?b*t!=a%ml)P^rzJK3)VWEI|qPP1^XG@Z`2?*qP+D@da#GdQ}qd1 z-EVP0d{Gwrr=}YiA;1ofgNp)8u}yB(;d$B@8%CdgKh%=7wiwJ3Q_Xv+FD;R?(y}jJ z1nemA)7)qz<5Y>;KoyWH{gz&J1HbD*|E#o^4-{bAnv6sfkXN0u)w6ne4B9NPGXY_^bn31CHA z*`=rgeO|_`A7iq#-_Dl)B4r?g!a`p?hPOQ?49Ah{Rer64%&IA zZw$Y1={e#2C!G}j`m6Vb>u$L}eDU^u;ogT2@(v%fnNK@)eRv8bqB;M(-SBMy z2`lOAU3fnp9t5Aq2p)|*e} zu&yxEh!aBaaDs|^ouBvQO<~7r8^TFzS5xvxI&skP_6Y1l@LiS%d?b;>Rg&+Z?>dsa zU3>TQ2`TnJSmHzycK1GXko9)o^9aWRSpc(&Fn|7;CxsWDy_w%Y0TyC{F7_w4(YCkq z_J?@ihlKgu(>HL8+i1VFmQGSZLo)oxL6W`iQBIEU-Y3a3+ocpcsOz!GgAN0_!K=yPgSKmXwB18%tL*%iv3c z!~pk$GuhG`P9wfnY1%M|ICxa^PVC* zY4~{HkwcsqzJU0ze(o9J0_?ykd4wG#l>lP;zVFz^zF-44xFb1zCMB5X(g6wE9THqz zemncr4dL{Y*YFeNNGP$?{kgs8w>~a*JFr|!S9G-tBwsa8bwB;vf#=z_*aKyjZK;k~ zVrWBRu{~7^VCz()Dh~J$()NI4LRp|gAL|d>6Os@d5Ns-0TQdsCh@6hk{G(IT%l1pm z>%&9s5A{*es*guQD+Yk^fAT3C!t*I1!>uKs`^JOe&%bncxQCKBTqUsIF9m2giPQr* z?P~bs@CTlKTKIuyofe)=Cy@9~<75&01tes6#n>)N2$8tK?$!;mFZrPtp2@ypX$SfL zo~NJ64*)*>^pj}6v6-);V85}4PEc`9hp-`wWEF3yxPC_*b z*q%WrUYnWd~U@jaY(6mC3Ehsjif4 zD@&rKOW_N0ErL}1xR<)Qi1igNnG|W2QCOcX6!Inro5aay=4q|Q1JHtY%|GmpiOWND z@LBPtL!RPK1ShTNQtNU8@K6xBK*uIWrBF&2nT9hsyesaxl*r-I70&Xpv&8*Q4?J?1 zXBP4P$+eWQVYc9&hnD!|#5gE$GI{d4u$z)eB$U{hBeBCy8c8H}{`XVD`1x->7_Q&_ zAn#<4P`sE?fIj6AtoOhp2RTuF5)wl4hu0BdM}Qel*n0Uhw{g<@$(!z_xE>7OphOYx zbAvwo?V}yWr*6J4{LNSI<4bQ}BR}8TcaZy%Rs%0pMfhUuYdj-;lgva@Fnyw-M%mU_09M4tBP;}3|%~Q_{G~FV&B-; z;0+ac!0+a}_J?b3+ROVwoLu7s8smW3awI4?0YdVLSyoOa$+;$zNSw>ru)ghO9Ol<7 z(cNzd`s{}eeXxw?eA1ao(MP&KSb*lSE{fsbX9m+ixv;`VpS5*G&7TW@W@7N zxE3N=T5IWuoN&9mtOwXSV13NXQ*zIO zq;)GLndD2xM>)a!4DINVIBs6QCY-(rm#jDN!5)`{kocWKJJpvUnIs-AFW-9aqm;lc zv8+A`j-4-(zrXm>x5D3k{r+$Z?PMQaIubV14j4Q6L-dXR@*NL{TkhG<@1sK^dG6^O z`OA-k4_^1fm$T^r2Hg+daA)}JHy#LgVFr{AHaIw(K?x`h9{>HbcZH9C<(}|$vW3|a zB$|9e!X)2+R_0jI(=x`Q>}~B=F03KEw~t!3Rz#u@AwO z3G9cEJmMBAJP5dzbTQL}gz$fU@mt~ATkdBWBzWhXwvpeLi){q^BFr*kKZEyRzW(`V zhLhK?3ZMJtzVL|~_j36>)p45*W+h=0S2CV@>iY1S=bg^`A?#xy_dj>u&HFIy+mJBg zk~{q2L=`irIGM#hi<3#}BW|y$CzFaAZ5&$G##`LLz8Gc~+*>scRSV z)Ic(clLV|=)IqCjX4LqJ27M!d>PlJ2Ay&@0GIZChB?_gAqi^A3g8jENPUyWwt>aVMxA$Bs*s?%)A ze5HAmGVUZ>EC|mJdF1hagifkSeZiTkkll|*og|5yhN zNhGBtX%zZV_B-p0(RO2oSOy>(kh8L78-2-5S!-Idq)=3>T(?Y1z zhIBH@xKQoi;b)He*3v@TR-Ff$rT5$S(hAY&`@X4WTVi%tn`26e+*8ZT6ru9^=_)6H zl`UmJ2irAfACa_Sw(sMVsNHt|0lv)zvzwSnL<0E;?HBOy%UaBKMuS)&F~j}X*VFz3 zPmANq1%Ukq?zP3f1^WiGZrDQU(L7N-tp8hJ7 zDa*XuUoJ|!ZDf9X_`8@z)i1w8a5F$9jOA%G!Dj)-INIB)WQ; z;(4~M^{##!b=mT?V%Q!MV%$uQ%Q!dE4iuM?a9R1R&FjMRpR$?H!f|lm#D|e>VrPzr zclf6n$t^BFz3{1&=$83&F~|~UOOG4rdyN;GUwZzw@bdFd<$JbqB?J2f%oggC-wJ%y zaYFi~JrD6!3dDnlV-U*$O7>tA`y+YFgWS4<2Wc>qh!aoL4KE;v9%8`$>*;6U-po_NrO(>NuS3GDDIN&IET<-|+uE@F_kR)YKNX zWxGA2D$)|6Xylp&;uYDbM;}?grU6>n^HK_$aS^LPvd^s&HChK9d$q_SR5li))>?~34p|gHmzQGP256K`w zgtkVhw$SDS3@ta&nLB0`fA|Gw@>Ab$|9&dpcTV`l@81!AmfsV zVl#WTI(`1i$ZT6`-i|HV+Zc?O_L5#pzW=qYMZXymx5jBL;2pHMhY_=gcnw$Wp&RcH zuoBoe;Fbl<82kSf8>@UDz zpY#Dfze0=(65Z8Qa3YHPuH_w(c%rwA2TC|Gg)Z#iwi^H^yx4F3i|;u*{QRX)3;&8@ z{F#?MHT)3K@RPH601!zmY_UBx3PTQJaW*8o5#DN_&3XLQ;A7}RpU-RuQeDaZzSMR4 zU^R05?c?ooP$ZMtnGKYn`Di^I5Rf$D zK!6zl+~kg#0>o3sO(5Tr55jA?rIjtfA4&;bp#p4Ls*KWt)RzsNz0GBzF2k0T!4k#7rhGE93i&r@-;h9v+5y3MGxWPZqO`u!{#=kR&2$ z<^2wJBw~d>(Sje`SBv{ALAC1!bmqU>T#EppE?izSg= ztbdD@FpDCajes{Y>n{rFj6*G)C{lb%k+!02MX0bt+?FHIAm1HX*?JSH&=DJ|qUW0$ ze>Rg_nMR-n`xDoLP`gr>PgPYq_la%!7zm!=+7i7(vPU3U-VxApP}V_M`H!~&(Be8@ z4onxM(NCz7>d^7UpLR_ZX~~_4MQO!SkXw47j{S`b2Gw=vddGu!y8C{9f)kgM@O#*J z>Jh(yf!P9?B~Zxw( zDI<_JY?OT|WyOya4{y;!bV_^KlF+SP^&`@C>sCH%WO#2SX!QB}wSq4(fAwxTDa3ak z54_+v{PB7&%m(sT$pHx@&lIixXz)(Pe|Oc_Y2UCr{9o7a3IFho z2l-u(d~%xXNkwARc({>)cBE-vFbj(P5KQ5+HLijn2E?T5uqCssm}8Q$MN*7Q>Hp{7 z-X7k4wd|`9AC|vI?f-bqt>ORpn{S3sd}S~9fv=Qgg9GC^4qkt`_Qb;St&D{ZM;C2Q z9gxpDWeG^?*6cz<-#6Ke&qan@W*veXxF*99!uAt>#NWc3RH6vkr37?TC)HDy7{ zBN#=4VJRiI$7U>DlsYO`mmP`JSNdX>o#`UViqCPf8J3WB$E9$(U+PLC}cIDqv7D8qkIGUjdwmAcHZ_N?|gAUz*CoY=L}#*QeRw-dyMd; z?yY+t4ZAP{L1*|{aJG+G4!qU}_cW0hzhVP+F+=g0-S_h=g)n=8_q*Z#B>~-2;8iXgcb)m#EQCZJ31U_nZrn2TGVKLFoTuMf@bKkF1|BVBBcgQ$g+KahG-?H z#nM|*?s6~`(-5Fvra>)0W)TsW$QHHcebjx*H%y{epdaOUh_3w}d5vtxLnUjYlX)59 z+sbBcs}H4x7KUVy;NiQCr?79Pq!Qn`?>cvD_`YXt3m2WUC2ZNahGz}6;Jb*|>wNDs zPvs}EA&-Y*K*K%*-*LPd;_H;0VrCNi7hHwFliu7D7Z7MG==;UXSO8tBJig<&nu7Z? zVH>Y2!&BsV#Tf3(JmciGybr-iF=j3Cj!L|$=-^?zh)&cOipU!cZy?rL z1`1=~ZB)R?dqYTf^UVamhJo0`7wlz&E&q(Mej|81`GcEnD1S(%)_&;NSv%yhji- zkdVP;^xfp=uW!1SpQyyn8!s-#tBLSn4=%04*8xfz@nT~njhL~-y_aVaA5WU&aye#0 z^Z;&qmm`(_p@61TEVW+3TB)E=va(XgCq$H!B&=xJ=pef&Bh1kQ-LwVVQp2Mz0XzK> z)zS8+94Teo0-;#BGLEiBKf3ZQ3u7$z>*~HFqipG{W(`>r5;dtzgCts|b_m-h7EkbBwj@ZS>HEqm%m+?dkPXY(B?uPcyj!vJ0IeQ`j7a?=hX$XvZ9Ts#2>UJUgYaM=emDUM zD#jxptH+?d{*OH#E6=)Cjz478^!x6f3_xq#N=@&Xv^F~SB0vXA3pN~;Raze`-|8wl zAuGROxUfe>cO7g;2u0Byi~5!pL1kO$;}wuEJ!XioW5Jna0&EyNsB)Y&D8tHAz{Pk~ zWX3|rVGh+g)Q~e+wGSdnU;KVxs)iz3gQz++zPAUNNW&~1k}v*c2ul2@8SZ-U0KWzU z?`p%29qR)7aU^h%#{)Q+VZaU*my&R%j(a2VdJbIr!TnV@tH;3tPitdmjrYCbWx$xt zz)Z$-&f3J=&0b1O@pAJwyy!_h8-Ukx{2e8%c!{w7x(0Uoa-dqzG2cP);j$aLfcM^E z2Y=R99K1I1EGB-x`&*>Tzt~f3L<$`w@G`%YR9lvem?>}BEh7(03ao9h>H{Mr^cR*s_0eiSIfvXny zB^;cHVnz}_cZhqjasMS|NO97MU-`nm0f{Ycr}=K$AK@2nuoe}b-nN5L+lf? zsd!Zomgo!iUHBme^bz|sa?g5r(K_~2X)=lXQd+2)M0~pYWjLXN7lARX+3G{KGpl9~ zoGEFx?dPOH7QGlNW99mET0ElT-XbJ^m?^~5kNAz-TkqY^FA~SjQj$p#$Bp!mm)Byf z;5L3~20uk6mwpb02PheQfb_6kz%FJVVH=mk@d_Z6twh^MJ8aBy;2?vYJl-pbOHX(o z91aFZPVw|8)*WUp@c<6CF+9|R7bf3Jal$_y&e=p|D-~Rx!odnNriOh~yy_b7RN zl5J!y)Y=x)H<2Nn_^AV2MIX4Tsd$uvO?T+<01lZ}brF^VM-0q;sWY&@Q0rL8< zB$H^B6pz{-qy|5BR%@IEMOV5{A3QB}pa*>x`Y=Xstc=QGUEz{69;Cx}6yG__-0=&^ zsg9pR#k*$lJ;%x1&2&cGtfIlz||YWCFMu&??$w(-t}@t%RgX$hi(tw#DTw2uGsM1>YR&u01B#Tc=ZNuwoTRm8G{U~Phr@C!><5AkESq6N3zKuE# z)Ob(@CvIn-x}Lv-SWn7V+gPuX93JF-3ic)ZZrLrguc746CR^BV;3pLEq&R@24*MXi zbDS{Z$^>R6aStQ*GtfmG@QdRP5<2`GB2Gx~z##m>68L*HYdly~CiB=w;pz+egnbqEo$!y@b6CB5-vQqLAh!OK#2&csT!B8`6ArPj z#TxKX831w^ciMN*nFf;myXfQs+fCd&BfPzuAED_wlwr@lc)LI{*>#`c+f|T&uFFB$ z8dS-~K7dhiDQ2*r?Dpxjctpq9;ri7p=uA$M$x-$ql%=8!+pxMddP{Z&=&r&if$885 zD68co@_V-lYr_e!8hs?PmF0y=G$M@=fm6*GR3zo0`jzg9=_zTr>_@6+KKxeC3ZPrh%NC3UOI3QiDQAI=(iKU zx{rKQAt-YUte&*G+Oyazfw=9uli)qTtkJtrsdhxxR&pAb264S8E5%upC4icJdb`~= z*3Gc`+Ro&09qU8&!TQ`9BSUM z76qOQdg%>oyX{l=8_?-W$@g2ok63y5s^XYY+b_trC7>KlbO`x7E-xvMV<^b0y_8%qHDCyjeJv0bVpP$3R*&PL4DCY&J*<9M#pqbwRWXMn-G!#Ue z$Qv9LZaSZ`PNE&7YUV+g zf`KmEhYg~M4Jue_oe>AFfbvyFLG9Qd4_PO|sA-lkb_!bLR~~RV0<{QPMQtmUR67#qX|uul2jm-)G|c?<{?dqg?*p>UUS| znXoj4?`y(pq-|7NrN}O3nePJp4izrNhekmuhB^{O*1uUSieB0N#0UOQ^ zT1=_%9*;=<=|xRSw?kcenVL=wc6+Y*5IelPv!YQJG&=kl6haY7YsAwRX3boIL4gMP zoE{HU{3ntbX|1JQ<~(t$&KO-IGw;y$?czNZ?IAxczUbDt0~QCyLA3R=-&U=!sYQOU zgUtJ?hBYFJjOw#qIx;+${6pzOeQ_lP9qMGbr4Xu?ar>>R2gd1&}q}; zKc#sUhU)b|V`-skw}3{{W2In4k7O7-=yI~cd5fY|K$MD#qzEeV>C0-%07i-p!1V`6 zaofL)ibfMwHrb+%roLJ<3`;mQrf`NZEaB{H)tTqGV_iTkYn(epsF>;^>b3=Xph~pl zlbzT$RY51(E*lmve;^aB?>dxE~T_ka@sM7 zq_fV}gog>50>$K53hi>7vMdB|~i`1r>7f({R_ z53c2;0@l}}AX{Rt#&qamdHCLU`e+HLI{`@%GI1cXOV&lL{Y}eYiF{p`A-ZjV zm&H+Qjpta1{;(yLP^ngwebKitj}D-Et;nt2{@y9ZM>5XdIM{KmVBX<5MnE{l_N~WQ zMKxa0S~S$+n6;52NW(pEJjoKHY#n3xv2H@6YRmU?fqw3I*tL@ezZdX~+3k+1MUhbY z-e}#@6i&9V>v#-qTLvwQ6=8V<%L0^b=~}PQeZRZVuV0q(I{o_N;q5AH$TcW`bR0E< zk>-w(bu%Co~La~*XUXv z6@7jzylsWnYj%cwkA?PQQT9fFk6Bty5a`L9#bX$rx*oq)>^eMw#&rPQ$%L`6BQ!Kh z<45sWW2J6qK5V6Hw+c80A48Sqcv?>+%j-pqDA{xE*yQ4obR3MXsn)>)v@u{HB6v)4 zGq+9QxiB`WOsRH?g6fGBgYkY+jE1IA-Nfn;B7B=*`_3vAnS>gTmC~Zyj$l95nXOx> zqE$BVGA`ua-1moa@Ihx%o0*<#^n>AU3R+Nes8-V|o@H5SXUYLCg| zFza^jq0u({Qas(5@UUcgWvq(Q_#T4 zpvea{X$o3)EWIYv(i;hWYBs{VYMJic0cEAqgzB_(YtC8$Sa=!rs^xeh`$R9hJ?)EW zIk8$@Z>MivDf;5F7%TT!V%GSU+n-}F9>v)wq3Qy}A>Z|_@YsRkQFk-D9qLOT`gQA} zM4et$%S2t?NZD3TU6GLo6huJEl4K7r-NPH?Chsu}% zQ9rp$>Xn`-PjzHbq7##VW$}U1kO5C;!sJst$Y%wXQL`Cotz#>zTRjkB$>Tt9o+W#c1-3apOz+(_^fU z4cL{VzdbXZ~1!|{nqg0(J z3sIR7h&ra4;eBbnX1|3Y9d&HWFvdw8OQ1hmN(iOKb5VD?+aU}RZHq5*w~THhE_Z^sJ~fsW|5r* zh-DG|pVh`#`C0p|^2chtJjV3H%FGhqa{Dlg{_z>FGHIxvvW~~{v@>Q|C-b8A(yX!m z7S+!&X!!zNi^9g=j_7O|*b5V~raUgLRPZO4-OWJ{Dy8%(qt zTP31ST$E#!h_Pr43T-RR%bQZQtFU#z^9^LaOJ8qDO$C(OpK`0}e zRq3e?FJs$U7h7t+3k^hz1FL&K5phhi+qRd|exF<)#8I|Xk1JEPhNaeR913mMcs11h zCeV7X&87`o_jdcFEzerEHu}94;CNeh1iXKs{~4^F5YWxHTqm}#fNy*FarLrFtMM%Q zcoyCk{QDRHON&U`pUU(>Vh45TjEe)}E#HmzOse78 zI@*-(F*EFeB)V&;L}Hh6LuySl28-crc3h8$b>_JGAL5L+$qcfy#`9rc6J+kFa zi7<u1)oKk{0N)pf+V{r5@1v#FuQNYw78aw`?fA#vkjq z&whz4)t2uZmn{aimok}z9V(i!B7^Ny2hSD@#FXMfJys!zI!bkal>}ooUWYC0Sy=sw zP__RMlZ8MVprbIc0%xmZ8U*%lEVyRUVcA#|+V*rsO;(|8O9N_jJ5HTWM0lN90Grls z;adtPena-7Y*A)GOTJ-~w)zgO1Li}rBII>_n;;}dY{8DB9nUf7lW*`U;Tkl!Oj5dE zDq`DicPWTS16f&&qCv1ST1kWms@rDSXh3aP*}C4Wt02Lfx6m<3{FWd9ULU^(8#U_Q zM$M*KH`QWLtkbcm+3Jq&?Bnfi0@j!3S@=FL7j-*)ux^^K>l`Z#jjz4r*&p{9^!2S4 ziI;6fYwZs8s1`$=21{}c#q)YVW7M0Bb>WA{{v6m$yw@SK|wd>phWZ`64L6$FG zT5iQsqL*a_XeS|49Fp@W%k}tyh2<4sMh0)gW(5^0ko7j4K9C1OkW`51)tgWQP*5e% ztYLuLhFFsUYK~R5CJ$J;m$P+iyJoAg!4y#YBSaJW`+J+cvYtgc^qGo#$e(bumb?>VWi-=72o#sU^Y0h-xL|Dh+VvXGY5|(8p7Zk+Cr@E8dDS zNLqT~1uB6L+L#sULk%0BqheM-kBXPK+ZLN{88m(iM7s$!{ZbbaEiXgdF2=?+7T*7m z4U(lXEVV}H6h6j4ZCX90&C^c{=+9y!$6sm<^=VP@D#T9RnvG>BF71yNMYde-gS#GQ zn`Ik9-}u-Vt(?+r9DR0xA-;#o`z86Tjt`bzRv;;r)TE^n){aN+U)IR9(vHIN$3iTW zc1p0P-gRE%W!g#~S9Vr$8|5U|qXryv5jq2P+b)oqH4%6$ZgYnpStR(jHVS(&u9^>k zRluYY^VS4ZO;ZqM6VnTMN8Ba>YglclthJ-X+lgq9hoSxo`%0t&8o+ddw#UHh0>*R! z^>0e=EFcWc{&^AVw!H z`cNHIO}$zOqb}V?*$gp|HU&1J!NkJfWzY%6A0ySH0LL8*ko3Hg76_45O^1GKj$M&&?vAQ)mAIH!b>KF~l0q~~pup>bO0QU>!xkO=nn6z+VK+iGvCQMBbmbbbX`FJ89c_)p$CSPFLF2H}HO(6B z#K2Hgi-`I}==sp`LetToz}0S6(~Labw}1_+5#jv>=#htb8N8pF|Wgy`$m ztO`%wx;AV(X)Vik$MN_e<~G@XC2kgp^tr;cHlsg)tC2ppU5lC#>lvBJD401~Py!{kWRjTfK|Si~aD zu)q)=D+`z(g_N9hA|Z)`K@DfK0PjFRj6%GiHj}I((4spSa~3O!wtwkxxc$BZ;r{)H zSVqN;Pz{> z0%lB`>tG=MIuAr*_UZort2@^nNpd0z_s;A{T!cWp{{zAgBtU?)Fc*Bnah%8Du+y1U z)eYaSEA6r?)@;_RHJv ze)YK)H%C2ek?I# z#?Bz@=&xev!-wSC+b>_gzy0MOU*G;&ZYsO1vJs!lr@$Y6^Yh#9fBnVg$NSo$aR6J^ z&n%W?_D5oM9Z@ZCC6?^wFfLvL4kG+r|f=D8L4vnFi_^dU>pySbzE@iW zS&#IR+)S3`^d_Wh(T`4x@#8TmxU(EEJ7i*kSCiD^lugBRZ@}QbmgC1pje+~;&p-LU zRcVefmUg24nGg0=9MI=z`aJp(s78!aG>0H=l`}(gGF=e$VjTTyzthi5_ao8sIO}i6 zzhm1W)>2GQMYYH(afakFJ$nZlW&K0p+3N=9%uXwD5l?SHUx-3juG-lFfcV~P(wKz;*EJqKm zevJp5w8VgG@Mu4jOwHhnG*jX?lh`pGnCJpETdv18J9GVb`Wh^#Zw6&|(Hh9(deCs^ zRs97`2C|}Qe+dJ}GPFH__4!|p&?Y%o8;ENVb;&I6ipDO`yVJ*2SSR+cWV%{2YJ~Vf zKGvGWtKLMrp_*AtRui^Nn|42RshV>fc`5dP^z25^=>(0=M;LP)#(Y8tg)%|)f#@f* z+!-3Hxp59=YejvWgIR3U6uL4s4v*ba^kn2_va4PX@eM0}D0XO<(Whi_t6ciBSg@)^ zf(Fhb%AJ*662+92@5P**gSIrpTQ0C@#A9KjixWHJmOB}J>LC}wCp9vH3 zw{vI@25;E%pUUQ9B5KIFHlputy;-fS_T3s6iO=;rP)q)p zT(^6~FIHAs>(|MBR=TgM`D#95udY(H$``X2rQ~mOhs@gBf>_BDU`7|#Nj6ltth

aZ&$>g{lrqrNN-HtolOscRE`iovED5P^>)0^60oI~{j$ z-F{7jA92uJpt)@>%=9~Zn+UN`R(KGq#0~XayfJUTIq${+glaf*?GXF^NMfURh#Fk$ zAaQ+_kU!nZM`{F6!#~K=dU$THTpLfImOQVyy3%TXz5dU%VocqV@z3S1vZ}rEt$0RX z;I78qzRwtS@1JGW^2G&)-!ti#f{zuMKiGzb==bBYVLFpKEl+%^g}JTO=(H5o#XQsr zOBnDA)$>hGjSnyscV>1!<)Mud&o{cPMYy7*3DxN-j57_2iC|bN$Azy!@%Uo07S3So zl@@35YP+JRFwc)TbW-|UM(5e{tno2d7d9;q>gRQ*pdQSbZ%T~iTAGJmCTVpt2RO4w zKec>ts!aED<`9uCBns!)2V=IEwjX;MwbpKIQweZ&DnmM&F!4eVjX?H zEMpP-ESA^Ff=}6B`Q3rK8|PMg^Tms4dtfz>qSnLiwP2K3%XuH_9$l-3@ZlUcG|p~x zqptF)UeR?if764#42xy3+c0JgG+h>voE4p3nPbh7g1^=1Q`BL(foinarpJ*`cMg+& zxrFgLA`^RSW!6_&<9{`KnDxeWBRG9IpsjEo#=Xv>6{6LWR0#B&8{ zgA-=DioKT0F~&~oAkdwBZTQhH2ZMsmAgHj$6gyn%Zium~f#HU(6c?SbqkT7m*D;6f zhtylb9`0~AkKo)eVh^xgWz1>&)9^aBPi2~wfluK&+fVLuHYR94nCZH;7~Mor4swS zL(SLU{*cr4r(>s<9HHl?ij~;wBKQsztVSm>Q>`Pz8yJ$&Le8i z=BwPE^paAUDSP?`L#-GVZnz-v2t%2>b%)Jb3Ee0{{doLek#o9|iymivruJy` zV$)bU^w`_Ds#?eDB{X@T#Bx}-H}UBFf5A?|$1Zoy`9iFGFb>cc_x-(I*QmQ_@4Y!% zsk3@hak0U;mkn8a3~yRiH`ID#^U<`+;zm!|dA$D2SYfBqm2PV6aWiQ*!p}=Wtk`W-A?Hcy7^-;Leow9Y8E=MyRE}PCp^~biK+W)Zb`*Agj%B zhQ`GGh#ITjn48tL>)7WC zo7xNNe*0nquD$HLK94fftv%(}L7#KXSLa~9;6HTw`n1Q|SgTL{rjz8X$YNnKZOnB@ zENx5JXPK(r4BY{qW5a5X*_mFnWwoQe`LH%ywZ|M%?KMBs(HxBp-ns!I`3h3c(ZY0wwA28d!A?_sB+!ABGpX5v_d8^c??ThjX z&G+Wy9N@lunu`QH58=7sYUgcq8UvBd=XmSwdP8d~A{2Y|N5$R_>{D(g(Q+Bqxwh|i zwc+kr?%peGNn?3Ot2To8V0Yc@tBA_#v^A&djKHRL=+$9&^_qjZt$n0=-cRaNL_da} z=7OWoV+frD7N`?+ZOy@aUpJpZn~&z^L^N7UlUVwhVv`#C6Mnc`{TLCKH8iHx!L

Nn1s5opF}Jx*dtniGiWx{f}gI@BkuqcQeo%}@zuuA0icU0SL^Xp@{qjHkBQ z@t^_Ox{&Eor)#cYDRc%@dIno)9}^+GO}z~*?UMs59+7VP;Z!P*=Nta2pAR7ox^gF~CV+Y{s+z=~6G^6|#R;Q-lg^3KG6&JAmE`u66J_>%41^@s6_iz=Z0004QX+uL$X=7sm z04R}lkvmHRK@^3*B#Pn#MJ+^9Y@uSIAPQosv9JjeVgz4JcHK>cBrdxfB-jcz7J`Ln z4ZV9P+GoX6?zwgh{xMaEpgzAMy4BPR9$Asgno~?LOxWmY% zmxK?5`yDx;@TJoAfS(j+0)A8Mq>>X5Rz0;$73)=OWSzWlpKy0RS2W7!Q8#Ha3p3MF zyJSXjNismh;h~^=Qw(Y!egCxw>bBu)%lAJIegEqSA`ft0sQ6bTr_p!Ow<=oqKD4aE=|x4; zci?OTI`8eYlm3w+GQ*YyFDKAE2kqC;un=TbUg!7@Qfu-=SFpPWy|TY$@f)R4XW*u* zc8mZ3010qNS#tmY3labT3lag+-G2N403ZNKL_t(|UhKUKw7o}FC%Eh10Zl+20U}@? zKq4;{K@eJr5R?R6_G>9Ybd=?q8PoEx2Z08=$AvA+wuRv}BZ8(~Yh3CmlGxMT)2wLop$$st%Gmyi-^gH^0S1Z6cbM=%33Az&U0YQ9&tb7RRX2c zi3Ht7Gh+g6M=CZFFuT)rpR)Xw-s~?!i9=g|Ewd+^zjc2^Hv9 zvKElzR@9@tb*=Ns#EsnS@TC-0Q|DN3#$J4l?>Eb#L>%+4__jc4&2P463-;NwSU)k! zB`0G9rd_KAuTHO6YE_e?{2l9w8b7lGRcXZ%9Fr?1lYffQ0at0I(274~fQt{El1A$_ zF<%fV_N}T}I!6pD#$+>!*Xa2wGUtIkBl<$RRlnu2t?BaS12z{$h^rATaEJlc7Bq$I zobr|QnIA>`X3@EsUYw;8EvyK1@Sps)9I$>B$fklX@Iyy&-I#+Bd0ytUD2dtywEN8WtXf^V^V2sG}2`i(@!?>E5vp1)X9Fx0Du;AmtTU7&QsYh9&VQfN}DzD1ER^=~^h5zSs|mz9ld-7y68bDsGlS}!qd6EV^{0y$K&qS~I!cEX*!L|h8 zN%KIPWhJ>2tn|}7cv`e)RlEaH78|&XNgL}RI01Hb1=KdSx6T%LPs)^3;bsOSYNB?2 zUqwqF2?QT&h^CjYQo>v)0E?Y>@DW$0$-3Bg;#)PlMu(!VL{nL(C&?8R6}z&thKtFc z?2qw^8P#>ON%dJ%1SGwc#+|1uc%oKAgr<}272%uE=DKb-l-(SXfHjgF+`LRW`?ke&`e7NQtEjODGKfSIR0n5(|+T05H<98HtQ4>4zSioXG^Dr`c4x;TW591Xe z&rCxyG4Hv@w8ofnE|>S1MEruz>IZ8qlk<-&(_{b-)AKda;y9}^4eMmv09hv4d6EQh zr6V0Uut3lqUVPyL-QSWtneezCP2pD1roXp)0DghLSoe^s_6mGKJMg=A-;q8Ra1CTfU%rEbNH z6s$stbq+z2off;F{R7OJ^{CAlgVhGP7Cb%`eC_|rJc&Gv**2yvDWxk}s4|x> zKA>ZPU&4snxRk_Kv=kMGbymCKX3S~Qy;#j#q{fF3V*TmlLv( zYl)CG^l*^RR2<^de8uOvcp!(N`Yx|z-6XS*iqHL;3mh>F!J?QgiW2~y zN^jSLZvYfCd@F=EVUBAvyV-=|Bg$Vrmc|Rea-J1e;S1~W9g^WO4{qd|krWTIHQzWYOt<0fve#Q+zr@hP_KgI~swK0EQwOV?zBWE!j$ z3H1P&8dXi#G!9J#FS0w-GU6a9CL-bJfnUHu0`iud;~t(M7vr2$rcaGBK&3O}mF~>D zC08^Wxvp4dAg&Y6zQrMQtvW9D6GU#yOS? z*$f3Z#0+t*!ZER3A>^Ac`612n*`6FrCQ~|FVm%UcHQI_X$CGn{e4G7+UM!De%xg3k zen4i>{FcO5oNFTII0pN4ErbVlC=xo@ybf88xNI~MZgxzwYFZ5{>Ey<+0SCpnq zdPMA|HoD^1fs8zT$bMihVht-4LXkjgH%(fz1P{DvG#`oH#M3yq1s%+nc=C;6K9{go zGC$o^@wiITIM!kCvG95ok0(eAt-DQg^Gh-;t^~ofXln_2Kvc0UJ`~#+O~NpmBKCrSFrMU;bfP%W6=LxmIYZg(Da< z3j&HjNDS~2Sk_9X^RvHKe&R-X&`-bF=|fUCJ4^pwG5#`2a}t z{e!_R|8}^OL}npK-3$YJ-3?!oKls3>!^-{cemgnmZl@&Ll_WRd*^j%AoO9YK@-=t* zO8K)-e?cz#wf~R!>Y`pYlEa1AFAM8CNuo78(NS!={y=8knv2-CUM4#f%oml77?r`>A9@zI5Il)-OG^eSw`SfrwHvVx zo^WFZ-*K%HzR|PS27)Y)a$!0LVZ3tax1ea|S2ZpYZL}Qkp#dGQfiKfxoq=_%YBs}) zc16>G>wX1Uce}Fe?3dzjwhgIuC4+cQa#BxNnQ)Zw9b9+gm-K6Oz>8e+PamueI%nF) zkA1*hD{XxG3t>A32=qkv!SG>JAtbq&Gv-Sxs> zdRJJx>`y)=kA2`>#zX^Xx~v$J4o+AvJQb#jyy*sXoaCfWMV9EoIOoy`5}xA3H+d>h z&xP7&^hC7sK}cRMW)b&|d0`sd6MC3Uadx&?7t#K)=Jv-A{SaSqd0ZbkNm)MKT7sDd4O7X7Qz>Xgh6wH*w0-UenhfdG?*VEp1ocu7LAj^7gSy&!IFQ6`+<2aNNan zF|Be#{>*}nXpvH--?~Yb0J~J4`NB3R1e@u~bnQmWzD@ol|5x;)q_$RN`Q7v77%B^? zbUppukm?xkj0WHI2Y-T%62WL7f6xte{Ix9blV2udV|~(}AUB|+c=P?3`PnYW%knX2 zy6HaO7%TncO`)!_vLpMk&!;VWrkF>0mhM>%1@6b-i|iJZrovK`{joxJnSCw zs<(fXWK?%o6V@IGSA6USYyQoa_UgwX3ct0JKWr`ioyTi`{9$Ih$*M^YOIU z4^T6X;uvzVLBa@=rHinQZW_lcAM=D8gkouHASJu1yXo>PiBOp)oBhr7WRHcb7@5lf zZ!y{m9GW`O7Osb($Vk$H#F)d*nGVa#Uso>424hG#aW|JTdVD!{GRwnQ@j)IptCcMb zE8F>+*BM7+(;sTd<=9#V;8tr4uwiA#%DGJ&Vxv$*uhnLmsNv=1rUnfee#3RF~UoNaeLJ}krsqlk^>q~nyKjw6)z z+m(QJWYURLJw9{{H~E&&dCsXSuv7CBZZ6Mj{92i=&7lzX!DCQKkBBj{9fr?!L_RKW zgp%ll^2)iGlu3w{-M}o1e5^``ORz)r*qG!5oC(rlS~K^F54zaDuB%AQ9mW#*Kx1+U z77b;H+e4S==X?-NUdMO^%(EpUL&mX5>TC zgZz65{`o?r<+3^{CNw1%C@Ln~T{0;w-CbLA{=rAwv!63R{gU^|ufFBl5WHt!aBunE zZ~AIGj|TJ)38Txdr>GLO!OskBag1}Z1$jv}U`K!IDYtRibThT){4c-jKJoa0qZeS{ z+qXPUz5Q**nfFe}i!Z!?KN_(jXQ)G8dzifY!+)_{+lIVO@i9F4oZUBCK#FOn&oO0r zMf}68K{H#tvT>CsT3_XV83(1C07?0H|NiM{=dmL z|Lj{vbJm-oeNgzm=RHdP)jvF^H|Ib7_>Fyc{jdI?D|<7$|8w6cZYcsn@m6hz>~7nUMQf0Ky^Lt#47&Zjk@n%^44(Y_WNgj3ok1fg<9H)NYvSWx zEpqv@9^SuSZM*Z5Glq=o*y7G)C0vZB09j@8#zFBq!s#1?m}0E4Hu>PpayIxw`k+tO zVT*+!=em;0LNDpOPEGb#*hY4g5<=P6>_(LYh2jqTSsrK-uIcE0AO@NjlTV7wZ;Bhg z2ny3N)bxQF^RkTG#2RQFOn)y!kcMhBK~AzQ;K7r1uk2zvzV5L<0QhqpfoDA|gV%Ws z6$@3Pe4sDU@!V7;A)Rf++U^-Mw*;O3Ee;C=X-LFQE3r0raQ0nKk(cgvi*4upoPT$P zT=mJ%8_~r-^!5G7<124-qJL2MzyAC8%VqEVR1#W9=*nC18utO&0i$Z7ue&DufqrI= zUpi^WH0@g);A=bNwszyspWiWWz4|l#+IH6Mc!R$k(%@N_ULh~P{6m&d4@Z?J|N0;I zZ)iZ*TR-@ju|ubu_np`Lh3T?yjC4S}lOF~t=pEQ^)nV*4PsGOji3I&zkg%~| zE0I-~DMXiPgz!vk`q_>Qs+VaYr{X)!Sx53)L`@?8ED!mr%HQLf;w2@bvR-qW^HXCC z3-P5Uv-~B7Qdm;J2mVz~6J6MCqgF^)-i-&HM*-JC@@vX1+r{f!^dZL*<*#_zmeoA0 zb>=ayngj81Co5I(jh*-oM6U=G64fs*XNYI}{aU#`+OYp7VF<(`4dl4%WV#~ne(odX zYfn2xUiP{V$+Lg^3S(`}8N2G2Tz0j7gCj%ni>c1uCc88^6zW7PKJIA71EP*W!Sq7> zByY>MP&k>deZ%9_@r{iid+ma5VRy*GA}rBneQ$*0>`$96W%bC#F+^cw3a@qu18j0zh;A_-DT;-|^zN$yJ}YLD-6RB=UU^yN8^9=daxD zB)=$+dC}Ws4cVUIw8cB$&@tMMH`SH=5~D8|hhHkF;*4Y18|~8{CK<-Bg1^ZTtHxk> ze6BpqQ4AUn(_wUq&WaTu68_?}`?!CyUQ*#{PAS5nkTiFV4#KB%srt2M-jb5Yjv zId>c%kf~38f0sEfu!RZxoN!9(-+dM_J1lG|7NLm{xG~OG=^?_la6`*{Ov83F-9nbF zC*J&h-mt6YTA6Y#vyY5pdytP6eS>|z0N0?~Aw=7=BWQkh9BF=}TD6$EqmD^4<;GD|B*k;ail!j8LwfT8+zTK8nW z`O53;LqNN!vo5xek}>9^$O?^TA;s6A!-R*4=R+W?BpP_4^1~YOgtd~c1l4{hNr`;n z22DP(pSFZ~iudZKmvpQkh&9P(nE;OAGUfvpe^U=i3jUYr+&19IZ!<3X5v)pUtLC$8 zz>;iXyK`{ZR-%uwCP?@$h3Jj0hB``}?O=_J#zH)fwCl8v zbdJTiR!A4g@MUexsMF%MOb-I^m9ch`W}0*&=um{9$&8cnRk_w^+rO%E)oup~P0#yB z=X8qa{>Lj7Kj621O5$5RZ^jm1t8(!fxd; zr$%yfKAcZnk0rXzp|ki}6{1_jNg?RcrS|o@(bz#$LM)>*Rs5GB@FUsaixel;oM@&h z5R#SFCKTBd>?Gj25%$edCQE6#A%+L&<_%p5RrDttE3&i?DseMNv3b}9sl?!~nUdt9 z@o*CaH`~vR0X%$Vc_hbx>Cq^eu9TI>4l5li9LBl1N~#*Rb1Np(zt6k~TqqJz8pvGGNlo&H?zl@{*$#*?b3YlkpfUG&p7H>@40JvCo?}=RL$*nr z*KFJa-Mp2)5~q`_)@+N73lpm>lA^%AtgrFI8;@5FcKSS|EH{cO9&_-nnhE8lVlCu| zl{oM86*iW|bH=aMVc!aJZLVZzNV39%1?yY90G7cvtbFJ+oxsxxTvh{IaauIz-b8^f zBCE!U`T!3fIV>A!P4ZigNjGR4EE_dYW;f8TcSX~5*b&4XP=0!&*2mNgl_h+IBxP!anntd7y>Y<;I|e46I`PAHr)rxFUgUS4BcYRsX(!>Xop zQJUL0o@bxPPJY0Q9kedm+r$cqB(2hzpq(YRug;BMvKahN`qtC??ztUbTt&a#_d|dG zYkJe}=g~WS`+)G7=ij^kD)szAs#c)hj>AKwLJToyBIDUC73TFp|OL6@e=j3 z%n2_fgKbN;8bD`CIcWLFr0>ji4jTIxRhH6OXi=+i-N8f8yhFc^zeqRuzx(UPFJ`o} zW$?FebF@Z$={rBBOc~dU{I_x>XY+-jeZ5%lz${8y*^5(VaCK zAFT&l^;JNdWNq2FgnerbU<=!n?Ge!?BV4`CZ#A5tBHoOuJQ+~@n(3ExisL32@Fc@F zIl8tyyC(d|(?EmI_>+FSA}~tBwGtiz6u=WPl(?p3<-$@4r`=)v1-lo#`hx;F_rJ&O zJ8pdNHvoUl?)iopb zLgllFIj#vnY`JFJ*k)a;#w31&f=_h>d$YgF&*n@ql@v#co7s}+JBn>d`V6+#V5ibp zp7fPe!G~(R(5>T=^{_ueegIwPac<80=!o7ZR*f1BZPD1!_>AB3TWu}KV4jsN(8D(G zkuj|^m7VWusI+ASq@jFHJukEy*iHP++YJJ&1x6TSKhtCel+5I z##}2OzlqBiaeR`WVR~?|ZECZ9i(|IDjwvUnqd`~Y9(Vl8D0p}Mijz+Cb7setzDsWB z)K`6coIAJk=XTuDjz<9YO%KzDx6`Dc>KG1N!HYce{Cmm0?|QPy``{-(FE4uS2W1;_ z)Hp~n@E#+kE2O^~zKl!}DX&)+Cj_VCiUu_H zA&mi{fN-t7>9*!O6s2ck*o{}p zs{siP{G$Abnf@?;h2jWC_TOdv)fB&+Q%NzdsXtX^;TUI#euZ5nkH;dh6$#3~TXheW z+Zm^K$r2DMTfb#pE`*;%q~7xb{+h79(ZnIL$*k0PR1;r zE?Pr_5647;?WwK+03ZNKL_t){Tg7EA01pRVXIbGWBFU6G^k>{sUnQS~`~#VZA-+2B z&>ifW?#Y2`H09){zNUugSzsDb7tba9TSmEo?VN=)! z+Dg7Eg!z{LyoPm+3H+PnY{?*7fD5~Ip?{Omd<@AX&x27}avK&RLLx9P&V!pb7;RZ2 z{cP9Drg6Pezpi)soxdVAXpj%O_Kguf0%^N#*Hxw?l1tzD=Y733&bA|mC*y%|FMS}~ zAQwIA47vLqzCv#J;$O~QL5B+L6 zPyZHngV(?RdUb1&%?U^GTvdR@xOz+{8MJ0HQk+$eHD+-YhrM=ke;QJl&s!>zB@+{38gvMWnA^+_u%O z*xsfqAD`!ravgIt=4p2+sHGZk%W;X<>7l~pE4=dNW*puj$l|CaL&mdQ=1ca5wG`vL zTeq_ZN}sRdMrR00*>fg@9MwJ+*Kj zXY88q?Y`pg$y0lyExV>V{7zMc`i_su*ElyFctxGzKw$LH=#LF@m$fhtRoSi#M3AT>^7w6GJeC&VH zlizI^f91ai9i)PQJV8-_wht@40;8k|Iu!gM3sr70TGa*7c5|rQG{5)_AMVZWL0@xw zdD-_rus6aXuk>blpWV**dH?AugrU>7hJD_B@6wxN+d2Q+H(o1%%Xhok-dH)9oi+KT z@>*lXeZ4vkhfRs&ceA!mRr!jiI$Y|)Ml+VMgA z`yelR%?JC@M}wDr{{v(N4>{uw{aYtED)Hom?8_Vr_n{qcaKu~lZ%w~FEEqoixi5B_ zw*Ibb@Gq#0_+8)rS*&)%Lb0)<9V$N8Qp*FfX4 z?C7JTEU1*y*)rp{8mT_cjS8UaW0w-W$&Je(*W!wn^)Xbvm9U;|o9dk8W|_RszJfo| zp)v7p-*N^R;{djucjne?+Zxq}BE|(fb%aLZ zJnZJnSPwp1_-W^>YoV#CtUe`wma!+s z&`hf~4s!CDeB`gLfd>BMrtC_@W5nx8XCJ2+O#YKB*cEok0y0a|?4$G}4lp55&M;fT z(SyZ=tL#N$6(RZE8lq=CNk&;^jCd;HG$8z8;kDO)L2h^3zt@j4T$aoG#AmcAs_X$wZf8Ub1BfCB+H$ z6g@rwZ6C6{8rMS(p~!Pl)u*;rf<7U+tgo!Epl@^Fa+A#0Ot%8m(LyHgG>w*N7zaGl zvaO5@H*xh>qMfWRc00P96+g$J@J^89U#Fqn+{Nc;3f62Dtg%5%9XP-Jb&Z{%G^1SN z?H_gIhGg=)TN}G{$OTW2lSCk%RYM$87lH?r$;v)~H0uFWiM-5%5~+i5=m{=hao}Js z@k|>avO|AU#Ii!GYKawvUc2_?=MtJzh@Qs_fU18(OKMOv^OL<28$uC-5mEZ8i=UlcZfm}MB zum6)*K?zx_bHh-8ri^r8KBY0uCCx?0D_M}k_^b=0MSlK?4+xrDQyt&XaGlTn?z3s9 zj3bDT31qeg&FlRe9Xq~rc8#+gmjEt($Dj8z?w9`F_}=wJ-*$Tcn%(pM)0M(7&cglp zWZ2}tCHTw>?$y7R*PuNh-uuzd$;0e>(BP^RbB#d=1K|usJVfF$M1l~m626AGLlE<> z@{w?CM}ype&}{66UC`&sWjjH`YZ<0^)TnPu#|E#hFYZG2eu~ZpuhEfyYoY=l~K&TX2jyrrz<*Jim%lE1;T)QRP-cqBgX<^YX& zujS-Y9rT#9O%V@AJI)())-W40gG#NEXfvPALqwPskoZ9#B0#pXZlzgT9B#x&AN(}Y zBxMyEgUtm6Wkz9F{s1+uxn3-n8OQONVv=bYIQA>QNZzWdGAwiAV5YBl5)88_pP&sy z#K&^NO}Ab%{*`YxbBxeg2(zw4D+{{mp)~p2{&&Bp5MA}7y;>Q^Ddu(KmN_^BHq;ro?3km~8ua@5 z&^wMemPx37Etuz*sFlbFk68{EVG)h~fRdFXI%I9t(lR{c*$+<|W-Q zbpUiQX`q2VQF*GDeSmGjS6W-@WgK)Q`y+#_6A9lI!|w+9Q^UvliK;zCNQ)3Q9NZU+X% zUt!0ua=es0=3Y{+tB8o+=JKkUlBsZ7W<{|Lw9tvSQKdCm!BWqp-nMtrNBuH^5Nk?qgx zJYLCYK>R@xt)m8}2bTHa%+_QHrp8%ZmNg`%g&N4J^eV*D-5;{!?$Yrj*oQrC5yxd_ z>q54dI0o$$8_<=whGu6X3UsL{*%lM$egzWsy{H~eFYpaGWxVs|`+ABMH2{C8NZq1j^+W>8NVk-CwC5)2|pooM_iF+8D6Y~`x2S=e_9AEWEMYS=kzA{$D{F-=3P7d&j zR#-;ySC4w4BjC3#ac$_WD${lqDy&wfXMd#Q$1?oQbYtvpCI1W;@xeli85UE#z2bb% zz-xJ)ubC#VL)ZGQiC551H_bvG%VGIUo5wnzZ%U+QRZB9-K*I>T#Abr%JgE1{*6z38 zK2l~{CShGD9mUt=6Yp{jw^S@a7MEhMr$#@Fw)i;2wE9}Ohg8n0bnJGZc*3vDQ%O$F z<%)KcOtmpyxPO_B0kT)+#dhI-_j2+3x8cTb5(ck}c*7_&YRWVP5de8T5iY|MP3szb zjVeW(&-Mxsuvvd}wiFvphFwK(CG#_12Y)di zD`8iIajla)f<+jl;z3b72dPrcPOenox;(;dU5~=$G26iM8N9M5n~jfgSr_?SR%?nY zGV)OM8f2+%?DUE;h=h2vDWP68X9!KMH(j^gB>40dnrhMcBK1^4od!GY&_IjouQJ)g zM+zN9G_22di{kc%2Mki9ILNh!1;>OS_%B6MfNLPtxi`C2*+G%nSY|m4LRFMFcBLoL6tl!S~>F)=iXFe1zj@!#Uwd z7P`PD_Jb`cN|GBcuJ94Ms_As8?6;^LpbK6bN2KL(@vFukg#iAr%Z4r&LbQxisP-f3 zHqsPZt)yUeuB32-mo+{^sJAp$ods2#F?mB;%jUd%L7{V0k|0k8yEVyf%C(p=6G;O(R!Lp?OKgp!lAAN~i%>g)Zui5nn>Ovkr)2+ay&(QSqv;jvI0WKk-%kNXCZD z=c|avK|GeFL_GPhx*6C`-aV+Xkw3(4DU4{vdA$;&O!&uh#xss{v#QGZuJASB@KQOI z*E22SQ;{=G;dQn(+fdW-MbW0*mbYR_=?I~hH0P_i!1_g&O_Ul_ct}7N({ctVLRO(Uh7_MI zTe2_F0h;r*IIUhiy#5xc=hH{oWF9&UD-ON(HL&5IVEGxyVo%X-l)Kv=H3IYMvXkIcd$+QMmNxt${yrmVD z6C&ZVE6EVhW&nTkjm4V3HI4H+nAqM7b*6{C>`V3wyy2R0Stq|WT;!meQfIbKiuj2v ztI24KzcAlgOLA<-n(0ADGK2Q4YgLb1)t%`Ux+Z^%?+AN#b*<`uVyx7-Iy&xh%hHBH z7fLuzgd27Tq*7CBO`H-L@~U*9`5Gb7k}Te3XTIEEw|07f9R8v*gBgny`pcWu&;epT z4+dW~*=UMbU&S9bdns{{Yrw%4jPp8|&MMVehU@n8WVUq`6TW7g{-O#r^VOX}Hp1=* zBs}ku>sWBBwBU)?bW{>Tqw09Gn>?t~fQ=y=My0yl6MfY0!1xr=kfj{2A=7qXh)Ho< zA}iLi{iK73M7u7Ql;t3n^lLunoDk%S@oJxGvmDlS99Yq_O|X^k6df0=BhN+bI^r8b zNHO8v;(%VKC?DW7j%eC_p4TPva9P-m3nb+P#|jMmAm}u?X0$v&mz!@9kHPd1P~QsV zS~+MOU}EtQV;wK2cSkVi9i=y{qBy2?`RmfF6Fxsc6x)Bo-a#j117ty1q~F#bUd91Vm`B1C5~1RybYWg4$mr6emIAp_h>nFD6+vBKhg>g~Dk#`N z4E=`abKliL7aU;et8vN>ql}#7QV2&@h$q-3am|Tj0>rov-JTlw$W9i4ysRn_0&)df zE+b5qfyS$kh>PpvHLhru0brY8^TN&^Nv(lO^3h;>}G1Hq(X%TFKU018;07E-xIjFrTO$RooXD@3m*m zl#v^8!Vs-T%t^q{Ta4oI)`iz zK>x`rt}Y>s;NW@PxDszEF0ry45~Bko%@QWOE0W1HQQ8U`5myufm29hw%e*{beV+kJ$G+_uim*GUjZ z;!!=Te|oCRE4-rxUdgt?FYXu9V!l-}%p05Y+^pclz@veJUnvTbK}it_w7fGOd`>sp z<~!NU+kYaJ#52cb#zh#m9u;e&p5ijEk6%fS;-~NUt^W?u+}h9{n!((dCUXYvUbYCoPCh_DN}jJMdFnHrX;7 zm|rt(meVV<@VT|yjI_p@COja~F~7+?ijR2{Z0&?cy8MAYm%yL{P0_e`3X9QkX}Mkr zP_@1xBbPDOG3HXj3o+yeCG=H_c#>pdXGJ zUA9Yvm}#Elz`iMUhy%-28&E)ncYy=%Zm(luc5`)in&LdUDq>X$LtW7KB4@rF8_=rG zvXbA&nay*8xBg>6(WQhCho)_7Od+Z^=bcq)9cnG+EGhMKqvL|teKnMiHzI~qN(9B$-7e-E z#91CcR25_Ntlu9dYdMVLIOaHHnlX+KA;-Hvo{ws4ZLT}$5(}UpDg4%p!zj2k4LYovvI_aQcK(8sc@o3tuL=kxJN2Kd>z%6>Z7z;ro=nST{?=ZkBZ{sf9) zguEMBfFL*(UCHMgd z)l>-0yL4mrYfwc3i_R^pvrVBn0u<-F$^}gWjt5r;@dO#g@l)=3+(YHD;TMF2%<|HsV%=?5HY_ za}?Ji-8w#K)B|8QR$<1kem&aHgGQ&dmmnxShE%<#KeKpBJO?@k3};@_y@aUc}@$ zo$s7;xK`nt(pdeK{Y8$_?X{GV=Wc&oJHZXf*Q(|MoJnJrQm28^;pK^Sx;cRXR~%K@ zgAQI8qu6;)beinQ!}QRn_Ep7MR#7?3Bc5GD4cbvilEQ%4rFpPb0|n;FHYf$pt)j;d z->D{=n&Yd$6{fuN&8@rFS}i@xmnbjnFt>nGU?{8ZA*0^_To;*Fln!>Y z3?p@!oj%-QdPNFUmo?2q=MQ7Yn~e~KSh3B-7G#r6NK4iDR$3cZU+b(!=f@aMR}){g z@j$cVV)6trulZbiT`SJVQ2L8~03E+pcn&N#zrIrVy7mv?v2&dJgV;G8&sRJchtjFQ z4SyMOsMvP44X zm_vy&7rHC?+}D9)L6ykJX1*g=c$nN#$$k;jF_~;@+7fb=by-58<-D2zmrwL1(elnU zG$x?U6>m`0>~_n;jCS4boI!fPz6kZoADJ5gLVkxaW>r3nwA4d927*{3#g z3{2J_R9;V&?+}5HO%_aOz!7{c(m_7oj~WD#P`Z8{4u{mMT?~WX9b#F-2WCi)D^0xm z5e2VV{Uq7hZidzEljkd7nfW&A?65&dUX6Hu}RNFWpbVrf+eOl&Tz- zG>+^Xcvw{}N|}!49JP)j8-g)~=9RL-S1RZ!8*CreVMk3k<{{joz$dv&VE*DPh%wnZ z+0QXHm61n^-h9Q|r`;T8eleZ`8BCkQqj|v5MQ*leCQKi8Ht^F7k^pujl0#lxM`-Ax z%}cx)B6X{Wi0>%X{&W|IV-3oOHAxve@V*OAvCO`kJlLc51wP@}St@J}DV&HEP0{$c z4JoivwLIn~9(SL44LUqw&W@eyz`HH$CJ8#XQvw~&a}(e4nPXNdpDcHbJ;n?v6eiF4 z=DfgG!xfcYg>p(VIDz;X8ekW*H^Kkkc?a|aHHnNxHw8`ns+*u!JtSk9g!6w zffdado^bCt+u37^Qt+%wkO*8M7dS#DgP;;!rS%Zu;@3@3oh43vBI4QLMV47f53b`? z*e8R83t7l9Bz(bDv%oUVRK!a>Hp&*vsI=P)rWB`z91L>~C_L8iEN_u6>|_mHG<0OM z%yS!?Xm6_6y&Da`>Y!!bL6#c4(A9}1J9xK?hf$Wz`pq_uJ?o@l7gyrjcHa5r#x$+n?oa0n1peHL!K$ z5@SHsSD~xEtWJwB33Qey#$+j&{uMsk%zlggCZQ}mRIQBLTu+)-IKebWhAozqQDlqB z7T&IGH~yTBs?#)mgBfxB- zv!0w|ZA4q9T_<@IobS=fXHymfT3tRML`VYNs|-njYO^j+IZ~&-sNtCX&4G-e>(|oK z!?NUbnX!7z{iJRpTr*Fp_MGCcuDmnn&mm76@=dG+roTjQY_D4GsELyRhOU3z@9(Z# z_pcoXKt3OGv8%XP1~v~!V1@Gm_7K*_)=RHy*x6-;hPNULyvkN1twkYPyv@{&ci0=oJi~ zR7ox*pzo@67fIX<8+?wIxE(d;xt*0ORkVH0srOkevn}r8Xgjm)a@3nZZH}zNm_)hn z+3JFY;AoA)oW?XYW&Qtg0RX}t95h8PcQc3fYNu^bA%6N6q?rA%;fur=QaY2a2C~o` zy{VV6BX_H=hNm@)F|s&cg0ubFoVs5os_-3^7*kM3O^e8O^@v%T(3wHiyRoUmwl8v# zdnBknxFPeC^ufo(pm)<8RI@`@xfD?(aahj{nw8dwzm`q@uyabyXIQ)SaRBfUB1BJV z^Db-!n(%{A#Ji5dRlfDHEe!d)c^A4lW-OupH@K{20%a$*J z3(rlyJUg?Qf`=~Kt<71&$zj=UzK}+1;u(COqv{&!ssHF6M66Qh+M}wa-8=xI%`{+BY-Ov{mA2?Q`H9mywdMqV zca!yM+9(MJ{>^q)yVQC`OCu)0WtBZ4;BQL?#LmBMFIIfV_Z2S4f0ID$dsSZC}`*HdG8i zMWFZsWw92mSb^{KB^J9VBP9{a0htGRFP&AsF8>>+>X(~X=!YnlT}}-u4q_V%g%J*% zN*_=){v+|mnW9hQn-SS!W1QzS!R;SpH(@gH){l{=t^>4T{$0O#!;xG=FhBA!Fn>EX zoPH7F)_QNVxq8)M0_?m9%KC#eI1f3SLXO5h9-dz9H@OP@%Rcl3v>j0s3*T^n9P+%^ zk;{u0?WpeCA>j-cn5}{CV+QEvrylC_F`v)j;T=BE?~&1|G2A?bYY;B=$m_UwXgj%W z1AiR;40&sqdH`H%XTe%Lf*CA8W~}#qBO8}38ZnlyFKQpJ*(duv(T?ZZ>!qH=hv&TB zzNhQ@FhcPxNbBn7Gi)HrIV%llRxV=z)=L8Ilbnqb`^aZH?A``OU`b+8c9tz%^*bFc zx@|)|VG5fHKo|39eI#j@uN0VVe={jsaLnQlT#_I9AOpHDv{ggi)`K2KSj$d3*DioZ zmx2aSK0$?zr^5Qgp37;R!G}A!eUSE`E|j|6qh%5JT;<^cbkxM}yXJCsv@P||c=l}0 z`~JwTDNXO>U*k|3q(#{$);Hg^5zoa}#Kw&RCvX0e9)3f$ajueEL7-OS9p}(z$r-jw zTUYbEhr=lbU5=KSwM%5kJawtQQ#+K~GyOyG<~yr#GvLdwi*%0id$FIAVtz@HK8|W) zxQ`{~2hK7Bq_0U1YS;%Re}zJpzI393iLbDxv&sEhQ~XxXh%YFkn9-gZWF%*yV?Go1 zhfqsGz};c6jZ%f+cvD(wyHOpbu!yu5)p^jWwxvsu#R=HE{#DKI%V2D4es-+b+v5jC%9j#;FVj%~y{_byNt0beQiC z-R`eN)@BE6(QR6qK1#^JcznC>FJ>)Ki>7&h)6YyE#(yZ4(_v(P6n1|S8(K9;UQAdt zA7oVir)+x|XFt_iQ@@cOfyq|<>A2iQNT3GC7mV*R!5}ufdBH*ReNuYQ=r>!3jEfgI zQ5L4Sw6}$y;hPD8pF#(&8jesCn4fRQotw*QJCqu?d}ZOUdK<69taA{lXNEkdZm zmk#)nsmH&<+(JjFt>WbUE7RH?3C&>#navG0i7~%(RSOjMuX%rT8_n|w&35=!1q$fY z+1igcKjN`>Xh-VWXX8Ekqv&}{aGD!!9Q=)J}_p1p5BHMZ{dkGyirGYL=AFiMlx%ds(Fc= z=}g{29tV7GD@p5=ws<;LQ=UD{IHRp$Es4SD&nMeBqAJl2taYmP-(b3NaV3J`=RGZ0km<_qohuW*{hFj|N$;{F^;kdAQ+x||VO#jsw* zy_4;xu0+iex{?ae0XvS6dDomPfH73*@{YYkb)ajWv$Ozt4NyoU6|R5!h9!PkM2GY+ zvNMXc`rq8LXAphs2-czH+K8rsop1Z@lRjZ*<6u(xNa4A*!TPR$yd|R`z{_M-F!i#b zb>Ii#{d|^}qM_f7+hW-K-h(#5SyL zZO7=scgs&2!)2mSX(X5rltT zY*fU^*w>xJ7ezp`OKvdYp`Xt%@-K)ud3xVW7)K^HseWaro2nWh%9|!uSnZ67UelZm zddqq{5HW|9{l=BG^*tRc?3tLmXhUR2mP}6~8m9}s2AEoNLThG4c!)$v$5h8~VjBy0 zmMtk~>EHhsJssYq`qy-Se81L1STmjy_5wch3Z+YaokkEOp!pldMRGGIvd6*%t+@y>=alpn#SC$T(&O0nkTm!P^c0-cCHl+l8yIfW|bUnV>i>#A0 zHJlimyVpzhSiQZHIxlOzf7|28jjWj2I?S1k7Uk~9bnbTsbEn0=OB6jlm)MJ1zzGe$ zeroRY?IbgdX(twW=_(6O8aKZ*n95oS=}WBw`fti< zhwL!6A{ml(8(@{ulIt>OYw73)h48Mc4PlO;IvW+=9c_b%0pw7xMKGX2*H*kq9eJnlfe;F;f=du_zJWtXaPVd+G;PJk~wiW4#({dc` zh+v!k2ntfB59zv=ts4v?SnlC6X+SCUn5~ejsu1)7zhI zu^UtYhWImOIp#V@@S<==+gD%JD80l3V%YO5J(t!%uP(;F@3EIZV&7G)T|RNY_H(Zu zm$BNdnr^=v_UIkDYKo^$y8uThtScZVcb9F4NAsiAJ}c01DUpa@mExdRYCG(zg>&hj z!`bte{11b~b>~l$3Cnx5pK)c+O+~+PRl)_@3c7Fj99aXq<{W;LCw1D`5uzi|JMO~v zZ&I8<|K7;DdZjHbR|Y}!4Nrp%g*$M+Vn@>CLgkzmxSQzI1iZ?r5}GF3QMkl7!X~ie zt)x0TZHc`I-$e1$VU)DP9p8XEY+UL)`0F?)fF8Kf{vleP1{n2me8WFv(ehN}B^dFH zFG6ZVM{gdZsJ(Ti22Yp+-5ca~#b67T+!hC>H(rXFh@vPyiN~3zNS;q`Q>Af69wx)& z`*`|szlbv9ernMnf;%o^1?l#P4$4qP4t3tVv^R%D2>GjMSBFrLDf!=15&!6iz!MS_(_<4E=66W?W;+ zwsk1~7v4BuS#es=|3#|j!VWv8;)C-iV{otb)`}u;)gS;U)(|`>cA7J*5#eu0_qCo* z7px`xa2EqdC^y>KILq(CL~KS*Tn>_NM|Jz^?&c^ln&*y1=qiSMo^{_!1(+Fyff>J?r&S zG8(^IPorCV8pP)4z5;w@9y?0uR`Ar&O3VQ0@;2)U#8l}(kuAXjJA*EPdQPDH&k8Xt?NQC zEDzyfWLpEKaHA*&jn7Ogi<9E~V;@wqX`}O1eKRBW{HQWPyy7hb@j$|!r>0yA-fNUV z1X0rqAQ|5h7X`)+yA?+mHyM3urtmgSP!1VScMirKH`{2Rdn(@yzwGY032rqs+%=J% zV@Sq&mbD=W9tN%PIjo>#zUG-f@!Zy1c@O4<=BmjS-7Hle>I&+^_BDz9izhL!X*O`2 z12TVHg#8+H%6)^G^2+-k`29P_53bKEc3zqVQk2wDu{3X~43aVwkF+p)2O9IP)4Yfw z#Xo~N)ZadHszq#8X7g~Z5BQC6|8;Ohqt-arbzHN3{cj@;8<|IQ5@wg>J6?fGYD#=j z8lPv}OXx8KOCZPt;$ahI$%AtvqqkN@fNd#DzwRR=@pGHP2*#!{+o$1lZGT36H|aPl zzEtyP?YmMlY0Jll&&q+I=^jtCR3jY5SAGk?b*u5w_-!f?pRMVXRVQ!p=zmMu3Lg$d z%v-9R2#}rxw@sFMBR-){m6#UgLgzOxXx{s`(W;&3=-{0R%s5nB%Boka}&W^P% zFw@66P@r>jtc$bedlkCc#F|0TnP00I5w7*Gtu`*gzSrSfk7((xPjgJ#se|*PUK-!M z>d513k;(@aI!ZN3Ebh~_WR}Sd`OT58d%ZrMs1)JB(?C~~=KhiV?z>7J@nn~`(pY1E zhdgJ4EsJj3&s;bjbuTc*u~m9)uB+{q7I#13dI*qtGrd%-B1)k(IKkOrtd&oZ7cVkj zqkGm*v^93l$w8Gh6UWaO{hmnD=fiVa%?=AzP|l~9(Pw5^{%o;x-rVEA4n9Wnm6rXL zXVeo}6Dj@sJp-SnN)W`|@mKW*@D_v+&~3Xt=M{MHxry%atKD^khP%sr$)VP##y#L+ z32d4v*B3C(1DmaUK~?YCoBWDQHqmpYO`HCmu}@{H&K`;}*24|yGv zxpX7tc`P>OBYx4C=^yT(2sJM{2;Lk{*9Pg`uuLNEGjQmuw9+Z;A;xR~ zt(^Z{1WccCta<$5Stk%XV%^eT?JytR_h0;w5`C?Gq^cx3X*a)mAIYoDd-vzg1uU)uif`VI zD~YPo_VJjj`z2SKLMj~;+3y%6;O>m77^|OnH{7)dcF6V2{9>3x-%c}r)6mcN0GekE z=9-BPXs?ONi(Yy}5*P^WsISlG{;>asJfoYtE|rupYsvEeQ&9#K%;XLOUqzgwkcrtv2u@En|s@|5)IbsaS=49Zp+*(v1^xE;8uc_LHnv86ULH9=Fnuo1%>^(=^{l{q&APehwtK+ z7$Uo)b@|M!#7+m{%RxwsnBurnGN%It(@JquL59WgAhT4b$Xpk!m01cyr;JKCy6bH? zC_jjRH+`XtaO%*sx$ImfYk>@|VRDJ@J{H0mur8wou-U~EVxw+oAPxb?nlvS}zha*G zOh)>`tTKgYx@^$@TxZd}dwl2>CGNJh4&N`R1`eX@#N4|JTPMP+a(96yHhcURrM4VB zWQVH3o-^F(Bj0VKNE3;TpsYYa#a22%yr)A{Sisiv6f5%F8n)dz=qI{~hAq;CZd+nM zXaPw9mM-SY8P`Ibd#gReTvDf;J&GE0hwP_pT@X>%#q}7pD`?nFf-)mP-b!{@g~u}M z6>APFB6TpEd|1Ik;Eml{R*pm$I+49UdAwisLv8P)B$@)pkV*81H?dFDTL+q>Fk1xo z^>0FF{{&jE?0APn@b*ZZtN(xRZFFDR(z}>uZ3~}q@un~#Y*A`RMby2UyPRYZ^KXRPe5}|P zT9=K=@HayueYA?}T=ksP8n&@f$9N%&PveoW6W9Hv=04z_ zM_O45k!-U>CRP3B1nU4hyN{%eFduoGAOQgx*xiH_j@e0QTgVVnAlEJj)o*< zLlL_0(t(_0w0W!$HIO{;mw2Eu?E(qN-iur?{$Dp`$+Y2s$DoPil=EMa6I4edQfxF zDHBPn>vtBH)n>i+U#!X4+cm&wHug7-A{4!!zOe|wIK>54jXp5u=3*< z*N3-inP+6WJr2)-BrB-g3G_y>%6>=cd7`=sVgCbff6swJ(&zC^5{m*_7^95>PEbZCsk`cTK zv!dGf@ygFnrtg-8FLXy2 z!xTVsEJvwtx=&aW>A(1G0d+p9LM&jB>9OUkOlT>7+<;-%x)<%e!Au7ew;qNxn?DGj z{})2$y5PI{$6bQvs|-7iPPa|M|0WKfv3>cSZBU8-SPOrGd`V_ISovP?@V7@_K=tCu z#BV2ldE{oZpoN)`_+(I^Tg;E$+40n2)o~gyxQf6AejldUX55XgQ)^S>Q0^asmZ}}p zFbaDO6Q%fmb17{}9EN36v_vD2rQK)}V$Gbr>-&Jr0trs9OV>gqtD=yj>uA`hRy4np zjoJ;KjuTBs1f`F(?~Xd~&*M55{7MrbtfvbW5GH4Lt6WrWrq-C3G2s%{QDm>2muMb1 zQ>H;mn~bz*tz$;cY4*9ou5*|IqB{P$@#hHk`uNkCE}NN*iVu4&BQu3M;O$3(hrzmP z8;y5mOOAyDU4-?A?nZtKCC2b5&D$6rj3^Twrw&n|bOScJl%m952JETMxS{(#0DbE9 zu1k0>%onrie%#HI9?cnwEL(yj3n%FD%MB^4ET4z#@8SylQ-o>SE)IcR=CGV;b6*AW z@5uL$Bm*RdN}SCA-=_Y-^3Vuf_HFK-=<<2N>+crok-P*z7XizWdPy`LwXqS8i|>|7 zSwNh_ru@UfG)3D{T?cEVtD@smeNA0;F!T}!j}p%RffjY1Bt3mKfB8wvXu<}kX;B)0 zu(6djn|u(bTCMH`{G7c-YgcF7dm?}_DD^E(ON|nYPN1IoW%}nJ+!j0bx~;e|2TcLO zvucbj;qBjO;~kRS{!T{Ap+HI<=3WOFo;~M;V|7ilk4;!~@neUN{p{#!n$O%8 zmJ+=m^Yd}n^Xs6Vr-Nb!3+}!3S?Iw6;khmKSpA6F{x6#vb0sM@^lgJ8mv(T!d$5HE zb2ROVWU;yFkVnuRIyXe@KSSgU10{?Xe@til);+tmn(|Rwb@RJ&^I{%AxLc0kM7Gjl zZ>$HpJ}jIV64jXxBMvNY=DAT@0{5;nOBL4r)`fAJ^m4IRMcvfbR>UbU!(BMLWt|VF zDpuC5@#xK~3SHSk^AT`LB9KYsrkI@>^u5c;UzeM9G>h2YA5$^XMJ8}dbC>v1*JLbG z3(`3+Htm#Ecu^u{J*(boEL*x$|BiSd#K~V@kzihMb;;hUTe9RC)dFrFOtcgKwJ&gM+ zADzCrG%6wBzF{_A`bOC4v209sYwdw;ZhA+;;O>b+$B|<7d$&~e!0WV{MoCHG@=qjmIkquGtVkc@?cgMWJu=@p}%>DK83zh zR_=lA_&{k~D9s>ZjR93>X-WwgTYK;QI}Vd;w^obQR5&0bmON+u?CW@1&L_^UJ@_K6 zy|-xxi^mFSJC=BIgBG4MO%Ro${)Xx1&B1(^|BP)sQyQO`edha^hdH3MHi4#txwE#R zW=XvkY2`m~fzwyRZnH@bffo(@L&VJxQ`u0@pa|WadytGdXO!-BL)#+>+vcX1DUvQ^ zt@KsdYv}5O3KRuD_$`iIN+O@6J!mCa$*Y7aD(j`gS7CT2l)0{x?2Y%SCOSJ1%3uA% zYx>o!=O-&$g05p#P(U?>@#lxzaw7;|j`?X^^Ntb36q%|P3`|coA z7Sjifn;{epXBho{7cn{I$+e5I=X;CGb?Pg76Hv4lZabfz{4^7nU#d}_vElm-U(jqp z+17XsSqQ#t<>at7#%22$F?zKC8tZNiDI8x6vhUvl8_Ey*h<%lw6CH$bk&J|rpS{Lu z&Ba8>m-{&IBEmR^syD3h6r?Gi|Kc|NzWk#yYC7FBV26lgyY_g&xceu#|&(hR7Dgheq2Xm78&26<(wwlUP0QcD$p8|7Ks0HPM$J%gtwS)U#7S>#w7`R^mxWgF73-`?>YSY)?D``Y*qM~2QlYY0h$pviE{z`uR z%#ISn;gUa^)V7qkyQ*XSZQnF~fjPdI&6SZlPANbNZ=&R@?dE|f{jox<7Qbts&n2nGIBL*OU! z2MwUPHnKlA{ss4kUT9IT*>b&Wt0ww*z4!ZxoLC=0y?&KHyBGjQj~_ocvf{>cKQhb? zvTdjS6ClBA?{l&%y`7(r#;vbuTu`;n*f*WY6Og7jnS5g3ioW?Jy*by2q5o{tsa?KB z=y1-TDd%HkF13h6o2F9j@1bELt(T@bqY}OZ&3JEt4FrhWGKZE$YX|*@Dv7 z@T~gtWM_Ibk+hm85#tQ#u8>4H^mwYPcjqnfyD6tq#wNwiwGTGP5ay{rVT#z5!j}G0 z-xmmtHAR(=PBdd3a(86C+yLZt`K{fN`3Qt0b zq*Svp+!MbFkL3UcUXQUBl6p)r`bqVy*VVPXCng_c|TScr+Ak2Ffm(w>j1hdiFn@P4a2BGtXQPg99}#^j&yu&KbLw=CxM^W z13sqthv7`%Jojd>`j$LFni9Ikp-Af=+WVe5`vK#fxTVqu#rObLn%-=p2^n#Wb&MT( z7E31dg)QE3hNV{`ZxamBs*F^sX{%%QPykbn0C*?lX=JURpiWr?}@n z*Pi;j3n{bC-RiC2#rvVXR&D}g)Og5R>4vUP(}H{*3g;qiDC5L7=SzzEL+cXFM!jAC z71Q&77Kxvs8WKYMde53&kgOam2HQ)6Mo2yX)i*9Od$gBtGb&5>^KCg-m*snlf>T~L z*j$tu{o0pu*Ey9-7L;KROaCXCb2z(bsHu=SqQMCbAhH<(BOTerFf?m>p4}56>X?Af zsI&H0WacZ25i(2!-pdoJ51j2p~je8fayS=TY@ih@E1NNva>m#<$ppb!7S zU9pO^80SCWOUQWh_~U0@j;AHTT{2Njfc9m7zo2N!I)N2V&vV+*dkof0hq->RrYK#`S(% zh|`=i*z>u4QVJcQIW7yFotkq`j&J_C^;gD{PxA^!S2C#cCZaFdtff*=M7it<#}C^j zl5yeGhmX1Lsnohf+C8@k#?buW7+>Q_>7ZjBlvN-~hXEtz`<4U~pKr=jrG@$@U#Ht{ zL-tg&&?75X|3r*4DOXqK5(MjaRF&Sc*OtT!j;4S_ALsF0JwmHPNv1tmT_pcKc^Ro8 z2dmQfD-1Fk{w1HDVuIvu$1?;Y1 zSbVtW;&a_yu?d&rZt;nd-sj`HA^0x5ubxJ2yj17GV-g8VQ7+uv()$!Ygw)9$ zcB#OkLyM9`zAmn4Rspvsji`Hg=)Rc~zRt$|VRU!nJ>Ey&1*h}nZj8pB{qR@j zhhjQ^Lm}s*=e&a$je}9Hc}lnD7gSCYauR2QU@Gw@$qJE?E{`dGgJ`EpuLSblYn`xp z%Dl1jD7##Y8+}UH{>#^r^d3<6iU&bY-pH*Gc!FDF)NC2hgWJc3($JvCGm!_YKHRC| zj4bOk%0U-JRg)a#N2&J9nk$_0y9KzRqsK57a@|0Q%bO~8BAlmx_6GMaGWv!-9wqx; zNrQLPxUFyI(harGtcV z8~zAhWO{1#C%W*Z`wLNp3}J=yxr-8()U+kSzWbR|Jo*uwbeNW~4kdUQtbIMlmRMC= zMoM2@%q?s3W@EKQe)~QAC#MCquW-~wudhN{EidfNB)$ZUJe!XUa%oXLbTBvZUT=A$ zR;%$Nq+0D!@PWqgBEjd|7o91x8IfhY^boH*aWeS32b`CZFMTifOx$A40!B}@ z)U_fsENL(g6%^R zbuglSOi`rgleG?Vxh`S)9~aXasQ4ptO!%O!5V<$JN~3>t555%DH$D4av_QGO!Rr9} zE`DoZskd7o5SgFfJub|E zat7)M*SeG?#U7{nUoK`&-vwDMVg|kgr(C7YF`yRCdsoVx^A(;V%VEM;B#w9GCTp^X zBh^{iL!Od2MJ?r`7TJ+v;2XH6uXT8{vejdky!S1GWKLle^Bm=@2^~d(! zG^gc!Dxz$gV-J%7@T;<*i96x1w-qS^lmQ95PLbc86OyKA&A9I}6JoEj?Krz(@$=}& zGh0C&89RKWl-#T2YR|KVi_C$>878sWXyrQLTK7wkEemsR_gN@q_x0!6ba zO;NV(O-hy7B85^vMkOA;>t0Ok55G3n1yI>8D*nZU#;00FxotRz=`r@%utNz6!k*Ph zTuBhp@BD6g0qBj}$2KS@O#E@F*_k3gK$I%e09w0r^q+L+x1liE!myWe*FPLS2?KB5 zIj9G#A~aI|*t)6fOn1g5p~IK}7*nH)Y`1w1{l>$Z^cHvGItYw1;y+SHf>?;h>>Aa?-S}4mq3j9$3T9X)ZaqtuNT=shNpqfc#V&paI@gjFr=&_hsC{u#d4G{iAO+N2&GWfwMoucu&KsWL4Lz$u9 zUE(nZ;__}gzOnskebYoml7Cme26P?; z!F0cJK@W`x*>bOS@Ez6UN;m57PQK12UfFjT-mk*S-XD<<)BWq?@>}(M)&i}AYPV_h z%q9IA=SwES3t2YE(YXn*zF*I}|LU<1yq_%bqm%HuZF*LtcS6Ot{BZJlkhuLEFf`V- z9V%y#cHHlg$PsBS7$sA9e-QZW};71F%N4|csTCSdDt9Mts8X3772MH`%^j zOL{E2yNUL}9tdko$0#&v*CEJ$EP=OGd9Nh-ejl`_N!sjVVG%*Be%9vAwVbQLrX{7kTae|TTh7mgwObGVSsXMAa9@tq+QZg|?+ zX4OF*9JfqX^%owzxngxEMTwt}*^xM$@z2cC$XU5rBDz<5#=G}C&RE+lP<2|D(>6ibh) z5nS@bSVqZeVP3ssNiX8${u;R#IfA(ht(gyy^BE zBh(&hT}IWMEg^2CuVG^nY6e#sV<5}_lDql7|8|&b1h!jE%()snc@}5fc>MCmwFdGx zb8iQcB55i?(+I?A>q2bxlXI%96PN4Sj~ZJ6mPVz+pHi3Q{UYlarY<9>rIzE=exxLi z=ZgFLUc2hGj_o{0@T0fx!W)&E{eB)2-V6KQnPD#h zmhZ1%wXm$Vf?YtR@MhGL809g^Wc`8=F1(@pXrEcUQ%&4i%aciGh<%J#y<8fx;>2W= zLO9-L;Zn7gRPkLbUVm8k`Fn<1tQY(x^_#~QF{%RH$1Lx|dA;t3fMPTz*e9iS?W|Jw zS%wV?!1mD_q*btiE~G$#IeKw&4Zk1`&|>jkWIq!@EVak;t;{6rtrk)AtiZVoQp=X#1>|0?-?G(NP4pjUX18 zr$(}+GHuL3FL@9p26bBWKk=D3LI@PNXnFUjQr<8~2tN$toAtV4Tc+*m8wcfH%HI~k zy^-(W!e1V#7W`snS)^`ei=yf~uEj8wSMIf4fo@v19D>l^fZ9}5Me&6eXgaIW!z8)} zIPPEdu-dK<=F*h5GQ7S7%sj=vk)1E08SYfd+3D3uwl^@ucm&Zeq(W@BoBLi|niJlE z{2o4U{`H{2YTM_L`f=V)4SL+~*h; zP06JNCWZT)5qfVBZ9J9V-=lpB1mnBTM*&!fnEDrfuWSV!>6}CUMh^V8=_6PH$W006 z>6{4!`hGckvtV>`v2}w4-Q=#P0amc|LDcIZGXBS&f8jttmGHq;`{p#Z50IxiIa_Y} z^$d}7oN?>^(NF4oEPeTf2=3QWP_kx;~)*AI*NgD3o;cSuOl-BFQG& z;Mo~@QlE$0QZb{!{s?ka)4g`&V1Hbt*L`t1$FI~FKQr6zh06|M8{`URr-Jy$?q3O_ zH~QYpEAI7MYP-1!%k_H1D%_-XPkcFTzUYZZ~Ra z-PgQLBW&>@a-o21jt9$D4*B#?Wb}WK9CW3Wn*_0uWE<;(1^`dGf{9d}ro)KfutJ%qy>cxeYduQY(@T$~rpfayp`0SJ!3qu%lgpNBt8k9Na!2wD9a8q&}^nPqCW?ihkO+qab7)$|4fc6cg zf3buqLb5YV_zy%{cLMdOBFOExnlXlY0G;< z>UizsmVZ0CA89DTd_>p)3c@_@p(^@91uwW21MYFCztphmDsyI+e?F-j~u3ec+w`Lb*zID>44b0o@&%}t6| zR>wDw;hu^N&I_$%6PVkzn77B@_)dCgll&7udUYTP!&_7CWTltELLe;tNl>=6yY7KJ z{ZmJ%d($X>0~;a!#~gJ!c@B$)(Ys6C6E{La3{S^TN1`5ry083DPOvRH#2V_~sS@!h zb3`bsSunAlh(}U#mT|PSvAx#ULZ_tWTEQ+2`5|IFv`Q|R=}orLM=BW>4|a=${i0(| zA&ZGIhkLRm!*(JR36(F!gx%u3>hl>-Itf%dia^JV*}T2*hPY51)FCqkuIN$e+zwq| zpeuE{Kga3^-Fp|d3A3fHZ2ab&-|#FON55*wzI{nhVSWdqnqLe#3_qSG9WG#9{&Xv7f8SGcR{RLU zu1Mn8-!8+}wl$st`kkfx6pFfUB!r=$vuSLQxwIesPcaq#$>x=ZR@I}8 zR>)Kh#1CHORxM?%1vDx2CtPlH`zwCHaG<$-j^1fd&Askr@T(Al1_iWynDYGwaHyLN zcN}Bi`7J%zDJRfBsHYpOwWcBxkrBX0FHWtTmM+B_(ETcg48cp+uxG-Rb&Wx$f2ys4 zBg^eny>$_%Q5(Jom_S}@w_7>SXmfby8flMIR>Z4uJ)c|e!hN~RvGOUy4Sk+Neaq0M zU+{E!Jaxn8*_YHm;>5R9i2R(m(jY6nq)Fqi_vEF^f%piWupsk zgt-4w3UvR@i76P)I^SKJ9GVz`HJw_G!Jn4!Ao16NX7?3ifY`e_v44ep2dYu)3h&+i zM!OGdaPjl_Y#PV4)G6CD?U(Cma6=MWyQ7AX;wyFDL-$q)*~FCA-Ie>Q#tyZ`S*ON5 zYg9b`a+PrSI>&ypM8!Q1FNs4qL__-6bT2>O7`c`du^1iR6B& zXog>HWAKUa?{8rQ4eHxzw+V8s_r2IF?k5sdA9>NheG9)Nt=K8s(%H=yo^@hzms$pB&TBLqLfZ43C+kxsEQOfP!#K?Eef4hxqko{iYjFzuuwIkk30pF< z5MFot<;Z%)@rz}ZV{~U~SybxBUPG)@>#*~onZ7{?EeJ=3jFovp7cipb+yGaQ?}41Yb_;coBV-OmY3XCOzZk-478wc zv8IB0vj+>4y_FyrX$iE@L$+zS`T4Pd_z`G#`O17WaOKquBf>yiJs5mvo>zV35U+D= z0n|@8CWMk!DVX?lKy)F{j&HaVhj?i4GsxDs7}k|%2daz``x@|PNhZFlPq$a(%s}N^ zvIMd?&iTpXkF}}g^R=<=d{sYUE?wOvd2#OgqkXGuDo1LS;M$u-q>FM?#trt*SRHR# zLLpUaj0O$ssU+2OMHq_LLE9VLh$b%_Xd9%}K2_ ztWRBhDjznsMejie-V*5#fQ}Yl{*kZbS7T4Uv7x#WKAo?PO&N*@s@j_p_^Zl1te1kv zJN}r5X_^lYV)76JoZxWflQ#5+qF~5^ntg9JD*%G48({{}=Q5hTYfJ!H%vT$@*{vMA zLP+AD>MO-hTx5HDHokX!4)~?70eUs61TT49)-ycI@jA&*$Aj*yf#!8MfWM+VAg|k% zj@RP+rid-$+XfN1)DE(0YH>c@LgH7NuA<>Kj9Teuk zchFir4{UUu*Td!|X(a*&*&*{Nz!Jlq!5wzeJmG_aWSHGz-^s=d@moe27u%f0fn#?m zBF?++|4c4l8P7FRik=1mXAO#|WS;3`*vZs5jqBNIH#YAxJ2RWuj=Vm7EkXh%(UFa;z{O#^r0rI!!oU zXL=Z7jLCK)Y;px?07ynSjnnJ&>Dtepb?nq}!qr%N4KcDDcV+-~i~h zk4af@vH(W>>hf6-GI<$raD=D&k9k+v$V2jKww@EG6H_ z^qTNNU-4_1_=BKL%1T%6@JuJMy;v&XML-(imbYopD4zbo7?9{@h6EoZsb9J38| z87_|zsm}SBbyf>lr_{#wWoCjV1JmTmgVjnfYwqv_zu{OIYugm z{W3c%UjkTRvK;!<;=RL??1zCPA@m!MpjAcetNA#S&4-G`a0cE&vYzIVX;?o8xrKKZ z!{1JET)gh^86U4rZ6W`lsdEkZvv`oTi>#H#*Ur{g*vnp_C9-!>iv@ExfF8~fH|g^q z0uIox$nKgv)11LQPT*jfW6L?#YrqX#>pa+aff(!~I6;es(AZ#??WbR#Bv5k_^W0@D zBuQnyOErBB-L;_d(iq9LY{rbsXaHHvn0^-~U1z|l7fp9-OZ7a|m0Lt8yQD)Q&sm?=uE~cPv(}T8Bz<0rcm{$&vvbT)#eOB=CxR9KA{xN zxe`O8`CO~`HCfURx+8bWQ_PNLxvh+zSK+w|H0V1T1`Rl9F6m~TyXqud_ypi%QI75;$O6Iv9*q!u5Z7jm&VkMU_dNFei{kQqQvosnXi zpfee(TuHB)QsX?hL8%O55aLyt6ESJRD{;1QBmU~Vw#-7v2y4W=X{SNPvZ)W~J8Bwy zE40*(g-YhCGvm`jphRDUK@-i<W)IFafgA@!D>z z@mgY2qW&ztItKJ)0nhP(pxIzXX>eeT!EwQ%N=uW^M6ba)HW8ZHcBAg_XX{xKG+7z= zTGfIMF>QWjyRk3B{=`xZ*GrA-40g@utXcJ!HhIRK-2gJ+o5ER>RlNYN2D3qEE}fD= zv$L0PBKpbT#%7fVvuHz8p2cGY%!M@Zm5RAEFKV+(u2fW7oaI;RC2`^_dtXCR|&?V-@L)(a;(t4TpXB${U!*lhgZmvp`s0r)1 z?)W>X4Js2p+nfkE7KlBrQ?EDBl0XnVFHDGhvlGID2DjOW%f{O^j-|O|NlAjUG^dRn z$5J&~lXOJ`>2P)uycV>1E$H%pyv3%; zv)Bk7L{m$|n~ECeYzo*pY$0WiUw96vbR|MIKa&?^5Q$z3(QzofO5I+_Ei+si@}11e z2)ZsIZ$v_itcP({m(_8WS3?wQShFc__P9G`ay71FzBI92Q^Sp<5KpMTV`a{b$>d#X zmb$LWgtU>QhRL!s}hPxyWDw%$jJ7PA%ur`0bW_sBzrCn37hQguIJM$Dv zuf!_E0f{FRZXAvI8eQ7t+{Jo=hHw-92H90?v5>&SjQ|a^*D|?In@d^6#zjjALldUv zmrAcjuLGzi$flT2mL;)GT=$xrHZ>X-%mHK&HsaO$p_H(BTXXx+7_fGs8DD~|F9 lhy|{={l}Pc4H}Ko{{gKr_ Date: Mon, 7 Oct 2024 15:36:02 -0400 Subject: [PATCH 02/65] Remove reference to long-gone script --- README.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/README.md b/README.md index fbf8b4c0c6..939240a294 100644 --- a/README.md +++ b/README.md @@ -990,17 +990,6 @@ Notes: [AWS CLI Command Reference (s3)](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/s3/index.html) for documentation on how to use the command line to list and to manage the backup objects. -#### Create a New Admin User (Production) - -Task: create a new user who is a site administrator - -Run: - -```bash -# Run from the `deploy` directory in the project on the host machine -ansible-playbook playbook_admin_user.yaml --limit -u sillsdev -K -``` - #### Delete a Project Task: Delete a project From 1cae5cf70bb631d977793b6326eb2299dcc349e9 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Mon, 7 Oct 2024 16:50:01 -0400 Subject: [PATCH 03/65] Starting allowing for arm64 --- deploy/Dockerfile | 3 ++- .../ansible/playbook_k3s_airgapped_files.yml | 10 ++++----- .../roles/container_engine/defaults/main.yml | 1 + .../roles/container_engine/tasks/main.yml | 2 +- .../roles/container_images/defaults/main.yml | 1 + .../roles/container_images/tasks/main.yml | 8 +++---- .../roles/helm_install/defaults/main.yml | 4 ++-- .../ansible/roles/helm_install/tasks/main.yml | 7 +++---- deploy/ansible/vars/k3s_versions.yml | 1 + deploy/scripts/package_images.py | 21 +++++++++++++------ 10 files changed, 35 insertions(+), 23 deletions(-) diff --git a/deploy/Dockerfile b/deploy/Dockerfile index d78140dd22..cc91417e91 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -16,7 +16,8 @@ RUN apt-get update && \ rm -rf /var/lib/apt/lists/* # Install kubectl and helm -RUN curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" && \ +RUN MACH=$(case $(uname -m) in *86*) echo amd64;; *aarch*) echo arm64;; *arm*) echo arm64;; esac) && \ + curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/${MACH}/kubectl" && \ install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl && \ curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 && \ chmod 700 get_helm.sh && \ diff --git a/deploy/ansible/playbook_k3s_airgapped_files.yml b/deploy/ansible/playbook_k3s_airgapped_files.yml index 7911849fef..d92a92da5d 100644 --- a/deploy/ansible/playbook_k3s_airgapped_files.yml +++ b/deploy/ansible/playbook_k3s_airgapped_files.yml @@ -32,13 +32,13 @@ dest: "{{ package_dir }}/{{ item }}" url: "https://github.com/k3s-io/k3s/releases/download/{{ k3s_version }}/{{ item }}" loop: - - k3s-airgap-images-amd64.tar.zst + - k3s-airgap-images-{{ linux_arch }}.tar.zst - k3s - - sha256sum-amd64.txt + - sha256sum-{{ linux_arch }}.txt - name: Verify k3s downloads shell: - cmd: sha256sum --check --ignore-missing sha256sum-amd64.txt + cmd: sha256sum --check --ignore-missing sha256sum-{{ linux_arch }}.txt chdir: "{{ package_dir }}" changed_when: false @@ -50,9 +50,9 @@ - name: Download kubectl get_url: dest: "{{ package_dir }}/kubectl" - url: "https://dl.k8s.io/release/{{ kubectl_version }}/bin/linux/amd64/kubectl" + url: "https://dl.k8s.io/release/{{ kubectl_version }}/bin/linux/{{ linux_arch }}/kubectl" - name: Download helm get_url: dest: "{{ package_dir }}/helm.tar.gz" - url: "https://get.helm.sh/helm-{{ helm_version }}-linux-amd64.tar.gz" + url: "https://get.helm.sh/helm-{{ helm_version }}-linux-{{ linux_arch }}.tar.gz" diff --git a/deploy/ansible/roles/container_engine/defaults/main.yml b/deploy/ansible/roles/container_engine/defaults/main.yml index 1276e993b3..07e4da8d67 100644 --- a/deploy/ansible/roles/container_engine/defaults/main.yml +++ b/deploy/ansible/roles/container_engine/defaults/main.yml @@ -3,3 +3,4 @@ container_packages: - containerd.io keyring_location: /etc/apt/keyrings +linux_arch: amd64 diff --git a/deploy/ansible/roles/container_engine/tasks/main.yml b/deploy/ansible/roles/container_engine/tasks/main.yml index 8508c4bc46..b7552fcc5a 100644 --- a/deploy/ansible/roles/container_engine/tasks/main.yml +++ b/deploy/ansible/roles/container_engine/tasks/main.yml @@ -40,7 +40,7 @@ - name: Add Docker repository apt_repository: - repo: "deb [arch=amd64 signed-by={{ keyring_location }}/docker.gpg] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable" + repo: "deb [arch={{ linux_arch }} signed-by={{ keyring_location }}/docker.gpg] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable" state: present filename: docker diff --git a/deploy/ansible/roles/container_images/defaults/main.yml b/deploy/ansible/roles/container_images/defaults/main.yml index 02994e8520..84508871db 100644 --- a/deploy/ansible/roles/container_images/defaults/main.yml +++ b/deploy/ansible/roles/container_images/defaults/main.yml @@ -4,3 +4,4 @@ source_image_dir: ../airgap-images airgap_image_dir: /var/lib/rancher/k3s/agent/images +linux_arch: amd64 diff --git a/deploy/ansible/roles/container_images/tasks/main.yml b/deploy/ansible/roles/container_images/tasks/main.yml index d5edea306d..51f72f30d1 100644 --- a/deploy/ansible/roles/container_images/tasks/main.yml +++ b/deploy/ansible/roles/container_images/tasks/main.yml @@ -18,9 +18,9 @@ group: root mode: 0644 loop: - - k3s-airgap-images-amd64.tar.zst - - middleware-airgap-images-amd64.tar.zst - - combine-airgap-images-amd64.tar.zst + - k3s-airgap-images-{{ linux_arch }}.tar.zst + - middleware-airgap-images-{{ linux_arch }}.tar.zst + - combine-airgap-images-{{ linux_arch }}.tar.zst # Add k3s, kubectl and the k3s installation script to # /usr/local/bin @@ -51,7 +51,7 @@ - name: Create link to helm binary file: - src: /opt/helm/{{ helm_version }}/linux-amd64/helm + src: /opt/helm/{{ helm_version }}/linux-{{ linux_arch }}/helm dest: /usr/local/bin/helm state: link owner: root diff --git a/deploy/ansible/roles/helm_install/defaults/main.yml b/deploy/ansible/roles/helm_install/defaults/main.yml index 54401b3cdc..f582c0a10b 100644 --- a/deploy/ansible/roles/helm_install/defaults/main.yml +++ b/deploy/ansible/roles/helm_install/defaults/main.yml @@ -1,5 +1,5 @@ --- helm_version: v3.15.2 -helm_arch: linux-amd64 +linux_arch: amd64 -helm_download_dir: /opt/helm-{{ helm_version }}-{{ helm_arch }} +helm_download_dir: /opt/helm-{{ helm_version }}-linux_{{ linux_arch }} diff --git a/deploy/ansible/roles/helm_install/tasks/main.yml b/deploy/ansible/roles/helm_install/tasks/main.yml index 6956f09f06..0e6f506efd 100644 --- a/deploy/ansible/roles/helm_install/tasks/main.yml +++ b/deploy/ansible/roles/helm_install/tasks/main.yml @@ -9,8 +9,7 @@ - name: Get Latest Release get_url: - # https://get.helm.sh/helm-v3.13.2-linux-amd64.tar.gz - url: "https://get.helm.sh/helm-{{ helm_version }}-{{ helm_arch }}.tar.gz" + url: "https://get.helm.sh/helm-{{ helm_version }}-linux_{{ linux_arch }}.tar.gz" dest: "{{ helm_download_dir }}/helm.tar.gz" owner: root group: root @@ -20,11 +19,11 @@ command: cmd: "tar -zxvf {{ helm_download_dir }}/helm.tar.gz" chdir: "{{ helm_download_dir }}" - creates: "{{ helm_download_dir }}/{{ helm_arch }}/helm" + creates: "{{ helm_download_dir }}/linux_{{ linux_arch }}/helm" - name: Link to extracted helm file file: - src: "{{ helm_download_dir }}/{{ helm_arch }}/helm" + src: "{{ helm_download_dir }}/linux_{{ linux_arch }}/helm" path: /usr/local/bin/helm state: link owner: root diff --git a/deploy/ansible/vars/k3s_versions.yml b/deploy/ansible/vars/k3s_versions.yml index e3a79b990f..845b0b8057 100644 --- a/deploy/ansible/vars/k3s_versions.yml +++ b/deploy/ansible/vars/k3s_versions.yml @@ -2,3 +2,4 @@ k3s_version: "v1.30.1%2Bk3s1" kubectl_version: "v1.30.2" helm_version: "v3.15.2" +linux_arch: "amd64" diff --git a/deploy/scripts/package_images.py b/deploy/scripts/package_images.py index 0fa690db54..b3943c8897 100755 --- a/deploy/scripts/package_images.py +++ b/deploy/scripts/package_images.py @@ -7,6 +7,8 @@ helm templates for the middleware used by The Combine and for The Combine itself. The image names are extracted from the templates and then pulled from the repo and stored in ../images as compressed tarballs; zstd compression is used. + +By default, packs images for amd64 architecture; use --arm to pack for arm64 instead. """ import argparse import logging @@ -38,6 +40,11 @@ def parse_args() -> argparse.Namespace: ) parser.add_argument("output_dir", help="Directory for the collected image files.") # Add Optional arguments + parser.add_argument( + "--arm", + help="Package for arm64 instead of the default amd64", + default=False, + ) parser.add_argument( "--config", "-c", @@ -87,7 +94,7 @@ def package_images(image_list: List[str], tar_file: Path) -> None: def package_middleware( - config_file: str, *, cluster_type: str, image_dir: Path, chart_dir: Path + config_file: str, *, cluster_type: str, image_dir: Path, chart_dir: Path, arm=False ) -> None: logging.info("Packaging middleware images.") # read in cluster configuration @@ -128,10 +135,11 @@ def package_middleware( logging.debug(f" - Found image {match.group(1)}") middleware_images.append(match.group(1)) logging.debug(f"Middleware images: {middleware_images}") - package_images(middleware_images, image_dir / "middleware-airgap-images-amd64.tar") + out_file = f"middleware-airgap-images-{"arm64" if arm else "amd64"}.tar" + package_images(middleware_images, image_dir / out_file) -def package_thecombine(tag: str, image_dir: Path) -> None: +def package_thecombine(tag: str, image_dir: Path, *, arm=False) -> None: logging.info(f"Packaging The Combine version {tag}.") logging.debug("Create helm charts from templates") combine_charts.generate(tag) @@ -158,7 +166,8 @@ def package_thecombine(tag: str, image_dir: Path) -> None: combine_images.append(image) logging.debug(f"Combine images: {combine_images}") # Logout of AWS to allow pulling the images - package_images(combine_images, image_dir / "combine-airgap-images-amd64.tar") + out_file = f"combine-airgap-images-{"arm64" if arm else "amd64"}.tar" + package_images(combine_images, image_dir / out_file) def main() -> None: @@ -181,9 +190,9 @@ def main() -> None: # Update helm repos package_k3s(image_dir) package_middleware( - args.config, cluster_type="standard", image_dir=image_dir, chart_dir=chart_dir + args.config, cluster_type="standard", image_dir=image_dir, chart_dir=chart_dir, arm=args.arm ) - package_thecombine(args.tag, image_dir) + package_thecombine(args.tag, image_dir, arm=args.arm) if __name__ == "__main__": From 9bb2118c3088831c1b7cb97e4cd0b632345d853d Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Mon, 7 Oct 2024 16:52:13 -0400 Subject: [PATCH 04/65] Fix _/- error --- deploy/ansible/roles/helm_install/defaults/main.yml | 2 +- deploy/ansible/roles/helm_install/tasks/main.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/deploy/ansible/roles/helm_install/defaults/main.yml b/deploy/ansible/roles/helm_install/defaults/main.yml index f582c0a10b..8164665260 100644 --- a/deploy/ansible/roles/helm_install/defaults/main.yml +++ b/deploy/ansible/roles/helm_install/defaults/main.yml @@ -2,4 +2,4 @@ helm_version: v3.15.2 linux_arch: amd64 -helm_download_dir: /opt/helm-{{ helm_version }}-linux_{{ linux_arch }} +helm_download_dir: /opt/helm-{{ helm_version }}-linux-{{ linux_arch }} diff --git a/deploy/ansible/roles/helm_install/tasks/main.yml b/deploy/ansible/roles/helm_install/tasks/main.yml index 0e6f506efd..b8ac983fc2 100644 --- a/deploy/ansible/roles/helm_install/tasks/main.yml +++ b/deploy/ansible/roles/helm_install/tasks/main.yml @@ -9,7 +9,7 @@ - name: Get Latest Release get_url: - url: "https://get.helm.sh/helm-{{ helm_version }}-linux_{{ linux_arch }}.tar.gz" + url: "https://get.helm.sh/helm-{{ helm_version }}-linux-{{ linux_arch }}.tar.gz" dest: "{{ helm_download_dir }}/helm.tar.gz" owner: root group: root @@ -19,11 +19,11 @@ command: cmd: "tar -zxvf {{ helm_download_dir }}/helm.tar.gz" chdir: "{{ helm_download_dir }}" - creates: "{{ helm_download_dir }}/linux_{{ linux_arch }}/helm" + creates: "{{ helm_download_dir }}/linux-{{ linux_arch }}/helm" - name: Link to extracted helm file file: - src: "{{ helm_download_dir }}/linux_{{ linux_arch }}/helm" + src: "{{ helm_download_dir }}/linux-{{ linux_arch }}/helm" path: /usr/local/bin/helm state: link owner: root From 8ad50e7af5c6de3905cbf71714f41d578176a067 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Tue, 8 Oct 2024 14:35:19 -0400 Subject: [PATCH 05/65] [Docker] Move away from legacy ENV assigment --- Dockerfile | 12 ++++++------ deploy/Dockerfile | 4 ++-- maintenance/Dockerfile | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8bc54bbf60..74acc47977 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,9 +9,9 @@ # User guide build environment FROM python:3.12.5-slim-bookworm AS user_guide_builder -ENV PYTHONDONTWRITEBYTECODE 1 -ENV PYTHONUNBUFFERED 1 -ENV PIP_NO_CACHE_DIR 1 +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV PIP_NO_CACHE_DIR=1 WORKDIR /app @@ -40,9 +40,9 @@ FROM nginx:1.27 WORKDIR /app -ENV HOST_DIR /usr/share/nginx -ENV USER_GUIDE_HOST_DIR ${HOST_DIR}/user_guide -ENV FRONTEND_HOST_DIR ${HOST_DIR}/html +ENV HOST_DIR=/usr/share/nginx +ENV USER_GUIDE_HOST_DIR=${HOST_DIR}/user_guide +ENV FRONTEND_HOST_DIR=${HOST_DIR}/html RUN mkdir /etc/nginx/templates RUN mkdir /etc/nginx/page_templates diff --git a/deploy/Dockerfile b/deploy/Dockerfile index d78140dd22..b0a7cc68e3 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -23,8 +23,8 @@ RUN curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/s ./get_helm.sh && \ rm kubectl get_helm.sh -ENV HOME /root -ENV PATH ${PATH}:${HOME}/scripts +ENV HOME=/root +ENV PATH=${PATH}:${HOME}/scripts COPY requirements.txt ${HOME} RUN pip3 install -r ${HOME}/requirements.txt diff --git a/maintenance/Dockerfile b/maintenance/Dockerfile index 83c22944e1..9e86310ad8 100644 --- a/maintenance/Dockerfile +++ b/maintenance/Dockerfile @@ -39,7 +39,7 @@ RUN chown user:user ${FONT_DIR} USER user WORKDIR ${HOME} -ENV PATH ${PATH}:${SCRIPT_DIR} +ENV PATH=${PATH}:${SCRIPT_DIR} COPY --chown=user:user requirements.txt . From d9c194e6965b92b65a22d7dafa9270482c8969ea Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Tue, 8 Oct 2024 14:51:34 -0400 Subject: [PATCH 06/65] [build] Clean up --quiet usage --- deploy/scripts/build.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/deploy/scripts/build.py b/deploy/scripts/build.py index 31390e39d8..31ac0c6c6e 100755 --- a/deploy/scripts/build.py +++ b/deploy/scripts/build.py @@ -223,14 +223,14 @@ def parse_args() -> Namespace: action="store_true", help="Always attempt to pull a newer version of an image used in the build.", ) - parser.add_argument( + logging_group = parser.add_mutually_exclusive_group() + logging_group.add_argument( "--quiet", "-q", action="store_true", help="Run the docker build commands with '--quiet' instead of '--progress plain'.", ) - group = parser.add_mutually_exclusive_group() - group.add_argument( + logging_group.add_argument( "--debug", "-d", action="store_true", @@ -269,6 +269,7 @@ def main() -> None: # Setup build options if args.quiet: build_cmd += ["--quiet"] + push_cmd += ["--quiet"] else: build_cmd += ["--progress", "plain"] if args.no_cache: @@ -296,11 +297,8 @@ def main() -> None: logging.debug(f"Adding job {build_cmd + job_opts}") job_set[component].add_job(Job(build_cmd + job_opts, spec.dir)) if args.repo is not None: - if args.quiet: - push_args = ["--quiet"] - else: - push_args = [] - job_set[component].add_job(Job(push_cmd + push_args + [image_name], None)) + logging.debug(f"Adding job {push_cmd + [image_name]}") + job_set[component].add_job(Job(push_cmd + [image_name], None)) logging.info(f"Building component {component}") # Run jobs in parallel - one job per component From d055b66fb1c79713590f78bb0173534d1618fff6 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Tue, 8 Oct 2024 14:54:15 -0400 Subject: [PATCH 07/65] Clean up comments --- deploy/scripts/build.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/deploy/scripts/build.py b/deploy/scripts/build.py index 31ac0c6c6e..e351a858df 100755 --- a/deploy/scripts/build.py +++ b/deploy/scripts/build.py @@ -91,8 +91,8 @@ def check_jobs(self) -> JobStatus: """ Check if all jobs in the queue have completed. - If the current job has finished, and there are more jobs to run, it will start the next - job. + If the current job has finished, and there are more jobs to run, + it will start the next job. """ if self.status != JobStatus.RUNNING: return self.status @@ -113,8 +113,7 @@ def check_jobs(self) -> JobStatus: self.status = JobStatus.ERROR return self.status self.curr_job = None - # start the next job. If there are no more jobs to run, we have - # finished successfully + # Start the next job; if there are no more to run, we have finished successfully if not self.start_next(): self.status = JobStatus.SUCCESS return self.status @@ -149,8 +148,7 @@ def no_op() -> None: pass -# Create a dictionary to look up the build spec from -# a component name +# Create a dictionary to look up the build spec from a component name build_specs: Dict[str, BuildSpec] = { "backend": BuildSpec(project_dir / "Backend", "backend", no_op, no_op), "database": BuildSpec(project_dir / "database", "database", build_semantic_domains, no_op), @@ -304,7 +302,7 @@ def main() -> None: # Run jobs in parallel - one job per component build_returncode = 0 while True: - # loop through the running jobs until there is no more work left + # Loop through the running jobs until there is no more work left running_jobs = False for component in job_set: if job_set[component].check_jobs() == JobStatus.RUNNING: From cde7859177c5181e904df3c43c278d48e79d0333 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Tue, 8 Oct 2024 16:54:51 -0400 Subject: [PATCH 08/65] [setup_combine] Add --debug logging --- deploy/scripts/build.py | 12 +--- deploy/scripts/helm_utils.py | 4 +- deploy/scripts/kube_env.py | 34 +++++------ deploy/scripts/setup_cluster.py | 2 +- deploy/scripts/setup_combine.py | 102 ++++++++++++++++---------------- deploy/scripts/utils.py | 8 +-- 6 files changed, 73 insertions(+), 89 deletions(-) diff --git a/deploy/scripts/build.py b/deploy/scripts/build.py index e351a858df..fa395b26b1 100755 --- a/deploy/scripts/build.py +++ b/deploy/scripts/build.py @@ -28,6 +28,7 @@ from enum_types import JobStatus from sem_dom_import import generate_semantic_domains from streamfile import StreamFile +from utils import init_logging @dataclass(frozen=True) @@ -240,16 +241,7 @@ def parse_args() -> Namespace: def main() -> None: """Build the Docker images for The Combine.""" args = parse_args() - - # Setup the logging level. The command output will be printed on stdout/stderr - # independent of the logging facility - if args.debug: - log_level = logging.DEBUG - elif args.quiet: - log_level = logging.WARNING - else: - log_level = logging.INFO - logging.basicConfig(format="%(levelname)s:%(message)s", level=log_level) + init_logging(args) # Setup required build engine - docker or nerdctl container_cli = os.getenv("CONTAINER_CLI", "docker") diff --git a/deploy/scripts/helm_utils.py b/deploy/scripts/helm_utils.py index 4a6a8af18d..8241901691 100644 --- a/deploy/scripts/helm_utils.py +++ b/deploy/scripts/helm_utils.py @@ -43,9 +43,9 @@ def create_secrets( return secrets_written -def get_installed_charts(helm_opts: List[str], helm_namespace: str) -> List[str]: +def get_installed_charts(helm_cmd: List[str], helm_namespace: str) -> List[str]: """Create a list of the helm charts that are already installed on the target.""" - lookup_results = run_cmd(["helm"] + helm_opts + ["list", "-n", helm_namespace, "-o", "yaml"]) + lookup_results = run_cmd(helm_cmd + ["list", "-n", helm_namespace, "-o", "yaml"]) chart_info: List[Dict[str, str]] = yaml.safe_load(lookup_results.stdout) chart_list: List[str] = [] for chart in chart_info: diff --git a/deploy/scripts/kube_env.py b/deploy/scripts/kube_env.py index 413ded5b32..0ca05e999c 100755 --- a/deploy/scripts/kube_env.py +++ b/deploy/scripts/kube_env.py @@ -16,7 +16,7 @@ def __init__(self, args: argparse.Namespace, *, prompt_for_context: bool = True) else: self.kubeconfig = None if "context" in args and args.context is not None: - # if the user specified a context, use that one. + # If the user specified a context, use that one. self.kubecontext = args.context elif prompt_for_context: context_list: List[str] = [] @@ -33,7 +33,7 @@ def __init__(self, args: argparse.Namespace, *, prompt_for_context: bool = True) else: context_list.append(line.split()[0]) - # If there is more than one context available, prompt the user to make sure + # If there is more than one context available, prompt the user to make sure # that the intended context will be used. curr_context = choose_from_list("context", curr_context, context_list) if curr_context: @@ -52,27 +52,23 @@ def get_kubeconfig(self) -> List[str]: return ["--kubeconfig", self.kubeconfig] return [] - def get_helm_opts(self) -> List[str]: - """ - Create list of general helm options. - """ - helm_opts = self.get_kubeconfig() + def get_helm_cmd(self) -> List[str]: + """Create list of helm command with general options.""" + helm_cmd = ["helm"] + self.get_kubeconfig() if self.kubecontext is not None: - helm_opts.extend(["--kube-context", self.kubecontext]) + helm_cmd.extend(["--kube-context", self.kubecontext]) if self.debug: - helm_opts.append("--debug") - return helm_opts + helm_cmd.append("--debug") + return helm_cmd - def get_kubectl_opts(self) -> List[str]: - """ - Create list of general kubectl options. - """ - kubectl_opts = self.get_kubeconfig() + def get_kubectl_cmd(self) -> List[str]: + """Create list of kubectl command with general options.""" + kubectl_cmd = ["kubectl"] + self.get_kubeconfig() if self.kubecontext is not None: - kubectl_opts.extend(["--context", self.kubecontext]) - return kubectl_opts + kubectl_cmd.extend(["--context", self.kubecontext]) + return kubectl_cmd def add_helm_opts(parser: argparse.ArgumentParser) -> None: @@ -138,5 +134,5 @@ def add_kube_opts(parser: argparse.ArgumentParser) -> None: args = parser.parse_args() kube_env = KubernetesEnvironment(args) - print(f"kubectl {kube_env.get_kubectl_opts()} ...") - print(f"helm {kube_env.get_helm_opts()} ...") + print(f"{kube_env.get_kubectl_cmd()} ...") + print(f"{kube_env.get_helm_cmd()} ...") diff --git a/deploy/scripts/setup_cluster.py b/deploy/scripts/setup_cluster.py index e65abd52fe..e588d011cc 100755 --- a/deploy/scripts/setup_cluster.py +++ b/deploy/scripts/setup_cluster.py @@ -107,7 +107,7 @@ def main() -> None: for chart_descr in this_cluster: chart_spec = config[chart_descr]["chart"] # install the chart - helm_cmd = ["helm"] + kube_env.get_helm_opts() + helm_cmd = kube_env.get_helm_cmd() if chart_spec["name"] in curr_charts: helm_action = HelmAction.UPGRADE else: diff --git a/deploy/scripts/setup_combine.py b/deploy/scripts/setup_combine.py index 4320a0cf6f..052103d824 100755 --- a/deploy/scripts/setup_combine.py +++ b/deploy/scripts/setup_combine.py @@ -19,6 +19,7 @@ The script also adds value definitions from a profile specific configuration file if it exists. """ import argparse +import logging from pathlib import Path import sys import tempfile @@ -82,12 +83,19 @@ def parse_args() -> argparse.Namespace: help="Profile name for the target. " "If not specified, the profile will be read from the config file.", ) - parser.add_argument( + logging_group = parser.add_mutually_exclusive_group() + logging_group.add_argument( "--quiet", "-q", action="store_true", help="Print less output information.", ) + logging_group.add_argument( + "--debug", + "-d", + action="store_true", + help="Print extra debugging information.", + ) parser.add_argument("--repo", "-r", help="Pull images from the specified image repository.") parser.add_argument( "--tag", @@ -136,21 +144,18 @@ def main() -> None: # Verify the Kubernetes/Helm environment kube_env = KubernetesEnvironment(args) - # Cache options for helm commands used to alter - # the target cluster - helm_opts = kube_env.get_helm_opts() + # Cache helm command used to alter the target cluster + helm_cmd = kube_env.get_helm_cmd() # Check AWS Environment Variables init_aws_environment() - # create list of target specific variable values - target_vars = [ - f"global.imageTag={args.image_tag}", - ] + # Create list of target specific variable values + target_vars = [f"global.imageTag={args.image_tag}"] if args.repo: target_vars.append(f"global.imageRegistry={args.repo}") - # add any value overrides from the command line + # Add any value overrides from the command line if args.set: target_vars.extend(args.set) @@ -159,34 +164,36 @@ def main() -> None: for filepath in args.values: addl_configs.extend(["-f", filepath]) - # lookup directory for helm files + # Lookup directory for helm files helm_dir = scripts_dir.parent / "helm" - # open a temporary directory for creating the secrets YAML files + # Open a temporary directory for creating the secrets YAML files with tempfile.TemporaryDirectory() as secrets_dir: for chart in config["profiles"][profile]["charts"]: - # create the chart namespace if it does not exist + logging.debug(f"Chart: {chart}") + + # Create the chart namespace if it does not exist chart_namespace = config["charts"][chart]["namespace"] - if add_namespace(chart_namespace, kube_env.get_kubectl_opts()): + logging.debug(f"Namespace: {chart_namespace}") + if add_namespace(chart_namespace, kube_env.get_kubectl_cmd()): installed_charts: List[str] = [] else: - # get list of charts in target namespace - installed_charts = get_installed_charts(helm_opts, chart_namespace) + # Get list of charts in target namespace + installed_charts = get_installed_charts(helm_cmd, chart_namespace) + logging.debug(f"Installed charts: {installed_charts}") - # set helm_action based on whether chart is already installed + # Set helm_action based on whether chart is already installed helm_action = HelmAction.INSTALL if chart in installed_charts: if args.clean: - # delete existing chart if --clean specified - run_cmd( - ["helm"] + helm_opts + ["--namespace", chart_namespace, "delete", chart], - print_cmd=not args.quiet, - print_output=True, - ) + # Delete existing chart if --clean specified + delete_cmd = helm_cmd + ["--namespace", chart_namespace, "delete", chart] + logging.debug(delete_cmd) + run_cmd(delete_cmd, print_cmd=not args.quiet, print_output=True) else: helm_action = HelmAction.UPGRADE - # build the secrets file + # Build the secrets file secrets_file = Path(secrets_dir).resolve() / f"secrets_{chart}.yaml" include_secrets = create_secrets( config["charts"][chart]["secrets"], @@ -194,31 +201,27 @@ def main() -> None: env_vars_req=this_config["env_vars_required"], ) - # create the base helm install command + # Create the base helm install command chart_dir = helm_dir / chart - helm_install_cmd = ( - ["helm"] - + helm_opts - + [ - "--namespace", - chart_namespace, - helm_action.value, - chart, - str(chart_dir), - ] - ) - - # set the dry-run option if desired + helm_install_cmd = helm_cmd + [ + "--namespace", + chart_namespace, + helm_action.value, + chart, + str(chart_dir), + ] + + # Set the dry-run option if desired if args.dry_run: helm_install_cmd.append("--dry-run") - # set wait and timeout options + # Set wait and timeout options if args.wait or args.timeout is not None: helm_install_cmd.append("--wait") if args.timeout is not None: helm_install_cmd.extend(["--timeout", args.timeout]) - # add the profile specific configuration + # Add the profile specific configuration add_profile_values( config, profile_name=profile, @@ -227,14 +230,9 @@ def main() -> None: helm_cmd=helm_install_cmd, ) - # add the secrets file + # Add the secrets file if include_secrets: - helm_install_cmd.extend( - [ - "-f", - str(secrets_file), - ] - ) + helm_install_cmd.extend(["-f", str(secrets_file)]) if config["charts"][chart]["install_langs"]: add_language_overrides(this_config, chart=chart, langs=args.langs) @@ -246,7 +244,7 @@ def main() -> None: helm_cmd=helm_install_cmd, ) - # add any additional configuration files from the command line + # Add any additional configuration files from the command line if len(addl_configs) > 0: helm_install_cmd.extend(addl_configs) @@ -257,11 +255,11 @@ def main() -> None: # Note that this operation is performed on the local helm charts # so the kubeconfig and context arguments are not passed to the # helm command. - run_cmd( - ["helm", "dependency", "update", str(chart_dir)], - print_cmd=not args.quiet, - print_output=True, - ) + helm_deps_cmd = ["helm", "dependency", "update", str(chart_dir)] + logging.debug(helm_deps_cmd) + run_cmd(helm_deps_cmd, print_cmd=not args.quiet, print_output=True) + + logging.debug(helm_install_cmd) run_cmd(helm_install_cmd, print_cmd=not args.quiet, print_output=True) diff --git a/deploy/scripts/utils.py b/deploy/scripts/utils.py index fed4be3582..0af9cff521 100644 --- a/deploy/scripts/utils.py +++ b/deploy/scripts/utils.py @@ -45,17 +45,15 @@ def run_cmd( sys.exit(err.returncode) -def add_namespace(namespace: str, kube_opts: List[str]) -> bool: +def add_namespace(namespace: str, kube_cmd: List[str]) -> bool: """ Create a Kubernetes namespace if and only if it does not exist. Returns True if the namespace was added. """ - lookup_results = run_cmd( - ["kubectl"] + kube_opts + ["get", "namespace", namespace], check_results=False - ) + lookup_results = run_cmd(kube_cmd + ["get", "namespace", namespace], check_results=False) if lookup_results.returncode != 0: - run_cmd(["kubectl"] + kube_opts + ["create", "namespace", namespace]) + run_cmd(kube_cmd + ["create", "namespace", namespace]) return True return False From 70394dd10ec020e639db4f0a5d63c30d9b699110 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Tue, 8 Oct 2024 17:07:06 -0400 Subject: [PATCH 09/65] Fix comments re The Combine --- deploy/scripts/install-combine.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/deploy/scripts/install-combine.sh b/deploy/scripts/install-combine.sh index 79bf2ad6d2..3a46c8c441 100755 --- a/deploy/scripts/install-combine.sh +++ b/deploy/scripts/install-combine.sh @@ -141,12 +141,12 @@ install-the-combine () { deactivate } -# Wait until all the combine deployments are "Running" +# Wait until all The Combine deployments are "Running" wait-for-combine () { - # Wait for all combine deployments to be up + # Wait for all The Combine deployments to be up while true ; do combine_status=`kubectl -n thecombine get deployments` - # Assert the The Combine is up; if any components are not up, set it to false + # Assert The Combine is up; if any components are not up, set it to false combine_up=true for deployment in frontend backend database maintenance ; do deployment_status=$(echo ${combine_status} | grep "${deployment}" | sed "s/^.*\([0-9]\)\/1.*/\1/") @@ -266,7 +266,7 @@ while [ "$STATE" != "Done" ] ; do next-state "Base-charts" if [ -f /var/run/reboot-required ] ; then echo -e "***** Restart required *****\n" - echo -e "Rerun combine installer after the system has been restarted.\n" + echo -e "Rerun The Combine installer after the system has been restarted.\n" read -p "Restart now? (Y/n) " RESTART if [[ -z $RESTART || $RESTART =~ ^[yY].* ]] ; then sudo reboot @@ -295,7 +295,7 @@ while [ "$STATE" != "Done" ] ; do fi ;; Wait-for-combine) - # Wait until all the combine deployments are up + # Wait until all The Combine deployments are up echo "Waiting for The Combine components to download and setup." echo "This may take some time depending on your Internet connection." echo "Press Ctrl-C to interrupt." From c69987b28c2297b82bac612c08e918545831613d Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Wed, 9 Oct 2024 09:29:22 -0400 Subject: [PATCH 10/65] Change linux_arch to cpu_arch --- deploy/ansible/playbook_k3s_airgapped_files.yml | 10 +++++----- .../ansible/roles/container_engine/defaults/main.yml | 2 +- deploy/ansible/roles/container_engine/tasks/main.yml | 2 +- .../ansible/roles/container_images/defaults/main.yml | 2 +- deploy/ansible/roles/container_images/tasks/main.yml | 8 ++++---- deploy/ansible/roles/helm_install/defaults/main.yml | 4 ++-- deploy/ansible/roles/helm_install/tasks/main.yml | 6 +++--- deploy/ansible/vars/k3s_versions.yml | 2 +- 8 files changed, 18 insertions(+), 18 deletions(-) diff --git a/deploy/ansible/playbook_k3s_airgapped_files.yml b/deploy/ansible/playbook_k3s_airgapped_files.yml index d92a92da5d..99522e8d4e 100644 --- a/deploy/ansible/playbook_k3s_airgapped_files.yml +++ b/deploy/ansible/playbook_k3s_airgapped_files.yml @@ -32,13 +32,13 @@ dest: "{{ package_dir }}/{{ item }}" url: "https://github.com/k3s-io/k3s/releases/download/{{ k3s_version }}/{{ item }}" loop: - - k3s-airgap-images-{{ linux_arch }}.tar.zst + - k3s-airgap-images-{{ cpu_arch }}.tar.zst - k3s - - sha256sum-{{ linux_arch }}.txt + - sha256sum-{{ cpu_arch }}.txt - name: Verify k3s downloads shell: - cmd: sha256sum --check --ignore-missing sha256sum-{{ linux_arch }}.txt + cmd: sha256sum --check --ignore-missing sha256sum-{{ cpu_arch }}.txt chdir: "{{ package_dir }}" changed_when: false @@ -50,9 +50,9 @@ - name: Download kubectl get_url: dest: "{{ package_dir }}/kubectl" - url: "https://dl.k8s.io/release/{{ kubectl_version }}/bin/linux/{{ linux_arch }}/kubectl" + url: "https://dl.k8s.io/release/{{ kubectl_version }}/bin/linux/{{ cpu_arch }}/kubectl" - name: Download helm get_url: dest: "{{ package_dir }}/helm.tar.gz" - url: "https://get.helm.sh/helm-{{ helm_version }}-linux-{{ linux_arch }}.tar.gz" + url: "https://get.helm.sh/helm-{{ helm_version }}-linux-{{ cpu_arch }}.tar.gz" diff --git a/deploy/ansible/roles/container_engine/defaults/main.yml b/deploy/ansible/roles/container_engine/defaults/main.yml index 07e4da8d67..12c2d6e30e 100644 --- a/deploy/ansible/roles/container_engine/defaults/main.yml +++ b/deploy/ansible/roles/container_engine/defaults/main.yml @@ -3,4 +3,4 @@ container_packages: - containerd.io keyring_location: /etc/apt/keyrings -linux_arch: amd64 +cpu_arch: amd64 diff --git a/deploy/ansible/roles/container_engine/tasks/main.yml b/deploy/ansible/roles/container_engine/tasks/main.yml index b7552fcc5a..c884eb68e1 100644 --- a/deploy/ansible/roles/container_engine/tasks/main.yml +++ b/deploy/ansible/roles/container_engine/tasks/main.yml @@ -40,7 +40,7 @@ - name: Add Docker repository apt_repository: - repo: "deb [arch={{ linux_arch }} signed-by={{ keyring_location }}/docker.gpg] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable" + repo: "deb [arch={{ cpu_arch }} signed-by={{ keyring_location }}/docker.gpg] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable" state: present filename: docker diff --git a/deploy/ansible/roles/container_images/defaults/main.yml b/deploy/ansible/roles/container_images/defaults/main.yml index 84508871db..aae28e44e4 100644 --- a/deploy/ansible/roles/container_images/defaults/main.yml +++ b/deploy/ansible/roles/container_images/defaults/main.yml @@ -4,4 +4,4 @@ source_image_dir: ../airgap-images airgap_image_dir: /var/lib/rancher/k3s/agent/images -linux_arch: amd64 +cpu_arch: amd64 diff --git a/deploy/ansible/roles/container_images/tasks/main.yml b/deploy/ansible/roles/container_images/tasks/main.yml index 51f72f30d1..c03e75e0b4 100644 --- a/deploy/ansible/roles/container_images/tasks/main.yml +++ b/deploy/ansible/roles/container_images/tasks/main.yml @@ -18,9 +18,9 @@ group: root mode: 0644 loop: - - k3s-airgap-images-{{ linux_arch }}.tar.zst - - middleware-airgap-images-{{ linux_arch }}.tar.zst - - combine-airgap-images-{{ linux_arch }}.tar.zst + - k3s-airgap-images-{{ cpu_arch }}.tar.zst + - middleware-airgap-images-{{ cpu_arch }}.tar.zst + - combine-airgap-images-{{ cpu_arch }}.tar.zst # Add k3s, kubectl and the k3s installation script to # /usr/local/bin @@ -51,7 +51,7 @@ - name: Create link to helm binary file: - src: /opt/helm/{{ helm_version }}/linux-{{ linux_arch }}/helm + src: /opt/helm/{{ helm_version }}/linux-{{ cpu_arch }}/helm dest: /usr/local/bin/helm state: link owner: root diff --git a/deploy/ansible/roles/helm_install/defaults/main.yml b/deploy/ansible/roles/helm_install/defaults/main.yml index 8164665260..785842645d 100644 --- a/deploy/ansible/roles/helm_install/defaults/main.yml +++ b/deploy/ansible/roles/helm_install/defaults/main.yml @@ -1,5 +1,5 @@ --- helm_version: v3.15.2 -linux_arch: amd64 +cpu_arch: amd64 -helm_download_dir: /opt/helm-{{ helm_version }}-linux-{{ linux_arch }} +helm_download_dir: /opt/helm-{{ helm_version }}-linux-{{ cpu_arch }} diff --git a/deploy/ansible/roles/helm_install/tasks/main.yml b/deploy/ansible/roles/helm_install/tasks/main.yml index b8ac983fc2..76e51b4ea4 100644 --- a/deploy/ansible/roles/helm_install/tasks/main.yml +++ b/deploy/ansible/roles/helm_install/tasks/main.yml @@ -9,7 +9,7 @@ - name: Get Latest Release get_url: - url: "https://get.helm.sh/helm-{{ helm_version }}-linux-{{ linux_arch }}.tar.gz" + url: "https://get.helm.sh/helm-{{ helm_version }}-linux-{{ cpu_arch }}.tar.gz" dest: "{{ helm_download_dir }}/helm.tar.gz" owner: root group: root @@ -19,11 +19,11 @@ command: cmd: "tar -zxvf {{ helm_download_dir }}/helm.tar.gz" chdir: "{{ helm_download_dir }}" - creates: "{{ helm_download_dir }}/linux-{{ linux_arch }}/helm" + creates: "{{ helm_download_dir }}/linux-{{ cpu_arch }}/helm" - name: Link to extracted helm file file: - src: "{{ helm_download_dir }}/linux-{{ linux_arch }}/helm" + src: "{{ helm_download_dir }}/linux-{{ cpu_arch }}/helm" path: /usr/local/bin/helm state: link owner: root diff --git a/deploy/ansible/vars/k3s_versions.yml b/deploy/ansible/vars/k3s_versions.yml index 845b0b8057..8081a4acb5 100644 --- a/deploy/ansible/vars/k3s_versions.yml +++ b/deploy/ansible/vars/k3s_versions.yml @@ -2,4 +2,4 @@ k3s_version: "v1.30.1%2Bk3s1" kubectl_version: "v1.30.2" helm_version: "v3.15.2" -linux_arch: "amd64" +cpu_arch: "amd64" From a6b2aa9050014769ed582823411c5e3e549ffbc2 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Wed, 9 Oct 2024 09:55:33 -0400 Subject: [PATCH 11/65] Toxy wants a cracker --- deploy/Dockerfile | 1 + deploy/scripts/package_images.py | 14 +++++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/deploy/Dockerfile b/deploy/Dockerfile index e0d36f110d..0783932bd4 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -3,6 +3,7 @@ # # Supported Platforms: # - Intel/AMD 64-bit +# - ARM 64-bit ############################################################ FROM ubuntu:22.04 diff --git a/deploy/scripts/package_images.py b/deploy/scripts/package_images.py index b3943c8897..6b1a14a71a 100755 --- a/deploy/scripts/package_images.py +++ b/deploy/scripts/package_images.py @@ -94,7 +94,7 @@ def package_images(image_list: List[str], tar_file: Path) -> None: def package_middleware( - config_file: str, *, cluster_type: str, image_dir: Path, chart_dir: Path, arm=False + config_file: str, *, cluster_type: str, image_dir: Path, chart_dir: Path, arm: bool = False ) -> None: logging.info("Packaging middleware images.") # read in cluster configuration @@ -135,11 +135,11 @@ def package_middleware( logging.debug(f" - Found image {match.group(1)}") middleware_images.append(match.group(1)) logging.debug(f"Middleware images: {middleware_images}") - out_file = f"middleware-airgap-images-{"arm64" if arm else "amd64"}.tar" + out_file = f"middleware-airgap-images-{'arm64' if arm else 'amd64'}.tar" package_images(middleware_images, image_dir / out_file) -def package_thecombine(tag: str, image_dir: Path, *, arm=False) -> None: +def package_thecombine(tag: str, image_dir: Path, *, arm: bool = False) -> None: logging.info(f"Packaging The Combine version {tag}.") logging.debug("Create helm charts from templates") combine_charts.generate(tag) @@ -166,7 +166,7 @@ def package_thecombine(tag: str, image_dir: Path, *, arm=False) -> None: combine_images.append(image) logging.debug(f"Combine images: {combine_images}") # Logout of AWS to allow pulling the images - out_file = f"combine-airgap-images-{"arm64" if arm else "amd64"}.tar" + out_file = f"combine-airgap-images-{'arm64' if arm else 'amd64'}.tar" package_images(combine_images, image_dir / out_file) @@ -190,7 +190,11 @@ def main() -> None: # Update helm repos package_k3s(image_dir) package_middleware( - args.config, cluster_type="standard", image_dir=image_dir, chart_dir=chart_dir, arm=args.arm + args.config, + cluster_type="standard", + image_dir=image_dir, + chart_dir=chart_dir, + arm=args.arm, ) package_thecombine(args.tag, image_dir, arm=args.arm) From 0cb248e3a1ec2c2c1341085f0191f259ce549cc2 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Wed, 9 Oct 2024 10:55:25 -0400 Subject: [PATCH 12/65] Allow package_k3s with arm --- deploy/ansible/roles/container_images/tasks/main.yml | 5 +++++ deploy/scripts/package_images.py | 9 ++++++--- installer/make-combine-installer.sh | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/deploy/ansible/roles/container_images/tasks/main.yml b/deploy/ansible/roles/container_images/tasks/main.yml index c03e75e0b4..e4a68fd04c 100644 --- a/deploy/ansible/roles/container_images/tasks/main.yml +++ b/deploy/ansible/roles/container_images/tasks/main.yml @@ -1,6 +1,11 @@ --- +############################################################## +# Role: container_images +# # Setup airgap images in {{ airgap_image_dir }} to be # available when k3s and subsequent helm charts are installed. +# +############################################################## - name: Create airgap image directory file: diff --git a/deploy/scripts/package_images.py b/deploy/scripts/package_images.py index 6b1a14a71a..241e224702 100755 --- a/deploy/scripts/package_images.py +++ b/deploy/scripts/package_images.py @@ -65,14 +65,17 @@ def parse_args() -> argparse.Namespace: return parser.parse_args() -def package_k3s(dest_dir: Path) -> None: +def package_k3s(dest_dir: Path, *, arm: bool = False) -> None: logging.info("Packaging k3s images.") + extra_vars = f"'package_dir':'{dest_dir}'" + if arm: + extra_vars += ",'cpu_arch':'arm64'" run_cmd( [ "ansible-playbook", "playbook_k3s_airgapped_files.yml", "--extra-vars", - f"package_dir={dest_dir}", + "{" + extra_vars + "}", ], cwd=str(ansible_dir), ) @@ -188,7 +191,7 @@ def main() -> None: os.environ["AWS_DEFAULT_REGION"] = "" # Update helm repos - package_k3s(image_dir) + package_k3s(image_dir, arm=args.arm) package_middleware( args.config, cluster_type="standard", diff --git a/installer/make-combine-installer.sh b/installer/make-combine-installer.sh index 908f4ecd4d..2fa5f31ac6 100755 --- a/installer/make-combine-installer.sh +++ b/installer/make-combine-installer.sh @@ -54,7 +54,7 @@ if [[ $NET_INSTALL == 0 ]] ; then # Package The Combine for "offline" installation TEMP_DIR=/tmp/images-$$ pushd scripts - ./package_images.py ${COMBINE_VERSION} ${TEMP_DIR} + ./package_images.py ${COMBINE_VERSION} ${TEMP_DIR} --debug INSTALLER_NAME="combine-installer.run" popd rm -rf venv From 95a7237eb8bf66443e6cf74cbec3ccb4c299292c Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Thu, 10 Oct 2024 16:32:47 -0400 Subject: [PATCH 13/65] Use --dependency-update; Add debugging; Remove junk dirs --- README.md | 1 - deploy/scripts/package_images.py | 132 +++++++++++++++++----------- deploy/scripts/setup_combine.py | 15 ++-- deploy/scripts/utils.py | 2 +- installer/make-combine-installer.sh | 19 +++- 5 files changed, 104 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 939240a294..0595c16089 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,6 @@ Create and activate an isolated Python virtual environment ```bash python3 -m venv venv -# This command is shell-specific, for the common use case of bash: source venv/bin/activate ``` diff --git a/deploy/scripts/package_images.py b/deploy/scripts/package_images.py index 0fa690db54..a2cbc9c0b3 100755 --- a/deploy/scripts/package_images.py +++ b/deploy/scripts/package_images.py @@ -8,6 +8,7 @@ image names are extracted from the templates and then pulled from the repo and stored in ../images as compressed tarballs; zstd compression is used. """ + import argparse import logging import os @@ -19,7 +20,7 @@ from utils import init_logging, run_cmd import yaml -# Define configuration and output directories' +# Define configuration and output directories scripts_dir = Path(__file__).resolve().parent ansible_dir = scripts_dir.parent / "ansible" helm_dir = scripts_dir.parent / "helm" @@ -58,46 +59,57 @@ def parse_args() -> argparse.Namespace: return parser.parse_args() -def package_k3s(dest_dir: Path) -> None: +def package_k3s(dest_dir: Path, *, debug: bool = False) -> None: logging.info("Packaging k3s images.") - run_cmd( - [ - "ansible-playbook", - "playbook_k3s_airgapped_files.yml", - "--extra-vars", - f"package_dir={dest_dir}", - ], - cwd=str(ansible_dir), - ) - - -def package_images(image_list: List[str], tar_file: Path) -> None: + ansible_cmd = [ + "ansible-playbook", + "playbook_k3s_airgapped_files.yml", + "--extra-vars", + f"package_dir={dest_dir}", + ] + if debug: + ansible_cmd.append("-vv") + run_cmd(ansible_cmd, cwd=str(ansible_dir), print_cmd=debug, print_output=debug) + + +def package_images(image_list: List[str], tar_file: Path, *, debug: bool = False) -> None: container_cli_cmd = [os.getenv("CONTAINER_CLI", "docker")] if container_cli_cmd[0] == "nerdctl": container_cli_cmd.extend(["--namespace", "k8s.io"]) + # Pull each image for image in image_list: pull_cmd = container_cli_cmd + ["pull", image] - logging.debug(f"Running {pull_cmd}") - run_cmd(pull_cmd) + run_cmd(pull_cmd, print_cmd=debug, print_output=debug) + # Save pulled images into a .tar archive - run_cmd(container_cli_cmd + ["save"] + image_list + ["-o", str(tar_file)]) + save_cmd = container_cli_cmd + ["save"] + image_list + ["-o", str(tar_file)] + run_cmd(save_cmd, print_cmd=debug, print_output=debug) + # Compress the tarball - run_cmd(["zstd", "--rm", "--force", "--quiet", str(tar_file)]) + tar_cmd = ["zstd", "--rm", "--force", str(tar_file)] + if not debug: + tar_cmd.append("--quiet") + run_cmd(tar_cmd, print_cmd=debug, print_output=debug) def package_middleware( - config_file: str, *, cluster_type: str, image_dir: Path, chart_dir: Path + config_file: str, *, cluster_type: str, image_dir: Path, chart_dir: Path, debug: bool = False ) -> None: logging.info("Packaging middleware images.") - # read in cluster configuration + + # Read in cluster configuration with open(config_file) as file: config: Dict[str, Any] = yaml.safe_load(file) - # get current repos + + # Get current repos curr_repo_list: List[str] = [] middleware_images: List[str] = [] + helm_add_cmd = ["helm", "repo", "list", "-o", "yaml"] + if debug: + helm_add_cmd.append("--debug") helm_cmd_results = run_cmd( - ["helm", "repo", "list", "-o", "yaml"], print_cmd=False, check_results=False + helm_add_cmd, check_results=False, print_cmd=debug, print_output=debug ) if helm_cmd_results.returncode == 0: curr_helm_repos = yaml.safe_load(helm_cmd_results.stdout) @@ -105,60 +117,76 @@ def package_middleware( curr_repo_list.append(repo["name"]) for chart_descr in config["clusters"][cluster_type]: - # add the chart's repo if we don't already have it + logging.debug(f"Chart: ${chart_descr}") + + # Add the chart's repo if we don't already have it repo = config[chart_descr]["repo"] if repo["name"] not in curr_helm_repos: - run_cmd(["helm", "repo", "add", repo["name"], repo["url"]]) + helm_add_cmd = ["helm", "repo", "add", repo["name"], repo["url"]] + if debug: + helm_add_cmd.append("--debug") + run_cmd(helm_add_cmd, print_cmd=debug, print_output=debug) curr_repo_list.append(repo["name"]) - # pull the middleware chart + # Pull the middleware chart chart = config[chart_descr]["chart"] dest_dir = chart_dir / chart["name"] dest_dir.mkdir(mode=0o755, parents=True, exist_ok=True) - helm_cmd = ["helm", "pull", chart["reference"], "--destination", str(dest_dir)] + helm_pull_cmd = ["helm", "pull", chart["reference"], "--destination", str(dest_dir)] + if debug: + helm_pull_cmd.append("--debug") if "version" in chart: - helm_cmd.extend(["--version", chart["version"]]) - run_cmd(helm_cmd) - # render chart templates and extract images + helm_pull_cmd.extend(["--version", chart["version"]]) + run_cmd(helm_pull_cmd, print_cmd=debug, print_output=debug) + + # Render chart templates and extract images for chart_file in dest_dir.glob("*.tgz"): - results = run_cmd(["helm", "template", chart_file]) + results = run_cmd(["helm", "template", chart_file], print_cmd=debug) for line in results.stdout.splitlines(): match = re.match(r'[-\s]+image:\s+"*([^"\n]*)"*', line) - if match: + if match and not match.group(1) in middleware_images: logging.debug(f" - Found image {match.group(1)}") middleware_images.append(match.group(1)) + logging.debug(f"Middleware images: {middleware_images}") - package_images(middleware_images, image_dir / "middleware-airgap-images-amd64.tar") + package_images( + middleware_images, image_dir / "middleware-airgap-images-amd64.tar", debug=debug + ) -def package_thecombine(tag: str, image_dir: Path) -> None: +def package_thecombine(tag: str, image_dir: Path, *, debug: bool = False) -> None: logging.info(f"Packaging The Combine version {tag}.") logging.debug("Create helm charts from templates") combine_charts.generate(tag) + logging.debug(" - Get template for The Combine.") - results = run_cmd( - [ - "helm", - "template", - "thecombine", - str(helm_dir / "thecombine"), - "--set", - "global.imageRegistry=public.ecr.aws/thecombine", - "--set", - f"global.imageTag={tag}", - ] - ) + helm_template_cmd = [ + "helm", + "template", + "thecombine", + str(helm_dir / "thecombine"), + "--dependency-update", + "--set", + "global.imageRegistry=public.ecr.aws/thecombine", + "--set", + f"global.imageTag={tag}", + ] + if debug: + helm_template_cmd.append("--debug") + results = run_cmd(helm_template_cmd, print_cmd=debug) + combine_images: List[str] = [] for line in results.stdout.splitlines(): match = re.match(r'^[-\s]+image:\s+"*([^"\n]*)"*', line) if match: image = match.group(1) - logging.debug(f" - Found image {image}") if image not in combine_images: + logging.debug(f" - Found image {image}") combine_images.append(image) logging.debug(f"Combine images: {combine_images}") + # Logout of AWS to allow pulling the images - package_images(combine_images, image_dir / "combine-airgap-images-amd64.tar") + package_images(combine_images, image_dir / "combine-airgap-images-amd64.tar", debug=debug) def main() -> None: @@ -179,11 +207,15 @@ def main() -> None: os.environ["AWS_DEFAULT_REGION"] = "" # Update helm repos - package_k3s(image_dir) + package_k3s(image_dir, debug=args.debug) package_middleware( - args.config, cluster_type="standard", image_dir=image_dir, chart_dir=chart_dir + args.config, + cluster_type="standard", + image_dir=image_dir, + chart_dir=chart_dir, + debug=args.debug, ) - package_thecombine(args.tag, image_dir) + package_thecombine(args.tag, image_dir, debug=args.debug) if __name__ == "__main__": diff --git a/deploy/scripts/setup_combine.py b/deploy/scripts/setup_combine.py index 052103d824..e1b627fddd 100755 --- a/deploy/scripts/setup_combine.py +++ b/deploy/scripts/setup_combine.py @@ -204,6 +204,7 @@ def main() -> None: # Create the base helm install command chart_dir = helm_dir / chart helm_install_cmd = helm_cmd + [ + "--dependency-update", "--namespace", chart_namespace, helm_action.value, @@ -211,6 +212,10 @@ def main() -> None: str(chart_dir), ] + # Set the debug option if desired + if args.debug: + helm_install_cmd.append("--debug") + # Set the dry-run option if desired if args.dry_run: helm_install_cmd.append("--dry-run") @@ -251,15 +256,7 @@ def main() -> None: for variable in target_vars: helm_install_cmd.extend(["--set", variable]) - # Update chart dependencies - # Note that this operation is performed on the local helm charts - # so the kubeconfig and context arguments are not passed to the - # helm command. - helm_deps_cmd = ["helm", "dependency", "update", str(chart_dir)] - logging.debug(helm_deps_cmd) - run_cmd(helm_deps_cmd, print_cmd=not args.quiet, print_output=True) - - logging.debug(helm_install_cmd) + # Install the chart run_cmd(helm_install_cmd, print_cmd=not args.quiet, print_output=True) diff --git a/deploy/scripts/utils.py b/deploy/scripts/utils.py index 0af9cff521..d451b3aa26 100644 --- a/deploy/scripts/utils.py +++ b/deploy/scripts/utils.py @@ -22,7 +22,7 @@ def run_cmd( ) -> subprocess.CompletedProcess[str]: """Run a command with subprocess and catch any CalledProcessErrors.""" if print_cmd: - print(f"Running: {' '.join(cmd)}") + print(f"Running: {' '.join([str(arg) for arg in cmd])}") try: process_results = subprocess.run( cmd, diff --git a/installer/make-combine-installer.sh b/installer/make-combine-installer.sh index 908f4ecd4d..b7ad7085dd 100755 --- a/installer/make-combine-installer.sh +++ b/installer/make-combine-installer.sh @@ -13,11 +13,15 @@ error () { # cd to the directory where the script is installed SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +DEBUG=0 NET_INSTALL=0 # Parse arguments to customize installation while (( "$#" )) ; do OPT=$1 case $OPT in + --debug) + DEBUG=1 + ;; --net-install) NET_INSTALL=1 ;; @@ -48,23 +52,30 @@ if [[ $NET_INSTALL == 0 ]] ; then fi source venv/bin/activate # Update the environment if necessary - python -m pip install --upgrade pip pip-tools + python -m pip $((( DEBUG == 0)) && echo "-q" ) install --upgrade pip pip-tools python -m piptools sync requirements.txt # Package The Combine for "offline" installation TEMP_DIR=/tmp/images-$$ pushd scripts - ./package_images.py ${COMBINE_VERSION} ${TEMP_DIR} + ./package_images.py ${COMBINE_VERSION} ${TEMP_DIR} $((( DEBUG == 1 )) && echo "--debug") INSTALLER_NAME="combine-installer.run" popd - rm -rf venv else # Package The Combine for network installation INSTALLER_NAME="combine-net-installer.run" fi +# Remove unwanted folders +for DIR in venv scripts/__pycache__ ; do + if [ -d $DIR ] ; then + (( DEBUG == 1 )) && echo "Removing ../deploy/$DIR/" + rm -rf $DIR + fi +done + cd ${SCRIPT_DIR} -makeself --tar-quietly ../deploy ${INSTALLER_NAME} "Combine Installer" scripts/install-combine.sh ${COMBINE_VERSION} +makeself $((( DEBUG == 0)) && echo "--tar-quietly" ) ../deploy ${INSTALLER_NAME} "Combine Installer" scripts/install-combine.sh ${COMBINE_VERSION} if [[ $NET_INSTALL == 0 ]] ; then makeself --append ${TEMP_DIR} ${INSTALLER_NAME} rm -rf ${TEMP_DIR} From 9326b1448b1cf0d7cbd98280633a53f3e6a98d98 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 11 Oct 2024 10:02:32 -0400 Subject: [PATCH 14/65] Fix redundant debugging --- deploy/scripts/install-combine.sh | 19 ++++++----- deploy/scripts/kube_env.py | 13 ++++---- deploy/scripts/setup_cluster.py | 11 +++++-- deploy/scripts/setup_combine.py | 12 ++----- deploy/scripts/uninstall-combine | 52 +++++++++++++++---------------- 5 files changed, 57 insertions(+), 50 deletions(-) diff --git a/deploy/scripts/install-combine.sh b/deploy/scripts/install-combine.sh index 3a46c8c441..5981923843 100755 --- a/deploy/scripts/install-combine.sh +++ b/deploy/scripts/install-combine.sh @@ -7,8 +7,9 @@ set -eo pipefail # It should only be executed directly by developers. For general users, it is packaged in # a stand-alone installer (see ./installer/README.md or ./installer/README.pdf). # -# The options for this script and for the packaged installer are the same. Note that 2 -# additional debugging options are available that aren't documented in the readme file: +# The options for this script and for the packaged installer are the same. Note that some +# additional dev options are available that aren't documented in the readme file: +# - debug - show more verbose output for debugging. # - single-step - run the next "step" in the installation process and stop. # - start-at - start at the step named and run to completion. # @@ -78,9 +79,9 @@ install-kubernetes () { cd ${DEPLOY_DIR}/ansible if [ -d "${DEPLOY_DIR}/airgap-images" ] ; then - ansible-playbook playbook_desktop_setup.yml -K -e k8s_user=`whoami` -e install_airgap_images=true + ansible-playbook playbook_desktop_setup.yml -K -e k8s_user=`whoami` -e install_airgap_images=true $(((DEBUG == 1)) && echo "-vv") else - ansible-playbook playbook_desktop_setup.yml -K -e k8s_user=`whoami` + ansible-playbook playbook_desktop_setup.yml -K -e k8s_user=`whoami` $(((DEBUG == 1)) && echo "-vv") fi } @@ -97,7 +98,7 @@ set-k3s-env () { export KUBECONFIG=${K3S_CONFIG_FILE} ##### # Start k3s if it is not running - if ! systemctl is-active --quiet k3s ; then + if ! systemctl is-active $(((DEBUG == 0)) && echo "--quiet") k3s ; then sudo systemctl start k3s fi } @@ -137,7 +138,7 @@ install-the-combine () { cd ${DEPLOY_DIR}/scripts set-combine-env set-k3s-env - ./setup_combine.py --tag ${COMBINE_VERSION} --repo public.ecr.aws/thecombine --target desktop ${SETUP_OPTS} --debug + ./setup_combine.py --tag ${COMBINE_VERSION} --repo public.ecr.aws/thecombine --target desktop ${SETUP_OPTS} $(((DEBUG == 1)) && echo "--debug") deactivate } @@ -175,7 +176,7 @@ next-state () { # Verify that the required network devices have been setup for Kubernetes cluster wait-for-k8s-interfaces () { - echo "Waiting for k8s interfaces" + echo "Waiting for k8s interfaces: $@" for interface in $@ ; do while ! ip link show $interface > /dev/null 2>&1 ; do sleep 1 @@ -192,6 +193,7 @@ CONFIG_DIR=${HOME}/.config/combine mkdir -p ${CONFIG_DIR} SINGLE_STEP=0 IS_SERVER=0 +DEBUG=0 # See if we need to continue from a previous install STATE_FILE=${CONFIG_DIR}/install-state @@ -211,6 +213,9 @@ while (( "$#" )) ; do rm ${CONFIG_DIR}/env fi ;; + debug) + DEBUG=1 + ;; restart) next-state "Pre-reqs" ;; diff --git a/deploy/scripts/kube_env.py b/deploy/scripts/kube_env.py index 0ca05e999c..d1a9480eb3 100755 --- a/deploy/scripts/kube_env.py +++ b/deploy/scripts/kube_env.py @@ -108,17 +108,18 @@ def add_helm_opts(parser: argparse.ArgumentParser) -> None: ) -def add_kube_opts(parser: argparse.ArgumentParser) -> None: +def add_kube_opts(parser: argparse.ArgumentParser, *, add_debug: bool = True) -> None: """Add commandline arguments for Kubernetes tools.""" parser.add_argument( "--context", help="Context in kubectl configuration file to be used.", ) - parser.add_argument( - "--debug", - action="store_true", - help="Enable debugging output for helm commands.", - ) + if add_debug: + parser.add_argument( + "--debug", + action="store_true", + help="Enable debugging output for helm commands.", + ) parser.add_argument( "--kubeconfig", help="Specify the kubectl configuration file to be used.", diff --git a/deploy/scripts/setup_cluster.py b/deploy/scripts/setup_cluster.py index e588d011cc..0342a7f633 100755 --- a/deploy/scripts/setup_cluster.py +++ b/deploy/scripts/setup_cluster.py @@ -26,7 +26,7 @@ def parse_args() -> argparse.Namespace: description="Build containerd container images for project.", formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) - add_kube_opts(parser) + add_kube_opts(parser, add_debug=False) add_helm_opts(parser) parser.add_argument( "--chart-dir", help="Directory for the chart files when doing an airgap installation." @@ -37,12 +37,19 @@ def parse_args() -> argparse.Namespace: help="Configuration file for the cluster type(s).", default=str(scripts_dir / "setup_files" / "cluster_config.yaml"), ) - parser.add_argument( + logging_group = parser.add_mutually_exclusive_group() + logging_group.add_argument( "--quiet", "-q", action="store_true", help="Print less output information.", ) + logging_group.add_argument( + "--debug", + "-d", + action="store_true", + help="Print extra debugging information.", + ) parser.add_argument( "--type", "-t", diff --git a/deploy/scripts/setup_combine.py b/deploy/scripts/setup_combine.py index e1b627fddd..e871486d50 100755 --- a/deploy/scripts/setup_combine.py +++ b/deploy/scripts/setup_combine.py @@ -52,7 +52,7 @@ def parse_args() -> argparse.Namespace: formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) # Arguments used by the Kubernetes tools - add_kube_opts(parser) + add_kube_opts(parser, add_debug=False) # Arguments used by Helm add_helm_opts(parser) # Arguments specific to setting up The Combine @@ -187,8 +187,7 @@ def main() -> None: if chart in installed_charts: if args.clean: # Delete existing chart if --clean specified - delete_cmd = helm_cmd + ["--namespace", chart_namespace, "delete", chart] - logging.debug(delete_cmd) + delete_cmd = helm_cmd + [f"--namespace={chart_namespace}", "delete", chart] run_cmd(delete_cmd, print_cmd=not args.quiet, print_output=True) else: helm_action = HelmAction.UPGRADE @@ -205,17 +204,12 @@ def main() -> None: chart_dir = helm_dir / chart helm_install_cmd = helm_cmd + [ "--dependency-update", - "--namespace", - chart_namespace, + f"--namespace={chart_namespace}", helm_action.value, chart, str(chart_dir), ] - # Set the debug option if desired - if args.debug: - helm_install_cmd.append("--debug") - # Set the dry-run option if desired if args.dry_run: helm_install_cmd.append("--dry-run") diff --git a/deploy/scripts/uninstall-combine b/deploy/scripts/uninstall-combine index 0f6c5ffebf..973ae69c58 100755 --- a/deploy/scripts/uninstall-combine +++ b/deploy/scripts/uninstall-combine @@ -2,6 +2,7 @@ set -euo pipefail delete-files () { + # Deletes the specified files for file in "$@" ; do if [[ -f "$file" ]] ; then echo "Removing $file" @@ -10,45 +11,44 @@ delete-files () { done } -kill-service () { - # Stops and disables the specified service - if systemctl is-active $1 ; then - echo "Stopping service $1" - sudo systemctl stop $1 2>&1 >/dev/null - fi - if systemctl is-enabled $1 ; then - echo "Disabline service $1" - sudo systemctl disable $1 - fi +kill-services () { + # Stops and disables the specified services + for service in "$@" ; do + if systemctl is-active $service ; then + echo "Stopping service $service" + sudo systemctl stop $service 2>&1 >/dev/null + fi + if systemctl is-enabled $service ; then + echo "Disabling service $service" + sudo systemctl disable $service + fi + done } -# Stop & disable combine services -echo "Stopping combine services" -kill-service k3s -kill-service create_ap - -# Delete $HOME/thecombine; $HOME/.config/combine -delete-files ${HOME}/thecombine ${HOME}/.config/combine +# Stop and remove The Combine +echo "Stopping The Combine services" +kill-services k3s create_ap +echo "Deleting The Combine files" +delete-files ${HOME}/thecombine ${HOME}/.config/combine /usr/local/bin/combinectl -# Remove support tool - -kill-service display-eth.service -kill-service display-eth.timer +# Stop and remove support tools +echo "Stopping display-eth services" +kill-services display-eth.service display-eth.timer +echo "Deleting display-eth files" delete-files /lib/systemd/system/display-eth.service /lib/systemd/system/display-eth.timer -# Remove combine management tool -delete-files /usr/local/bin/combinectl - - # Uninstall k3s if [[ -x /usr/local/bin/k3s-uninstall.sh ]] ; then + echo "Uninstalling k3s" /usr/local/bin/k3s-uninstall.sh fi # Remove network configurations if nmcli c show dummy-vip >/dev/null 2>&1 ; then + echo "Removing dummy-vip" sudo nmcli c delete dummy-vip fi -# delete create_ap files +# Delete create_ap files +echo "Deleting create_ap files" delete-files /etc/create_ap /usr/lib/systemd/system/create_ap.service From 17e70fb9bc44388ca0420157ca713536ce8e259a Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 11 Oct 2024 10:12:36 -0400 Subject: [PATCH 15/65] Revert README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 0595c16089..939240a294 100644 --- a/README.md +++ b/README.md @@ -203,6 +203,7 @@ Create and activate an isolated Python virtual environment ```bash python3 -m venv venv +# This command is shell-specific, for the common use case of bash: source venv/bin/activate ``` From 7af53863f756c682e511484d5d82631fa87133f2 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 11 Oct 2024 10:28:25 -0400 Subject: [PATCH 16/65] Fix variable name --- deploy/scripts/package_images.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deploy/scripts/package_images.py b/deploy/scripts/package_images.py index a2cbc9c0b3..2a0fa5eb50 100755 --- a/deploy/scripts/package_images.py +++ b/deploy/scripts/package_images.py @@ -105,11 +105,11 @@ def package_middleware( # Get current repos curr_repo_list: List[str] = [] middleware_images: List[str] = [] - helm_add_cmd = ["helm", "repo", "list", "-o", "yaml"] + helm_list_cmd = ["helm", "repo", "list", "-o", "yaml"] if debug: - helm_add_cmd.append("--debug") + helm_list_cmd.append("--debug") helm_cmd_results = run_cmd( - helm_add_cmd, check_results=False, print_cmd=debug, print_output=debug + helm_list_cmd, check_results=False, print_cmd=debug, print_output=debug ) if helm_cmd_results.returncode == 0: curr_helm_repos = yaml.safe_load(helm_cmd_results.stdout) From 6ccbf7de22e37f84ad0d5833b62e5e81e62cc099 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 11 Oct 2024 11:46:20 -0400 Subject: [PATCH 17/65] Condense and cleanup README for PDF readability --- installer/README.md | 98 ++++++++++++++++++++------------------------- 1 file changed, 43 insertions(+), 55 deletions(-) diff --git a/installer/README.md b/installer/README.md index d88dc5e07b..6817b8efcd 100644 --- a/installer/README.md +++ b/installer/README.md @@ -4,10 +4,10 @@ This README describes how to install _The Combine_ Rapid Word Collection tool on ## Contents -1. [System Requirements](#system-requirements) -2. [Install _The Combine_](#install-the-combine) -3. [Running _The Combine_](#running-the-combine) -4. [Advanced Installation Options](#advanced-installation-options) + - [System Requirements](#system-requirements) + - [Install _The Combine_](#install-the-combine) + - [Running _The Combine_](#running-the-combine) + - [Advanced Installation Options](#advanced-installation-options) ## System Requirements @@ -35,29 +35,15 @@ The installation script has been tested on _Ubuntu 22.04_ and _Wasta Linux 22.04 _Note for Wasta Linux users_ - _Wasta Linux_ includes Skype in its list of available software. Skype no longer supports installing it on Linux from - an `apt` software repository. As a result, when the installation script, or a user, updates the list of available - software, the process fails. To address this issue, you can either: - - 1. Remove the file directly: + _Wasta Linux_ includes Skype in its list of available software. Skype no longer supports installation + via `apt`. (It's available as a Snap package.) As a result, when the installation script, or a user, updates the list of available + software, the process fails. To address this issue, run: ```console sudo rm /etc/apt/sources.list.d/skype-stable.list - sudo apt update + sudo apt update && sudo apt upgrade -y ``` - or - - 2. Deselect _Skype_ in the Software Updater settings - - 1. Open the _Software Settings_ application - 2. Click the _Other Software_ tab - 3. Uncheck the entry for Skype (`https://repo.skype.com/deb stable`) - 4. Click the "Close" button - 5. Click the "Reload" button in the dialog window that is displayed - - Skype is available on _Wasta Linux_ or _Ubuntu_ as a Snap package. - 4. Download the installation script from [https://s3.amazonaws.com/software.thecombine.app/combine-installer.run](https://s3.amazonaws.com/software.thecombine.app/combine-installer.run) 5. Open a terminal window (Ctrl-Alt-T) and make the script executable: @@ -90,6 +76,13 @@ The installation script has been tested on _Ubuntu 22.04_ and _Wasta Linux 22.04 [The Combine](https://software.sil.org/thecombine/#contact) - When run with no options, ./combine-installer.run will install the current version of _The Combine_. - If the previous installation did not run to completion, it will resume where the previous installation left off. + - If you get the error `Job for k3s.service failed because the control process exited with error code.`, + make sure no other instance of k3s is running. For example, if Docker Desktop is active on the current user, run: + + ```console + systemctl --user stop docker-desktop + systemctl --user disable docker-desktop + ``` _The Combine_ will not be running when installation is complete. @@ -118,10 +111,7 @@ If you would like to change the WiFi passphrase, see the options described in [c #### Connecting to the App -Open a web browser and navigate to [local.thecombine.app](https://local.thecombine.app). - -If your browser tries to do a web search, add the `https://` to the beginning, that is, -[https://local.thecombine.app](https://local.thecombine.app) +Open a web browser and navigate to [https://local.thecombine.app](https://local.thecombine.app). ### Shutting Down _The Combine_ @@ -134,17 +124,17 @@ combinectl stop ### combinectl Tool Once installation is complete, you can use the `combinectl` command to manage the installation. The `combinectl` command -is entered in a terminal window as `combinectl COMMAND [parameters]` The possible commands are: - -| Command | Parameters | Description | -| ------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| help | N/A | Print a usage message. | -| start | N/A | Start the combine services. | -| stop | N/A | Stop the combine services. | -| status | N/A | List the status for the combine services. | -| cert | N/A | Print the expiration date for the web certificate. | -| update | release-number | Update the version of The Combine to the "release-number" specified. You can see the number of the latest release at [The Combine on GitHub](https://github.com/sillsdev/TheCombine/releases). Note that not all releases can be updated this way. If The Combine does not run properly, download and run the updated install package. | -| wifi | [wifi-passphrase] | If no wifi-passphrase is provieded, the current wifi passphrase is printed. If a new passphase is provided, the wifi passphrase is updated to the new phrase. If your passphrase has spaces or special characters, it is best to enclose your pass phrase in quotation marks (""). | +is entered in a terminal window as `combinectl COMMAND [parameters]`, where the possible commands are: + +| Command | Parameters | Description | +| ------- | -------------- | ------------------------------------------------------------------- | +| help | N/A | Print a usage message. | +| start | N/A | Start the combine services. | +| stop | N/A | Stop the combine services. | +| status | N/A | List the status for the combine services. | +| cert | N/A | Print the expiration date for the web certificate. | +| update | release-number | Update the version of The Combine to the `release-number` specified. You can see the latest release number at [The Combine on GitHub](https://github.com/sillsdev/TheCombine/releases). (This only works if the release begins with a "v".) | +| wifi | [passphrase] | If no passphrase is provided, print the current passphrase. If a passphrase is provided, update the wifi passphrase. A passphrase with spaces or special characters should be enclosed in quotation marks (""). | If the command is omitted or unrecognized, the help message is printed. @@ -159,30 +149,28 @@ certificate will be valid for a time between 60 and 90 days. You can use the com current certificate will expire, for example: ```console -$combinectl cert +$ combinectl cert Web certificate expires at Jul 8 08:54:11 2024 GMT ``` ## Advanced Installation Options -To run `combine-installer.run` with options, the option list must be started with `--`. - -`combine-installer.run` supports the following options: +To run `combine-installer.run` with options, the option list must be started with `--` . The following options are supported: -| option | description | -| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| clean | Remove the previously saved environment (AWS Access Key, admin user info) before performing the installation. | -| restart | Run the installation from the beginning; do not resume a previous installation. | -| server | Install _The Combine_ in a server environment so that _The Combine_ is always running by default. | -| timeout TIMEOUT | Use a different timeout when installing. The default timeout is 5 minutes. With slow internet connections, it is helpful to extend the timeout. See for timeout formats. | -| uninstall | Remove software installed by this script. | -| update | Update _The Combine_ to the version number provided. This skips installing the support software that was installed previously. | -| version-number | Specify a version to install instead of the current version. A version number will have the form `vn.n.n` where `n` represents an integer value, for example, `v1.20.0`. | +| option | description | +| --------------- | ---------------------------------------------------------------------------- | +| clean | Remove the previously saved environment (AWS Access Key, admin user info) before performing the installation. | +| restart | Run the installation from the beginning; do not resume a previous installation. | +| server | Install _The Combine_ in a server environment so that _The Combine_ is always running by default. | +| timeout TIMEOUT | Use a different timeout when installing. (Default: 5 minutes.) With slow internet, it is helpful to extend the timeout. See for timeout formats. | +| uninstall | Remove software installed by this script. | +| update | Update _The Combine_ to the version number provided. This skips installing support software that was installed previously. | +| version-number | Specify a version to install instead of the current version. A version number will have the form `vn.n.n` where `n` represents an integer value, for example, `v1.20.0`. | ### Examples -| Command | Effect | -| ------------------------------------------ | ------------------------------------------------------------ | -| `./combine-installer.run -- v2.0.1` | Install version `v2.0.1` of _The Combine_. | -| `./combine-installer.run -- update v2.2.0` | Update an existing Combine installation to version `v2.2.0` | -| `./combine-installer.run -- restart` | Restart the current installation process from the beginning. | +| Command | Effect | +| ------------------------------------------------------------------------------------ | -------------------------------------------| +| `./combine-installer.run -- v2.0.1` | Install version `v2.0.1` of _The Combine_. | +| `./combine-installer.run -- update v2.2.0` | Update installation to version `v2.2.0` | +| `./combine-installer.run -- restart` | Restart process from the beginning. | From c654e0b608f7f78a20bcadbffeda56b6f02c241e Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 11 Oct 2024 15:03:36 -0400 Subject: [PATCH 18/65] Tidy --- deploy/scripts/install-combine.sh | 2 +- installer/make-combine-installer.sh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/deploy/scripts/install-combine.sh b/deploy/scripts/install-combine.sh index 5981923843..0432f2b427 100755 --- a/deploy/scripts/install-combine.sh +++ b/deploy/scripts/install-combine.sh @@ -58,7 +58,7 @@ create-python-venv () { python3 -m venv venv source venv/bin/activate echo "Install pip and pip-tools" - python -m pip install --upgrade pip pip-tools + python -m pip $((( DEBUG == 0)) && echo "-q") install --upgrade pip pip-tools echo "Install dependencies" python -m piptools sync requirements.txt } diff --git a/installer/make-combine-installer.sh b/installer/make-combine-installer.sh index b7ad7085dd..71eaa578ce 100755 --- a/installer/make-combine-installer.sh +++ b/installer/make-combine-installer.sh @@ -32,7 +32,7 @@ while (( "$#" )) ; do error "Invalid version number, $OPT" fi ;; - *) + *) warning "Unrecognized option: $OPT" ;; esac @@ -52,7 +52,7 @@ if [[ $NET_INSTALL == 0 ]] ; then fi source venv/bin/activate # Update the environment if necessary - python -m pip $((( DEBUG == 0)) && echo "-q" ) install --upgrade pip pip-tools + python -m pip $((( DEBUG == 0)) && echo "-q") install --upgrade pip pip-tools python -m piptools sync requirements.txt # Package The Combine for "offline" installation From d9a496a500e2e19b82c9118f654cd7cb49b01289 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 11 Oct 2024 15:45:41 -0400 Subject: [PATCH 19/65] More arm stuff --- deploy/scripts/build.py | 4 ++-- deploy/scripts/install-combine.sh | 2 +- deploy/scripts/package_images.py | 29 +++++++++++++++-------------- installer/make-combine-installer.sh | 10 +++++++--- 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/deploy/scripts/build.py b/deploy/scripts/build.py index fa395b26b1..6d8beec108 100755 --- a/deploy/scripts/build.py +++ b/deploy/scripts/build.py @@ -247,10 +247,10 @@ def main() -> None: container_cli = os.getenv("CONTAINER_CLI", "docker") match container_cli: case "nerdctl": - build_cmd = [container_cli, "-n", args.namespace, "build"] + build_cmd = [container_cli, "-n", args.namespace, "build", "--platform", "linux/amd64,linux/arm64"] push_cmd = [container_cli, "-n", args.namespace, "push"] case "docker": - build_cmd = [container_cli, "buildx", "build"] + build_cmd = [container_cli, "buildx", "build", "--platform", "linux/amd64,linux/arm64"] push_cmd = [container_cli, "push"] case _: logging.critical(f"Container CLI '{container_cli}' is not supported.") diff --git a/deploy/scripts/install-combine.sh b/deploy/scripts/install-combine.sh index 5981923843..0432f2b427 100755 --- a/deploy/scripts/install-combine.sh +++ b/deploy/scripts/install-combine.sh @@ -58,7 +58,7 @@ create-python-venv () { python3 -m venv venv source venv/bin/activate echo "Install pip and pip-tools" - python -m pip install --upgrade pip pip-tools + python -m pip $((( DEBUG == 0)) && echo "-q") install --upgrade pip pip-tools echo "Install dependencies" python -m piptools sync requirements.txt } diff --git a/deploy/scripts/package_images.py b/deploy/scripts/package_images.py index 92ff5d0d41..bead4b755a 100755 --- a/deploy/scripts/package_images.py +++ b/deploy/scripts/package_images.py @@ -43,6 +43,7 @@ def parse_args() -> argparse.Namespace: # Add Optional arguments parser.add_argument( "--arm", + action="store_true", help="Package for arm64 instead of the default amd64", default=False, ) @@ -66,30 +67,30 @@ def parse_args() -> argparse.Namespace: return parser.parse_args() -def package_k3s(dest_dir: Path, *, arm: bool = False) -> None: +def package_k3s(dest_dir: Path, *, arm: bool = False, debug: bool = False) -> None: logging.info("Packaging k3s images.") extra_vars = f"'package_dir':'{dest_dir}'" if arm: extra_vars += ",'cpu_arch':'arm64'" - run_cmd( - [ - "ansible-playbook", - "playbook_k3s_airgapped_files.yml", - "--extra-vars", - "{" + extra_vars + "}", - ], - cwd=str(ansible_dir), - ) + ansible_cmd = [ + "ansible-playbook", + "playbook_k3s_airgapped_files.yml", + "--extra-vars", + "{" + extra_vars + "}", + ] + if debug: + ansible_cmd.append("-vv") + run_cmd(ansible_cmd, cwd=str(ansible_dir), print_cmd=debug, print_output=debug) -def package_images(image_list: List[str], tar_file: Path, *, debug: bool = False) -> None: +def package_images(image_list: List[str], tar_file: Path, *, arm: bool = False, debug: bool = False) -> None: container_cli_cmd = [os.getenv("CONTAINER_CLI", "docker")] if container_cli_cmd[0] == "nerdctl": container_cli_cmd.extend(["--namespace", "k8s.io"]) # Pull each image for image in image_list: - pull_cmd = container_cli_cmd + ["pull", image] + pull_cmd = container_cli_cmd + ["pull", f"--platform=linux/{'arm64' if arm else 'amd64'}", image] run_cmd(pull_cmd, print_cmd=debug, print_output=debug) # Save pulled images into a .tar archive @@ -160,7 +161,7 @@ def package_middleware( logging.debug(f"Middleware images: {middleware_images}") out_file = f"middleware-airgap-images-{'arm64' if arm else 'amd64'}.tar" - package_images(middleware_images, image_dir / out_file, debug=debug) + package_images(middleware_images, image_dir / out_file, arm=arm, debug=debug) def package_thecombine(tag: str, image_dir: Path, *, arm: bool = False, debug: bool = False) -> None: @@ -196,7 +197,7 @@ def package_thecombine(tag: str, image_dir: Path, *, arm: bool = False, debug: b # Logout of AWS to allow pulling the images out_file = f"combine-airgap-images-{'arm64' if arm else 'amd64'}.tar" - package_images(combine_images, image_dir / out_file, debug=debug) + package_images(combine_images, image_dir / out_file, arm=arm, debug=debug) def main() -> None: diff --git a/installer/make-combine-installer.sh b/installer/make-combine-installer.sh index b7ad7085dd..0ad68ce77c 100755 --- a/installer/make-combine-installer.sh +++ b/installer/make-combine-installer.sh @@ -13,12 +13,16 @@ error () { # cd to the directory where the script is installed SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +ARM=0 DEBUG=0 NET_INSTALL=0 # Parse arguments to customize installation while (( "$#" )) ; do OPT=$1 case $OPT in + --arm) + ARM=1 + ;; --debug) DEBUG=1 ;; @@ -32,7 +36,7 @@ while (( "$#" )) ; do error "Invalid version number, $OPT" fi ;; - *) + *) warning "Unrecognized option: $OPT" ;; esac @@ -52,13 +56,13 @@ if [[ $NET_INSTALL == 0 ]] ; then fi source venv/bin/activate # Update the environment if necessary - python -m pip $((( DEBUG == 0)) && echo "-q" ) install --upgrade pip pip-tools + python -m pip $((( DEBUG == 0)) && echo "-q") install --upgrade pip pip-tools python -m piptools sync requirements.txt # Package The Combine for "offline" installation TEMP_DIR=/tmp/images-$$ pushd scripts - ./package_images.py ${COMBINE_VERSION} ${TEMP_DIR} $((( DEBUG == 1 )) && echo "--debug") + ./package_images.py ${COMBINE_VERSION} ${TEMP_DIR} $((( ARM == 1 )) && echo "--arm") $((( DEBUG == 1 )) && echo "--debug") INSTALLER_NAME="combine-installer.run" popd else From 18e355cf39999ab5cb25e62c776f7466df7c32fd Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 11 Oct 2024 15:56:48 -0400 Subject: [PATCH 20/65] Tox-ify --- deploy/scripts/build.py | 9 ++++++++- deploy/scripts/package_images.py | 20 +++++++++++++++----- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/deploy/scripts/build.py b/deploy/scripts/build.py index 6d8beec108..a687a90c3f 100755 --- a/deploy/scripts/build.py +++ b/deploy/scripts/build.py @@ -247,7 +247,14 @@ def main() -> None: container_cli = os.getenv("CONTAINER_CLI", "docker") match container_cli: case "nerdctl": - build_cmd = [container_cli, "-n", args.namespace, "build", "--platform", "linux/amd64,linux/arm64"] + build_cmd = [ + container_cli, + "-n", + args.namespace, + "build", + "--platform", + "linux/amd64,linux/arm64", + ] push_cmd = [container_cli, "-n", args.namespace, "push"] case "docker": build_cmd = [container_cli, "buildx", "build", "--platform", "linux/amd64,linux/arm64"] diff --git a/deploy/scripts/package_images.py b/deploy/scripts/package_images.py index bead4b755a..5916d74829 100755 --- a/deploy/scripts/package_images.py +++ b/deploy/scripts/package_images.py @@ -83,15 +83,17 @@ def package_k3s(dest_dir: Path, *, arm: bool = False, debug: bool = False) -> No run_cmd(ansible_cmd, cwd=str(ansible_dir), print_cmd=debug, print_output=debug) -def package_images(image_list: List[str], tar_file: Path, *, arm: bool = False, debug: bool = False) -> None: +def package_images( + image_list: List[str], tar_file: Path, *, arm: bool = False, debug: bool = False +) -> None: container_cli_cmd = [os.getenv("CONTAINER_CLI", "docker")] if container_cli_cmd[0] == "nerdctl": container_cli_cmd.extend(["--namespace", "k8s.io"]) # Pull each image + pull_cmd = container_cli_cmd + ["pull", f"--platform=linux/{'arm64' if arm else 'amd64'}"] for image in image_list: - pull_cmd = container_cli_cmd + ["pull", f"--platform=linux/{'arm64' if arm else 'amd64'}", image] - run_cmd(pull_cmd, print_cmd=debug, print_output=debug) + run_cmd(pull_cmd + [image], print_cmd=debug, print_output=debug) # Save pulled images into a .tar archive save_cmd = container_cli_cmd + ["save"] + image_list + ["-o", str(tar_file)] @@ -105,7 +107,13 @@ def package_images(image_list: List[str], tar_file: Path, *, arm: bool = False, def package_middleware( - config_file: str, *, cluster_type: str, image_dir: Path, chart_dir: Path, arm: bool = False, debug: bool = False + config_file: str, + *, + cluster_type: str, + image_dir: Path, + chart_dir: Path, + arm: bool = False, + debug: bool = False, ) -> None: logging.info("Packaging middleware images.") @@ -164,7 +172,9 @@ def package_middleware( package_images(middleware_images, image_dir / out_file, arm=arm, debug=debug) -def package_thecombine(tag: str, image_dir: Path, *, arm: bool = False, debug: bool = False) -> None: +def package_thecombine( + tag: str, image_dir: Path, *, arm: bool = False, debug: bool = False +) -> None: logging.info(f"Packaging The Combine version {tag}.") logging.debug("Create helm charts from templates") combine_charts.generate(tag) From 993cbb2dad01cb595f7de516c838557f749e32fb Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Tue, 15 Oct 2024 14:34:19 -0400 Subject: [PATCH 21/65] Set up Buildx in workflows for multi-platform builds --- .github/workflows/backend.yml | 5 ++++- .github/workflows/database.yml | 5 ++++- .github/workflows/frontend.yml | 5 ++++- .github/workflows/maintenance.yml | 5 ++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 189bf44b6f..adeebab869 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -170,9 +170,12 @@ jobs: ts-crl.ws.symantec.com:80 # For subfolders, currently a full checkout is required. # See: https://github.com/marketplace/actions/build-and-push-docker-images#path-context - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - name: Checkout repo + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - name: Build backend run: | deploy/scripts/build.py --components backend diff --git a/.github/workflows/database.yml b/.github/workflows/database.yml index c33c59d25e..ca6c969ed3 100644 --- a/.github/workflows/database.yml +++ b/.github/workflows/database.yml @@ -26,9 +26,12 @@ jobs: registry-1.docker.io:443 # For subfolders, currently a full checkout is required. # See: https://github.com/marketplace/actions/build-and-push-docker-images#path-context - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - name: Checkout repo + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - name: Build database image run: | deploy/scripts/build.py --components database diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index c417bb1892..3a967513c9 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -128,9 +128,12 @@ jobs: pypi.org:443 registry-1.docker.io:443 registry.npmjs.org:443 - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - name: Checkout repo + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - name: Build frontend run: | deploy/scripts/build.py --components frontend diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml index 2982b3f454..3c504258a8 100644 --- a/.github/workflows/maintenance.yml +++ b/.github/workflows/maintenance.yml @@ -30,9 +30,12 @@ jobs: security.ubuntu.com:80 # For subfolders, currently a full checkout is required. # See: https://github.com/marketplace/actions/build-and-push-docker-images#path-context - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - name: Checkout repo + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - name: Build maintenance image run: | deploy/scripts/build.py --components maintenance From a1a3dff5d6e35787da05f5a2f7abc5d1d0d7af40 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Tue, 15 Oct 2024 14:51:04 -0400 Subject: [PATCH 22/65] Debug --- .github/workflows/backend.yml | 2 +- .github/workflows/database.yml | 6 +++--- deploy/scripts/sem_dom_import.py | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index adeebab869..20a855b53d 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -178,7 +178,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Build backend run: | - deploy/scripts/build.py --components backend + deploy/scripts/build.py --components backend --debug shell: bash - name: Image digest run: | diff --git a/.github/workflows/database.yml b/.github/workflows/database.yml index ca6c969ed3..a05a5b82e4 100644 --- a/.github/workflows/database.yml +++ b/.github/workflows/database.yml @@ -30,11 +30,11 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + #- name: Set up Docker Buildx + # uses: docker/setup-buildx-action@v3 - name: Build database image run: | - deploy/scripts/build.py --components database + deploy/scripts/build.py --components database --debug shell: bash - name: Image digest run: | diff --git a/deploy/scripts/sem_dom_import.py b/deploy/scripts/sem_dom_import.py index d91823f013..924514a7cc 100755 --- a/deploy/scripts/sem_dom_import.py +++ b/deploy/scripts/sem_dom_import.py @@ -218,7 +218,6 @@ def get_sem_doms(node: ElementTree.Element, parent: SemDomTreeMap, prev: SemDomM elif field.tag == "Abbreviation": for abbrev_node in field: lang, id_text = get_auni_text(abbrev_node) - logging.debug(f"id[{lang}]='{id_text}'") domain_set[lang].id = id_text elif field.tag == "Description": for descr_node in field: From 03b8dcaf29945fb8ae3dd46146d240cd039869ba Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Tue, 15 Oct 2024 16:51:13 -0400 Subject: [PATCH 23/65] Add registry-1.docker.io:443 to backend endpoints --- .github/workflows/backend.yml | 1 + .github/workflows/database.yml | 4 ++-- .github/workflows/frontend.yml | 2 +- .github/workflows/maintenance.yml | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 20a855b53d..4a282d4cbe 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -166,6 +166,7 @@ jobs: deb.debian.org:80 github.com:443 mcr.microsoft.com:443 + registry-1.docker.io:443 security.ubuntu.com:80 ts-crl.ws.symantec.com:80 # For subfolders, currently a full checkout is required. diff --git a/.github/workflows/database.yml b/.github/workflows/database.yml index a05a5b82e4..f9e0f48494 100644 --- a/.github/workflows/database.yml +++ b/.github/workflows/database.yml @@ -30,8 +30,8 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 - #- name: Set up Docker Buildx - # uses: docker/setup-buildx-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 - name: Build database image run: | deploy/scripts/build.py --components database --debug diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 3a967513c9..c38c5d32cb 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -136,7 +136,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Build frontend run: | - deploy/scripts/build.py --components frontend + deploy/scripts/build.py --components frontend --debug shell: bash - name: Image digest run: | diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml index 3c504258a8..940adfb877 100644 --- a/.github/workflows/maintenance.yml +++ b/.github/workflows/maintenance.yml @@ -38,7 +38,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Build maintenance image run: | - deploy/scripts/build.py --components maintenance + deploy/scripts/build.py --components maintenance --debug shell: bash - name: Image digest run: | From c3989006de8887303b58e8377308ba5bbec34f86 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Tue, 15 Oct 2024 17:07:35 -0400 Subject: [PATCH 24/65] Try with QEMU --- .github/workflows/backend.yml | 1 + .github/workflows/database.yml | 2 ++ .github/workflows/maintenance.yml | 2 ++ 3 files changed, 5 insertions(+) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 4a282d4cbe..1a41abccef 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -162,6 +162,7 @@ jobs: *.symcb.com:80 api.nuget.org:443 archive.ubuntu.com:80 + auth.docker.io:443 dc.services.visualstudio.com:443 deb.debian.org:80 github.com:443 diff --git a/.github/workflows/database.yml b/.github/workflows/database.yml index f9e0f48494..c1f94f345e 100644 --- a/.github/workflows/database.yml +++ b/.github/workflows/database.yml @@ -30,6 +30,8 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Build database image diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml index 940adfb877..cd810db4db 100644 --- a/.github/workflows/maintenance.yml +++ b/.github/workflows/maintenance.yml @@ -34,6 +34,8 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build maintenance image From fc58d55462493ee5230b1d2af0ae11585866a655 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Tue, 15 Oct 2024 17:15:47 -0400 Subject: [PATCH 25/65] Try more things --- .github/workflows/backend.yml | 1 + .github/workflows/database.yml | 4 ++-- .github/workflows/frontend.yml | 2 ++ .github/workflows/maintenance.yml | 2 -- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 1a41abccef..b918057026 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -167,6 +167,7 @@ jobs: deb.debian.org:80 github.com:443 mcr.microsoft.com:443 + ports.ubuntu.com:80 registry-1.docker.io:443 security.ubuntu.com:80 ts-crl.ws.symantec.com:80 diff --git a/.github/workflows/database.yml b/.github/workflows/database.yml index c1f94f345e..0ef70155e8 100644 --- a/.github/workflows/database.yml +++ b/.github/workflows/database.yml @@ -31,9 +31,9 @@ jobs: with: fetch-depth: 0 - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Build database image run: | deploy/scripts/build.py --components database --debug diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index c38c5d32cb..83cc76ffc0 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -132,6 +132,8 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build frontend diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml index cd810db4db..940adfb877 100644 --- a/.github/workflows/maintenance.yml +++ b/.github/workflows/maintenance.yml @@ -34,8 +34,6 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build maintenance image From 81a8c6523b925b650d581d653b1d95eb6a88a66b Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Tue, 15 Oct 2024 17:28:49 -0400 Subject: [PATCH 26/65] Shuffle --- .github/workflows/backend.yml | 2 ++ .github/workflows/database.yml | 2 -- .github/workflows/maintenance.yml | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index b918057026..661d7047bf 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -177,6 +177,8 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build backend diff --git a/.github/workflows/database.yml b/.github/workflows/database.yml index 0ef70155e8..b449232b15 100644 --- a/.github/workflows/database.yml +++ b/.github/workflows/database.yml @@ -30,8 +30,6 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build database image diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml index 940adfb877..cd810db4db 100644 --- a/.github/workflows/maintenance.yml +++ b/.github/workflows/maintenance.yml @@ -34,6 +34,8 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build maintenance image From f86d2e5b40846ecae6cd3d6eda4eb67e15bc5ff4 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Tue, 15 Oct 2024 18:07:45 -0400 Subject: [PATCH 27/65] Another backend endpoint --- .github/workflows/backend.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 661d7047bf..e32b5e3661 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -168,6 +168,7 @@ jobs: github.com:443 mcr.microsoft.com:443 ports.ubuntu.com:80 + production.cloudflare.docker.com:443 registry-1.docker.io:443 security.ubuntu.com:80 ts-crl.ws.symantec.com:80 From c6d47f67071fc2ca7de67c3995b79e32f37a77d4 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Wed, 16 Oct 2024 07:14:49 -0400 Subject: [PATCH 28/65] FROM --platform= --- Backend/Dockerfile | 10 +++++----- Dockerfile | 6 +++--- database/Dockerfile | 2 +- maintenance/Dockerfile | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Backend/Dockerfile b/Backend/Dockerfile index 10357c023e..8580dc0e91 100644 --- a/Backend/Dockerfile +++ b/Backend/Dockerfile @@ -7,7 +7,7 @@ ############################################################ # Docker multi-stage build -FROM mcr.microsoft.com/dotnet/sdk:8.0.402-jammy AS builder +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0.402-jammy AS builder WORKDIR /app # Copy csproj and restore (fetch dependencies) as distinct layers. @@ -19,7 +19,7 @@ COPY . ./ RUN dotnet publish -c Release -o build # Build runtime image. -FROM mcr.microsoft.com/dotnet/aspnet:8.0.8-jammy +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/aspnet:8.0.8-jammy ENV ASPNETCORE_URLS=http://+:5000 ENV COMBINE_IS_IN_CONTAINER=1 @@ -43,9 +43,9 @@ RUN mkdir -p $HOME # Setup app user and group to known UID/GID; no login. RUN groupmod --gid 999 app RUN usermod --uid 999 --gid app \ - --shell /sbin/nologin \ - --comment "Docker image user" \ - app + --shell /sbin/nologin \ + --comment "Docker image user" \ + app ## Set up application install directory. RUN mkdir $APP_HOME && \ diff --git a/Dockerfile b/Dockerfile index 74acc47977..7310841115 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ ############################################################ # User guide build environment -FROM python:3.12.5-slim-bookworm AS user_guide_builder +FROM --platform=$BUILDPLATFORM python:3.12.5-slim-bookworm AS user_guide_builder ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 @@ -24,7 +24,7 @@ COPY docs/user_guide docs/user_guide RUN tox -e user-guide # Frontend build environment. -FROM node:20.17.0-bookworm-slim AS frontend_builder +FROM --platform=$BUILDPLATFORM node:20.17.0-bookworm-slim AS frontend_builder WORKDIR /app # Install app dependencies. @@ -36,7 +36,7 @@ COPY . ./ RUN npm run build # Production environment. -FROM nginx:1.27 +FROM --platform=$BUILDPLATFORM nginx:1.27 WORKDIR /app diff --git a/database/Dockerfile b/database/Dockerfile index 778443a6fc..72ff5ab878 100644 --- a/database/Dockerfile +++ b/database/Dockerfile @@ -5,7 +5,7 @@ # - Intel/AMD 64-bit # - ARM 64-bit ############################################################ -FROM mongo:7.0.14-jammy +FROM --platform=$BUILDPLATFORM mongo:7.0.14-jammy WORKDIR / diff --git a/maintenance/Dockerfile b/maintenance/Dockerfile index 9e86310ad8..41b53d72bc 100644 --- a/maintenance/Dockerfile +++ b/maintenance/Dockerfile @@ -16,7 +16,7 @@ # - ARM 64-bit ############################################################ -FROM sillsdev/aws-kubectl:0.3.0 +FROM --platform=$BUILDPLATFORM sillsdev/aws-kubectl:0.3.0 USER root From 8d191fb2e16a33829e68686318d3cba9441b862a Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Wed, 16 Oct 2024 09:06:43 -0400 Subject: [PATCH 29/65] Add docker debugging --- deploy/scripts/build.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/deploy/scripts/build.py b/deploy/scripts/build.py index a687a90c3f..8e84c46616 100755 --- a/deploy/scripts/build.py +++ b/deploy/scripts/build.py @@ -244,23 +244,29 @@ def main() -> None: init_logging(args) # Setup required build engine - docker or nerdctl - container_cli = os.getenv("CONTAINER_CLI", "docker") - match container_cli: + container_cmd = [os.getenv("CONTAINER_CLI", "docker")] + if args.debug: + container_cmd.append("--debug") + match container_cmd[0]: case "nerdctl": - build_cmd = [ - container_cli, + build_cmd = container_cmd + [ "-n", args.namespace, "build", "--platform", "linux/amd64,linux/arm64", ] - push_cmd = [container_cli, "-n", args.namespace, "push"] + push_cmd = container_cmd + ["-n", args.namespace, "push"] case "docker": - build_cmd = [container_cli, "buildx", "build", "--platform", "linux/amd64,linux/arm64"] - push_cmd = [container_cli, "push"] + build_cmd = container_cmd + [ + "buildx", + "build", + "--platform", + "linux/amd64,linux/arm64", + ] + push_cmd = container_cmd + ["push"] case _: - logging.critical(f"Container CLI '{container_cli}' is not supported.") + logging.critical(f"Container CLI '{container_cmd[0]}' is not supported.") sys.exit(1) # Setup build options From ba465fd530f2ee50177263d6ea212d46292d67e0 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Wed, 16 Oct 2024 09:13:57 -0400 Subject: [PATCH 30/65] Try without QEMU and with more debugging --- .github/workflows/backend.yml | 6 +++--- .github/workflows/database.yml | 4 +++- .github/workflows/frontend.yml | 6 +++--- .github/workflows/maintenance.yml | 6 +++--- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index e32b5e3661..b74a4bd361 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -178,8 +178,8 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + #- name: Set up QEMU + # uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build backend @@ -188,5 +188,5 @@ jobs: shell: bash - name: Image digest run: | - docker image inspect combine_backend:latest -f '{{json .Id}}' + docker --debug image inspect combine_backend:latest -f '{{json .Id}}' shell: bash diff --git a/.github/workflows/database.yml b/.github/workflows/database.yml index b449232b15..dcf7317428 100644 --- a/.github/workflows/database.yml +++ b/.github/workflows/database.yml @@ -30,6 +30,8 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 + #- name: Set up QEMU + # uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build database image @@ -38,5 +40,5 @@ jobs: shell: bash - name: Image digest run: | - docker image inspect combine_database:latest -f '{{json .Id}}' + docker --debug image inspect combine_database:latest -f '{{json .Id}}' shell: bash diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 83cc76ffc0..b3c42cb480 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -132,8 +132,8 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + #- name: Set up QEMU + # uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build frontend @@ -142,5 +142,5 @@ jobs: shell: bash - name: Image digest run: | - docker image inspect combine_frontend:latest -f '{{json .Id}}' + docker --debug image inspect combine_frontend:latest -f '{{json .Id}}' shell: bash diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml index cd810db4db..a4fe2b30e0 100644 --- a/.github/workflows/maintenance.yml +++ b/.github/workflows/maintenance.yml @@ -34,8 +34,8 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + #- name: Set up QEMU + # uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build maintenance image @@ -44,5 +44,5 @@ jobs: shell: bash - name: Image digest run: | - docker image inspect combine_maint:latest -f '{{json .Id}}' + docker --debug image inspect combine_maint:latest -f '{{json .Id}}' shell: bash From d46c7a3147c48e6dd5980d04f324ae4902a13d32 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Wed, 16 Oct 2024 11:20:51 -0400 Subject: [PATCH 31/65] Restore QEMU --- .github/workflows/backend.yml | 4 ++-- .github/workflows/database.yml | 4 ++-- .github/workflows/frontend.yml | 4 ++-- .github/workflows/maintenance.yml | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index b74a4bd361..420a0c984c 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -178,8 +178,8 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 - #- name: Set up QEMU - # uses: docker/setup-qemu-action@v3 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build backend diff --git a/.github/workflows/database.yml b/.github/workflows/database.yml index dcf7317428..23aa825ca8 100644 --- a/.github/workflows/database.yml +++ b/.github/workflows/database.yml @@ -30,8 +30,8 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 - #- name: Set up QEMU - # uses: docker/setup-qemu-action@v3 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build database image diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index b3c42cb480..6abe04c594 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -132,8 +132,8 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 - #- name: Set up QEMU - # uses: docker/setup-qemu-action@v3 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build frontend diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml index a4fe2b30e0..0dc2bf82ec 100644 --- a/.github/workflows/maintenance.yml +++ b/.github/workflows/maintenance.yml @@ -34,8 +34,8 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 - #- name: Set up QEMU - # uses: docker/setup-qemu-action@v3 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build maintenance image From 15f4e55eab08146c43b3ac3d7fe915c4cd79c8d7 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Wed, 16 Oct 2024 11:35:21 -0400 Subject: [PATCH 32/65] List docker images --- .github/workflows/backend.yml | 3 +++ .github/workflows/database.yml | 3 +++ .github/workflows/frontend.yml | 3 +++ .github/workflows/maintenance.yml | 3 +++ 4 files changed, 12 insertions(+) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 420a0c984c..287e8c1f01 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -186,6 +186,9 @@ jobs: run: | deploy/scripts/build.py --components backend --debug shell: bash + - name: List images + run: | + docker images - name: Image digest run: | docker --debug image inspect combine_backend:latest -f '{{json .Id}}' diff --git a/.github/workflows/database.yml b/.github/workflows/database.yml index 23aa825ca8..ea741a7a6b 100644 --- a/.github/workflows/database.yml +++ b/.github/workflows/database.yml @@ -38,6 +38,9 @@ jobs: run: | deploy/scripts/build.py --components database --debug shell: bash + - name: List images + run: | + docker images - name: Image digest run: | docker --debug image inspect combine_database:latest -f '{{json .Id}}' diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 6abe04c594..02570fa928 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -140,6 +140,9 @@ jobs: run: | deploy/scripts/build.py --components frontend --debug shell: bash + - name: List images + run: | + docker images - name: Image digest run: | docker --debug image inspect combine_frontend:latest -f '{{json .Id}}' diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml index 0dc2bf82ec..344f9fd875 100644 --- a/.github/workflows/maintenance.yml +++ b/.github/workflows/maintenance.yml @@ -42,6 +42,9 @@ jobs: run: | deploy/scripts/build.py --components maintenance --debug shell: bash + - name: List images + run: | + docker images - name: Image digest run: | docker --debug image inspect combine_maint:latest -f '{{json .Id}}' From 87fe95296d63ec6f24b366a4afbca7e9e64aaf13 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Mon, 21 Oct 2024 16:14:12 -0400 Subject: [PATCH 33/65] Simplify build_cmd construction --- deploy/scripts/build.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/deploy/scripts/build.py b/deploy/scripts/build.py index 8e84c46616..58dec880cc 100755 --- a/deploy/scripts/build.py +++ b/deploy/scripts/build.py @@ -249,25 +249,15 @@ def main() -> None: container_cmd.append("--debug") match container_cmd[0]: case "nerdctl": - build_cmd = container_cmd + [ - "-n", - args.namespace, - "build", - "--platform", - "linux/amd64,linux/arm64", - ] + build_cmd = container_cmd + ["-n", args.namespace, "build"] push_cmd = container_cmd + ["-n", args.namespace, "push"] case "docker": - build_cmd = container_cmd + [ - "buildx", - "build", - "--platform", - "linux/amd64,linux/arm64", - ] + build_cmd = container_cmd + ["buildx", "build"] push_cmd = container_cmd + ["push"] case _: logging.critical(f"Container CLI '{container_cmd[0]}' is not supported.") sys.exit(1) + build_cmd.extend(["--platform", "linux/amd64,linux/arm64"]) # Setup build options if args.quiet: From 84acfc661a31b65be3b703df70b16edeacd62e91 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Thu, 24 Oct 2024 16:04:31 -0400 Subject: [PATCH 34/65] Fix build.py docker debugging --- deploy/scripts/build.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/deploy/scripts/build.py b/deploy/scripts/build.py index 58dec880cc..597dbeedaa 100755 --- a/deploy/scripts/build.py +++ b/deploy/scripts/build.py @@ -50,7 +50,8 @@ class Job: class JobQueue: """Class to manage a queue of jobs.""" - def __init__(self, name: str) -> None: + def __init__(self, name: str, debug: bool = False) -> None: + self.debug = debug self.name = name self.status = JobStatus.RUNNING self.job_list: List[Job] = [] @@ -88,6 +89,18 @@ def start_next(self) -> bool: logging.debug(f"{self.name}.start_next(): no more jobs to run.") return False + def print_out(self) -> None: + logging.debug("####################") + logging.debug("Printing the stdout:\n") + self.output_stream.print() + logging.debug("####################") + + def print_err(self) -> None: + logging.debug("####################") + logging.debug("Printing the stderr:\n") + self.error_stream.print() + logging.debug("####################") + def check_jobs(self) -> JobStatus: """ Check if all jobs in the queue have completed. @@ -104,12 +117,14 @@ def check_jobs(self) -> JobStatus: # Current job has finished if self.curr_job.returncode == 0: logging.info(f"{self.name} job has finished.") - self.output_stream.print() + self.print_out() + if self.debug: + self.print_err() else: logging.error(f"{self.name} job failed.") - self.error_stream.print() + self.print_err() self.returncode = self.curr_job.returncode - # skip remaining jobs + # Skip remaining jobs self.job_list = [] self.status = JobStatus.ERROR return self.status @@ -245,13 +260,15 @@ def main() -> None: # Setup required build engine - docker or nerdctl container_cmd = [os.getenv("CONTAINER_CLI", "docker")] - if args.debug: - container_cmd.append("--debug") match container_cmd[0]: case "nerdctl": + if args.debug: + container_cmd.append("--debug-full") build_cmd = container_cmd + ["-n", args.namespace, "build"] push_cmd = container_cmd + ["-n", args.namespace, "push"] case "docker": + if args.debug: + container_cmd.extend(["-D", "-l", "debug"]) build_cmd = container_cmd + ["buildx", "build"] push_cmd = container_cmd + ["push"] case _: @@ -286,7 +303,7 @@ def main() -> None: spec.pre_build() image_name = get_image_name(args.repo, spec.name, args.tag) job_opts = ["-t", image_name, "-f", "Dockerfile", "."] - job_set[component] = JobQueue(component) + job_set[component] = JobQueue(component, debug=args.debug) logging.debug(f"Adding job {build_cmd + job_opts}") job_set[component].add_job(Job(build_cmd + job_opts, spec.dir)) if args.repo is not None: From 0ac5834432c99e3a68c6b06917b9c824ba2dd65e Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Thu, 24 Oct 2024 16:24:26 -0400 Subject: [PATCH 35/65] Use --load with docker build --- deploy/scripts/build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/scripts/build.py b/deploy/scripts/build.py index 597dbeedaa..9d3fe4f74c 100755 --- a/deploy/scripts/build.py +++ b/deploy/scripts/build.py @@ -269,7 +269,7 @@ def main() -> None: case "docker": if args.debug: container_cmd.extend(["-D", "-l", "debug"]) - build_cmd = container_cmd + ["buildx", "build"] + build_cmd = container_cmd + ["buildx", "build", "--load"] push_cmd = container_cmd + ["push"] case _: logging.critical(f"Container CLI '{container_cmd[0]}' is not supported.") From 1ca51d37c796da7196e6bddd3b3416f2e59c9b14 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Thu, 24 Oct 2024 16:53:16 -0400 Subject: [PATCH 36/65] Try --push instead of --load --- deploy/scripts/build.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/deploy/scripts/build.py b/deploy/scripts/build.py index 9d3fe4f74c..1e268d17b5 100755 --- a/deploy/scripts/build.py +++ b/deploy/scripts/build.py @@ -269,8 +269,10 @@ def main() -> None: case "docker": if args.debug: container_cmd.extend(["-D", "-l", "debug"]) - build_cmd = container_cmd + ["buildx", "build", "--load"] - push_cmd = container_cmd + ["push"] + build_cmd = container_cmd + ["buildx", "build"] + if args.repo is not None: + build_cmd.append("--push") + push_cmd: list[str] = [] case _: logging.critical(f"Container CLI '{container_cmd[0]}' is not supported.") sys.exit(1) @@ -306,7 +308,7 @@ def main() -> None: job_set[component] = JobQueue(component, debug=args.debug) logging.debug(f"Adding job {build_cmd + job_opts}") job_set[component].add_job(Job(build_cmd + job_opts, spec.dir)) - if args.repo is not None: + if args.repo is not None and container_cmd[0] == "nerdctl": logging.debug(f"Adding job {push_cmd + [image_name]}") job_set[component].add_job(Job(push_cmd + [image_name], None)) logging.info(f"Building component {component}") From 300605ba43c115d5e5553f495e158fe9a82383c8 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Thu, 24 Oct 2024 17:18:08 -0400 Subject: [PATCH 37/65] Try single-platform builds; only do multiplatform for the repo push --- .github/actions/combine-build/action.yml | 1 + .github/workflows/backend.yml | 11 +++++------ .github/workflows/database.yml | 7 +++++-- .github/workflows/frontend.yml | 9 +++++---- .github/workflows/maintenance.yml | 9 +++++---- deploy/scripts/build.py | 9 ++++++++- 6 files changed, 29 insertions(+), 17 deletions(-) diff --git a/.github/actions/combine-build/action.yml b/.github/actions/combine-build/action.yml index c20e6521f5..1c759cdeb3 100644 --- a/.github/actions/combine-build/action.yml +++ b/.github/actions/combine-build/action.yml @@ -57,6 +57,7 @@ runs: - name: Build The Combine run: > deploy/scripts/build.py + --arch amd64 arm64 --components ${{ inputs.build_component }} --tag ${{ env.IMAGE_TAG }} --repo ${{ inputs.image_registry }}${{ inputs.image_registry_alias}} diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 287e8c1f01..387e026d80 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -146,8 +146,11 @@ jobs: uses: github/codeql-action/analyze@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3 docker_build: + if: ${{ github.event.type }} == "PullRequest" runs-on: ubuntu-22.04 - # if: ${{ github.event.type }} == "PullRequest" + strategy: + matrix: + arch: ["amd64", "arm64"] steps: # See https://docs.stepsecurity.io/harden-runner/getting-started/ for instructions on # configuring harden-runner and identifying allowed endpoints. @@ -178,13 +181,9 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - name: Build backend run: | - deploy/scripts/build.py --components backend --debug + deploy/scripts/build.py --components backend --debug --arch ${{ matrix.arch }} shell: bash - name: List images run: | diff --git a/.github/workflows/database.yml b/.github/workflows/database.yml index ea741a7a6b..164273649a 100644 --- a/.github/workflows/database.yml +++ b/.github/workflows/database.yml @@ -10,7 +10,10 @@ permissions: # added using https://github.com/step-security/secure-workflows jobs: docker_build: if: ${{ github.event.type }} == "PullRequest" - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 + strategy: + matrix: + arch: ["amd64", "arm64"] steps: # See https://docs.stepsecurity.io/harden-runner/getting-started/ for instructions on # configuring harden-runner and identifying allowed endpoints. @@ -36,7 +39,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Build database image run: | - deploy/scripts/build.py --components database --debug + deploy/scripts/build.py --components database --debug --arch ${{ matrix.arch }} shell: bash - name: List images run: | diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 02570fa928..e120bf36c4 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -110,8 +110,11 @@ jobs: name: Frontend docker_build: - runs-on: ubuntu-latest if: ${{ github.event.type }} == "PullRequest" + runs-on: ubuntu-22.04 + strategy: + matrix: + arch: ["amd64", "arm64"] steps: # See https://docs.stepsecurity.io/harden-runner/getting-started/ for instructions on # configuring harden-runner and identifying allowed endpoints. @@ -132,13 +135,11 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build frontend run: | - deploy/scripts/build.py --components frontend --debug + deploy/scripts/build.py --components frontend --debug --arch ${{ matrix.arch }} shell: bash - name: List images run: | diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml index 344f9fd875..944b2c2baa 100644 --- a/.github/workflows/maintenance.yml +++ b/.github/workflows/maintenance.yml @@ -10,7 +10,10 @@ permissions: # added using https://github.com/step-security/secure-workflows jobs: docker_build: if: ${{ github.event.type }} == "PullRequest" - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 + strategy: + matrix: + arch: ["amd64", "arm64"] steps: # See https://docs.stepsecurity.io/harden-runner/getting-started/ for instructions on # configuring harden-runner and identifying allowed endpoints. @@ -36,11 +39,9 @@ jobs: fetch-depth: 0 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - name: Build maintenance image run: | - deploy/scripts/build.py --components maintenance --debug + deploy/scripts/build.py --components maintenance --debug --arch ${{ matrix.arch }} shell: bash - name: List images run: | diff --git a/deploy/scripts/build.py b/deploy/scripts/build.py index 1e268d17b5..bbcff09107 100755 --- a/deploy/scripts/build.py +++ b/deploy/scripts/build.py @@ -206,6 +206,13 @@ def parse_args() -> Namespace: description="Build containerd container images for project.", formatter_class=RawFormatter, ) + parser.add_argument( + "--arch", + choices=["amd64", "arm64"], + default=["arm64"], + help="Target cpu architecture(s).", + nargs="*", + ) parser.add_argument( "--build-args", nargs="*", help="Build arguments to pass to the docker build." ) @@ -276,7 +283,7 @@ def main() -> None: case _: logging.critical(f"Container CLI '{container_cmd[0]}' is not supported.") sys.exit(1) - build_cmd.extend(["--platform", "linux/amd64,linux/arm64"]) + build_cmd.extend(["--platform", ",".join([f"linux/{arch}" for arch in args.arch])]) # Setup build options if args.quiet: From 1329012d7c9f4e2b599be7d3d9a6d55c345b4b46 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Thu, 24 Oct 2024 17:27:06 -0400 Subject: [PATCH 38/65] Make tox happier --- .github/workflows/backend.yml | 7 ------- .github/workflows/database.yml | 7 ------- .github/workflows/frontend.yml | 7 ------- .github/workflows/maintenance.yml | 7 ------- deploy/scripts/build.py | 9 +++++---- 5 files changed, 5 insertions(+), 32 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 387e026d80..3f8df2017e 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -185,10 +185,3 @@ jobs: run: | deploy/scripts/build.py --components backend --debug --arch ${{ matrix.arch }} shell: bash - - name: List images - run: | - docker images - - name: Image digest - run: | - docker --debug image inspect combine_backend:latest -f '{{json .Id}}' - shell: bash diff --git a/.github/workflows/database.yml b/.github/workflows/database.yml index 164273649a..d6bf829d35 100644 --- a/.github/workflows/database.yml +++ b/.github/workflows/database.yml @@ -41,10 +41,3 @@ jobs: run: | deploy/scripts/build.py --components database --debug --arch ${{ matrix.arch }} shell: bash - - name: List images - run: | - docker images - - name: Image digest - run: | - docker --debug image inspect combine_database:latest -f '{{json .Id}}' - shell: bash diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index e120bf36c4..28cc069240 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -141,10 +141,3 @@ jobs: run: | deploy/scripts/build.py --components frontend --debug --arch ${{ matrix.arch }} shell: bash - - name: List images - run: | - docker images - - name: Image digest - run: | - docker --debug image inspect combine_frontend:latest -f '{{json .Id}}' - shell: bash diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml index 944b2c2baa..7fe8b48994 100644 --- a/.github/workflows/maintenance.yml +++ b/.github/workflows/maintenance.yml @@ -43,10 +43,3 @@ jobs: run: | deploy/scripts/build.py --components maintenance --debug --arch ${{ matrix.arch }} shell: bash - - name: List images - run: | - docker images - - name: Image digest - run: | - docker --debug image inspect combine_maint:latest -f '{{json .Id}}' - shell: bash diff --git a/deploy/scripts/build.py b/deploy/scripts/build.py index bbcff09107..7d53d5c06a 100755 --- a/deploy/scripts/build.py +++ b/deploy/scripts/build.py @@ -267,19 +267,20 @@ def main() -> None: # Setup required build engine - docker or nerdctl container_cmd = [os.getenv("CONTAINER_CLI", "docker")] + build_cmd = container_cmd + push_cmd = container_cmd match container_cmd[0]: case "nerdctl": if args.debug: container_cmd.append("--debug-full") - build_cmd = container_cmd + ["-n", args.namespace, "build"] - push_cmd = container_cmd + ["-n", args.namespace, "push"] + build_cmd.extend(["-n", args.namespace, "build"]) + push_cmd.extend(["-n", args.namespace, "push"]) case "docker": if args.debug: container_cmd.extend(["-D", "-l", "debug"]) - build_cmd = container_cmd + ["buildx", "build"] + build_cmd.extend(["buildx", "build"]) if args.repo is not None: build_cmd.append("--push") - push_cmd: list[str] = [] case _: logging.critical(f"Container CLI '{container_cmd[0]}' is not supported.") sys.exit(1) From 994061714542803f7af92a1c16940a3d42ae90b0 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Thu, 24 Oct 2024 17:32:14 -0400 Subject: [PATCH 39/65] Remove unnecessary QEMU, buildx --- .github/workflows/backend.yml | 4 ++++ .github/workflows/database.yml | 8 ++++---- .github/workflows/frontend.yml | 6 ++++-- .github/workflows/maintenance.yml | 6 ++++-- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 3f8df2017e..2c8cf9eb93 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -185,3 +185,7 @@ jobs: run: | deploy/scripts/build.py --components backend --debug --arch ${{ matrix.arch }} shell: bash + - name: Image digest + run: | + docker --debug image inspect combine_backend:latest -f '{{json .Id}}' + shell: bash diff --git a/.github/workflows/database.yml b/.github/workflows/database.yml index d6bf829d35..a97a0e8666 100644 --- a/.github/workflows/database.yml +++ b/.github/workflows/database.yml @@ -33,11 +33,11 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - name: Build database image run: | deploy/scripts/build.py --components database --debug --arch ${{ matrix.arch }} shell: bash + - name: Image digest + run: | + docker --debug image inspect combine_database:latest -f '{{json .Id}}' + shell: bash diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 28cc069240..e061162dbf 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -135,9 +135,11 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - name: Build frontend run: | deploy/scripts/build.py --components frontend --debug --arch ${{ matrix.arch }} shell: bash + - name: Image digest + run: | + docker --debug image inspect combine_frontend:latest -f '{{json .Id}}' + shell: bash diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml index 7fe8b48994..e663effab3 100644 --- a/.github/workflows/maintenance.yml +++ b/.github/workflows/maintenance.yml @@ -37,9 +37,11 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - name: Build maintenance image run: | deploy/scripts/build.py --components maintenance --debug --arch ${{ matrix.arch }} shell: bash + - name: Image digest + run: | + docker --debug image inspect combine_maint:latest -f '{{json .Id}}' + shell: bash From ab59191d310748f23a44dbdb2945e88544daf282 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Thu, 24 Oct 2024 17:35:56 -0400 Subject: [PATCH 40/65] Try to deploy to QA --- .github/workflows/codeql.yml | 4 ++-- .github/workflows/deploy_qa.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 6449b30c20..bbb20c1766 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -13,10 +13,10 @@ name: "CodeQL" on: push: - branches: ["master"] + branches: [master] pull_request: # The branches below must be a subset of the branches above - branches: ["master"] + branches: [master] schedule: - cron: "21 8 * * 3" diff --git a/.github/workflows/deploy_qa.yml b/.github/workflows/deploy_qa.yml index e1b7bcce2f..0f1e158d13 100644 --- a/.github/workflows/deploy_qa.yml +++ b/.github/workflows/deploy_qa.yml @@ -2,7 +2,7 @@ name: "Deploy Update to QA Server" on: push: - branches: [master] + branches: [arm, master] permissions: contents: read From 330a07ee4d08d61d485d482c6e38fdb977783d61 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 25 Oct 2024 09:30:42 -0400 Subject: [PATCH 41/65] Add multiplatform to action --- .github/actions/combine-build/action.yml | 6 ++++++ .github/workflows/backend.yml | 4 ---- .github/workflows/database.yml | 3 --- .github/workflows/frontend.yml | 3 --- .github/workflows/maintenance.yml | 3 --- 5 files changed, 6 insertions(+), 13 deletions(-) diff --git a/.github/actions/combine-build/action.yml b/.github/actions/combine-build/action.yml index 1c759cdeb3..263b250f44 100644 --- a/.github/actions/combine-build/action.yml +++ b/.github/actions/combine-build/action.yml @@ -54,6 +54,12 @@ runs: username: ${{ inputs.aws_access_key_id }} password: ${{ inputs.aws_secret_access_key }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Build The Combine run: > deploy/scripts/build.py diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 2c8cf9eb93..a80be1bdb0 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -165,14 +165,10 @@ jobs: *.symcb.com:80 api.nuget.org:443 archive.ubuntu.com:80 - auth.docker.io:443 dc.services.visualstudio.com:443 deb.debian.org:80 github.com:443 mcr.microsoft.com:443 - ports.ubuntu.com:80 - production.cloudflare.docker.com:443 - registry-1.docker.io:443 security.ubuntu.com:80 ts-crl.ws.symantec.com:80 # For subfolders, currently a full checkout is required. diff --git a/.github/workflows/database.yml b/.github/workflows/database.yml index a97a0e8666..45d1763ba4 100644 --- a/.github/workflows/database.yml +++ b/.github/workflows/database.yml @@ -23,10 +23,7 @@ jobs: disable-sudo: true egress-policy: block allowed-endpoints: > - auth.docker.io:443 github.com:443 - production.cloudflare.docker.com:443 - registry-1.docker.io:443 # For subfolders, currently a full checkout is required. # See: https://github.com/marketplace/actions/build-and-push-docker-images#path-context - name: Checkout repo diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index e061162dbf..9e0d2ff0f6 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -124,12 +124,9 @@ jobs: disable-sudo: true egress-policy: block allowed-endpoints: > - auth.docker.io:443 files.pythonhosted.org:443 github.com:443 - production.cloudflare.docker.com:443 pypi.org:443 - registry-1.docker.io:443 registry.npmjs.org:443 - name: Checkout repo uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml index e663effab3..9e2dc62ac1 100644 --- a/.github/workflows/maintenance.yml +++ b/.github/workflows/maintenance.yml @@ -24,12 +24,9 @@ jobs: egress-policy: block allowed-endpoints: > archive.ubuntu.com:80 - auth.docker.io:443 files.pythonhosted.org:443 github.com:443 - production.cloudflare.docker.com:443 pypi.org:443 - registry-1.docker.io:443 security.ubuntu.com:80 # For subfolders, currently a full checkout is required. # See: https://github.com/marketplace/actions/build-and-push-docker-images#path-context From ebedce3cabba00ad1e8eb9ac349e58e886389488 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 25 Oct 2024 09:41:35 -0400 Subject: [PATCH 42/65] Default to local arch --- .github/workflows/database.yml | 1 + .github/workflows/deploy_qa.yml | 2 +- .github/workflows/frontend.yml | 1 + .github/workflows/maintenance.yml | 1 + deploy/scripts/build.py | 5 +++-- 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/database.yml b/.github/workflows/database.yml index 45d1763ba4..21576cecc2 100644 --- a/.github/workflows/database.yml +++ b/.github/workflows/database.yml @@ -24,6 +24,7 @@ jobs: egress-policy: block allowed-endpoints: > github.com:443 + registry-1.docker.io:443 # For subfolders, currently a full checkout is required. # See: https://github.com/marketplace/actions/build-and-push-docker-images#path-context - name: Checkout repo diff --git a/.github/workflows/deploy_qa.yml b/.github/workflows/deploy_qa.yml index 0f1e158d13..e1b7bcce2f 100644 --- a/.github/workflows/deploy_qa.yml +++ b/.github/workflows/deploy_qa.yml @@ -2,7 +2,7 @@ name: "Deploy Update to QA Server" on: push: - branches: [arm, master] + branches: [master] permissions: contents: read diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 9e0d2ff0f6..f8eca9aea3 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -127,6 +127,7 @@ jobs: files.pythonhosted.org:443 github.com:443 pypi.org:443 + registry-1.docker.io:443 registry.npmjs.org:443 - name: Checkout repo uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml index 9e2dc62ac1..b38b6b7c20 100644 --- a/.github/workflows/maintenance.yml +++ b/.github/workflows/maintenance.yml @@ -27,6 +27,7 @@ jobs: files.pythonhosted.org:443 github.com:443 pypi.org:443 + registry-1.docker.io:443 security.ubuntu.com:80 # For subfolders, currently a full checkout is required. # See: https://github.com/marketplace/actions/build-and-push-docker-images#path-context diff --git a/deploy/scripts/build.py b/deploy/scripts/build.py index 7d53d5c06a..8bc1b45860 100755 --- a/deploy/scripts/build.py +++ b/deploy/scripts/build.py @@ -209,7 +209,7 @@ def parse_args() -> Namespace: parser.add_argument( "--arch", choices=["amd64", "arm64"], - default=["arm64"], + default=[], help="Target cpu architecture(s).", nargs="*", ) @@ -284,7 +284,8 @@ def main() -> None: case _: logging.critical(f"Container CLI '{container_cmd[0]}' is not supported.") sys.exit(1) - build_cmd.extend(["--platform", ",".join([f"linux/{arch}" for arch in args.arch])]) + if len(args.arch): + build_cmd.extend(["--platform", ",".join([f"linux/{arch}" for arch in args.arch])]) # Setup build options if args.quiet: From 845cbb5e7c26f38a432917d9f9fcf7fee699ca86 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 25 Oct 2024 09:45:19 -0400 Subject: [PATCH 43/65] Start restoring normality --- .github/workflows/backend.yml | 2 +- .github/workflows/database.yml | 3 ++- .github/workflows/frontend.yml | 3 ++- .github/workflows/maintenance.yml | 3 ++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index a80be1bdb0..7d96dc2ab2 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -183,5 +183,5 @@ jobs: shell: bash - name: Image digest run: | - docker --debug image inspect combine_backend:latest -f '{{json .Id}}' + docker image inspect combine_backend:latest -f '{{json .Id}}' shell: bash diff --git a/.github/workflows/database.yml b/.github/workflows/database.yml index 21576cecc2..562fbc01c2 100644 --- a/.github/workflows/database.yml +++ b/.github/workflows/database.yml @@ -23,6 +23,7 @@ jobs: disable-sudo: true egress-policy: block allowed-endpoints: > + auth.docker.io:443 github.com:443 registry-1.docker.io:443 # For subfolders, currently a full checkout is required. @@ -37,5 +38,5 @@ jobs: shell: bash - name: Image digest run: | - docker --debug image inspect combine_database:latest -f '{{json .Id}}' + docker image inspect combine_database:latest -f '{{json .Id}}' shell: bash diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index f8eca9aea3..ce5428c3e2 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -124,6 +124,7 @@ jobs: disable-sudo: true egress-policy: block allowed-endpoints: > + auth.docker.io:443 files.pythonhosted.org:443 github.com:443 pypi.org:443 @@ -139,5 +140,5 @@ jobs: shell: bash - name: Image digest run: | - docker --debug image inspect combine_frontend:latest -f '{{json .Id}}' + docker image inspect combine_frontend:latest -f '{{json .Id}}' shell: bash diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml index b38b6b7c20..9f7814211c 100644 --- a/.github/workflows/maintenance.yml +++ b/.github/workflows/maintenance.yml @@ -23,6 +23,7 @@ jobs: disable-sudo: true egress-policy: block allowed-endpoints: > + auth.docker.io:443 archive.ubuntu.com:80 files.pythonhosted.org:443 github.com:443 @@ -41,5 +42,5 @@ jobs: shell: bash - name: Image digest run: | - docker --debug image inspect combine_maint:latest -f '{{json .Id}}' + docker image inspect combine_maint:latest -f '{{json .Id}}' shell: bash From aea9b4dd0e98ffffdbc9989aeb856b854dafad70 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 25 Oct 2024 09:50:46 -0400 Subject: [PATCH 44/65] More restoration --- .github/workflows/backend.yml | 2 +- .github/workflows/database.yml | 3 ++- .github/workflows/frontend.yml | 3 ++- .github/workflows/maintenance.yml | 3 ++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 7d96dc2ab2..6ebaedb17b 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -179,7 +179,7 @@ jobs: fetch-depth: 0 - name: Build backend run: | - deploy/scripts/build.py --components backend --debug --arch ${{ matrix.arch }} + deploy/scripts/build.py --components backend --arch ${{ matrix.arch }} shell: bash - name: Image digest run: | diff --git a/.github/workflows/database.yml b/.github/workflows/database.yml index 562fbc01c2..931a37cdc1 100644 --- a/.github/workflows/database.yml +++ b/.github/workflows/database.yml @@ -25,6 +25,7 @@ jobs: allowed-endpoints: > auth.docker.io:443 github.com:443 + production.cloudflare.docker.com:443 registry-1.docker.io:443 # For subfolders, currently a full checkout is required. # See: https://github.com/marketplace/actions/build-and-push-docker-images#path-context @@ -34,7 +35,7 @@ jobs: fetch-depth: 0 - name: Build database image run: | - deploy/scripts/build.py --components database --debug --arch ${{ matrix.arch }} + deploy/scripts/build.py --components database --arch ${{ matrix.arch }} shell: bash - name: Image digest run: | diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index ce5428c3e2..01481f42bf 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -127,6 +127,7 @@ jobs: auth.docker.io:443 files.pythonhosted.org:443 github.com:443 + production.cloudflare.docker.com:443 pypi.org:443 registry-1.docker.io:443 registry.npmjs.org:443 @@ -136,7 +137,7 @@ jobs: fetch-depth: 0 - name: Build frontend run: | - deploy/scripts/build.py --components frontend --debug --arch ${{ matrix.arch }} + deploy/scripts/build.py --components frontend --arch ${{ matrix.arch }} shell: bash - name: Image digest run: | diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml index 9f7814211c..29fceb692d 100644 --- a/.github/workflows/maintenance.yml +++ b/.github/workflows/maintenance.yml @@ -27,6 +27,7 @@ jobs: archive.ubuntu.com:80 files.pythonhosted.org:443 github.com:443 + production.cloudflare.docker.com:443 pypi.org:443 registry-1.docker.io:443 security.ubuntu.com:80 @@ -38,7 +39,7 @@ jobs: fetch-depth: 0 - name: Build maintenance image run: | - deploy/scripts/build.py --components maintenance --debug --arch ${{ matrix.arch }} + deploy/scripts/build.py --components maintenance --arch ${{ matrix.arch }} shell: bash - name: Image digest run: | From b708408b30030c63669ce8177ecd67f9c3270807 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 25 Oct 2024 11:59:48 -0400 Subject: [PATCH 45/65] Cleanup workflows --- .github/workflows/backend.yml | 22 +++++++++------------- .github/workflows/database.yml | 2 +- .github/workflows/frontend.yml | 5 +++-- .github/workflows/maintenance.yml | 2 +- 4 files changed, 14 insertions(+), 17 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 6ebaedb17b..5380891cec 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -35,15 +35,14 @@ jobs: github.com:443 md-hdd-t032zjxllntc.z26.blob.storage.azure.net:443 objects.githubusercontent.com:443 - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - name: Checkout repository + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup dotnet uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1 with: dotnet-version: ${{ matrix.dotnet }} - name: Install ffmpeg uses: FedericoCarboni/setup-ffmpeg@36c6454b5a2348e7794ba2d82a21506605921e3d # v3 - - # Coverage. - name: Run coverage tests run: dotnet test Backend.Tests/Backend.Tests.csproj shell: bash @@ -54,15 +53,12 @@ jobs: name: coverage path: Backend.Tests/coverage.cobertura.xml retention-days: 7 - - # Development build. - - run: dotnet build BackendFramework.sln - - # Release build. - - run: dotnet publish BackendFramework.sln - - # Fmt. - - run: dotnet format --verify-no-changes + - name: Development build + run: dotnet build BackendFramework.sln + - name: Release build + run: dotnet publish BackendFramework.sln + - name: Format check + run: dotnet format --verify-no-changes upload_coverage: needs: test_build @@ -173,7 +169,7 @@ jobs: ts-crl.ws.symantec.com:80 # For subfolders, currently a full checkout is required. # See: https://github.com/marketplace/actions/build-and-push-docker-images#path-context - - name: Checkout repo + - name: Checkout repository uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 diff --git a/.github/workflows/database.yml b/.github/workflows/database.yml index 931a37cdc1..97a095e818 100644 --- a/.github/workflows/database.yml +++ b/.github/workflows/database.yml @@ -29,7 +29,7 @@ jobs: registry-1.docker.io:443 # For subfolders, currently a full checkout is required. # See: https://github.com/marketplace/actions/build-and-push-docker-images#path-context - - name: Checkout repo + - name: Checkout repository uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 01481f42bf..a91ec8d256 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -64,7 +64,8 @@ jobs: with: node-version: ${{ matrix.node-version }} - run: npm ci - - run: npm run test-frontend:coverage + - name: Run tests and generate coverage + run: npm run test-frontend:coverage env: CI: true - name: Upload coverage artifact @@ -131,7 +132,7 @@ jobs: pypi.org:443 registry-1.docker.io:443 registry.npmjs.org:443 - - name: Checkout repo + - name: Checkout repository uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml index 29fceb692d..0b32504aa6 100644 --- a/.github/workflows/maintenance.yml +++ b/.github/workflows/maintenance.yml @@ -33,7 +33,7 @@ jobs: security.ubuntu.com:80 # For subfolders, currently a full checkout is required. # See: https://github.com/marketplace/actions/build-and-push-docker-images#path-context - - name: Checkout repo + - name: Checkout repository uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 From fddf9bb7c4decc552224cc747dc44e12c7cf197b Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 25 Oct 2024 12:00:20 -0400 Subject: [PATCH 46/65] Update installer arch/arm arguments --- deploy/scripts/install-combine.sh | 15 ++++++++--- deploy/scripts/package_images.py | 41 ++++++++++++++--------------- installer/make-combine-installer.sh | 4 +-- 3 files changed, 34 insertions(+), 26 deletions(-) diff --git a/deploy/scripts/install-combine.sh b/deploy/scripts/install-combine.sh index 0432f2b427..7815df6b81 100755 --- a/deploy/scripts/install-combine.sh +++ b/deploy/scripts/install-combine.sh @@ -78,11 +78,16 @@ install-kubernetes () { # Setup Kubernetes environment and WiFi Access Point cd ${DEPLOY_DIR}/ansible + # Set -e/--extra-vars for ansible-playbook + EXTRA_VARS="-e k8s_user=${whoami}" if [ -d "${DEPLOY_DIR}/airgap-images" ] ; then - ansible-playbook playbook_desktop_setup.yml -K -e k8s_user=`whoami` -e install_airgap_images=true $(((DEBUG == 1)) && echo "-vv") - else - ansible-playbook playbook_desktop_setup.yml -K -e k8s_user=`whoami` $(((DEBUG == 1)) && echo "-vv") + EXTRA_VARS="${EXTRA_VARS} -e install_airgap_images=true" + fi + if [ $ARM == 1 ] ; then + EXTRA_VARS="${EXTRA_VARS} -e cpu_arch=arm64" fi + + ansible-playbook playbook_desktop_setup.yml -K ${EXTRA_VARS} $(((DEBUG == 1)) && echo "-vv") } # Set the KUBECONFIG environment variable so that the cluster can @@ -193,6 +198,7 @@ CONFIG_DIR=${HOME}/.config/combine mkdir -p ${CONFIG_DIR} SINGLE_STEP=0 IS_SERVER=0 +ARM=0 DEBUG=0 # See if we need to continue from a previous install @@ -207,6 +213,9 @@ fi while (( "$#" )) ; do OPT=$1 case $OPT in + arm) + ARM=1 + ;; clean) next-state "Pre-reqs" if [ -f ${CONFIG_DIR}/env ] ; then diff --git a/deploy/scripts/package_images.py b/deploy/scripts/package_images.py index 5916d74829..9bfb0b2a24 100755 --- a/deploy/scripts/package_images.py +++ b/deploy/scripts/package_images.py @@ -8,7 +8,7 @@ image names are extracted from the templates and then pulled from the repo and stored in ../images as compressed tarballs; zstd compression is used. -By default, packs images for amd64 architecture; use --arm to pack for arm64 instead. +By default, packs images for amd64; use --arch for a different architecture. """ import argparse @@ -42,10 +42,10 @@ def parse_args() -> argparse.Namespace: parser.add_argument("output_dir", help="Directory for the collected image files.") # Add Optional arguments parser.add_argument( - "--arm", - action="store_true", - help="Package for arm64 instead of the default amd64", - default=False, + "--arch", + choices=["amd64", "arm64"], + default="amd64", + help="Target cpu architecture.", ) parser.add_argument( "--config", @@ -67,16 +67,15 @@ def parse_args() -> argparse.Namespace: return parser.parse_args() -def package_k3s(dest_dir: Path, *, arm: bool = False, debug: bool = False) -> None: +def package_k3s(dest_dir: Path, *, arch: str = "arm64", debug: bool = False) -> None: logging.info("Packaging k3s images.") - extra_vars = f"'package_dir':'{dest_dir}'" - if arm: - extra_vars += ",'cpu_arch':'arm64'" ansible_cmd = [ "ansible-playbook", "playbook_k3s_airgapped_files.yml", "--extra-vars", - "{" + extra_vars + "}", + f"package_dir={dest_dir}", + "--extra-vars", + f"cpu_arch={arch}", ] if debug: ansible_cmd.append("-vv") @@ -84,14 +83,14 @@ def package_k3s(dest_dir: Path, *, arm: bool = False, debug: bool = False) -> No def package_images( - image_list: List[str], tar_file: Path, *, arm: bool = False, debug: bool = False + image_list: List[str], tar_file: Path, *, arch: str = "amd64", debug: bool = False ) -> None: container_cli_cmd = [os.getenv("CONTAINER_CLI", "docker")] if container_cli_cmd[0] == "nerdctl": container_cli_cmd.extend(["--namespace", "k8s.io"]) # Pull each image - pull_cmd = container_cli_cmd + ["pull", f"--platform=linux/{'arm64' if arm else 'amd64'}"] + pull_cmd = container_cli_cmd + ["pull", f"--platform=linux/{arch}"] for image in image_list: run_cmd(pull_cmd + [image], print_cmd=debug, print_output=debug) @@ -112,7 +111,7 @@ def package_middleware( cluster_type: str, image_dir: Path, chart_dir: Path, - arm: bool = False, + arch: str = "amd64", debug: bool = False, ) -> None: logging.info("Packaging middleware images.") @@ -168,12 +167,12 @@ def package_middleware( middleware_images.append(match.group(1)) logging.debug(f"Middleware images: {middleware_images}") - out_file = f"middleware-airgap-images-{'arm64' if arm else 'amd64'}.tar" - package_images(middleware_images, image_dir / out_file, arm=arm, debug=debug) + out_file = f"middleware-airgap-images-{arch}.tar" + package_images(middleware_images, image_dir / out_file, arch=arch, debug=debug) def package_thecombine( - tag: str, image_dir: Path, *, arm: bool = False, debug: bool = False + tag: str, image_dir: Path, *, arch: str = "amd64", debug: bool = False ) -> None: logging.info(f"Packaging The Combine version {tag}.") logging.debug("Create helm charts from templates") @@ -206,8 +205,8 @@ def package_thecombine( logging.debug(f"Combine images: {combine_images}") # Logout of AWS to allow pulling the images - out_file = f"combine-airgap-images-{'arm64' if arm else 'amd64'}.tar" - package_images(combine_images, image_dir / out_file, arm=arm, debug=debug) + out_file = f"combine-airgap-images-{arch}.tar" + package_images(combine_images, image_dir / out_file, arch=arch, debug=debug) def main() -> None: @@ -228,16 +227,16 @@ def main() -> None: os.environ["AWS_DEFAULT_REGION"] = "" # Update helm repos - package_k3s(image_dir, arm=args.arm, debug=args.debug) + package_k3s(image_dir, arch=args.arch, debug=args.debug) package_middleware( args.config, cluster_type="standard", image_dir=image_dir, chart_dir=chart_dir, - arm=args.arm, + arch=args.arch, debug=args.debug, ) - package_thecombine(args.tag, image_dir, arm=args.arm, debug=args.debug) + package_thecombine(args.tag, image_dir, arch=args.arch, debug=args.debug) if __name__ == "__main__": diff --git a/installer/make-combine-installer.sh b/installer/make-combine-installer.sh index 0ad68ce77c..13c2d0cc3f 100755 --- a/installer/make-combine-installer.sh +++ b/installer/make-combine-installer.sh @@ -62,7 +62,7 @@ if [[ $NET_INSTALL == 0 ]] ; then # Package The Combine for "offline" installation TEMP_DIR=/tmp/images-$$ pushd scripts - ./package_images.py ${COMBINE_VERSION} ${TEMP_DIR} $((( ARM == 1 )) && echo "--arm") $((( DEBUG == 1 )) && echo "--debug") + ./package_images.py ${COMBINE_VERSION} ${TEMP_DIR} $((( ARM == 1 )) && echo "--arch arm64") $((( DEBUG == 1 )) && echo "--debug") INSTALLER_NAME="combine-installer.run" popd else @@ -79,7 +79,7 @@ for DIR in venv scripts/__pycache__ ; do done cd ${SCRIPT_DIR} -makeself $((( DEBUG == 0)) && echo "--tar-quietly" ) ../deploy ${INSTALLER_NAME} "Combine Installer" scripts/install-combine.sh ${COMBINE_VERSION} +makeself $((( DEBUG == 0)) && echo "--tar-quietly" ) ../deploy ${INSTALLER_NAME} "Combine Installer" scripts/install-combine.sh ${COMBINE_VERSION} $((( ARM == 1 )) && echo "arm") if [[ $NET_INSTALL == 0 ]] ; then makeself --append ${TEMP_DIR} ${INSTALLER_NAME} rm -rf ${TEMP_DIR} From d905beb1c1c0876781b092071326dadbd04ec13e Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 25 Oct 2024 12:07:15 -0400 Subject: [PATCH 47/65] See if --platform is necessary --- .github/workflows/deploy_qa.yml | 2 +- .github/workflows/maintenance.yml | 2 +- Backend/Dockerfile | 4 ++-- Dockerfile | 6 +++--- database/Dockerfile | 2 +- maintenance/Dockerfile | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/deploy_qa.yml b/.github/workflows/deploy_qa.yml index e1b7bcce2f..0f1e158d13 100644 --- a/.github/workflows/deploy_qa.yml +++ b/.github/workflows/deploy_qa.yml @@ -2,7 +2,7 @@ name: "Deploy Update to QA Server" on: push: - branches: [master] + branches: [arm, master] permissions: contents: read diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml index 0b32504aa6..33c0b3ed76 100644 --- a/.github/workflows/maintenance.yml +++ b/.github/workflows/maintenance.yml @@ -23,8 +23,8 @@ jobs: disable-sudo: true egress-policy: block allowed-endpoints: > - auth.docker.io:443 archive.ubuntu.com:80 + auth.docker.io:443 files.pythonhosted.org:443 github.com:443 production.cloudflare.docker.com:443 diff --git a/Backend/Dockerfile b/Backend/Dockerfile index 8580dc0e91..2e7a5b7faa 100644 --- a/Backend/Dockerfile +++ b/Backend/Dockerfile @@ -7,7 +7,7 @@ ############################################################ # Docker multi-stage build -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0.402-jammy AS builder +FROM mcr.microsoft.com/dotnet/sdk:8.0.402-jammy AS builder WORKDIR /app # Copy csproj and restore (fetch dependencies) as distinct layers. @@ -19,7 +19,7 @@ COPY . ./ RUN dotnet publish -c Release -o build # Build runtime image. -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/aspnet:8.0.8-jammy +FROM mcr.microsoft.com/dotnet/aspnet:8.0.8-jammy ENV ASPNETCORE_URLS=http://+:5000 ENV COMBINE_IS_IN_CONTAINER=1 diff --git a/Dockerfile b/Dockerfile index 7310841115..74acc47977 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ ############################################################ # User guide build environment -FROM --platform=$BUILDPLATFORM python:3.12.5-slim-bookworm AS user_guide_builder +FROM python:3.12.5-slim-bookworm AS user_guide_builder ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 @@ -24,7 +24,7 @@ COPY docs/user_guide docs/user_guide RUN tox -e user-guide # Frontend build environment. -FROM --platform=$BUILDPLATFORM node:20.17.0-bookworm-slim AS frontend_builder +FROM node:20.17.0-bookworm-slim AS frontend_builder WORKDIR /app # Install app dependencies. @@ -36,7 +36,7 @@ COPY . ./ RUN npm run build # Production environment. -FROM --platform=$BUILDPLATFORM nginx:1.27 +FROM nginx:1.27 WORKDIR /app diff --git a/database/Dockerfile b/database/Dockerfile index 72ff5ab878..778443a6fc 100644 --- a/database/Dockerfile +++ b/database/Dockerfile @@ -5,7 +5,7 @@ # - Intel/AMD 64-bit # - ARM 64-bit ############################################################ -FROM --platform=$BUILDPLATFORM mongo:7.0.14-jammy +FROM mongo:7.0.14-jammy WORKDIR / diff --git a/maintenance/Dockerfile b/maintenance/Dockerfile index 41b53d72bc..9e86310ad8 100644 --- a/maintenance/Dockerfile +++ b/maintenance/Dockerfile @@ -16,7 +16,7 @@ # - ARM 64-bit ############################################################ -FROM --platform=$BUILDPLATFORM sillsdev/aws-kubectl:0.3.0 +FROM sillsdev/aws-kubectl:0.3.0 USER root From c595f891d2e285dd0e07818ff10d53ff6655eb84 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 25 Oct 2024 12:22:01 -0400 Subject: [PATCH 48/65] Try qa deploy w/o QEMU --- .github/actions/combine-build/action.yml | 3 --- .github/workflows/backend.yml | 2 +- .github/workflows/codeql.yml | 2 +- .github/workflows/combine_deploy_image.yml | 2 +- .github/workflows/dependency-review.yml | 2 +- .github/workflows/deploy_qa.yml | 4 ++-- .github/workflows/deploy_release.yml | 2 +- .github/workflows/frontend.yml | 6 +++--- .github/workflows/pages.yml | 2 +- .github/workflows/python.yml | 2 +- .github/workflows/scorecards.yml | 2 +- Backend/Dockerfile | 4 ++-- Dockerfile | 6 +++--- database/Dockerfile | 2 +- deploy/Dockerfile | 2 +- maintenance/Dockerfile | 2 +- 16 files changed, 21 insertions(+), 24 deletions(-) diff --git a/.github/actions/combine-build/action.yml b/.github/actions/combine-build/action.yml index 263b250f44..c263915e6b 100644 --- a/.github/actions/combine-build/action.yml +++ b/.github/actions/combine-build/action.yml @@ -54,9 +54,6 @@ runs: username: ${{ inputs.aws_access_key_id }} password: ${{ inputs.aws_secret_access_key }} - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 5380891cec..139efc7ed7 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -62,7 +62,7 @@ jobs: upload_coverage: needs: test_build - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: # See https://docs.stepsecurity.io/harden-runner/getting-started/ for instructions on # configuring harden-runner and identifying allowed endpoints. diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index bbb20c1766..3044c16821 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -26,7 +26,7 @@ permissions: # added using https://github.com/step-security/secure-workflows jobs: analyze: name: Analyze - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 permissions: actions: read contents: read diff --git a/.github/workflows/combine_deploy_image.yml b/.github/workflows/combine_deploy_image.yml index c8f010de39..4a5dc47a72 100644 --- a/.github/workflows/combine_deploy_image.yml +++ b/.github/workflows/combine_deploy_image.yml @@ -11,7 +11,7 @@ permissions: # added using https://github.com/step-security/secure-workflows jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: # See https://docs.stepsecurity.io/harden-runner/getting-started/ for instructions on # configuring harden-runner and identifying allowed endpoints. diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 038fa8b486..40f2bd8e1b 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -14,7 +14,7 @@ permissions: jobs: dependency-review: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Harden Runner uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1 diff --git a/.github/workflows/deploy_qa.yml b/.github/workflows/deploy_qa.yml index 0f1e158d13..c58047234f 100644 --- a/.github/workflows/deploy_qa.yml +++ b/.github/workflows/deploy_qa.yml @@ -13,7 +13,7 @@ jobs: matrix: component: [frontend, backend, maintenance, database] - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 outputs: image_tag: ${{ steps.build_combine.outputs.image_tag }} steps: @@ -64,7 +64,7 @@ jobs: build_component: ${{ matrix.component }} clean_ecr_repo: needs: build - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: # See https://docs.stepsecurity.io/harden-runner/getting-started/ for instructions on # configuring harden-runner and identifying allowed endpoints. diff --git a/.github/workflows/deploy_release.yml b/.github/workflows/deploy_release.yml index 527c18d793..f364747637 100644 --- a/.github/workflows/deploy_release.yml +++ b/.github/workflows/deploy_release.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: component: [frontend, backend, maintenance, database] - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 outputs: image_tag: ${{ steps.build_combine.outputs.image_tag }} steps: diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index a91ec8d256..05ad394272 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -11,7 +11,7 @@ permissions: # added using https://github.com/step-security/secure-workflows jobs: lint_build: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 strategy: matrix: node-version: [20] @@ -40,7 +40,7 @@ jobs: - run: npm run build test_coverage: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 strategy: matrix: node-version: [20] @@ -78,7 +78,7 @@ jobs: upload_coverage: needs: test_coverage - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: # See https://docs.stepsecurity.io/harden-runner/getting-started/ for instructions on # configuring harden-runner and identifying allowed endpoints. diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 7c5482d3a2..eb1adabf76 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -12,7 +12,7 @@ permissions: # added using https://github.com/step-security/secure-workflows jobs: deploy: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: # See https://docs.stepsecurity.io/harden-runner/getting-started/ for instructions on # configuring harden-runner and identifying allowed endpoints. diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 7d79b2e4ef..707b6c463f 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -11,7 +11,7 @@ permissions: # added using https://github.com/step-security/secure-workflows jobs: tox: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 strategy: matrix: python-version: ["3.12"] diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index ca11ef0181..9c8cf78de4 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -21,7 +21,7 @@ permissions: read-all jobs: analysis: name: Scorecards analysis - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 permissions: # Needed to upload the results to code-scanning dashboard. security-events: write diff --git a/Backend/Dockerfile b/Backend/Dockerfile index 2e7a5b7faa..8580dc0e91 100644 --- a/Backend/Dockerfile +++ b/Backend/Dockerfile @@ -7,7 +7,7 @@ ############################################################ # Docker multi-stage build -FROM mcr.microsoft.com/dotnet/sdk:8.0.402-jammy AS builder +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0.402-jammy AS builder WORKDIR /app # Copy csproj and restore (fetch dependencies) as distinct layers. @@ -19,7 +19,7 @@ COPY . ./ RUN dotnet publish -c Release -o build # Build runtime image. -FROM mcr.microsoft.com/dotnet/aspnet:8.0.8-jammy +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/aspnet:8.0.8-jammy ENV ASPNETCORE_URLS=http://+:5000 ENV COMBINE_IS_IN_CONTAINER=1 diff --git a/Dockerfile b/Dockerfile index 74acc47977..7310841115 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ ############################################################ # User guide build environment -FROM python:3.12.5-slim-bookworm AS user_guide_builder +FROM --platform=$BUILDPLATFORM python:3.12.5-slim-bookworm AS user_guide_builder ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 @@ -24,7 +24,7 @@ COPY docs/user_guide docs/user_guide RUN tox -e user-guide # Frontend build environment. -FROM node:20.17.0-bookworm-slim AS frontend_builder +FROM --platform=$BUILDPLATFORM node:20.17.0-bookworm-slim AS frontend_builder WORKDIR /app # Install app dependencies. @@ -36,7 +36,7 @@ COPY . ./ RUN npm run build # Production environment. -FROM nginx:1.27 +FROM --platform=$BUILDPLATFORM nginx:1.27 WORKDIR /app diff --git a/database/Dockerfile b/database/Dockerfile index 778443a6fc..72ff5ab878 100644 --- a/database/Dockerfile +++ b/database/Dockerfile @@ -5,7 +5,7 @@ # - Intel/AMD 64-bit # - ARM 64-bit ############################################################ -FROM mongo:7.0.14-jammy +FROM --platform=$BUILDPLATFORM mongo:7.0.14-jammy WORKDIR / diff --git a/deploy/Dockerfile b/deploy/Dockerfile index 0783932bd4..eafc84d9a5 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -6,7 +6,7 @@ # - ARM 64-bit ############################################################ -FROM ubuntu:22.04 +FROM --platform=$BUILDPLATFORM ubuntu:22.04 USER root diff --git a/maintenance/Dockerfile b/maintenance/Dockerfile index 9e86310ad8..41b53d72bc 100644 --- a/maintenance/Dockerfile +++ b/maintenance/Dockerfile @@ -16,7 +16,7 @@ # - ARM 64-bit ############################################################ -FROM sillsdev/aws-kubectl:0.3.0 +FROM --platform=$BUILDPLATFORM sillsdev/aws-kubectl:0.3.0 USER root From 4c08fdb830786e38fc6ff82cc31918563ba28461 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Tue, 29 Oct 2024 08:54:51 -0400 Subject: [PATCH 49/65] Change some info to debug --- deploy/scripts/sem_dom_import.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deploy/scripts/sem_dom_import.py b/deploy/scripts/sem_dom_import.py index 924514a7cc..fa93a81333 100755 --- a/deploy/scripts/sem_dom_import.py +++ b/deploy/scripts/sem_dom_import.py @@ -295,7 +295,7 @@ def generate_semantic_domains( # Languages can be found in the Name element for sub_elem in elem: lang, name_text = get_auni_text(sub_elem) - logging.info(f"Language code: {lang}") + logging.debug(f"Language code: {lang}") if lang not in domain_tree: domain_tree[lang] = {} if lang not in domain_nodes: @@ -305,9 +305,9 @@ def generate_semantic_domains( prev_domain = get_sem_doms(root, {}, prev_domain) for lang in domain_nodes: - logging.info(f"Number of {lang} Domains: {len(domain_nodes[lang])}") + logging.debug(f"Number of {lang} Domains: {len(domain_nodes[lang])}") for lang in domain_tree: - logging.info(f"Number of {lang} Tree Nodes: {len(domain_tree[lang])}") + logging.debug(f"Number of {lang} Tree Nodes: {len(domain_tree[lang])}") if not flatten_questions: SemanticDomainFull.flatten_questions = False write_json(output_dir) From 9aba4481e792d8956f600dc3a184e26c780deeb3 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Thu, 7 Nov 2024 10:29:38 -0500 Subject: [PATCH 50/65] Try updated arch-tagged images --- maintenance/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maintenance/Dockerfile b/maintenance/Dockerfile index 41b53d72bc..c72f762ab2 100644 --- a/maintenance/Dockerfile +++ b/maintenance/Dockerfile @@ -16,7 +16,7 @@ # - ARM 64-bit ############################################################ -FROM --platform=$BUILDPLATFORM sillsdev/aws-kubectl:0.3.0 +FROM public.ecr.aws/thecombine/aws-kubectl:0.4.0-$TARGETARCH USER root From 2660f700324c2ffd7c754d82ac19fd9519cf1fc5 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Thu, 7 Nov 2024 10:38:00 -0500 Subject: [PATCH 51/65] Add aws endpoint --- .github/workflows/deploy_qa.yml | 1 + .github/workflows/maintenance.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/deploy_qa.yml b/.github/workflows/deploy_qa.yml index c58047234f..41605c4092 100644 --- a/.github/workflows/deploy_qa.yml +++ b/.github/workflows/deploy_qa.yml @@ -42,6 +42,7 @@ jobs: github.com:443 mcr.microsoft.com:443 production.cloudflare.docker.com:443 + public.ecr.aws:443 pypi.org:443 registry-1.docker.io:443 registry.npmjs.org:443 diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml index 33c0b3ed76..d51a31b25c 100644 --- a/.github/workflows/maintenance.yml +++ b/.github/workflows/maintenance.yml @@ -28,6 +28,7 @@ jobs: files.pythonhosted.org:443 github.com:443 production.cloudflare.docker.com:443 + public.ecr.aws:443 pypi.org:443 registry-1.docker.io:443 security.ubuntu.com:80 From 062f8b339fa5501eaa666701ff2e337ce13a3214 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Thu, 7 Nov 2024 10:48:56 -0500 Subject: [PATCH 52/65] Add cloudfront endpoint --- .github/workflows/deploy_qa.yml | 1 + .github/workflows/maintenance.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/deploy_qa.yml b/.github/workflows/deploy_qa.yml index 41605c4092..50154f0cc7 100644 --- a/.github/workflows/deploy_qa.yml +++ b/.github/workflows/deploy_qa.yml @@ -26,6 +26,7 @@ jobs: egress-policy: block allowed-endpoints: > *.actions.githubusercontent.com:443 + *.cloudfront.net:443 *.data.mcr.microsoft.com:443 ${{ secrets.AWS_ACCOUNT }}.dkr.ecr.${{ secrets.AWS_DEFAULT_REGION }}.amazonaws.com api.ecr.${{ secrets.AWS_DEFAULT_REGION }}.amazonaws.com:443 diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml index d51a31b25c..62ef4dc045 100644 --- a/.github/workflows/maintenance.yml +++ b/.github/workflows/maintenance.yml @@ -23,6 +23,7 @@ jobs: disable-sudo: true egress-policy: block allowed-endpoints: > + *.cloudfront.net:443 archive.ubuntu.com:80 auth.docker.io:443 files.pythonhosted.org:443 From 67a0ac1f2aef685d0181f07882d36422b09fc994 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 8 Nov 2024 08:59:34 -0500 Subject: [PATCH 53/65] More build experiments --- .github/actions/combine-build/action.yml | 3 +++ .github/workflows/database.yml | 2 ++ .github/workflows/frontend.yml | 2 ++ .github/workflows/maintenance.yml | 2 ++ Backend/Dockerfile | 4 ++-- database/Dockerfile | 2 +- 6 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/actions/combine-build/action.yml b/.github/actions/combine-build/action.yml index c263915e6b..e532211c6a 100644 --- a/.github/actions/combine-build/action.yml +++ b/.github/actions/combine-build/action.yml @@ -54,6 +54,9 @@ runs: username: ${{ inputs.aws_access_key_id }} password: ${{ inputs.aws_secret_access_key }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 diff --git a/.github/workflows/database.yml b/.github/workflows/database.yml index 97a095e818..49bc147084 100644 --- a/.github/workflows/database.yml +++ b/.github/workflows/database.yml @@ -33,6 +33,8 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 - name: Build database image run: | deploy/scripts/build.py --components database --arch ${{ matrix.arch }} diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 05ad394272..d7d72fa92b 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -136,6 +136,8 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 - name: Build frontend run: | deploy/scripts/build.py --components frontend --arch ${{ matrix.arch }} diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml index 62ef4dc045..f0c2b68109 100644 --- a/.github/workflows/maintenance.yml +++ b/.github/workflows/maintenance.yml @@ -39,6 +39,8 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 - name: Build maintenance image run: | deploy/scripts/build.py --components maintenance --arch ${{ matrix.arch }} diff --git a/Backend/Dockerfile b/Backend/Dockerfile index 8580dc0e91..2e7a5b7faa 100644 --- a/Backend/Dockerfile +++ b/Backend/Dockerfile @@ -7,7 +7,7 @@ ############################################################ # Docker multi-stage build -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0.402-jammy AS builder +FROM mcr.microsoft.com/dotnet/sdk:8.0.402-jammy AS builder WORKDIR /app # Copy csproj and restore (fetch dependencies) as distinct layers. @@ -19,7 +19,7 @@ COPY . ./ RUN dotnet publish -c Release -o build # Build runtime image. -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/aspnet:8.0.8-jammy +FROM mcr.microsoft.com/dotnet/aspnet:8.0.8-jammy ENV ASPNETCORE_URLS=http://+:5000 ENV COMBINE_IS_IN_CONTAINER=1 diff --git a/database/Dockerfile b/database/Dockerfile index 72ff5ab878..778443a6fc 100644 --- a/database/Dockerfile +++ b/database/Dockerfile @@ -5,7 +5,7 @@ # - Intel/AMD 64-bit # - ARM 64-bit ############################################################ -FROM --platform=$BUILDPLATFORM mongo:7.0.14-jammy +FROM mongo:7.0.14-jammy WORKDIR / From 681e957e2671297ec1a3a90150c341d2ba756f17 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 8 Nov 2024 09:09:28 -0500 Subject: [PATCH 54/65] Focus build experiments on maintenance --- .github/workflows/database.yml | 2 -- .github/workflows/frontend.yml | 2 -- Backend/Dockerfile | 4 ++-- database/Dockerfile | 2 +- maintenance/Dockerfile | 2 +- 5 files changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/database.yml b/.github/workflows/database.yml index 49bc147084..97a095e818 100644 --- a/.github/workflows/database.yml +++ b/.github/workflows/database.yml @@ -33,8 +33,6 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - name: Build database image run: | deploy/scripts/build.py --components database --arch ${{ matrix.arch }} diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index d7d72fa92b..05ad394272 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -136,8 +136,6 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - name: Build frontend run: | deploy/scripts/build.py --components frontend --arch ${{ matrix.arch }} diff --git a/Backend/Dockerfile b/Backend/Dockerfile index 2e7a5b7faa..8580dc0e91 100644 --- a/Backend/Dockerfile +++ b/Backend/Dockerfile @@ -7,7 +7,7 @@ ############################################################ # Docker multi-stage build -FROM mcr.microsoft.com/dotnet/sdk:8.0.402-jammy AS builder +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0.402-jammy AS builder WORKDIR /app # Copy csproj and restore (fetch dependencies) as distinct layers. @@ -19,7 +19,7 @@ COPY . ./ RUN dotnet publish -c Release -o build # Build runtime image. -FROM mcr.microsoft.com/dotnet/aspnet:8.0.8-jammy +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/aspnet:8.0.8-jammy ENV ASPNETCORE_URLS=http://+:5000 ENV COMBINE_IS_IN_CONTAINER=1 diff --git a/database/Dockerfile b/database/Dockerfile index 778443a6fc..72ff5ab878 100644 --- a/database/Dockerfile +++ b/database/Dockerfile @@ -5,7 +5,7 @@ # - Intel/AMD 64-bit # - ARM 64-bit ############################################################ -FROM mongo:7.0.14-jammy +FROM --platform=$BUILDPLATFORM mongo:7.0.14-jammy WORKDIR / diff --git a/maintenance/Dockerfile b/maintenance/Dockerfile index c72f762ab2..4bc9592572 100644 --- a/maintenance/Dockerfile +++ b/maintenance/Dockerfile @@ -16,7 +16,7 @@ # - ARM 64-bit ############################################################ -FROM public.ecr.aws/thecombine/aws-kubectl:0.4.0-$TARGETARCH +FROM --platform=$BUILDPLATFORM public.ecr.aws/thecombine/aws-kubectl:0.4.0-$TARGETARCH USER root From dcdeb55ff149313bd050cdc984356dc7e4dd7bde Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 8 Nov 2024 09:30:42 -0500 Subject: [PATCH 55/65] Take 3 --- .github/actions/combine-build/action.yml | 3 --- .github/workflows/maintenance.yml | 6 ++---- maintenance/Dockerfile | 2 +- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/actions/combine-build/action.yml b/.github/actions/combine-build/action.yml index e532211c6a..c263915e6b 100644 --- a/.github/actions/combine-build/action.yml +++ b/.github/actions/combine-build/action.yml @@ -54,9 +54,6 @@ runs: username: ${{ inputs.aws_access_key_id }} password: ${{ inputs.aws_secret_access_key }} - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml index f0c2b68109..0fe9e29b2a 100644 --- a/.github/workflows/maintenance.yml +++ b/.github/workflows/maintenance.yml @@ -39,13 +39,11 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Build maintenance image run: | deploy/scripts/build.py --components maintenance --arch ${{ matrix.arch }} shell: bash - - name: Image digest - run: | - docker image inspect combine_maint:latest -f '{{json .Id}}' - shell: bash diff --git a/maintenance/Dockerfile b/maintenance/Dockerfile index 4bc9592572..c72f762ab2 100644 --- a/maintenance/Dockerfile +++ b/maintenance/Dockerfile @@ -16,7 +16,7 @@ # - ARM 64-bit ############################################################ -FROM --platform=$BUILDPLATFORM public.ecr.aws/thecombine/aws-kubectl:0.4.0-$TARGETARCH +FROM public.ecr.aws/thecombine/aws-kubectl:0.4.0-$TARGETARCH USER root From bd12bf2e46767f2221d925ae96f51e82345c9ffe Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 8 Nov 2024 09:34:29 -0500 Subject: [PATCH 56/65] Add ports.ubuntu.com endpoint --- .github/workflows/deploy_qa.yml | 1 + .github/workflows/maintenance.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/deploy_qa.yml b/.github/workflows/deploy_qa.yml index 50154f0cc7..29485b9b49 100644 --- a/.github/workflows/deploy_qa.yml +++ b/.github/workflows/deploy_qa.yml @@ -42,6 +42,7 @@ jobs: files.pythonhosted.org:443 github.com:443 mcr.microsoft.com:443 + ports.ubuntu.com:80 production.cloudflare.docker.com:443 public.ecr.aws:443 pypi.org:443 diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml index 0fe9e29b2a..4d3e49d94a 100644 --- a/.github/workflows/maintenance.yml +++ b/.github/workflows/maintenance.yml @@ -28,6 +28,7 @@ jobs: auth.docker.io:443 files.pythonhosted.org:443 github.com:443 + ports.ubuntu.com:80 production.cloudflare.docker.com:443 public.ecr.aws:443 pypi.org:443 From 9be4711946535b81e2ebd61cbc0811789a7d1680 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 8 Nov 2024 09:45:02 -0500 Subject: [PATCH 57/65] Try to do more fully emulated builds --- .github/workflows/backend.yml | 5 +++++ .github/workflows/database.yml | 4 ++++ .github/workflows/frontend.yml | 3 +++ .github/workflows/maintenance.yml | 2 -- Backend/Dockerfile | 4 ++-- Dockerfile | 6 +++--- database/Dockerfile | 2 +- deploy/Dockerfile | 2 +- 8 files changed, 19 insertions(+), 9 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 139efc7ed7..a43af26d81 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -165,6 +165,7 @@ jobs: deb.debian.org:80 github.com:443 mcr.microsoft.com:443 + ports.ubuntu.com:80 security.ubuntu.com:80 ts-crl.ws.symantec.com:80 # For subfolders, currently a full checkout is required. @@ -173,6 +174,10 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 - name: Build backend run: | deploy/scripts/build.py --components backend --arch ${{ matrix.arch }} diff --git a/.github/workflows/database.yml b/.github/workflows/database.yml index 97a095e818..5d6b44aa2a 100644 --- a/.github/workflows/database.yml +++ b/.github/workflows/database.yml @@ -33,6 +33,10 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 - name: Build database image run: | deploy/scripts/build.py --components database --arch ${{ matrix.arch }} diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 05ad394272..a8928d6b5e 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -128,6 +128,7 @@ jobs: auth.docker.io:443 files.pythonhosted.org:443 github.com:443 + ports.ubuntu.com:80 production.cloudflare.docker.com:443 pypi.org:443 registry-1.docker.io:443 @@ -136,6 +137,8 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - name: Build frontend run: | deploy/scripts/build.py --components frontend --arch ${{ matrix.arch }} diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml index 4d3e49d94a..4a38141ef9 100644 --- a/.github/workflows/maintenance.yml +++ b/.github/workflows/maintenance.yml @@ -42,8 +42,6 @@ jobs: fetch-depth: 0 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - name: Build maintenance image run: | deploy/scripts/build.py --components maintenance --arch ${{ matrix.arch }} diff --git a/Backend/Dockerfile b/Backend/Dockerfile index 8580dc0e91..2e7a5b7faa 100644 --- a/Backend/Dockerfile +++ b/Backend/Dockerfile @@ -7,7 +7,7 @@ ############################################################ # Docker multi-stage build -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0.402-jammy AS builder +FROM mcr.microsoft.com/dotnet/sdk:8.0.402-jammy AS builder WORKDIR /app # Copy csproj and restore (fetch dependencies) as distinct layers. @@ -19,7 +19,7 @@ COPY . ./ RUN dotnet publish -c Release -o build # Build runtime image. -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/aspnet:8.0.8-jammy +FROM mcr.microsoft.com/dotnet/aspnet:8.0.8-jammy ENV ASPNETCORE_URLS=http://+:5000 ENV COMBINE_IS_IN_CONTAINER=1 diff --git a/Dockerfile b/Dockerfile index 7310841115..74acc47977 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ ############################################################ # User guide build environment -FROM --platform=$BUILDPLATFORM python:3.12.5-slim-bookworm AS user_guide_builder +FROM python:3.12.5-slim-bookworm AS user_guide_builder ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 @@ -24,7 +24,7 @@ COPY docs/user_guide docs/user_guide RUN tox -e user-guide # Frontend build environment. -FROM --platform=$BUILDPLATFORM node:20.17.0-bookworm-slim AS frontend_builder +FROM node:20.17.0-bookworm-slim AS frontend_builder WORKDIR /app # Install app dependencies. @@ -36,7 +36,7 @@ COPY . ./ RUN npm run build # Production environment. -FROM --platform=$BUILDPLATFORM nginx:1.27 +FROM nginx:1.27 WORKDIR /app diff --git a/database/Dockerfile b/database/Dockerfile index 72ff5ab878..778443a6fc 100644 --- a/database/Dockerfile +++ b/database/Dockerfile @@ -5,7 +5,7 @@ # - Intel/AMD 64-bit # - ARM 64-bit ############################################################ -FROM --platform=$BUILDPLATFORM mongo:7.0.14-jammy +FROM mongo:7.0.14-jammy WORKDIR / diff --git a/deploy/Dockerfile b/deploy/Dockerfile index eafc84d9a5..0783932bd4 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -6,7 +6,7 @@ # - ARM 64-bit ############################################################ -FROM --platform=$BUILDPLATFORM ubuntu:22.04 +FROM ubuntu:22.04 USER root From a5bd077f5d115be64a601bef9825cad4b8c6cfdc Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 8 Nov 2024 09:53:21 -0500 Subject: [PATCH 58/65] Use QEMU --- .github/actions/combine-build/action.yml | 4 ++-- .github/workflows/backend.yml | 7 +------ .github/workflows/database.yml | 6 ------ .github/workflows/frontend.yml | 4 ---- 4 files changed, 3 insertions(+), 18 deletions(-) diff --git a/.github/actions/combine-build/action.yml b/.github/actions/combine-build/action.yml index c263915e6b..c3006b787c 100644 --- a/.github/actions/combine-build/action.yml +++ b/.github/actions/combine-build/action.yml @@ -54,8 +54,8 @@ runs: username: ${{ inputs.aws_access_key_id }} password: ${{ inputs.aws_secret_access_key }} - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - name: Build The Combine run: > diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index a43af26d81..63a328fa41 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -166,6 +166,7 @@ jobs: github.com:443 mcr.microsoft.com:443 ports.ubuntu.com:80 + registry-1.docker.io:443 security.ubuntu.com:80 ts-crl.ws.symantec.com:80 # For subfolders, currently a full checkout is required. @@ -176,13 +177,7 @@ jobs: fetch-depth: 0 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - name: Build backend run: | deploy/scripts/build.py --components backend --arch ${{ matrix.arch }} shell: bash - - name: Image digest - run: | - docker image inspect combine_backend:latest -f '{{json .Id}}' - shell: bash diff --git a/.github/workflows/database.yml b/.github/workflows/database.yml index 5d6b44aa2a..58c6c8b9c3 100644 --- a/.github/workflows/database.yml +++ b/.github/workflows/database.yml @@ -35,13 +35,7 @@ jobs: fetch-depth: 0 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - name: Build database image run: | deploy/scripts/build.py --components database --arch ${{ matrix.arch }} shell: bash - - name: Image digest - run: | - docker image inspect combine_database:latest -f '{{json .Id}}' - shell: bash diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index a8928d6b5e..47581c96fe 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -143,7 +143,3 @@ jobs: run: | deploy/scripts/build.py --components frontend --arch ${{ matrix.arch }} shell: bash - - name: Image digest - run: | - docker image inspect combine_frontend:latest -f '{{json .Id}}' - shell: bash From f99cbf08ec814aa41cda607fa486be4c26ce3670 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 8 Nov 2024 09:54:35 -0500 Subject: [PATCH 59/65] Re-add endpoint --- .github/workflows/backend.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 63a328fa41..1e7d05f93c 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -161,6 +161,7 @@ jobs: *.symcb.com:80 api.nuget.org:443 archive.ubuntu.com:80 + auth.docker.io:443 dc.services.visualstudio.com:443 deb.debian.org:80 github.com:443 From 7230848e5512bd5d470f886e8d5b92f81cd2e855 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 8 Nov 2024 09:56:10 -0500 Subject: [PATCH 60/65] Use buildx for multiplatform build --- .github/actions/combine-build/action.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/combine-build/action.yml b/.github/actions/combine-build/action.yml index c3006b787c..c263915e6b 100644 --- a/.github/actions/combine-build/action.yml +++ b/.github/actions/combine-build/action.yml @@ -54,8 +54,8 @@ runs: username: ${{ inputs.aws_access_key_id }} password: ${{ inputs.aws_secret_access_key }} - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 - name: Build The Combine run: > From 8fabe3224b6d920a99044743816d2fc960990d63 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 8 Nov 2024 09:58:23 -0500 Subject: [PATCH 61/65] Re-add another endpoint --- .github/workflows/backend.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 1e7d05f93c..74d4954f7d 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -167,6 +167,7 @@ jobs: github.com:443 mcr.microsoft.com:443 ports.ubuntu.com:80 + production.cloudflare.docker.com:443 registry-1.docker.io:443 security.ubuntu.com:80 ts-crl.ws.symantec.com:80 From e76d356c9dcc3d879586cdb09b188b66f70cf55a Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Tue, 12 Nov 2024 08:49:28 -0500 Subject: [PATCH 62/65] Use BUILDPLATFORM just for backend builder --- Backend/Dockerfile | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Backend/Dockerfile b/Backend/Dockerfile index 2e7a5b7faa..98cac8e20d 100644 --- a/Backend/Dockerfile +++ b/Backend/Dockerfile @@ -7,7 +7,7 @@ ############################################################ # Docker multi-stage build -FROM mcr.microsoft.com/dotnet/sdk:8.0.402-jammy AS builder +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0.402-jammy AS builder WORKDIR /app # Copy csproj and restore (fetch dependencies) as distinct layers. @@ -33,8 +33,7 @@ ENV APP_FILES=${HOME}/.CombineFiles # Install system dependencies. RUN apt-get update \ - && apt-get install -y \ - ffmpeg \ + && apt-get install -y ffmpeg \ && rm -rf /var/lib/apt/lists/* # Create the home directory for the new app user. @@ -47,7 +46,7 @@ RUN usermod --uid 999 --gid app \ --comment "Docker image user" \ app -## Set up application install directory. +# Set up application install directory. RUN mkdir $APP_HOME && \ mkdir $APP_FILES && \ # Give access to the entire home folder so the backend can create files and folders there. From 29095e99edf9b6d178f12f73f362ed1441cc2224 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Wed, 13 Nov 2024 08:58:59 -0500 Subject: [PATCH 63/65] Add helm value for cpuArch --- .../helm/aws-login/templates/aws-ecr-login-cronjob.yaml | 2 +- .../helm/aws-login/templates/aws-ecr-login-oneshot.yaml | 2 +- deploy/helm/aws-login/values.yaml | 5 +++-- .../maintenance/templates/cronjob-daily-backup.yaml | 2 +- .../maintenance/templates/cronjob-update-fonts.yaml | 2 +- .../charts/maintenance/templates/get-fonts-hook.yaml | 2 +- deploy/helm/thecombine/charts/maintenance/values.yaml | 6 +++++- deploy/scripts/build.py | 3 +-- deploy/scripts/install-combine.sh | 8 +++++++- 9 files changed, 21 insertions(+), 11 deletions(-) diff --git a/deploy/helm/aws-login/templates/aws-ecr-login-cronjob.yaml b/deploy/helm/aws-login/templates/aws-ecr-login-cronjob.yaml index 168958b695..30abff4b87 100644 --- a/deploy/helm/aws-login/templates/aws-ecr-login-cronjob.yaml +++ b/deploy/helm/aws-login/templates/aws-ecr-login-cronjob.yaml @@ -18,7 +18,7 @@ spec: spec: serviceAccountName: {{ .Values.awsEcr.serviceAccount }} containers: - - image: {{ .Values.awsEcr.image }}:{{ .Values.awsEcr.imageTag }} + - image: {{ .Values.awsEcr.image }}:{{ .Values.awsEcr.imageVersion }}-{{ .Values.global.cpuArch }} imagePullPolicy: IfNotPresent name: {{ .Values.awsEcr.cronJobName }} command: diff --git a/deploy/helm/aws-login/templates/aws-ecr-login-oneshot.yaml b/deploy/helm/aws-login/templates/aws-ecr-login-oneshot.yaml index 8f7266da63..857ec0b184 100644 --- a/deploy/helm/aws-login/templates/aws-ecr-login-oneshot.yaml +++ b/deploy/helm/aws-login/templates/aws-ecr-login-oneshot.yaml @@ -18,7 +18,7 @@ spec: spec: serviceAccountName: {{ .Values.awsEcr.serviceAccount }} containers: - - image: {{ .Values.awsEcr.image }}:{{ .Values.awsEcr.imageTag }} + - image: {{ .Values.awsEcr.image }}:{{ .Values.awsEcr.imageVersion }}-{{ .Values.global.cpuArch }} imagePullPolicy: IfNotPresent name: "{{ .Values.awsEcr.jobName }}" command: diff --git a/deploy/helm/aws-login/values.yaml b/deploy/helm/aws-login/values.yaml index 2d81c4601a..6a681e543e 100644 --- a/deploy/helm/aws-login/values.yaml +++ b/deploy/helm/aws-login/values.yaml @@ -15,14 +15,15 @@ global: awsAccessKeyId: "Override" awsSecretAccessKey: "Override" pullSecretName: aws-login-credentials + cpuArch: "arm64" awsEcr: configName: aws-ecr-config cron: yes cronJobName: ecr-cred-helper-cron dockerEmail: noreply@thecombine.app - image: sillsdev/aws-kubectl - imageTag: "0.3.0" + image: "public.ecr.aws/thecombine/aws-kubectl" + imageVersion: "0.4.0" jobName: ecr-cred-helper schedule: "0 */8 * * *" secretsName: aws-ecr-credentials diff --git a/deploy/helm/thecombine/charts/maintenance/templates/cronjob-daily-backup.yaml b/deploy/helm/thecombine/charts/maintenance/templates/cronjob-daily-backup.yaml index 73b28a0ebf..fb3b7b8649 100644 --- a/deploy/helm/thecombine/charts/maintenance/templates/cronjob-daily-backup.yaml +++ b/deploy/helm/thecombine/charts/maintenance/templates/cronjob-daily-backup.yaml @@ -18,7 +18,7 @@ spec: spec: serviceAccountName: {{ .Values.serviceAccount.name }} containers: - - image: sillsdev/aws-kubectl:0.3.0 + - image: {{ .Values.awsEcr.image }}:{{ .Values.awsEcr.imageVersion }}-{{ .Values.global.cpuArch }} imagePullPolicy: Always name: daily-backup command: diff --git a/deploy/helm/thecombine/charts/maintenance/templates/cronjob-update-fonts.yaml b/deploy/helm/thecombine/charts/maintenance/templates/cronjob-update-fonts.yaml index 4fa9da913d..ca088114b7 100644 --- a/deploy/helm/thecombine/charts/maintenance/templates/cronjob-update-fonts.yaml +++ b/deploy/helm/thecombine/charts/maintenance/templates/cronjob-update-fonts.yaml @@ -18,7 +18,7 @@ spec: spec: serviceAccountName: {{ .Values.serviceAccount.name }} containers: - - image: sillsdev/aws-kubectl:0.3.0 + - image: {{ .Values.awsEcr.image }}:{{ .Values.awsEcr.imageVersion }}-{{ .Values.global.cpuArch }} imagePullPolicy: Always name: update-fonts command: diff --git a/deploy/helm/thecombine/charts/maintenance/templates/get-fonts-hook.yaml b/deploy/helm/thecombine/charts/maintenance/templates/get-fonts-hook.yaml index 0327191cfc..8c42de0438 100644 --- a/deploy/helm/thecombine/charts/maintenance/templates/get-fonts-hook.yaml +++ b/deploy/helm/thecombine/charts/maintenance/templates/get-fonts-hook.yaml @@ -26,7 +26,7 @@ spec: spec: serviceAccountName: {{ .Values.serviceAccount.name }} containers: - - image: sillsdev/aws-kubectl:0.3.0 + - image: {{ .Values.awsEcr.image }}:{{ .Values.awsEcr.imageVersion }}-{{ .Values.global.cpuArch }} imagePullPolicy: Always name: "install-fonts" command: diff --git a/deploy/helm/thecombine/charts/maintenance/values.yaml b/deploy/helm/thecombine/charts/maintenance/values.yaml index d1424183e6..a97954331f 100644 --- a/deploy/helm/thecombine/charts/maintenance/values.yaml +++ b/deploy/helm/thecombine/charts/maintenance/values.yaml @@ -26,6 +26,7 @@ global: imageRegistry: "" # Default AWS S3 location awsS3Location: "thecombine.app" + cpuArch: "arm64" imageName: combine_maint @@ -34,7 +35,10 @@ serviceAccount: role: role-maintenance roleBinding: role-maintenance-binding -serviceAccount.name: account-maintenance +awsEcr: + image: "public.ecr.aws/thecombine/aws-kubectl" + imageVersion: "0.4.0" + ####################################### # Variables controlling backups ####################################### diff --git a/deploy/scripts/build.py b/deploy/scripts/build.py index ef8a086f6c..e8630cfe67 100755 --- a/deploy/scripts/build.py +++ b/deploy/scripts/build.py @@ -277,8 +277,7 @@ def main() -> None: if args.debug: container_cmd.extend(["-D", "-l", "debug"]) build_cmd = container_cmd + ["buildx", "build"] - if args.repo is not None: - build_cmd.append("--push") + build_cmd.append("--load" if args.repo is None else "--push") case _: logging.critical(f"Container CLI '{container_cmd[0]}' is not supported.") sys.exit(1) diff --git a/deploy/scripts/install-combine.sh b/deploy/scripts/install-combine.sh index 7815df6b81..46b6c5a535 100755 --- a/deploy/scripts/install-combine.sh +++ b/deploy/scripts/install-combine.sh @@ -143,7 +143,13 @@ install-the-combine () { cd ${DEPLOY_DIR}/scripts set-combine-env set-k3s-env - ./setup_combine.py --tag ${COMBINE_VERSION} --repo public.ecr.aws/thecombine --target desktop ${SETUP_OPTS} $(((DEBUG == 1)) && echo "--debug") + ./setup_combine.py \ + $(((DEBUG == 1)) && echo "--debug") \ + --repo public.ecr.aws/thecombine \ + $(((ARM == 1)) && echo "--set global.cpuArch=arm64" ) \ + --tag ${COMBINE_VERSION} \ + --target desktop \ + ${SETUP_OPTS} deactivate } From 3fd2eb217c1de0f1ca795fc1e726e81ab9b67483 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Wed, 13 Nov 2024 09:09:37 -0500 Subject: [PATCH 64/65] Update buildx action from v2 to v3 --- .github/actions/combine-build/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/combine-build/action.yml b/.github/actions/combine-build/action.yml index c263915e6b..3c17fff60b 100644 --- a/.github/actions/combine-build/action.yml +++ b/.github/actions/combine-build/action.yml @@ -55,7 +55,7 @@ runs: password: ${{ inputs.aws_secret_access_key }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Build The Combine run: > From a7b71403b0cfd47e0fa5dc2f04dd40be577feb67 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 15 Nov 2024 18:04:19 -0500 Subject: [PATCH 65/65] Add endpoint to deploy_release that was added to deploy_qa --- .github/workflows/deploy_release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy_release.yml b/.github/workflows/deploy_release.yml index 494f54d065..64658cae99 100644 --- a/.github/workflows/deploy_release.yml +++ b/.github/workflows/deploy_release.yml @@ -40,6 +40,7 @@ jobs: github.com:443 mcr.microsoft.com:443 production.cloudflare.docker.com:443 + ports.ubuntu.com:80 public.ecr.aws:443 pypi.org:443 registry-1.docker.io:443