From 5c9f3364415693fe8e946a52867ae19ab4788f15 Mon Sep 17 00:00:00 2001 From: Miren Esnaola Date: Mon, 8 Aug 2022 15:45:12 +0200 Subject: [PATCH] Example of a multi-cluster mesh on GKE configuring managed control plane using the Fleet API --- .gitignore | 2 + .../.gitignore | 25 ++ .../README.md | 65 +++++ .../ansible/ansible.cfg | 9 + .../ansible/inventory/hosts.ini | 1 + .../ansible/playbook.yaml | 27 ++ .../tasks/endpoint-discovery-config.yaml | 21 ++ .../ansible/roles/install/tasks/install.yaml | 58 ++++ .../ansible/roles/install/tasks/main.yaml | 39 +++ .../roles/prerequisites/tasks/main.yaml | 38 +++ .../ansible/roles/test/tasks/main.yaml | 36 +++ .../ansible/roles/test/tasks/test.yaml | 63 +++++ .../architecture.png | Bin 0 -> 46273 bytes .../multi-cluster-mesh-gke-fleet-api/main.tf | 263 ++++++++++++++++++ .../templates/gssh.sh.tpl | 30 ++ .../templates/vars.yaml.tpl | 8 + .../variables.tf | 102 +++++++ .../__init__.py | 13 + .../fixture/main.tf | 28 ++ .../fixture/variables.tf | 107 +++++++ .../test_plan.py | 19 ++ 21 files changed, 954 insertions(+) create mode 100644 examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/.gitignore create mode 100644 examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/README.md create mode 100644 examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/ansible.cfg create mode 100644 examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/inventory/hosts.ini create mode 100644 examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/playbook.yaml create mode 100644 examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/roles/install/tasks/endpoint-discovery-config.yaml create mode 100644 examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/roles/install/tasks/install.yaml create mode 100644 examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/roles/install/tasks/main.yaml create mode 100644 examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/roles/prerequisites/tasks/main.yaml create mode 100644 examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/roles/test/tasks/main.yaml create mode 100644 examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/roles/test/tasks/test.yaml create mode 100644 examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/architecture.png create mode 100644 examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/main.tf create mode 100644 examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/templates/gssh.sh.tpl create mode 100644 examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/templates/vars.yaml.tpl create mode 100644 examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/variables.tf create mode 100644 tests/examples/cloud_operations/multi_cluster_mesh_gke_fleet_api/__init__.py create mode 100644 tests/examples/cloud_operations/multi_cluster_mesh_gke_fleet_api/fixture/main.tf create mode 100644 tests/examples/cloud_operations/multi_cluster_mesh_gke_fleet_api/fixture/variables.tf create mode 100644 tests/examples/cloud_operations/multi_cluster_mesh_gke_fleet_api/test_plan.py diff --git a/.gitignore b/.gitignore index eda2ee025..ec7ead32b 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,5 @@ examples/cloud-operations/binauthz/app/app.yaml env/ examples/cloud-operations/adfs/ansible/vars/vars.yaml examples/cloud-operations/adfs/ansible/gssh.sh +examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/vars.yaml +examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/gssh.sh diff --git a/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/.gitignore b/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/.gitignore new file mode 100644 index 000000000..15c75f37d --- /dev/null +++ b/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/.gitignore @@ -0,0 +1,25 @@ +# terraform local cache directory +**/.terraform/* + +# terraform state files +*.tfstate +*.tfstate.backup + +# terraform lock file +*.terraform.lock.hcl + +# terraform variable files +*.tfvars + +# terraform backend configuration files +backend.tf +*.tfbackend + +# node modules +**/node_modules/* + +# python virtual environment +**/venv/* + +ansible/vars/vars.yaml +ansible/gssh.sh diff --git a/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/README.md b/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/README.md new file mode 100644 index 000000000..73402b6cf --- /dev/null +++ b/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/README.md @@ -0,0 +1,65 @@ +# Multi-cluster mesh on GKE (fleet API) + +The following example shows how to create a multi-cluster mesh for two private clusters on GKE. Anthos Service Mesh with automatic control plane management is set up for clusters using the Fleet API. This can only be done if the clusters are in a single project and in the same VPC. In this particular case both clusters having being deployed to different subnets in a shared VPC. + +The diagram below depicts the architecture of the example. + +![Architecture](architecture.png) + +Terraform is used to provision the required infrastructure, create the IAM binding and register the clusters to the fleet. + +Ansible is used to execute commands in the management VM. From this VM there is access to the cluster's endpoint. More specifically the following is done using Ansible: + +1. Install required dependencies in the VM +2. Enable automatic control plane management in both clusters. +3. Verify the control plane has been provisioned for both clusters. +4. Configure ASM control plane endpoint discovery between the two clusters. +5. Create a sample namespace in both clusters. +6. Configure automatic sidecar injection in the created namespace. +7. Deploy a hello-world service in both clusters +8. Deploy a hello-world deployment (v1) in cluster a +9. Deploy a hello-world deployment (v2) in cluster b +10. Deploy a sleep service in both clusters. +11. Send requests from a sleep pod to the hello-world service from both clusters, to verify that we get responses from alternative versions. + +## Running the example + +Clone this repository or [open it in cloud shell](https://ssh.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https%3A%2F%2Fgithub.com%2Fterraform-google-modules%2Fcloud-foundation-fabric&cloudshell_print=cloud-shell-readme.txt&cloudshell_working_dir=examples%2Fcloud-operations%2Fmulti-cluster-mesh-gke-fleet-api), then go through the following steps to create resources: + +* `terraform init` +* `terraform apply -var billing_account_id=my-billing-account-id -var parent=folders/my-folder-id -var host_project_id=my-host-project-id -var fleet_project_id=my-fleet-project-id -var mgmt_project_id=my-mgmt-project-id` + +Once terraform completes do the following: + +* Change to the ansible folder + + cd ansible + +* Run the ansible playbook + + ansible-playbook -v playbook.yaml + + +## Testing the example + +The last two commands executed with Ansible Send requests from a sleep pod to the hello-world service from both clusters. If you see in the output of those two commands responses from alternative versions, everything works as expected. + +Once done testing, you can clean up resources by running `terraform destroy`. + + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [billing_account_id](variables.tf#L17) | Billing account id. | string | ✓ | | +| [fleet_project_id](variables.tf#L32) | Management Project ID. | string | ✓ | | +| [host_project_id](variables.tf#L27) | Project ID. | string | ✓ | | +| [mgmt_project_id](variables.tf#L37) | Management Project ID. | string | ✓ | | +| [parent](variables.tf#L22) | Parent. | string | ✓ | | +| [clusters_config](variables.tf#L54) | Clusters configuration. | map(object({…})) | | {…} | +| [istio_version](variables.tf#L98) | ASM version | string | | "1.14.1-asm.3" | +| [mgmt_server_config](variables.tf#L78) | Mgmt server configuration | object({…}) | | {…} | +| [mgmt_subnet_cidr_block](variables.tf#L42) | Management subnet CIDR block. | string | | "10.0.0.0/28" | +| [region](variables.tf#L48) | Region. | string | | "europe-west1" | + + diff --git a/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/ansible.cfg b/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/ansible.cfg new file mode 100644 index 000000000..4558c2b6b --- /dev/null +++ b/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/ansible.cfg @@ -0,0 +1,9 @@ +[defaults] +inventory = inventory/hosts.ini +timeout = 900 + +[ssh_connection] +pipelining = True +ssh_executable = ./gssh.sh +transfer_method = piped + diff --git a/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/inventory/hosts.ini b/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/inventory/hosts.ini new file mode 100644 index 000000000..842da83f4 --- /dev/null +++ b/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/inventory/hosts.ini @@ -0,0 +1 @@ +mgmt \ No newline at end of file diff --git a/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/playbook.yaml b/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/playbook.yaml new file mode 100644 index 000000000..30114d22c --- /dev/null +++ b/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/playbook.yaml @@ -0,0 +1,27 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +- hosts: mgmt + gather_facts: "no" + vars_files: + - vars/vars.yaml + environment: + USE_GKE_GCLOUD_AUTH_PLUGIN: True + roles: + - role: prerequisites + become: yes + become_method: sudo + - role: install + - role: test + diff --git a/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/roles/install/tasks/endpoint-discovery-config.yaml b/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/roles/install/tasks/endpoint-discovery-config.yaml new file mode 100644 index 000000000..99abc8923 --- /dev/null +++ b/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/roles/install/tasks/endpoint-discovery-config.yaml @@ -0,0 +1,21 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +- name: Configure endpoint discovery + shell: > + kubectl apply \ + -f ~/{{ item }}.secret \ + --context "gke_{{ project_id }}_{{ region }}_{{ cluster }}" + with_items: "{{ clusters }}" + when: cluster != item \ No newline at end of file diff --git a/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/roles/install/tasks/install.yaml b/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/roles/install/tasks/install.yaml new file mode 100644 index 000000000..b81c49622 --- /dev/null +++ b/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/roles/install/tasks/install.yaml @@ -0,0 +1,58 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +- name: Get cluster credentials + shell: > + gcloud container clusters get-credentials {{ cluster }} \ + --region {{ region }} \ + --project {{ project_id }} \ + --internal-ip + +- name: Set context + set_fact: + context: "gke_{{ project_id }}_{{ region }}_{{ cluster }}" + +- name: Install ASM in cluster + shell: > + gcloud container fleet mesh update \ + --control-plane automatic \ + --memberships {{ cluster }} \ + --project {{ project_id }} + +- name: Wait until MCP is provisioned + shell: > + for i in $(seq 12); do + result=$(gcloud container fleet mesh describe --project {{ project_id }} --format json \ + | jq -r '.membershipStates | to_entries[] | select(.key | endswith("{{ cluster }}")) | .value.servicemesh.controlPlaneManagement.state') + if [ "$result" = "ACTIVE" ]; then + break + fi + echo "ASM control plane is not ready yet..." + sleep 60 + done + +- name: Get endpoint IP + shell: > + gcloud container clusters describe "{{ cluster }}" \ + --project "{{ project_id }}" \ + --region "{{ region }}" \ + --format "value(privateClusterConfig.publicEndpoint)" + register: endpoint + +- name: Create secret + shell: > + ~/istio-*/bin/istioctl x create-remote-secret \ + --context={{ context }} \ + --name={{ cluster }} \ + --server=https://{{ endpoint.stdout }} > ~/{{ cluster }}.secret \ No newline at end of file diff --git a/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/roles/install/tasks/main.yaml b/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/roles/install/tasks/main.yaml new file mode 100644 index 000000000..9e181bfbf --- /dev/null +++ b/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/roles/install/tasks/main.yaml @@ -0,0 +1,39 @@ + +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +- name: Download istio bundle + get_url: + url: https://storage.googleapis.com/gke-release/asm/istio-{{ istio_version }}-linux-amd64.tar.gz + dest: ~/istio.tar.gz + +- name: Unarchive istio bundle + unarchive: + src: ~/istio.tar.gz + dest: ~/ + remote_src: yes + +- name: Install + include_tasks: install.yaml + vars: + cluster: "{{ item }}" + with_items: "{{ clusters }}" + +- name: Configure endpoint discovery + include_tasks: endpoint-discovery-config.yaml + vars: + cluster: "{{ outer_item }}" + with_items: "{{ clusters }}" + loop_control: + loop_var: outer_item \ No newline at end of file diff --git a/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/roles/prerequisites/tasks/main.yaml b/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/roles/prerequisites/tasks/main.yaml new file mode 100644 index 000000000..8b889230e --- /dev/null +++ b/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/roles/prerequisites/tasks/main.yaml @@ -0,0 +1,38 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +- name: Download the Google Cloud SDK package repository signing key + get_url: + url: https://packages.cloud.google.com/apt/doc/apt-key.gpg + dest: /usr/share/keyrings/cloud.google.gpg + +- name: Add Google Cloud SDK package repository source + apt_repository: + filename: google-cloud-sdk.list + repo: "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" + state: present + update_cache: yes + +- name: Install dependencies + apt: + pkg: + - kubectl + - google-cloud-sdk-gke-gcloud-auth-plugin + - jq + state: present + +- name: Install gke-gcloud-auth-plugin + apt: + name: google-cloud-sdk-gke-gcloud-auth-plugin + state: present \ No newline at end of file diff --git a/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/roles/test/tasks/main.yaml b/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/roles/test/tasks/main.yaml new file mode 100644 index 000000000..46a06c16e --- /dev/null +++ b/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/roles/test/tasks/main.yaml @@ -0,0 +1,36 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +- name: Deploy test app + include_tasks: test.yaml + vars: + cluster: "{{ item }}" + with_items: "{{ clusters }}" + loop_control: + index_var: index + +- name: Test + shell: > + for i in $(seq 400); do + kubectl exec \ + --context="gke_{{ project_id }}_{{ region }}_{{ item }}" \ + -n sample \ + -c sleep "$(kubectl get pod \ + --context="gke_{{ project_id }}_{{ region }}_{{ item }}" \ + -n sample \ + -l app=sleep \ + -o jsonpath='{.items[0].metadata.name}')" \ + -- curl -sS helloworld.sample:5000/hello + done + with_items: "{{ clusters }}" diff --git a/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/roles/test/tasks/test.yaml b/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/roles/test/tasks/test.yaml new file mode 100644 index 000000000..293690747 --- /dev/null +++ b/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/roles/test/tasks/test.yaml @@ -0,0 +1,63 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +- name: Set context + set_fact: + context: "gke_{{ project_id }}_{{ region }}_{{ cluster }}" + +- name: Create sample namespace + shell: + cmd: | + cat << EOF | kubectl apply --context {{ context }} -f - + apiVersion: v1 + kind: Namespace + metadata: + name: sample + EOF + +- name: Label the sample namespace for istio sidecar injection + shell: > + kubectl label namespace sample \ + istio-injection- istio.io/rev=asm-managed \ + --context {{ context }} \ + --overwrite + +- name: Create helloworld service + shell: > + kubectl apply + -f samples/helloworld/helloworld.yaml \ + -l service=helloworld \ + -n sample \ + --context {{ context }} + args: + chdir: ~/istio-{{ istio_version }} + +- name: Create helloworld deployment + shell: > + kubectl apply + -f samples/helloworld/helloworld.yaml \ + -l version=v{{ index + 1 }} \ + -n sample \ + --context {{ context }} + args: + chdir: ~/istio-{{ istio_version }} + +- name: Create sleep service and deployment + shell: > + kubectl apply \ + -f samples/sleep/sleep.yaml \ + -n sample \ + --context {{ context }} + args: + chdir: ~/istio-{{ istio_version }} \ No newline at end of file diff --git a/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/architecture.png b/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..0838570f4a143c2fbee0ffe9cd2d23d12e54fb82 GIT binary patch literal 46273 zcmd43bzD{5+BS+vBOoCtAfbe`bi<-slx{?cMRzSqKtM!9S{kK6x?4c$SagGQgS7M+ z3w@sb?EUWd{CCdx`4_+GWX>_}amRIE_caNA`a}i?ixdk92?<9|R#F8C>E>4?BxG_7 zWN@WI^eHnE(kmo6Nij7ygUxy~J2L&`Lf6KzhK8lHrX>gds60JZpI!9AF6S=St%TeW z!P-*61*KAVGc#8+mu2Zm$}qdPaQm$|m;Bv7n#gP|%HG8j1ed*y4;avj7qNb2=HLf9Bw^tYkc z|JR3%+=Gciiun0!=_`mMvM(Cq=by|}@WlT^x9ghsJ_J+grDAy?$w( zFg`ppQL%t6Z@kT2p9}Br=}~GM-x`T|VdeR`(C7Kfml8rdx|^GuZ~-<}mS<2u1FX_T zhiY;6#09PHSYn5his1*>OX5Q8EEkfe*7>=v$MAf!RQPJfw(x56*d*4c{_v4rczC+3 zd-0vk+RMB~^NYz@H^Z}ELDYM*Ua45!>Uh28?>{#f2RZKVqe{_=+;HtPY_K1koSo$T z8CW?Rz{KmfEu5WWN!4?wlAq4h)Ra=xYkR>bD=$k^XlG+{Gdm_GMz9vPhy6BEbx?JH zW9AN#v-2@CTj7TdCZe~et(h|o-u9fh?K@0)mYJDm-#D*V?1orzHYAT^+4#navCpBc zvCo-qN#oh#M=!YQb6g z@GA0j&8IeHzwj9QYS??%r*ifv%fqpwcF!pE^lY-_KH<9MTJ9{KDrXRGxB79P_X1s+ z_@_sV-ad!RTOP+Px+3Ka(X)7GV-qThN=kh_J-X_mrs-H^I{BH0?%^Lir@MGL&w3Vz zhI)H@k9vE@en|qaNcvFJ^vMW=3#{gcDPlE3z8bbW1-x68mui|9h0f|%<9I6jhFixM z!acMt#JO3cc~^VI6R19TGBR;pO$x^98|mHcceD4Sgb{oXT5UuJ4nYcd7Zp!!+lT?Kr4# z%O(_iSXX+OL`7uQH}l-SY+bz`rerfbhnJI^Q|mq{dbAeO(ZQf0z70ZF&N#ocyj+S- z-uon{oJEA1_LsMto0_7HLDSe+)xe&8{npm~k~zQ65pS|(j<w?aMdo>M)bWqJABPIeK#kBxoYk|r+x z(E3(a1u@j($#z2Wp#U?-vY6XJ$m_*|Z3-24oxIDYPm4qEhReA>QJ>g9F=$w-rm`T| zPOLmJ3dhT#=C-vl7xtKAyw19O_=q_82+6BBLhswKxJqDaJ!dQD(!Ua2UC&-Lj5n+} zr}PM^CLKQZ6RJtYZCAgd5kK_!l)`-)ySu)AStuf#mYRJq_1$tUZoIiili)?fboZv- z1{YU3Gc)U*=(oNpg@^8h^x!7qT&-Nk#`vsva1)!JNk97 zLHe)EBO`et{M=r^%inGCig$Q%B>&C))`66X{50h&aHMXne;Y(KiL)hM?6l^5H1FzZZeCKk#7W=wJ{g4D@XjHR)hm!hOnt%%pftns9>jO8k;JsGoFWss-9{b}l@_PceNPo}o@_`;8+}i2Z@6EjtJ5?NX8jT&Bzae7ai8rY85xAFuLvV;U4l$x^5n=CaYH8Ghvo(iAb ze*L=l+iMsQUK0b^=@A;_z9>{oEkC?U(~fPtOcC+n=N9gfcj@*I&WhL7VL37OhP0LI+V2LhX&3hCRYjZ# z$lT=>;u0djKU{rcw>s&HIv39FDlT4I<1xleV8SuPxl;JFkCKtG<6Wf6V1P%5zkf^R zh2Iy{noS8QHAUuNTd$X@s$E$h$EvV3ZS;=!l;pa!bal=0xncz2v4tLu-uoJl_}@^$ zH#W$O`*wpnnwbgaTbGF=7AlT#$&l|;+*w%giN#SeIyl5hY`Tq$$bhdAj$ni=Oe{Im zjRI@j&#FzhKTm2%U#PzCsw^7zx*;*s=Dwikt+pR!_2gBhD}EH@6L)t&q3H#H;fIdq2?eXOnnE6~l;ENQe}>l2S-Sk&JC@E@v)&7PWu$PhlI< z+Ge*z@qO|{Ut6Dcqo=2fpYY_qDn1(!D~*d z^?>!v=JqrgO*%AION3As_!4Cvg>dj$Pjwea zdODMWMDnyONf%`XlS*-$`bN9a8#C&Lt(q8*qC4ZpwU54pvAL#>7J8hBgeenkdk)?a zx7J^9T`6>OIedyo15e+xA|9spH$%RE@8VaCZK~eo#n{K*Z3@pfx54K5u08rAq%UhN z{;IO?Wvb=n@U~=?sfV1kcS)&pcMWsr&$?xekrj4$l!yCzKdSu-GKf;l4)aD4kKix6 zTY~%k+!`7h(rvAd`+K%CN20E4rwLQ8#*z1vrqkOiJ(1hrnK+yrbX$o#Ff!PBzUZ8q z@-*vn>q01vu zv?j*q7W<|9#!g8P+JDP~`Og%gg5ENk7b%=U$+_n{DZ@#4)?T|soZ9Vand|iGOv<9U z0k4U3eeknR>x=r1&KK;|rTt+WXE*AD0HRGi6gZwAPG~&uIWhEd^)(Ln9ynOqm`NWZ zBO@d2GFuZ-)6lPXPgwSLy~GNjd+?bO4=?P)tB6Apk7S6FY+9c|m3$AIVQut2k715l zME=Ooupy`$eY5(h8fL5?$s!6vlADc=ex<8f{Mslb>C>;1CL}4CYWKd-IBY?G11|yi zF^!^Ch2FS(?pfHQ(K^rUmJe{*0hm~7&T@0JpW)M=aoCyqjw8wvPB$xr9ODm8 zV{AW2ou&x66DyBmI$eB4iGSmM{_Ygd`!+sFimWES6ZCjITmGw#2fQB2cITOI;to2`ZoH@j& zQjCm_T5gcNNcld<%<-e*s8^y*z>;}3dthQD`9q>KxAs!zGBJm9==XOa3SBxP<>n1v z)D66;<(okz4+dA*JkCzZ65_Gn%8wU5oK=C_%EVLPUp(S1 zeD31H%FE^hb^B&<2bc%x2MwZd#6u#>3byeK$O}&m+mo-ndiyR5d3~rm8x`JOCS}`; z#Y&x+)a8~a*I)SeHyZ|YM@&DCv5k91ao4YV{SlFTbp{%L<&P24()@Uw1nx?T2F1aa zhiUm+_xIR@U7cL?Yg{(^Bl)7`>#fC#ekLW|4-hxn7W>3k<#Z$0T$|8QNkjV=3Qf;v zTq(Akw;Az!K}=ScFQh8u1qB)REB$m&YW5VEP6a=*J;pt=M6CHM<{#vn=js~s7M=5| z=3vuy<$S3h^|Wm+S65rG=b^>-xnsMew)Rym$%C}XOINJ7@@A%{g0(eiFJ2T*A22Yw zvlSqy2P8Wr(E29R!%c_wtWQ=a7AO1n;1zC-nRin%q0~3jzq?|S>@4LV!iO+-D6}R1 zyL0)#IYSkpH0x|`o^9O{q21AZIl#QEde!!Gd!fHxFAfjyG)j{pp8dlTu}(!_o-~!b zB|+Vd&YnBpC*Ho$Z(on`R4>qpHh=x4gF68$)Zr|MvO$o4$l?(6d2YsBY>fyX$$PgM z*t%R^&b`r3gm@Z~;Z5Y8;1w(c_D6bUhU~kH@|%A6p$%T+3%QB7?nVsI^B;a98R7cZ ztpd3H#{bv1f4e}f$mL$e_8h9%#J> z@S_03pm+a&ryb@mkwabeZy>cHgKwaF(YdWZgwRL_G71GyWBtRck z;Q*(cGEB8j+?P%5OBUYPN9A}<2k%P}wY9MLjG3XbwzXAXQE{|AU0Z28tI{zvq~_rv zs3hcZ^d&2+t%i5c)I~LJLd5%A>m~l`;h}RpRgh^>kt61x6zwJ-JP$xd38O2N8=-z9I*>PEVi0r>4l6Xx#7Ly{o0K zPy4dL<9IuP*VdRLZkY+W9<+^w;B9_N)md9_3=H+A*9M z+OMXfsu~#=XFi;Vb{&VW7Elq)+39Uxx+IgAkGY8B=%|wu6Vp45TY#lIIyy)bkf$dn z$?)+%6%(u;92|sEik4W|+uP^o=PRZTTX5$i9WpzKFR>ZaJs(W(?dt0K&B&3n{jpx% z#woWiT~od5j)9DfjEf5o*}L$R6y?^b(b3qbC`nu;+MJvmet!Pu6;y6w-`>v7_4Rcu zOiXaGUkj7#q_VKk*3r>XTYEf7&^1avqZUU8 zO40Z$s{yEy7;vYi)+P|KdI*XTgcuK1Eej(ffk6Zg=kBo9_GJW@rn-9grUkR|I~#DA z5fc+S(ehHXOiWA%4i*;j&?m@B6p8^>=UmTJRGJwH2L}fO0s?5@o91MG5Y@iL&Y|2- zpL{d9V`My|Vq%C%NU&X=VCNSUm<}SnifnG3)c)(_puQpouc^srC0S!5tIUjz-vCDD z2|KXRt4XBaF=@V2V`^eje1mK@Ju7SFdkBfpYzs1yoyb4#Azcwd`da*>EYBWfB(DEG zQ-TmD-KjxFdXGx3i1c?Z0hpBdrwcLCSL;8t=&Oe}{^8xb1x!dkwEmfn_*QO!N9cwA zNl?F!6X`z**8jgVU2AOt(yNy@&K)UotjCQiJedUGaKFhg?HqR{jnVQ9Xj>s%e8t+Iia z?23IIAvLoq#&u^yYq0+`)V=#s>D_sbEK2)7`go|8XM)oDX=_`HPwI}D!D#(ZR=YDo6WC1pY z7+*0#tprD^v;GMSIh1NUUX=+D_&*xegTG-MQCMjH~)LpzL1A+ zY6DeM?*|RF5ZO5weQyfBi72HkXoyFVVsP=%J`eFHM8B?2Wkar=RB(*>f;l2+4tiwT z&7?i9Kfi%^KjSRXYTW&ctnjr=-(7})6NN_l%A<%%HrsK#Ghxv$s=FgPRlA3c*EqQHJk}8VFKd%RV_}Q*mjVf|^!+6-Z?}n>? z&hmcLLEdPAF1FLgs^X97=REg!SLLKEkEifZYjW#jPqUuboSk6jLs4tY8VrR|#9o7w z7|PpCTc}EIT0_w#PvpB!Dle?C4-%J5AM+p7!+*}lw`xcGqMS|~l5IY5wDKHii|GwN z+=}n@)`*LO>1ERSdWXlen^kl6$U_B=FJuN*_cC2vNa6@Ce-@WRRL}jlU6j~DZ8oQV z)^L~RCzN71iMMV<)GXKSF{=D5Y3145tUZ><*QLgAfbyYE*vkql-q1w<*HRi7;LIt~ znUs>!X3HeHVZQGbywyj>fM;FcY`$3f3xit&*Q#33Ba6>E;lC#oF z*V@~ZrI3ZoN|wlOOR~-g2(-w539>6H3L{fdI^qBa`pz{alw9og2EyY09sR}ms%dFy zCmP*-eOVb9!=s}i8~jLdiU30h+B5uUJt9WG?h*j9i+OA9YeSLva~IF;_*4A1*v%6q zxf3y8o0N~oHCqw`i64@^w$D#|zblEQGVGeivD@#`G!^d#FV3ft308V~I?9VaWz}zZ zKme7ORQz&?v+R#7YKfH^e^h+xWU99*k-1gTgKXu*(%T zO}Pxo>haEOq+^Ie*gKURD+>#PSc*WM{04`mw^c-*sFS+nNc5n7fqc?h0Z~PZF|OWW zD#4N`J(XT&ZiyG=axKzMAKh$nWRZx}4#+qtsY$bSQ|}ipeJ~}StbVUEVS;l%#IgoW z3zNHp6?WAyrz$bKDt5e;sAF|6nZRH`T&gd~l%1_O^>w$mo07X7s~?qPmR72@1aeY% zxRL$?jDyZjc^4NK6%~SD?u6mV$%Ky|Wdk-gHtyfQ57ylXpoXJkDLe-DkUo>{3vP5& zY^hkkhVAa*ta;4dKqHS$$ zRU`LT`snHDv9Xbn5>5YP-Fo;{CgdT;{AYW8Upis^vZ9KL%##OjK|w))OaUSShuhH5 zz~u^sKH}qZv#`i5Dd7+hh(9>qnyl2;*5-&)T-_QkH5(!e=D<8ilL^bF;o&*zhy*Oc z;%5yQe+L$rSNpGc$Bc87Tnmpt2RZe@4>l(`A}F%d;CYYZ7nOUtKE zKm43)o|~Jis;VMkGr*t){R9t6d=>04SC5qhrKYYf@jRi&!rWX=QIQBGUNMQdvj#x6 z_~c|VAvDJCW+=&12H!2rtE zd7V|Xa=iA(Joxd0%Dh-UB=6&#@3^ieJ`#~f-FfLkrWl=5Y@?ex$Pif_#8 zEf(BYga{%NlT>wkPbqQ9{@hITRjJD?H2td1gdbm;Rx}!p`R#jimf^o37w^#3ROd$+& zy1Kj=7*Js}he9zhFj6gyjEoWz64b;uAR(rgI94w`LT>&5Gkiols(^Z3#h)Mo_h&Z0 z-2T8ogmcIGIwPk}D;Qc0;~SJvlN+~=1dKe@UH4T|}lDTZBQQ(j(fQV%7t*|VCX zExOT1jE(?)h$8$K0)d*n5JsEeN$;1FlT$Mkx{37T6yOO&HiCV^L!cBK6rGp%SgbG9 z1jzv5cX)JE!ajn8#EWS0&#=pe94Syp#iqCufype+SNRIe>;#&K(dlvj!z=zX*ddas?ls{hvL0W!?I-%NW>WegQax^38uFYvk+IWI+UnNE;65S0?#ltX z1`+U|HDBaSYKF}(u%rf}@`3084=MGM(s>x{oIF|3_C<2_cI!u*RTW#Qx(4&r`Rin;a(HQz#8T61o8b4Rk#!83|Cm+^|^De^l8+#*&33kl)SE6G(;76U+3w!-TE9qLhF%`(!diEkglp$QrquvvCh8{kFFd1V!ETLQ-+~9 z$)Cwwf8KFpuXz9CoHyWcA0IY+j;zGQw#e<4^`z*`3=+Vy zF=&JYk`m9SMVEW$cEI+2;wCunp4(KGDTH;hnSS`-q>)I1R=xj9RIyA^UoOq`|ehcl@?8fj%w_~ zVVQ=$hmbs?RoA4Ew=v?*mvbbdS!>^m!@lH|y~Y5rBK(uL%ap&25PvV&v*v9F85Nri%SP2X%|aI-o_;peMgBBpy@D3 z$;n(JQ5J_*2VrsDW6Y@&>lcfT`>xWnYUdLvl}djq5A_qc)wrWTI9iaHaG%2v%{WtX zrO}&_xolg4G`O(CK``!h0-fyq+5_KP$$D|8SEtJ>q?Qq6W|H!cL6*l%AQh}4$a zoW?*J`@-n8Nemve^WO9&egzQ($MhGQso<6M&M$JBBl^7FgS_hw<+{U|-1vhPcXp(mcxU5e76>GS+f(=^YAk`jf zcyEwuDt{#(()sTO9wbiQwJ=$qai@#6GG%UUu715}G4Xq(o(VWF=ng$8$jHeT4;k4x z;5#)P9It%6V78wNX*h=17JFHEg#0bAZj#CJc03J04Y!UoE;T=5s0Su48Kfv_qFyXN z;5alvi4$-ZXYCz%T~hv3`Lm*Y9VUxateRhNLFpfpRMFn@r3|-cO{0mr9s?YO1WaHGmzhCa+!=q;J|*NC%#3i%#}7 zOySh!Xq`7f%*;$Zs@Zu?-w&BRdH7}@uqea7`sX(9A#Zu|p~gFg-29678?Uf#hFEAV@Wf2D5Ai8j<(z zB~xJFGkv+rid2b&*-tF%10%QA&`5AL;x7Vx&_6uvH!4U% ziI23!D(g2M*d!xa3f$JdX!Gq`*PF?+rWhR!UCj-2N*{`j**nMGI-`K)B#b}a+$3dB zU$6vrK)S|th!v5X-iAu5z(kjZHHfL(qGu|O(XZNr(2!Z4>!y~&25U^J91l}_J+rDM zbSLty$%jK(Hb$dbo1`SpqHMe-`D%@zN$}|7wf0XP>7qv)HD*o<4hzY|#9kP;p+DCD zhHC(oL~w9xQO^k8KvzwZ@k;&l_pzQ>B3nO>uNZ3Gy>oNTp%}c+zOukYi?=G(P?J>l z%dt~yIIY*pJA+c&Azj8W7P9!(!wlhN-Tr_i9f$2Esf*T%XF>S)MPL7?fg+FrfBU5L zPgMYgh@ZVShk49y=~actNKu)|)TmcMX%;sQ+qz(;^Sal*LRx?NU;>Y8cV$LLX-9>2 zOW?EVZ@Km!qq(kO{~q7K`qvMqg1gvdL;7s)ju$Qa;aa<=6nM#tRvJ+~BW~rkJj_%N zD7GFDx&;$69Xoq5tY&0rt24v#!CDh{ef-s01jTea$F{i7KLeye70p+g1{K+>F3}XA zb^x3c1)Q@Xc+9H`H9g148-YD|BTp^fHEU+9;?TkP>uszkct;u5IY#g4^R%lkn~fFg zr1z7uXjgqQ(d(md9u@Fine_VS3jpx`>+#nPSdHULcYNQFdvi^smeaf#Hd{r->7=;U z3G316Zca9mDG(|(8k^z9a?RlChQ&n>O3hSkqE_CO z)i;>Gxq)dmSwA~HaR3$*niFB(avuptd%8*PL(DRR!j!~BB7@xF^^+6#yp!3;goN6f z8b(%UZa!4fSAR2`*j^Z14;XH*f+>eJx(pGt#0>S~KF_Us$J2Fc{oArNywlJ(Y=sFtR_7EN%94ss>%vD@ebeovi zv;>2z$HK-YGX=~v#0f|J{;Ssm$E1==Wvd@ZR34skuZ$fnxUbFe<DD-N0WY*99la6|YuP**Lp>eN)3mm>=3*BVWWwOmoeK@c0i4dKV9Wn* z++<*Ka;a3gpe4qa@)wui%WN?Yc7k$P(=AMSx|M#R*3{Qu%+!0(($WG3!w)j(1O{Ms zc6L$$d6#GZB^3f^#zvXI1$^p3pjfG`jyfq>>%p^~W7fuj#Bv6>D6ZxOhe6Wx=Oi6Y zUYaeDw~$FUT+n22nV0FYrX(bAYPULA+1k1~Ig!^)6!`7`8BxF9Csj&?*JIyDzpezU zu>=UZM^a_;HZ-lD$guvZ%F_onoyz-W1;&T)7FYR&&6B5ucTj3<4Q9F1`k({Sjk5~b z<34-#v-D=i;uG6qzLyt|4^u=n47%KoaUTVwMo zeI`g_XoxwX&Mg$ua3Vnz9ka&)Bs&++qTYJ5c7~$Gp5Ka2m+@sR`__J4@;|hOB_AT#W$>Z?{bh9~@A4)>E{Zhy zQ+JXEk9!gt%jP}bF893{TBJP_xO-p*-0~W@_Hr(LLKZGI%_fHz&vj)8PZVgkfWwdrWWCH>FQ-_E?j~w;f37n>uHvP@a};MV>0%kzNl0euUHzoK{%!hgF(J zrT%R^rR4;8t1{s!-i5$;FP>uhXT#$xJavXcos8wPy|h}ad)5X92G5^A*U{0@)6s)E5=mEqBLVUV29^QJeJl z_X7?OAw>v&{Tk&Hy5K9wWYZ{0ycn6C*Q6|oIj9a&`peY!j9Wv~YQ~k)Oe7n$-@BG) zMzg$ZD{C10+#8TIQDUIKUu7fq9{oExAXs$Pt8XDq{u?0LQ}X4}+0;Ic7Id?eUzinE z^)T5f&~Eq@TAabNy&1QI;T`$;^8*#Lxz?ZGzI{7At&vU5-+AB@T3;_bXoRdJu4ic2 zXP|@a^yJACpgQ#Qtb6M2?99!`DDP&@i^w5nj}wJK9%yao?Z@!r@7<(LtjV;h4A+jzFN_3$9)|?>AV|(nf`?`96t=AV4g2>unv2>g^30&_A z?hbiKA2!Glg^oD!EA8X4aqTB8+Gn>Ka~m&cy&Oq z(=;78T3Nl5Cm*Ds;JeB)=71KFMjh(PRY3Z{zy;0XF1%pvjBxii-6{LqU0p*}-*>GjDlm$)p6j z`}5~xrAN{7jQE%bo10denk2P@%otqJb+E&?bZ6Fo0AN;&zEpEjE?}FQ^6<5l;@tQajp?wPv8?Z2UzkYr7bKv>o z59*x026}p)2dm+aLo!!ZR@PRUo003&^)CPoFP+vuIjIZ2E>Z&39Dw^IrJbqj{U1#? z30A=@$YDE%?E_?RUNM;({$4P}z9#%Wvh|C!{ljHy=@5rBw3Z~=W&}_>e;OYjPa*29 zc(tIeu5MATBXrj8dH#!?o?b=|X7MI0%+}J9Fm?h+tsEsJ{HqRzepKDW1%&yZS{enq zuNmmn4EMIri61Y%hrru76mXNj_c&$ z?06@D9gasS!py|{kUl$Rw!x@qa@G~C?L-X$wmo7>wF5?~Gr#$2F8qWSyMW#e9e zsS-UNF$~DrW+x_I4621PC&mm05CVqii1#MUx`2 z)Ab&)mpMR)(0xmPX#nLyb9rS&Mp~MVh6XOh5e*k*zIdqG5kkWDQBb7=j9;=}r^jJc zR#pPi%Fs~x$&*a&2lv??J$l5*_!eO0(9rw$?tSvd8~&7bJ3&`fRRZnm*X}}p|1+4j zrlw3AWesUob5oNTv|EgD!vtcPJb>RX+~sV3Lq9-4U-usXk`TNYPG(P&J4=OD8mTpA@Gx>MZ*C~Y zDV_NcWiYP7bB@!sN`X)pb)ur_=htygl!+5C9K5ePbyU4M|2<@};j=i$G&qp~c@;Sh zD~|ro&Q5yeIIFY!M~;uq3!rX`{@}^k*jEf8 z+>GJR2ijJ;e2QoxuhS^K7!5jB*7qYHU`Izsbg{IM-R( zGM^k#5)pCn^UF1ITx@fR)jz|m{g4_AoO*u#1>Gu8f|bF_CY;nEFNC?c5Cb(}%b6L2 zTn6T@p1&tuG)s?=tuN`C@A)5ZOebn=eb=n@xsbew4I6HJrwJNmpQPuPraK+n^~0{` z?p%MhT^PXCeuL62x|O~Z%^Wo@d;)^`g#`)u++yhG$CAD@5t$vq9k|+&5&c9DMWkB_E0*x`{tGO0*0;rKB7E9DfsAS@W*GI^pg6R>8Hg^XMI4nz)v-pN$ zfMLwC+GbT}1j=tB><`4PZ{n&;Z;Sw+AC2gK#(6hy-hBA*q49cBQW6lCqNARIw9FMA8ygD-`ggFfvE@?D zgTZ*(DCw~;-JJ%{laiuUFkjEj%>`w$e*m?8!JGjcIvBhk)WWeatj#&Ku@Ary)y>UK zOzehv<#Gr_bss1$H*i&%F_PMs95$+-v>Xwy{H1&P8`!6vU#a$i zee~AaC5OggUTplh^Ftd^?7!(0GxsA_b+&kk@4P9DhVko4758dH5s!(d&1iITIW&|VstO=G77su~&S-1Sy z_x;uYQMTHEN65KCs)wvH@wzxYyW@*6j93?;Pg@i(rmtrZuupN2xGq50wx68Wg{ z-uFjh68RVy%&o0^y1GKV1sSQ=3=It{iJ_q;YeRR)Sb9+6YcfPpYX?$ead2?J;J=L2 z0vkvrgu=Myn%*Z#68s@o5!4PMHcEsz6!Um)Av?7hP6 zTLg^9La$lH^lAcvoy~$c+U)v&&~9+wZyg=wrJ-2_jq+wD$J$FLC$4%Llg+KIVJ(qF z5+Ki|ZI*q(NS$@-)~$EJ!PQP{1adCc)+}+fJrCG30%^Vgw4DBK~_g!MhF)oOqOMELRXPOiX&L6aa?_4t)G4p?dy79W=Nvl0NIYP*FEpq@} zAraK_KswU(XglS5c1W#LuCS-s9^OwyggOSnHJ=_7m6w0t9t8>kjF(YK-RI{*wOtAo zEMqKuM|sR((u9y0XKefZY2-PU*TwXwi)=hd_L$KCWd$Zt3!Z?;V-(s^4HBC_4YOlZB*)#SStfFjiBj z5RR4oPbG5KuSWmA8qf*eK!Uvf&prQmgrDtt_wlQWOO^f-j9`~BaA$3D`v9cG8M~eL)oB$ve|KhXk58?;vxpe~+s^+D1K;_hz%)hb%X1~4_;4jSqVuMNA~x0s zT34jobE|ov6+md1Rb0i;G0R|%auuJ&p7^+oXyV$4kRg~splZOC`m5Et7HjnSLpu->H9VQtAT{FQtMP z5CCM4(7OG<8RY0EBvqdlv7DqnzUgeBTPNj3CcTVb`o-FDyZ?r&`( z6`X*FOsIn$B1WKKVd;+d zK;0s*g$M!>?6p-99NP6wVCyl)k0XBk84^vrXJK8_L-Tb3Qf%d$2<7b7oG+wS{;vTM z%3n}QPX*+HeSR0;iUmdmL@uR*zjz{_${V8=`7p#Was%D0*vWf+$A(Nm%SNU(Xn9bD zWBg!Zo(Fte4vVZ$q|hy3E)*PF9E*%q9h+o#C_4Z4+sLS3Y6kv#ha_06!o0so9d8HLfe&aoyo#P|Z=5%~?%>5tKJhxtDsIx=AXpbV@cz~P3roqh z0{7P$s))zlgRi2Ggt-&sVG}_uEuV3Z9HBEyME9A6Uc{I!H63pc_W74S;kgaf3`UR* zKzKl+^WJQXmL?z0Uq>aFBIl6i|97FXMgIFhmnBgI zs7034Er@G(6}M}-92rO?j{s-isvoo50vay_6& zPT^7_%vYK=p5qcLIs4)L(H+%qyGPj?uF{(#sw`@*;pjzwr{Rsl!7(Jk-vLXz1Gs~=lJJ+gr7q1c8;OC4Fux7G@f$KJ2*i%l!(smRN}0s5 zerfib$Xn3AlXlV@NNKlHK`fCc6+gvViilb&=x@XE@dQN=BZXDH;+8VG!S_WelMTjm zmGxd@%Hu}JUJI>CE;p-+D?(pS!eY8JA*vVPP}uv!Hu6M^JApQkZlpI!P$nczuhH8- zJ&|;P^ev2TD^tYq8|{ER&u~Sl_GiOJ*t=fr69yTEjDAk@S!@6Iub_4rQI*)9{3u3 zoH^L5DHkYAHRrIRgAdVf%&DOwZ!!L@VTnMA--Ie#>Zp>|njMWwNYRUpEc}kkw+MlYq{alY4nX6_^fV}mRtG-6EkegnN;&OP z$a5^&?-8bN=4E zLjtG{c|M-ALiElIrYVQl#e_45dHmtLw)jBTQ18eM9SsSk4KZK3jt1`w@TWZ-rLGyNV@HzU-v+BJR<~FyN$kD?-l% zre2?;*)YJ#bZa#~udBWd`p)?kpU=fNrtJF`(qHs{6(K$}Zpqy-M{cc9rDW_YW}L}8 zhxx9k+%O#Qg(;Z=BZp>*I)_?r3mSRkbcr&0gaLM%U^b=2A!`=Yy!HHbRY$l~$-Zwg zrjeLZ`BqL$A5|yw@v9D~TvSJIoH2GEP|ifJED@>b<{GN@)07JN`9}$J^ZU~zbK4f| zq+<sw^&HqOPyi% zqL8oGslHSiF(;j7;#YZV#Cv*m+p z3Ooi&zb2k~dMIA3+>C<7i>S4=6);&?SXdgMb*4>9kPsLD7CnXW#CLgWe)gM<@8!1l zVam)SmfRHLzQ}g)<(8aW2lxywFevQG`v_1xF|o1VGc<@uNC3YRqHrIemcdt$5PJ{b z7_aJB&!VX~hEBFpZ_mTEzfx((D9Qx%UTJah%gw}*;<;Ybq={qUn$~C2n}NPxV9I8s z+5@h=tK2$~z0Y-?-PNTB(PZjN7s|Rm!V|g{Q`tRig|`N^vbu$bM|t-y6v$!^4{58% zCnsyYFKVi0G~a}VMt3dE4-Db}xhg4z?`6l*XQpR|>-uL~7T(?>R8*#K;Kv z#qZnrBSS-O?(WYbL8A+k3jjof8kCFl5GFkQB`+`S|6%UCG?Jrh#q5!oXvWbe)IbyWBGeD3e}`|t0c`|-Hzbk23X zulM^kp5t{ze;7?q!0N=M@_8tQf7M(O$b(Jek%U1mn^^<$Ht~z#7Mz-zy4|rFL`XuC zO3?QbAe@2`08)GW#OdhhWMpOe`T0M`TI5X4%n%X5fP|3PBBa=3JYF82FI;_Z(phM) z%&4Ei04R3nM{jR0I2F%FHr`=_DI7B(;TH(e^fR!vwpJ}JprfUwEimtjD+6H5V`HAN z|LENJ9^Ah6zmX_^7OiLep5U0Z1re_14`_*G@)mgAc$ANdK1im1J1+b>Iw}g_S=npX z9y=0DD1&MT7}%$+cjFLy&zhut+Qg^Bg}f&J02~Bw_{MeUP3M&9qc8K>Ts`l;`0c8^ zJmDVOn{fbNGBPp*uAWw)G5PxSYbU`RRYCvb8y(_EV(`LVn@By1P>RO_Z`+CbAwOcO z@CDov_atqV4o(dX4UKoz*46@X1w*j_jjeWHVW&MacinYCTdqmQBXF4KXX8PFIG6<73?U>eT z&}IGQsr&9y?g?A&jGZbobtBB@?+~6oU%-L^UxJ*1Lb4_R0hWbZRw7uBZC>70icZoO z6Fogz;ON!TI!Pd)-AcM$2*V(*{ZPzb+u!qC5;X&UADT5eB~W=SPZkf6D%u8HF%b7z zCY6^KWLfxLIN2AA9T_Jlex=8))RdGlT;(m`*XU)m zx@cXL#z;9eKX3bNqUCYMt(-6U{vsF+N!{S=4j@P5Y5<=qHp(*30W$+Q2%4Ik8Qvi? zD&M_hb>@tOstn5>{cf3mb`PK_#wuJ~owcho_I!@>zgbz&enQBJ*L*ZS;IZ6B>klf+ zmF_qAk7am2ht0lWG^8-U!DiLMb}n%(j|JmV`D!;t6fkZ7t!Jk&g0=&08}s^)0{Z&; zq;OvjaIml>rlf$*<>(ubu9c^@>!5udOF7P0&MXQtR^eJ?FS1DVQ3#8O+`af>@iMO5 z+(c8fk|qrKQRvo4WhQhsGGbBU;^7$r%;eJRCcv$1UsUm6{39aTy1J+-%*pG3Z~GCn zPE9^FJ#AryA|WARXHA>9LC`;e?;>r+4AJQ+F!sL>7JvYMHhEZ6VXb>AJlF9me)?ls z`|t3dFgzm@4E6Ep3*^9M>}xzS2jSoCKBIfF;@wL zL)foJ#vo}ZDRYl>0M<6k!}d@~*y-hAGcGvBknJC!&-G0`4C7MmK9V%GNRi2z`f9hXy{0$U5l4J;=bt*bE% zp`?HmVqD%%FT@y?SE783?3h`n;7$pBzxI3>?*ei0l|??s4%o{{siwd>1@l1i$;;Mv zzNCm@&&E%oo|x#*UJA>=%h@_VW+fDO@mSOD<=}-b;+f!baf!8F;X7X={mP!)t)1UD zvl321@^W(nn!|SH%rzOAr=yrh(94p?_ZQoN6X_X0ABtV-bgUeld3K7^)9=y*U3S=P?B5pOzQ#+#Ca}Jq4)fT zsP*Ihug~F#0gDt0NO?#|vQ0}}L3z2zsZ;mY{olVwUQXXbBRdiTest8woWf*2etCwC zjXKAD&rQkl?@+B!AJZwxS;mx@g|KA1N5g5P@nB&ZQp#ynEK zzjI>7a<;KvuiFU&9#jg@g?(uF)MFrJY9v7kX4Fwk1ursKkc5?-~RPTgKAvoKlJk+yu@(Z(Oz~5W!`p?1l?gI$eHpR?k-ikls3Mb)pFaov&4a!-5~XTj;IBHVme#Yi z9Ji?cxv`P3t%EoNF4s?r#K3hGIznRGg;B-b@c?2HBI&eBlcN}locNZGVHOo+sT3N8{9eNIrc{|$h_ zF2De`t2-bm7<=pyL(Z~BB0ymt43rP)URkQcUJivowN;*3IqpkMDL1m9F$nTSOuHTr z?1v5)z|yf!1T(w7a^PCn69{(JQ5_e7FLy#Z^)@d`TEWPZ`T6XU&Lg=CrOxdCci zD2}%hFw|TYn)G*S?Y-$TvEaa{dMiV*R3rb6g2((!Y3Ps6_9t)8yxxhs@x~T!-{4yr z$OYsF?ioo{ZG>gREXxZGnuRjHD9wVM09K6gru*<$Qq^$`V$X2H?6R}+xiWHLPR@*y z5-o!t)mcV9K2;UYVu>5akkrxqYoxtWhGg)M1P>1foD*DSodT4YtD? zjq#MS9ENXXd=FB4E(w=JX?}&$JR}HV^E^fleUn@V?Ri#gY-MOwXL7nYBkhK!uTRzT z*w_N$s&AZcwHiuOeche}rO*YAd13cAJ4Ke2C=58zBTf*oCjZIAW z!e9nOUo@|wL5Y20P7Wyru(Plbxhe?}1uwrP2DbGU1?FRF}{8bQ+8h z+1ZuY;hJBLjf!HoeHdukUHM(tOoOnX%bEk@##?>7b*_3{ZF4CQu{Rtso>V1L;VEfC zK|@9LXu7Mb%c?uu2#-v#kqi;WzX=70G;Swl^{bVwl}Ri+>?kXX=VD>KdK8c3A@d!W zHB!HSKc29hf(iPYYL7KS(xVcX&Bc+U^4SKJ!M)N1->D>~vPfg0-`&XPz?|o+Msg0~ zp2JX+;`VVQA{G3}raI{+V=c|BKUzBX$r~gSu@4GPcx%r4%iw_=kGDU(QYY(&g9?wE zS{1+1M8bR@Q?zhhWm5H73{!;J!ce7g+3VLh+d$^^0smk(T$PfN^56kN>%=uYGb>SC ziU()KJlBs$vIw3#cTV+APh^W}Y(fI?FA{HY!;vP`LKM^v8}oxI`-ZVng%oF%y>vID z65$esqir}ahCvA%rh(@$*S%zPWBf3W^{vel4sOJOKUP^q1%wI-;@%!Gl>=X(D8HcX zA+u+LshHR1B5bs}x@(Gx$AuNX;0t^gbD2H)T{>b0n0j;K2jSA!H%K%I;yv zK`F5@x8gbkd~JOJiFFy$iWpai4ZFQ3MF|V)rBIjUS82?pk0s+ratD!__c-b0joE~~ z^KC5up+oPw>gIhpD8~Y`(j?N5cgqlWJ1A(~*SF;9&4svk?t@GT-Noy%0jy4{HnNJ_ zjj?Z45yV-jGmk9r3s&h{fd>pcdKI0L@SFlcTp4(z_L1FS*(Cvi@s$IYkH9Da+r@&x zu*#v0l*?V_-Y%}JPOCet@poFnY9-ZhUrRyC=GIfiH%Sr+xcIfAO8o>I&s-sqMIW@i zcu=#%rgF0SEjbkxn@=G?+1K{~RC~HQI=Khv z4mKPR_AdZB#JNZo6 z*F0i7KqVuSr5Un^J9{VA!`nQ?y&dj$<+O14OqC}!iB+x+CN@9-3nuMg!MJgYALOp0 zj^f>ZKSSuOtJ^uaPi{k3h5of zl_8zu+!)5acM`+9Hk(%zQ8Md#JdCkJn^x|yKm00;jh&rWSEoCm*Xv2G-rwddLa(w( zTF-#g(%M*v2dC`aJBlB(E52KHAclbHHa&6|LfWe9Bbda%a2-0Gn#x{Lb{iG=CS+zh zuP(u7TW&V&!UEW{aWp%gE5U)#6-E4tC3CfEeu^9h|M*fL11Tl+Ci8qAbW#0MK zz8t2wn+v-Gb4;-tWnJ`2>u%K~3X=jt9ahyZU9W~%*?%k_)w5dke6R0+;VRn1!~{S9 zwKG+u&U$N=|A)%?2a*;D`wzjR45;4Qzjqb@afkOo3|1yZW(%nbv2sEI^ zlx}nRlpQ4CfKDKgQ#5Z@Y^4mlo*cOKVWR$PE%iSG32Q{NOkx5 z>@a7s_lh$`qK&@mK}=#TZw{JB5++Hn zdN(KNj0!fJU+|Mba+5!~4wWu+1hjDKj?;e(4H_+DoRv1mQ87}fCGnHFQ=DF_0H~z! zkK1jJ!o#V{H6U--UKaE{L-*|M z&o7v_pRQDkDq2-W4!H5?ln+d=zzwSXBU(VMF?)_eyuh#oM0)va-93+8%h%;c0rXi>AzFHw)9e zQp$2L?G~Zm9(>niutdZ)@;EXQ_gtqA8X6e@mc~BsM4(z`-aG3 z7ZYwSu1_HXrX9FCR=DE^mP}IoeTk%7A?shjLL=?QLb3Za4#HRvg2Cm^4sS1Px}vuqi+o0`b`EpG+CloAKehR7!+V!}Uiidxl`ZrIRqtOBS=6NW z?Rl|2yl%EPJoUk(nA5h>yqRMzz@E21epWpIZddq$LDXru<3warB2rhB(mdH8!JVjq zM!C+p+^cdgFZTSf2n*z!c`@=k_P-t|oN#$k_L1aeMOqX{8Gj!5 z3%EMDWvT5QD*62hllOdVnG#|O4_t29lf$#mF!DiZ68>#nCi$gpDUajo_(F}=$X&$} zjPuf(5I%Pm6(xl#a^t|rJbHnn!$xVo2aBU4h3ZQ9AGa0yBF)4V@7S-HSUn6HC_TfVt$;cTf0WGm71M26)TZBI!+){i(j|(@*{4-?W z!94kjgFuAxBJBYIZVkC@oUe%n=kh0b_l0f^a*l0*t>^y z)NzxCuuuCES1(A1D*w8RN9gDj{J(ub75&Ho{-NvzB0d2fJBL5FJsm-TQ?`k_<@4Ts zhvs|7?8Hs!u?^k-b+;`$5CtkofA?(veV4zg8wlU*{$#ZO+AmNjB(CboTsQxhkY;6e zp|4P}=qzm2l*x;dl2Ry5g8zy>Cz2^@s3UGu8=Ie=x_j;cYzx8yhF_bM149l<>4k?? z-^8NrjJ!Tev#s{t6v^@olG_SP2LV_S4~E z>xz1DV000$R8XeqFb!G*uZOK4A#05j#N4llS<=tG5_KQd(7$PAAO8tw zyOnpNQU2{w1-iHWb_UhlsUs~{Uf{-Tez~}~QX{pQaUFeEnL)TFawR|bX+kPa_dp*u zwh$KJ9LAe7II*WOPi&SJ6OkEpyNIj;(qcA*_grJxgNhq`gIXQ!L!G)Y2TN-q8fuQ) z8l82Er*Y`Mwl)|DJM4A_CAD#=D18q0-5ehc;IyC5A3gs(l(euHhEBFJoKk+?BowX^Rdv9lbo_;Pt$>*ixPM7}k_Z|U+XW7mq z_l5e}(;O3hH-sTzTCa!X-xTwr&#dyMN(Sd)WHkRKKTE`*-m$`R>!ebvUw+ zv~(t@Ntp>(qmZZ~1RyWEr!oBdyz5lsz9r5}KYIBmWwuSbsm!Oz!-?{uf4tRU!Mu3m z?j|%K==AM<4Q+LVxX&ierGz>O36JrfW)DQz%hATMFCSDE7v86tN)ih6T{E)P96@fp zPUpg*QwU-(NyMkC@Z33KT1WJxkgL~4|L)9IOo%ZZg;HUB{!3kYfT0dc&O3;?rS0!F zLtRF$-qmJ~?K8`tI3JV2sUAcl?Fe8?{XL`=Uqcfu%csdi)vv;kHEj#y5NA*b4ry^s1@Z)lTY!hwI{uqqks#9mQnRQ#vrHd`{KQD_h23U>B80m zIy0YV8Ak7k6F#b+VE~{3;^x&lL}l*m$%MSm3UY4N!G8lJD!8%Ys+G|uuQ9)s3*8TP zNplT^rdHFl3KJ*O5`4%(_R_~oQ5$lh)7x_+jJO$>XmzZl?zR6N#Z{N^;BxnYoi(GS zo#pp;^{d>J^yE<=RT6^9zn1j}k?wh&^x+~95H%S1LDCd}tX)V}jz1Bt`z^5~*rPjp z@L8=w%zO1D97nxdy=B?Rg`TCe4&C8+8%z?u+d7m?qMQ5%`I&!4jVH8D>Nn^K*C{rQS31YZ-zgEelQxuh^@EXc$9;M>+kr zdZi_GSvrbs2To9!pZ&4g3;V8ab15z2Uiv{^$qgOqxSf_JXFg(#voamH{1Pi9CD6pAmvtjV>V^wj4K!@w4^foqoYuq(b-0Wxo^DY`_6bIVL|{iQS9YlFG&+J)R$Ani9GA`^p{Ti#@+u817Wjtk8B5ob{Ty~WDcs+%db zONf6a4tky%3trJ!f8-&4!x~ecEs5rQ`O%hLa5aLbG2xU^?tjMD8QM>rqHZFgJCZFK zoOFIo#ZbRuOX|Etq-RByc5JcHduab64Q6zlCM?YyVl!;&)cYs1ryO0wJQoEAbK31^ z24uP?ZM!!s8YqS~$AG&nNGL76vgIT`47f;RV{nD@GoXw0b#$IaTSiRdmQ^@79~4H| z5tdJ=JigFFi3+z42_pToqU<0NxN&pnl>Y6?n{PH%IrTr~Wl8>o$$nLs08CsCDmLjP z)^E*pO6@8^w{C^CMzL(n=DWIo?Y=ylJ%5ik+wFw8vswn{+RV2Cq9Wo+gIqhVc03Y> zhan*f*RPk@k9>&aymhB6E;5qJgP85{v)<(ok;cooKV6uQ1>hwp;!(?57;xkh~#y=Nv}!J!BC|PW;tSUk2e4)i&!b zLN8ueM{Jcn??wHBUd~HJh0)gV;Q6FU5NF2F3(a?z7vnm%)YY}`lk^7!m!GPCQes@@M8 z8yf=`!(uEDiGaQ=-Rhj4pAYP~{X}c!`*+iz+L0BJhF4;q05bbawZSyLuU~Ozp0t=+ zSz9yE(UFq|Ai<7x+aU?ZlGC4HB`KAo;S1n|kuP36dg)dIm@5VY$OwliN-OyyH8)eB zpFB#lw7yn040(|AZ0o?K=-c4rku4|r_eGyRf4DKJViP7uSqQH#dSvHl_w#i2>hJzi|U_IJEqQSim-o z2hp7%hROW_qp>yax5Qm>I<|g1Szcb=1FSI`8k8i{m`pHi!m#z4;M>J8=@1b2v&wP! z440Yc>7@wzYAQh6QDui9W|me@YE6_10_dBMrkQ{{PQ;Z2Sm`d%iI@;czBDq`I!#yV zu?9m8vfCE4z-pXhSRF-Ozyb+OP=j#QaQ?Fpv*Ei6@=^=7vu8~@&-vfR^~;J~@i`iZj4mn%vLS@i!1et@CdD$V0d>r1 zvz7BD(lVNAZ&etnetWFa4;c0=k7f(F9yUOxC@su z$)*AVaMO=7!2oX%dXS!7`0>M1Z3OICM~_<7#(@xs=IlMK{KrfME1OFuYQ)HTfVwZB zEPEKL7Kj9*PRpB`nZd+Kz9)|#{}J}Ft|^)qBr<$?`S}knZEP_TS?=GX9l>JjP@clg z`);fEMdZ~}g*6T_5U`XyO*gViVxCypnE?7f@l(=KKg%9nOymcb22H8S9{@}O3SDMD zg1F36CfswU2f%+Y=;3-c0ce_6EjNb;S+Wwb*4_%hoOMxQ{~`fOCUz<@G1eJ;N=pY5 zwLzpHaM8jkvy_|}q0++ZY^XTTn>kF0>cVzJ*lxbf{jZYzXAW`Khe@Z7JG>q)T^r@5 zQ9k7vf!)3q4VchL^=QttnirboE`>5Kl`~R78D^ud8@YZqwWZae?*3A~^L<)qKsD=-Lg^K}JWbHGb-K7RjpKpTp=Pa&* zu!`mq8^FbGx~d4;50($>hC!2>7>b(+{K2KY0HSrtGLaLYoh7B9bwwPX5Pfvk03<>TOJVe+utO{JoC_L)( zZvpY%E)^h0Nc2`j=BVwj&*>H%kA6RTsw-){B~~!T4Iul1 z4H-b+{?@JhYMq~lk|BsU=3dx~RK4NlGIwm(?OyQdE^yfpo@U1k+g2S>a3OBGk4g!#02t~_s_hJAohS}0rQ%gHo6*1P(XsKqDAZl#aoo~L>8?!f3otg znVRAd)zW+z#&$3GR5=!{Mke>O%AcxIwH#i+ElW~FH-(AhdeWGlFD2_#th*7p|r^cDD|q2X|zNduhWTrLMj zd|Y9&i0+vsH#U^a{<^BNo@J9OFX}+tUK>AqRsTJxuK<*{7JpORF)*+@lT!_}SWl{| zu9}kbM@jsG@p(x!yuyG-ujIZ`g#Lsl!h3b9eRp@4P$DUZ3cDcxXr^TU!h2S4lZ&5k zdu~K1uuszB7HC$<`fli@xT|vQ!YplMQ+3I)R56p4q4g6HExz2cF$m(=C-I?);*a(D z!oJ47*b??3NO)%Jh*$r)smZSw{Q=(VARibMl-N3&$^hO^*xNudCFO%ci0lV6@`T)e zFX3Gj+q;fY@$q3@7l@R}IBEoR?h`zExpbIQF%d@25 z&o-CMYw~Kz-R^Gjj^+A~t>X;^2O=6qgax<&9G)+B+vK1!Pis=w5L+!O5dnhRS14*@ zc~H2#H}Jjd67yu;BMLI@Rj+GCN=hv+_|25#h1ZrR6nZXr@PfCLMg=xvumOdKNDL9l z-uP#V=?@#Ah)qxD5D2hbwBtA>sw>KNpHQliZw!tsnA*MfXTF1rJC6=Dds26`E|~`9 zCwm+Hn9|lSv9Fmlyg4QQz0z$dNp<}pAhk#S0%8E>I!~aB_Eu?%!+Vt#=ksnk^F!~& z@fTW)E-G%qGToh}CY_9iKX#{=8V&W|&AI1aEJfeO>3e|fw4liulv?S2^_9)E=rhi? z#DcQKvcgt#g=RHJsrL0~du#6h=)-`DjyQuk*Jis{D|n&h!<;5Q%IEp6&iT56P3AHq zBS**l&9-m?C(2+Vckg}A3km_%;y#7Yg(wyr?3nBfrE$_+y!DP=Pl|u2{feEk-;Dio zv^kkUjQ zNHy!6rk;YD+P&R0x%TvjYZLg+IJ$b%rHbh5=<=ZNZDMb?XZ(7hl49O4eN|;>p{re9 z6$hZG4eoz)sxo&FVGQyHpRF7ty&VqvkCx|7KExGfWtG~klf!?Tk#TchT-wEDJy4+? zFA~9c2&R>jqg+bGXj3YEeFZjhNbY2DVm?*BI`?o>_9j<-DqKq?7Z&t{s^u4$4RP@_ z11gY^w9UhZTWj!1E(&=73w99pPVji<-Z$46>=`+LDATCmhs`n0BH?6iqOU-1)}*la z?XF&l{ncsi$%6M@h(Ue<-+8022@8tQ4`Y*HeVxv9+PB&}z&}FHU%qi5HVf6+M9N#* zlPAeGbWYrMm6ww{;mXh*rgUQJQ<2-|FNEyBaN9_;#xR^dk@DG$S%(HHk)#gI4GG3{G1Z5#89*LKUrxyqoK> zwY3E@I0#4z3JU4xe6q-b9G0wx$VMwFM4KNF6!+_EY8F{_^Ed?IxT%1I(9|^5nfX|{ z7L4gGn92gKC)|32NF6b&#}-63B{8eG<2y;H8@Zd_2zFj@zjWe1!tEo&eu7(%96*m(z66(TGiS zyg(D)A|N0Du^*%~YQd<$>-BYYyFOdG_kT7NXBAQNv<5;O>Dz6tDM8fc<2j_J!`_1o z`}REB(kNlhz4Tx^RuoC*7Bm>|`K(v-Ut3(geU}9@RG8qmrStF+#3-?9z3V^GqF;61 zp-x8<%~G|*%s{}*z$`q#>NIpS8aj(LMYsGP`h3$FvW8iNsC{3D^#k{Qm!0_Jw+)xd zXYZAHn>Y=PUfzhDy*aLQlLHkPeZBg@5tPr$_pq2irh6_flJO}=Ojt1B9vmI2yf6lR z7DF?H!^HaZP?tq6`MMkvA%<`$RF%OYsG?F^3;m_{zMll4w4d%nAREK%ar)+#QX^OQ z$Ch%+7S~kfMOt0vPzjdZ2}`k`j_iv!(~SAbc+{?9j{e7*>T2%I#_dw+&M~+w&<; z#bP~!O-VliPiLCfe{yGe*~sGKrAD90hR0Qu6`jL8XZM+DRU8I-WcYI+O_C3f&Q^qDn2 zU%q^~_6SO2y>b_uJ9oxGFEA=AfEIx_89^uNF9`p+4izd$Skf{w5Kd}tJque+BSMZfo0bqr>85z6Iq#flr|tKFsAc-u*v-%rr6-~X>58T;DjAh z_*h$2CBDQz3#yz~9xw@o9W(uaIr#)W)SQtq`hTE&mPT>e;q6Dcti5{@m$lW@P~Kd* zFAo>yUsO3$pN_LLXcSdsBY~woUsh#M|HNR1%lh8c=VxguN5dJa3LRvJ|M8)Sc_X5D4f zLW}_oI8{_fXQ%UIDu)jm-70P{NXDYUWcvB+7YtJ9WWMvaOvT(VzR?{3}hAs zsJyId+v>#*R_g4sD-w|h_4BLVwCqe%cECZ22UEyLLeO7O{shVX7Df(fLMW4)=3;eL ziy}O}RgLQ2e{3Pd*9@>;m;fFE&%j`9UD-|IUa1pZ)mtaqWQy-%RkNwg)`21m764ccAbcEO9t`T3kTD{{zGr=uV{G zfd2xty%wAK!?`%mqM=B)CWHJOo0gYkHzP z&=ZKc#zOAE_s|YTBQpRA`^`)e7Sxp?=2nQ7-gXFHUa(Yb8ss0* z*4vuvGKkS~*gj?^`c-V+wIy4l+8&fB8J>fF9Zcn8t}Vxo@{uzahA0S~s(}&TqnvsW zL%>v!M5l2~hE2&g)^m=3ueDX#oFYzcfCEO~FtLqlyOF1+B&fP?E+#t5hI7|300`}- z?klm&kctHPV`y^q^Bkee`)ZNU<^_ihZSigoKYBza=U80a~rQcP9$1x>fmGNc&JW31Xfuj*jGOd1;RngdX71 zi3xYU$;glzRtz>!3vND0}5bW01n5!U^JJv05UBH{51S z8cZeoOF>QnzP?-w)w35JQ^GZ5zY z>WfQs$z55Gtc;SDLGjU7I=kCD0Y-2rAZlI|7iSv%c<09(P=rTs zUnrdDdsnuybjdEb%a-fpOTf1Pz6ttD+7SvZTi(zOLT-8-ffI)J+`f&u4^t=$JDY^^{Qg0) zkr%}|gYU!rAIYP_5BYThNumOq`mdLS64d%Um**#|MV_xrigZ**SNFv4LyGdP(stoZ zxl(M5u5z+;GrkhXdqM}u87%4WpYar4q@ecmdu1<#c(Y=4IwE-AzsP$i?`}`q`Lq&L z^ndgnieIW$sTA>Ea56UaR9mxGXlt$u9h^2+u{w%9fB(jCgPeo!8Iq_=pv;%2&`l}% zp!?BoLMx4-$BmsI-nZPx%)7r042-_rr;$7v{eM=-^1uxL?2<=_1bVN5P~#Bi{!8>R z*J}2p^a++!JruSuiBnE-V++y0+77rFh@pp^p5SlNx{~V`Cg*zfFKQkx97|xnYQll< zFjB|)S6S2lN?8Biy1#`;Xpo{DJ~>$7ctoW%>@!(R(+#*<5Ze4!bAwn5Sam5BJIjAD z5r5t0FXjPkhMcFjZx|Z#{wV;ssi}z-0H`P_g?TA(oP7{~4Xfn*x1Rp=#{k_p>xV9Q z3`hQP0Z^tAuv`o5`nN@?yTS2W)bXDXW*=>+3-_}*xx+p%^fBfi{nsNXq8Q~#)$67X zMGgTcU;dTNWcg7{gf5p%=}p}&1>9En`Y%wL8|$6!89mV~c|5F3H}1CtB2ES|r=PyU zC2+A{VyM3CTT6p=Eo_EECDhuBr`Ce-Z~EqZunHUHB&@6idx`DW(X@fR16X!HzHLfI z)9*j;Igsz#|0gOJN^`gtYzP72lHB#{kLeof_Uv*onE^ec#=>5gt)B(3bB5kNtA^b# zKMe`AAIV^gE?9}4x`xjJMungF-%o)Ki~lvycVRy`&Hv;=B9cFpMG|B#j{*8Plo+hW z$)Ze8N?}z)ee`GjAKMW9-UUCeLK2^k__sm{ekT!0A$h&-SMR$0hVxJcbeLQH$s~)o z|J-B0zH-}vn#JE*_qQzWzkXc(7NJ1x$7@pAw`EDxWxMe3pJ{_}Sme zB&_gge}^~UpMa&mxNtREb5$Adbp?UL!LoR z_EXt|y{lj6{X}vtw{j7Peg5$??eiVSB|rSSs4rube=X!0uL7$VWRhJ0|8c<#XW9K@ zj{V2ijF^$!ff)YtQ3^?8A7z~)bSj{(3U}EdXEBoeSqM!#1S4851U`fV82>#`ToF26 z&$S`5vzb_2eCxcN zii+%KnDdIvnv(`eZ^d;2NZwN{pA@>sz^(!K=kgaDmH+&nx*G`Z@lQ`+z}^zZ^c|yw z&n~p z%AL-C`LgK-X_84)AiSa~n@1EZdqjFtnSm!!GDKQDQQ^_&UP-!JSiE`sJuxv6l?9Q(JW#G=TFx`9XuuzHZdwA=eZ_6-0Q$x2Jt&_Sc15-0Q|Ha3J1V945c z01AV=LP

NPDQlykD`V^YKhztgQhFHOe&aTDk8Z^gebQuG}!S zGcGDB@NVK=4iRBect0B{lPK~EUFp47*|7(L>&M7|ZOw!u;% z<(A0Sj|T;+9zIa;kplL>SLrmVjL$iK@@cSTu8mN}UcdQ#XD1~C-AWP;p~%IH4+uVG zkWD4{?$h#wKLSt}mZ(M+yhin3U_d9Lrf>%@%Xe#nCblK8x8Cgn0}VC37VWWtp`mbs zPk9_C(12uw#wg3nKR8nB&9am<4se8<`39**ZTFI+KXNQ(?bCrc?`Fuu}^nBNzW$Xv=$!loIyz@PH9C5|=L@yB8?Lbj1Qc*CkzjD~w&;>ue8;j;*b&3`+skioy0rsAWgWW28Gv zLH{nQtMO4F>S1q6BapE&cQa$sL%UR4s$L-#?kzfgtGQs>%p>ozFvNYVy9VaG;T=2X zkAxS{e@IKY3)C>YtoZDrJo#-^6v^I;qb$M@0I}`O?^FE(M|jl;{6OsA&{$&Ww22PB zvznS3j4vLPI9(2xcn_B@kURTi_M0~-WdGh+%c$gdVBj$h-tPc%Vzmb{ zp(3E4{nyAC)TMXHUtCcR0V0bslLitacn@r1Vh{=A1NkAK0aaR>n{kMoVI(W49#lza z>-`6Pi3Oh14AD|hu=0!mbzg&ZVfUTm;UQtc7`yabI1kJ{eMUhxGC{}z@%6RC-A>f7 ziBhhuYtoWqbP8UJ-;P8DtkE(sfNvjyjZhZ@m8$KA;bU*0 zr5pwyqVOy03EF`H_%k$P3xpHf7vN2lXK_x5>!_=<5*n?HveU!+wE@qRl9IyC)_iKt z_tNTJufLyhQdt=oM;Q9fuN?s}RDSOvpT#NLC+%OpJg|6JQh?sx_Hd9ozFY5n`>sRB zag8XRjK}9g$}O+EzNLOjTpTsuiMmN#v8Sh^Li*~dt^Qd8fwCi7?n{VzbvG(4o6J`w z^$jq*QCs^(No3G}U#gZYS}?w#pdc=eL2IGZY4Uc$a@wU+_Rl?}>b?%iRFpey{%>Kc zudsp#U<_$#X^1Q~=H`zj(ue^p)=H{a6``c2{;-hZd7VPC;QoIkA>jxL3dx5qe@hMi z2&K@F)`Tl$8cd`0_XoWH zutaJ&_s^Ocq{))_+BERo|A|b07vf{76Zm~|hvIMo+?OY5e7++O6FBzM5$b>8xRFe$ z58K;q4Nw0W7qJz?pZ?H)sSEz}*3GGU#5`4lXRES`yHM)I&vA8v`s_8a-|^`1JAk;@ zT%HKoC6g0KO-qxlqJ573kJ}1?hp$kdGx)ddK&IB6pYT=@PtzKI1}(=mT^-P1{f?adV}-&rZtSQ;X*^k@|P77P=A^G1sKx3qt( z6ZFXSM7KcoTkkAmIS)EqkY_Ghs4cJGV^=(ZAojjC%FK4Jm^7KeR8L#;Ky>=1@h-pG z#WCj3KK2U^AN%#1Z|qj>c7E<~^(pP{+U& zN&=Chs-dXo(MswJz5&5!(Kj_7-wOUHL47NDJM0qU5^2Ki$g;k`YXn`p`-8Fzj=^~k zAB{*q@4Q)*bb`ydz?Yiz-IwL|Zy=pM$J=qN>}-OBcUW}CjbU!npqh|I<4D!gZFLIy zMG{34-biYnVBve;*R#r7*7V(|iJlG>C>;mI-_LXW`DYK?Wo&3^W8=NGJb|q>AkE4? zOpSS!kP!6jDjrL35>5w17{C)Dcp68+d={vvV7v$vLIe^??04t_#(e71FIxs{FT!LZ z8VWEzLE>M$CBRu2!N$YI^)To*y!w-^W^5Fu;%ZgeN|=*b1?uY@TBCE z0HjG75S{$|6x{uLb9g+;_sI(a!`O75;dnSY-ta(~%hHv76!`HlO?4B>EHLt2=asJr z^Yil?W$7Yb0|dB^5QjR+yTT%gQM4|VfPpa~K7MX$>ajue;K4Ctu_euiL6YcpN;-nf zqS8O>{yuV4>t`+@hh559Cu8%g=jFV16%`kHEAb{UjnB?1*TnFvuY@y-9g$E5t@5@8 zrTkEZVzu+qdCL!aIeKN9hF@J4x`Qx=Zv34LZ6q!#@=d{Fu8D<-W-|Dz?PH7GyR)Z7 z8GgizJ8p9HQ6h++$y}kNZITo*)WLJzEjVfwP|pa;Q?EURraw%Bm}$m+-#402$MBt( zn#l{qiY`J+v)@uR>aLV-O{^4yF!U;;#&!AYG6b5(J0?;tX_*0^`GC_T(cV1s=xalb zK(SV)u zy-{v$Y-|i;YCSxJ3#=_H2m)a6N1%L_?*UUQcQ!RL00@`cbjruKcsMyJRz5Z~B$KVM zv$0*ed^w5i6}RZgz91UJjE$Y0@A5c-9);5gb{u#Gb(n-VR@*K>9VC5Q=?pU!x)*BK z%w4dq=6XP`su$=Y^qt!$;&*g(yw(CL5=i?yf~b$`G1y9aRD#`<<}xoTI--!Am*IB( z8B}(9`g<$Nud0b}-x5-|ZRF}*zoPl3w3Jq#s5yX2=FuSUl-rf`(175t;D0B}5ni-u z&@pnU@X8zq+z%+~$X0kpz_5&uQL?&ewf-_h{L80o^0!mqAd$6zSf1Z!V{hvli((uo z>-oF9EFW}!C{jaEL=tx-eYa=c{$zy+v(T~tNiOZdCqX)|SF7@6{>i2DL@yt_qpsxi zLrQf?Z>?Dsi$`Y>E0|>6E>2%blbDM;X;9(2@xIWUKyT zA0Y>ohCbaV(KlBYeRjJWJvS2HlovDK##N;y3~zL!P%T|(>QO|xB0@O0ch7u(&}gDm zx61yXc8GpebYhmI{aqv>ONlX^GCPmY{Wg#@8o?ewNOB!BXBPM3I3~}A3cLz+aj<)$ zD+>;tFh^jD(PJ$NJ_R{B0tTRAPke<*^mF@pH+v_VFiJuxd%9MVIU_G~RXnewK3-E& zBBoRq1?jn#`!q3XP4b}>kpP!w=n)vb6-?e4gbA^XF}Jd+#W4VS05K$pFF=@b)%%F1 zR+&5wub~I$H;k5U`GGL)QtQ~7G3Pg%t9Zx?9)SRY=O(4_ZsXBC|M-xqy40Tr9Y{L` z&PR@EZ0b2%gFO5muFj2q1)3a5fy7u4?ep)?aFQ?xJ$zIf6fXM6H~i)G?f$qIYdG8A zPzi8{IVPiC&C*js{`4730!sl}gafM%dZRl7{{rY zAqZwheE${?4aQV0xxiA-`r^yiy zt?^M&iYJOTfKfn5EGi^cKh}6?;_hDRyyu65qf>?>*_E$fiZnUVwtM^P7j(`wbCk@E z7OhrJfdd=rqZMWC%IG~dcJ@&T#-X`yIon17zy{R|4BQGeII8%K#emTu+8pOLao#H! zRc`OPv>u$HipW8Ip7aZPJ#&9-K1aVsFXrIHLK_3K+X#S02IILW;)%M8>ilbdtRC~63Nk6-dtfZ5H*Hs%He z#Vn-=&QMW3z!jF*nIXG)4Qt!D8V>o;?6nX*rqp;SjJw5*-Zb{0_%+<%5IgO?_!A)bw7_lB8rD3J3E{DBW)^D z!hQLg9-c9&F1lU(dBFz#Q(qwZz@{&i9IzvAosA5!I5KPa1X`+F7vNnzX|G@7FGUhy zJEYaJ+(1xp!OJQ7YFb$fS&w~R+IMwz9s4>mNOns{9bGJeh3>#cbSvts6t&%O8Bs_; z6Umw~sj~``xHHbSr}+Ap*7$ONq?8d$4gd+{ejPQ4!LA<9)hUN&DyMlbnIC;$8KI;a z{a|dNoooMTM)5wf)_Y^2pI20u4prqc@iAZ}npeNVV|4RPX2C9)+R-M&3%ib_RwbSr zwlUcy>2z(zJRFyBJaRoR{^+EAu2<@;G%(NVk2G;vLhzMiCT^oXzRc9u*n30Jk!#kj z+cSW5Dgo3tA(RL8|C$*+;VO(oGB!>sK?Rnu29?yUW83Om<--U*?rbzD#aMEZPBarq zQ6Y%=sE&T#gj1b<^avGkctk`A7xZ5sySus|JcO9} z;nES2n})iwLS|n8nbg$v@$cV*(_jXU8u7x>ai*riorTlUadDxK0LH1HZl^PN;Cv}$ zv&sYq5%vfG55Ts9B?}^`-p_o{OB#oM+U?dkOyGS4x0wQt$43d%fj&$}?-$-kD0mAy zUB~R~Cc#ib9L1HIA|jZ4ZJ-a)ChuD1M+{b|t2HSueX$-XJJzRS7)`mNI9hPssm5Av z#2U+pGEB$T8*0qY=s9WQ=x|ld;?GDka^)Q~3GFB0a?vOR?Je{b>JFHY1h`RssUR~} z3%~hP^G3z{3!JV38%D%=h^dp;hf4m%t$MJz8| zr-Ynpp!beG6)EN0ern)^N3aZR4V(l~_XjCR9#}0>7O%%@nSb0-)4LfuVLW|7LEyCK z3{c0Kr2JLuE2H&9FOapDfMzAyjmF0#5?E#AyiXHO%kpOZ{t5F6MdzbKRo*)w11R7X zrKiWuIIDT%B)12vIde|u{+vynzz}i4qm9cKQS%E64dp@9xgTysH3hs~`afk8slZdl7qX;MW9?}{Go0FX;a-bYedbd;W z;pTR-E!bVn%*91hlnr?$ilajB{ztwL=SxL6XKr$J&gZc1irZ&l@VOXNj2P?i`S#^$ zQqXz>+$7%_ON3Cf0VdOun7RMg+Lgyc*>7W$W0#wKP1cYi zOQp!ZtK?N#vSb@6Dy3pX)({b4C-2a_{GJ|GD#D<9V8A`7P(1?{b1&VX@UmcHDS5I91;ZQkaF#wC7 zWWDQX8J>X?p=vEaQEaS5FRR9)u!oqDn#vEK9Xl+|%;Gwvg^-nYqXGaR4;1SU)N$Gu z76!IaE9&QaJSp&clIMB%gTnrF!>_t|o;FG~JYi0)ZV%F@Qq0ZI2>NmDp@JNeQsNrj zO*lRRg>Fb_0TLe^9E^?S6qxMp>2Z-u#oup{Mq~!RlmH&A1<&+=kOxj)Q?fgfE}s9A zR&k%}IaxNpovq{dJP77(U$paZ{I~T*&Z~&~7QjDKYoU~x1F{c)1tle%g~h%gm2D&Xc^}7TK~gb4Kd-a* z$`vT@M6;r5@OU$0}h#eOknI)p@{#!1DzRCTW)tXE$Z6d zuRYD(#GDYpY3qkKC9!xq{p){pMXM_XjCKfrtfP`S@tj(!k|7_44M|O+X%%PNtqCt9~IuNqW*t?nu_c2e_lGfk7d#1pT7&|I|s;R;TA$poe zifg)9((-*M=1&8ol=t2~J+EGNU?7%)E#KyLS=lV2%D9MuB~w^P$mrCm0kl4}9n83} zyeLbXa?|A!jD$I`;TmdO3V2L`MS zA{Y!%?v5Nt@Lb=2dVVBM14`ZTGo;k`zssFE&fp8YXc$RgK(<8J(A;di;@dUn=26Q) znqky(UL33KD<_jWS1Q`jZu=u*VqyTrvDt-24*-0jR*T70I)A^mmJDTc-=y?(mU^H| z-NP`QgfeA>e<_cBd5@~J^v#S6!JThwsgD7;XP7lBbOE+ofGkZ_+PUp_3tiGtP!jq= zy`7uunGN6HMfLr7qpj}F&dynz&R`Yx>r?BYAs=$N=#clVJ>Rsh?N)5;VKnX*NmX6l zk{heKx22&}W4Sq>=?x-*GBER;C}Z)^=3X)djF92(rARf`NBxRJOv?9z%&Jk#oo_7rcPE0R9hXVfj@(MoQ?S}FJNPJ)=! z!0~Q=wjGG|NVZm%Fc03TpCj#C-1(~aPt5D~OB4N05U(Az?2Wf4mn;0}C)@OJkW(?a z9ZKq@)wpm~;_i%?uA(PGmZwkC8$*Q4%LbX0BIht5XirPQLPZYcQu6xziG-$s{;sjg z9)|5zQY8Flr(G{gyjz}T5q?9$OS)I{^KGZ$>-`G3%cm)hC^z=# z>({sW{L`fs!U~x@#lr+vCgnK6d$ytC1j`_<(d=!_J&q&s6CXNBWruSM`Wk)OW&`$w ztaJ{zZuc&~Q31AMusv*7hURBp5#b=IXRZQ>$Rksk3vl$4uHT6aLkBt)sHs(1yxX5* z+%MN^_4EWG9&nAfivj9F`={(Ai_@pshrz4(2>B}w!|L{8>$EL7!H3^AZ!*@0A2pMw zF3F@@k)AQ8} z9zN8$jNo(X)U#abDRwurjg1L$Yg}in6c2+1<-Nx9Uxub3!NNWs6#B| zKhI<|y3WrqHY|`n1P564bGQYY6@O2pmjt#p#9u7~oU9C_zq4If)sv)2Q@MZrLGSSd zXco)lHUCcv*Y2s*j-Yox01_!6ti3P+o8HjS zkeZSb6%};=VBy-sNy*73I9zv2%YGS|&+ZD>{%-lNaU6%eboCH1m4wgp6bz{JEF>6o zOkFwFaJSInt(l~Y&nfTf;MbO_>BX@I;}e;)adq0K_ zV&dXDU*3(!0$jg4>D^hGG>ufmwIcT?ZSs_(D?MfIh!Rlrr2daZFYXmazx7i)Um5Ic z+rMm^^Q0fy>r}~wmc-=qDQQM}n*lo74i%j_64?{i>@6U>Ut=_iX5y7}I$ z&HndzRTr)o*bVCkIY|pZ=INbzmYq`w3^N69(2=FUhjG|jKoCM**uOSc-K+;8ISB|R z8;(g71)V80nYBG}T$B*%tO#eqG=hSlr<4(D#nfdH=MWC7>@SWpVhxdHh_4EA?Q-U) zfU_$GPG^P>Ltnl;LcQG1`O`Ir+kV}b27txe8G5|PjY2>K!s$xhaJsY4v)^J85B>Iv zeg`Hfn4Fw>)-wQ5q4^t7V;^9p-+cO%#q;3q_TL5)FsuV>JJ}9$<2jwFA+YEp|X#MXTOP2yj>AeC;(w~MrzdoKnQ=<(Wiw@hi zb|wP;Z&*{kua5&43B<{VIJfsVUeSgLn)W-EkhQ~r=hOnEp{ZSN&V%e+gG58#+H;~p%2%v+agQH`e zCB2V{$+iqfXJ-j9u{xUQM}SO0?Fjmh3O!V^V(8%j%oI|5pc`g5JvzoF-J@Qx&`zNn z0ypv(=vU4lYoHonE@&nQ_y^D4BD>SFLOY*hvp^#Nj$zW@p|GM0`p>3!h>d>p_-qjP z)B>f)cMPJR+xk#GKXfRhZDkH|{2?ji#9(BYODd79uGI`irHxm`i`NMyCMZ=PqJY_> zZ&43X&2xwzcO{$HS7WLoZeF*^0g|QXG^}@z9z8nPr4i1W9h8OXahB74ngM(X@iBye z2b8v7t+y!1x|~iHOl~#-1^2dQsQ+l|=(Hd_q8BC4P|KhNLhxB+>u6YD_bNHJCGChGjg3RtJ<{RiEELX=!n zl!F=D$3aTDgEC=&Mn{j30*GqwcK(L@|0Eu2RVTm@HvfYw_0Gcv8{x33-LTUL5iIKx za%KX1HJ2+c(Vxf}*(K?;i!gfUYtbx!l@Lf_q0U>@_>_XVV19DNnmL6i0Y+@Mh1b5$ zm_c4@J52hVKbevxM<7Am__c6gYn^4GO<~6(zHUgE5HzL@uNFB9-Vi(X)#G2;hLrzt zhaJb-*N8y&V11G-!qVheLqS)mpKtjX528cq*X~nwgax5X-UY?4cEIOQ_F|XSHO8*( z3!~c90p>H8u66ZJlfQ+s+Zt zn@lR(&wM8KQ!uGmZ$;f)ltVz59&H=2N^680lB3P}o7{!&ww9rq7Zc9(&^F9ajTDW z?Nqw8HwFs|9*D}i5Ct(&Gi2~e1+^i-brJ%mHuMq3P0V%uE45#O=&4TG?f)k zh}s?k6;%m62_3STDoHbojo~2GQwdbyXgg-AW(O5jRatwv5qJ4!J}RmpG#tqeJuInc nP%Knb&{2~Lv-AJ+)ib7NG7<@C`~Ou(1)rmu`iBd!R+s(_pq*73 literal 0 HcmV?d00001 diff --git a/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/main.tf b/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/main.tf new file mode 100644 index 000000000..034d7535c --- /dev/null +++ b/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/main.tf @@ -0,0 +1,263 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + np_service_account_iam_email = [for k, v in module.cluster_nodepools : v.service_account_iam_email] +} + +module "host_project" { + source = "../../../modules/project" + billing_account = var.billing_account_id + parent = var.parent + name = var.host_project_id + shared_vpc_host_config = { + enabled = true + service_projects = [] + } + services = [ + "container.googleapis.com" + ] +} + +module "mgmt_project" { + source = "../../../modules/project" + billing_account = var.billing_account_id + parent = var.parent + name = var.mgmt_project_id + shared_vpc_service_config = { + attach = true + host_project = module.host_project.project_id + service_identity_iam = null + } + services = [ + "cloudresourcemanager.googleapis.com", + "container.googleapis.com", + "serviceusage.googleapis.com" + ] +} + +module "fleet_project" { + source = "../../../modules/project" + billing_account = var.billing_account_id + parent = var.parent + name = var.fleet_project_id + shared_vpc_service_config = { + attach = true + host_project = module.host_project.project_id + service_identity_iam = { + "roles/compute.networkUser" = [ + "cloudservices", "container-engine" + ] + "roles/container.hostServiceAgentUser" = [ + "container-engine" + ] + } + } + services = [ + "anthos.googleapis.com", + "cloudresourcemanager.googleapis.com", + "container.googleapis.com", + "gkehub.googleapis.com", + "gkeconnect.googleapis.com", + "logging.googleapis.com", + "mesh.googleapis.com", + "monitoring.googleapis.com", + "stackdriver.googleapis.com" + ] + iam = { + "roles/container.admin" = [module.mgmt_server.service_account_iam_email] + "roles/gkehub.admin" = [module.mgmt_server.service_account_iam_email] + "roles/gkehub.serviceAgent" = ["serviceAccount:${module.fleet_project.service_accounts.robots.fleet}"] + "roles/monitoring.viewer" = local.np_service_account_iam_email + "roles/monitoring.metricWriter" = local.np_service_account_iam_email + "roles/logging.logWriter" = local.np_service_account_iam_email + "roles/stackdriver.resourceMetadata.writer" = local.np_service_account_iam_email + } + service_config = { + disable_on_destroy = false + disable_dependent_services = true + } +} + +module "svpc" { + source = "../../../modules/net-vpc" + project_id = module.host_project.project_id + name = "svpc" + mtu = 1500 + subnets = concat([for key, config in var.clusters_config : { + ip_cidr_range = config.subnet_cidr_block + name = "subnet-${key}" + region = var.region + secondary_ip_range = { + pods = config.pods_cidr_block + services = config.services_cidr_block + } + }], [{ + ip_cidr_range = var.mgmt_subnet_cidr_block + name = "subnet-mgmt" + region = var.mgmt_server_config.region + secondary_ip_range = null + }]) +} + +module "mgmt_server" { + source = "../../../modules/compute-vm" + project_id = module.mgmt_project.project_id + zone = var.mgmt_server_config.zone + name = "mgmt" + instance_type = var.mgmt_server_config.instance_type + network_interfaces = [{ + network = module.svpc.self_link + subnetwork = module.svpc.subnet_self_links["${var.mgmt_server_config.region}/subnet-mgmt"] + nat = false + addresses = null + }] + service_account_create = true + boot_disk = { + image = var.mgmt_server_config.image + type = var.mgmt_server_config.disk_type + size = var.mgmt_server_config.disk_size + } +} + +module "clusters" { + for_each = var.clusters_config + source = "../../../modules/gke-cluster" + project_id = module.fleet_project.project_id + name = each.key + location = var.region + network = module.svpc.self_link + subnetwork = module.svpc.subnet_self_links["${var.region}/subnet-${each.key}"] + secondary_range_pods = "pods" + secondary_range_services = "services" + private_cluster_config = { + enable_private_nodes = true + enable_private_endpoint = true + master_ipv4_cidr_block = each.value.master_cidr_block + master_global_access = true + } + master_authorized_ranges = merge({ + mgmt : var.mgmt_subnet_cidr_block + }, + { for key, config in var.clusters_config : + "pods-${key}" => config.pods_cidr_block if key != each.key + }) + enable_autopilot = false + release_channel = "REGULAR" + workload_identity = true + labels = { + mesh_id = "proj-${module.fleet_project.number}" + } +} + +module "cluster_nodepools" { + for_each = var.clusters_config + source = "../../../modules/gke-nodepool" + project_id = module.fleet_project.project_id + cluster_name = module.clusters[each.key].name + location = var.region + name = "nodepool-${each.key}" + node_service_account_create = true + initial_node_count = 1 + node_machine_type = "e2-standard-4" + node_tags = ["${each.key}-node"] +} + +module "firewall" { + source = "../../../modules/net-vpc-firewall" + project_id = module.host_project.project_id + network = module.svpc.name + custom_rules = merge({ allow-mesh = { + description = "Allow " + direction = "INGRESS" + action = "allow" + sources = [] + ranges = [for k, v in var.clusters_config : v.pods_cidr_block] + targets = [for k, v in var.clusters_config : "${k}-node"] + use_service_accounts = false + rules = [{ protocol = "tcp", ports = null }, + { protocol = "udp", ports = null }, + { protocol = "icmp", ports = null }, + { protocol = "esp", ports = null }, + { protocol = "ah", ports = null }, + { protocol = "sctp", ports = null }] + extra_attributes = { + priority = 900 + } + } }, + { for k, v in var.clusters_config : "allow-${k}-istio" => { + description = "Allow " + direction = "INGRESS" + action = "allow" + sources = [] + ranges = [v.master_cidr_block] + targets = ["${k}-node"] + use_service_accounts = false + rules = [{ protocol = "tcp", ports = [8080, 15014, 15017] }] + extra_attributes = { + priority = 1000 + } + } + } + ) +} + +module "nat" { + source = "../../../modules/net-cloudnat" + project_id = module.host_project.project_id + region = var.region + name = "nat" + router_create = true + router_network = module.svpc.name +} + +module "hub" { + source = "../../../modules/gke-hub" + project_id = module.fleet_project.project_id + clusters = { for k, v in module.clusters : k => v.id } + features = { + appdevexperience = false + configmanagement = false + identityservice = false + multiclusteringress = null + servicemesh = true + multiclusterservicediscovery = false + } + depends_on = [ + module.fleet_project + ] +} + +resource "local_file" "vars_file" { + content = templatefile("${path.module}/templates/vars.yaml.tpl", { + istio_version = var.istio_version + region = var.region + clusters = keys(var.clusters_config) + service_account_email = module.mgmt_server.service_account_email + project_id = module.fleet_project.project_id + }) + filename = "${path.module}/ansible/vars/vars.yaml" + file_permission = "0666" +} + +resource "local_file" "gssh_file" { + content = templatefile("${path.module}/templates/gssh.sh.tpl", { + project_id = var.mgmt_project_id + zone = var.mgmt_server_config.zone + }) + filename = "${path.module}/ansible/gssh.sh" + file_permission = "0777" +} diff --git a/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/templates/gssh.sh.tpl b/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/templates/gssh.sh.tpl new file mode 100644 index 000000000..c61460ba2 --- /dev/null +++ b/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/templates/gssh.sh.tpl @@ -0,0 +1,30 @@ +#!/bin/bash +# +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +host="$${@: -2: 1}" +cmd="$${@: -1: 1}" + +gcloud_args=" +--tunnel-through-iap +--zone=${zone} +--project=${project_id} +--quiet +--no-user-output-enabled +-- +-C +" + +exec gcloud compute ssh "$host" $gcloud_args "$cmd" \ No newline at end of file diff --git a/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/templates/vars.yaml.tpl b/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/templates/vars.yaml.tpl new file mode 100644 index 000000000..f31b4fd75 --- /dev/null +++ b/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/templates/vars.yaml.tpl @@ -0,0 +1,8 @@ +istio_version: ${istio_version} +clusters: +%{ for cluster in clusters ~} + - ${cluster} +%{ endfor ~} +region: ${region} +service_account_email: ${service_account_email} +project_id: ${project_id} \ No newline at end of file diff --git a/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/variables.tf b/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/variables.tf new file mode 100644 index 000000000..a973a3ac6 --- /dev/null +++ b/examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/variables.tf @@ -0,0 +1,102 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +variable "billing_account_id" { + description = "Billing account id." + type = string +} + +variable "parent" { + description = "Parent." + type = string +} + +variable "host_project_id" { + description = "Project ID." + type = string +} + +variable "fleet_project_id" { + description = "Management Project ID." + type = string +} + +variable "mgmt_project_id" { + description = "Management Project ID." + type = string +} + +variable "mgmt_subnet_cidr_block" { + description = "Management subnet CIDR block." + type = string + default = "10.0.0.0/28" +} + +variable "region" { + description = "Region." + type = string + default = "europe-west1" +} + +variable "clusters_config" { + description = "Clusters configuration." + type = map(object({ + subnet_cidr_block = string + master_cidr_block = string + services_cidr_block = string + pods_cidr_block = string + })) + default = { + cluster-a = { + subnet_cidr_block = "10.0.1.0/24" + master_cidr_block = "10.16.0.0/28" + services_cidr_block = "192.168.1.0/24" + pods_cidr_block = "172.16.0.0/20" + } + cluster-b = { + subnet_cidr_block = "10.0.2.0/24" + master_cidr_block = "10.16.0.16/28" + services_cidr_block = "192.168.2.0/24" + pods_cidr_block = "172.16.16.0/20" + } + } +} + +variable "mgmt_server_config" { + description = "Mgmt server configuration" + type = object({ + disk_size = number + disk_type = string + image = string + instance_type = string + region = string + zone = string + }) + default = { + disk_size = 50 + disk_type = "pd-ssd" + image = "projects/ubuntu-os-cloud/global/images/family/ubuntu-2204-lts" + instance_type = "n1-standard-2" + region = "europe-west1" + zone = "europe-west1-c" + } +} + +variable "istio_version" { + description = "ASM version" + type = string + default = "1.14.1-asm.3" +} diff --git a/tests/examples/cloud_operations/multi_cluster_mesh_gke_fleet_api/__init__.py b/tests/examples/cloud_operations/multi_cluster_mesh_gke_fleet_api/__init__.py new file mode 100644 index 000000000..6d6d1266c --- /dev/null +++ b/tests/examples/cloud_operations/multi_cluster_mesh_gke_fleet_api/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/examples/cloud_operations/multi_cluster_mesh_gke_fleet_api/fixture/main.tf b/tests/examples/cloud_operations/multi_cluster_mesh_gke_fleet_api/fixture/main.tf new file mode 100644 index 000000000..77d4ba691 --- /dev/null +++ b/tests/examples/cloud_operations/multi_cluster_mesh_gke_fleet_api/fixture/main.tf @@ -0,0 +1,28 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module "test" { + source = "../../../../../examples/cloud-operations/multi-cluster-mesh-gke-fleet-api" + billing_account_id = var.billing_account_id + parent = var.parent + host_project_id = var.host_project_id + fleet_project_id = var.fleet_project_id + mgmt_project_id = var.mgmt_project_id + region = var.region + clusters_config = var.clusters_config + mgmt_subnet_cidr_block = var.mgmt_subnet_cidr_block + istio_version = var.istio_version +} diff --git a/tests/examples/cloud_operations/multi_cluster_mesh_gke_fleet_api/fixture/variables.tf b/tests/examples/cloud_operations/multi_cluster_mesh_gke_fleet_api/fixture/variables.tf new file mode 100644 index 000000000..6c6b6c8fb --- /dev/null +++ b/tests/examples/cloud_operations/multi_cluster_mesh_gke_fleet_api/fixture/variables.tf @@ -0,0 +1,107 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +variable "billing_account_id" { + description = "Billing account id." + type = string + default = "123-456-789" +} + +variable "parent" { + description = "Parent." + type = string + default = "folders/123456789" +} + +variable "host_project_id" { + description = "Host project ID." + type = string + default = "my-host-project" +} + +variable "fleet_project_id" { + description = "Fleet project ID." + type = string + default = "my-fleet-project" +} + +variable "mgmt_project_id" { + description = "Management Project ID." + type = string + default = "my-mgmt-project" +} + +variable "mgmt_subnet_cidr_block" { + description = "Management subnet CIDR block." + type = string + default = "10.0.0.0/24" +} + +variable "region" { + description = "Region." + type = string + default = "europe-west1" +} + +variable "clusters_config" { + description = "Clusters configuration." + type = map(object({ + subnet_cidr_block = string + master_cidr_block = string + services_cidr_block = string + pods_cidr_block = string + })) + default = { + cluster-a = { + subnet_cidr_block = "10.0.1.0/24" + master_cidr_block = "10.16.0.0/28" + services_cidr_block = "192.168.1.0/24" + pods_cidr_block = "172.16.0.0/20" + } + cluster-b = { + subnet_cidr_block = "10.0.2.0/24" + master_cidr_block = "10.16.0.16/28" + services_cidr_block = "192.168.2.0/24" + pods_cidr_block = "172.16.16.0/20" + } + } +} + +variable "mgmt_server_config" { + description = "Mgmt server configuration" + type = object({ + disk_size = number + disk_type = string + image = string + instance_type = string + region = string + zone = string + }) + default = { + disk_size = 50 + disk_type = "pd-ssd" + image = "projects/ubuntu-os-cloud/global/images/family/ubuntu-2204-lts" + instance_type = "n1-standard-2" + region = "europe-west1" + zone = "europe-west1-c" + } +} + +variable "istio_version" { + description = "ASM version" + type = string + default = "1.14.1-asm.3" +} diff --git a/tests/examples/cloud_operations/multi_cluster_mesh_gke_fleet_api/test_plan.py b/tests/examples/cloud_operations/multi_cluster_mesh_gke_fleet_api/test_plan.py new file mode 100644 index 000000000..270a142d1 --- /dev/null +++ b/tests/examples/cloud_operations/multi_cluster_mesh_gke_fleet_api/test_plan.py @@ -0,0 +1,19 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +def test_resources(e2e_plan_runner): + "Test that plan works and the numbers of resources is as expected." + modules, resources = e2e_plan_runner() + assert len(modules) == 12 + assert len(resources) == 53