From 9300f328b155dda82c800efc4ac75347a1f493ad Mon Sep 17 00:00:00 2001 From: Ayman Farhat Date: Thu, 19 May 2022 11:26:15 +0200 Subject: [PATCH 01/16] Initial commit for adding a sample data playground --- .../data-solutions/data-playground/README.md | 42 ++++++++ .../data-solutions/data-playground/main.tf | 101 ++++++++++++++++++ .../data-solutions/data-playground/outputs.tf | 33 ++++++ .../data-playground/variables.tf | 50 +++++++++ .../data-playground/versions.tf | 27 +++++ 5 files changed, 253 insertions(+) create mode 100644 examples/data-solutions/data-playground/README.md create mode 100644 examples/data-solutions/data-playground/main.tf create mode 100644 examples/data-solutions/data-playground/outputs.tf create mode 100644 examples/data-solutions/data-playground/variables.tf create mode 100644 examples/data-solutions/data-playground/versions.tf diff --git a/examples/data-solutions/data-playground/README.md b/examples/data-solutions/data-playground/README.md new file mode 100644 index 0000000000..6287cac7ea --- /dev/null +++ b/examples/data-solutions/data-playground/README.md @@ -0,0 +1,42 @@ +# Data Playground + +This example creates a minimum viable template for a project with the needed APIs, basic VPC and Firewall set in place for starting a data project via Terraform. + +## Managed resources and services + +This sample creates several distinct groups of resources: + +- projects + - Service Project configured for GCE instances and GCS buckets +- networking + - VPC network + - One default subnet + - Firewall rules for [SSH access via IAP](https://cloud.google.com/iap/docs/using-tcp-forwarding) and open communication within the VPC +- Vertex AI notebook + - One Jupyter lab notebook instance with public access +- GCS + - One bucket initial bucket + + + +## Variables +| name | description | type | required | default | +| ---------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------ | -------- | ------------------- | +| billing\_account | Billing account id used as default for new projects. | string | ✓ | | +| project\_service\_name | Name for the project. | string | ✓ | | +| root\_node | The resource name of the parent Folder or Organization. Must be of the form folders/folder\_id or organizations/org\_id. | string | ✓ | | +| location | The location where resources will be deployed | string | | europe | +| region | The region where resources will be deployed. | string | | europe-west1 | +| zone | The zone where resources will be deployed. | string | | b | +| vpc\_ip\_cidr\_range | Ip range used in the subnet deployed in the project | string | | 10.0.0.0/20 | +| vpc\_name | Name of the VPC created in the project. | string | | data-playground-vpc | +| vpc\_subnet\_name | Name of the subnet created in the project | string | | default-subnet | + + +## Variables +| Name | Description | +| ----------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | +| bucket | GCS Bucket URL. | +| project | Project id | +| vpc | VPC Network name | +| notebook | Vertex AI notebook name | diff --git a/examples/data-solutions/data-playground/main.tf b/examples/data-solutions/data-playground/main.tf new file mode 100644 index 0000000000..fe43750334 --- /dev/null +++ b/examples/data-solutions/data-playground/main.tf @@ -0,0 +1,101 @@ +# 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. + +############################################################################### +# Project # +############################################################################### + +module "project" { + source = "../../../modules/project" + billing_account = var.billing_account + name = var.project_name + parent = var.root_node + prefix = "data-playground" + + services = [ + "stackdriver.googleapis.com", + "compute.googleapis.com", + "storage-component.googleapis.com", + "storage.googleapis.com", + "servicenetworking.googleapis.com", + "bigquery.googleapis.com", + "bigquerystorage.googleapis.com", + "bigqueryreservation.googleapis.com", + "dataflow.googleapis.com", + "notebooks.googleapis.com", + "composer.googleapis.com" + ] +} + +############################################################################### +# Networking # +############################################################################### + +module "vpc" { + source = "../../../modules/net-vpc" + project_id = module.project.project_id + name = var.vpc_name + subnets = [ + { + ip_cidr_range = var.vpc_ip_cidr_range + name = var.vpc_subnet_name + region = var.region + secondary_ip_range = {} + } + ] +} + +module "vpc-firewall" { + source = "../../../modules/net-vpc-firewall" + project_id = module.project.project_id + network = module.vpc.name + admin_ranges = [var.vpc_ip_cidr_range] +} + +############################################################################### +# GCS # +############################################################################### + +module "base-gcs-bucket" { + source = "../../../modules/gcs" + project_id = module.project.project_id + prefix = module.project.project_id + name = "base" +} + +############################################################################### +# Vertex AI Notebook # +############################################################################### + +resource "google_notebooks_instance" "playground" { + name = "data-play-notebook" + location = format("%s-%s", var.region, var.zone) + machine_type = "e2-medium" + project = module.project.project_id + + container_image { + repository = "gcr.io/deeplearning-platform-release/base-cpu" + tag = "latest" + } + + install_gpu_driver = true + boot_disk_type = "PD_SSD" + boot_disk_size_gb = 110 + + no_public_ip = false + no_proxy_access = false + + network = module.vpc.network.id + subnet = module.vpc.subnets[format("%s/%s", var.region, var.vpc_subnet_name)].id +} diff --git a/examples/data-solutions/data-playground/outputs.tf b/examples/data-solutions/data-playground/outputs.tf new file mode 100644 index 0000000000..22ee5e5b4e --- /dev/null +++ b/examples/data-solutions/data-playground/outputs.tf @@ -0,0 +1,33 @@ +# 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. + +output "bucket" { + description = "GCS Bucket URL." + value = module.base-gcs-bucket.url +} + +output "projects" { + description = "Project id" + value = module.project.project_id +} + +output "vpc" { + description = "VPC Network" + value = module.vpc.name +} + +output "notebook" { + description = "Vertex AI notebook" + value = resource.google_notebooks_instance.playground.name +} diff --git a/examples/data-solutions/data-playground/variables.tf b/examples/data-solutions/data-playground/variables.tf new file mode 100644 index 0000000000..797343590f --- /dev/null +++ b/examples/data-solutions/data-playground/variables.tf @@ -0,0 +1,50 @@ +variable "billing_account" { + description = "Billing account id used as default for new projects." + type = string +} + +variable "location" { + description = "The location where resources will be deployed." + type = string + default = "europe" +} + +variable "project_name" { + description = "Name for the project." + type = string +} + +variable "region" { + description = "The region where resources will be deployed." + type = string + default = "europe-west1" +} + +variable "zone" { + description = "The zone where resources will be deployed." + type = string + default = "b" +} + +variable "root_node" { + description = "The resource name of the parent Folder or Organization. Must be of the form folders/folder_id or organizations/org_id." + type = string +} + +variable "vpc_ip_cidr_range" { + description = "Ip range used in the subnet deployed in the project." + type = string + default = "10.0.0.0/20" +} + +variable "vpc_name" { + description = "Name of the VPC created in the project." + type = string + default = "data-playground-vpc" +} + +variable "vpc_subnet_name" { + description = "Name of the subnet created in the project." + type = string + default = "default-subnet" +} diff --git a/examples/data-solutions/data-playground/versions.tf b/examples/data-solutions/data-playground/versions.tf new file mode 100644 index 0000000000..32e9ab8acc --- /dev/null +++ b/examples/data-solutions/data-playground/versions.tf @@ -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 +# +# 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. + +terraform { + required_version = ">= 1.1.0" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 4.17.0" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 4.17.0" + } + } +} From b1693ac8436eaec194fe3d72157ce05ee2a8216b Mon Sep 17 00:00:00 2001 From: Ayman Farhat Date: Thu, 19 May 2022 11:29:43 +0200 Subject: [PATCH 02/16] Update README --- examples/data-solutions/data-playground/README.md | 4 ++-- examples/data-solutions/data-playground/outputs.tf | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/data-solutions/data-playground/README.md b/examples/data-solutions/data-playground/README.md index 6287cac7ea..305f61cf7e 100644 --- a/examples/data-solutions/data-playground/README.md +++ b/examples/data-solutions/data-playground/README.md @@ -1,6 +1,6 @@ # Data Playground -This example creates a minimum viable template for a project with the needed APIs, basic VPC and Firewall set in place for starting a data project via Terraform. +This example creates a minimum viable template for a data experimentation project with the needed APIs enabled, basic VPC and Firewall set in place, GCS bucket and an AI notebook to get started. ## Managed resources and services @@ -33,7 +33,7 @@ This sample creates several distinct groups of resources: | vpc\_subnet\_name | Name of the subnet created in the project | string | | default-subnet | -## Variables +## Outputs | Name | Description | | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | | bucket | GCS Bucket URL. | diff --git a/examples/data-solutions/data-playground/outputs.tf b/examples/data-solutions/data-playground/outputs.tf index 22ee5e5b4e..0a8426ace7 100644 --- a/examples/data-solutions/data-playground/outputs.tf +++ b/examples/data-solutions/data-playground/outputs.tf @@ -17,7 +17,7 @@ output "bucket" { value = module.base-gcs-bucket.url } -output "projects" { +output "project" { description = "Project id" value = module.project.project_id } From 2ed111150debd3d662235b0827bbabcee0b69a31 Mon Sep 17 00:00:00 2001 From: Ayman Farhat Date: Thu, 19 May 2022 11:33:29 +0200 Subject: [PATCH 03/16] Add license boilerplate to variables.tf --- .../data-solutions/data-playground/variables.tf | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/examples/data-solutions/data-playground/variables.tf b/examples/data-solutions/data-playground/variables.tf index 797343590f..dc621429ec 100644 --- a/examples/data-solutions/data-playground/variables.tf +++ b/examples/data-solutions/data-playground/variables.tf @@ -1,3 +1,17 @@ +# 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. + variable "billing_account" { description = "Billing account id used as default for new projects." type = string From 92d6cacfd5e117c6a70a83bb8c853eb3bfbaff1d Mon Sep 17 00:00:00 2001 From: Ayman Farhat Date: Thu, 19 May 2022 11:36:10 +0200 Subject: [PATCH 04/16] Apply linting rules --- .../data-solutions/data-playground/main.tf | 34 +++++++++---------- .../data-solutions/data-playground/outputs.tf | 6 ++-- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/examples/data-solutions/data-playground/main.tf b/examples/data-solutions/data-playground/main.tf index fe43750334..ff54a566bd 100644 --- a/examples/data-solutions/data-playground/main.tf +++ b/examples/data-solutions/data-playground/main.tf @@ -18,12 +18,12 @@ module "project" { source = "../../../modules/project" - billing_account = var.billing_account - name = var.project_name - parent = var.root_node + billing_account = var.billing_account + name = var.project_name + parent = var.root_node prefix = "data-playground" - services = [ + services = [ "stackdriver.googleapis.com", "compute.googleapis.com", "storage-component.googleapis.com", @@ -68,10 +68,10 @@ module "vpc-firewall" { ############################################################################### module "base-gcs-bucket" { - source = "../../../modules/gcs" - project_id = module.project.project_id - prefix = module.project.project_id - name = "base" + source = "../../../modules/gcs" + project_id = module.project.project_id + prefix = module.project.project_id + name = "base" } ############################################################################### @@ -79,23 +79,23 @@ module "base-gcs-bucket" { ############################################################################### resource "google_notebooks_instance" "playground" { - name = "data-play-notebook" - location = format("%s-%s", var.region, var.zone) - machine_type = "e2-medium" - project = module.project.project_id + name = "data-play-notebook" + location = format("%s-%s", var.region, var.zone) + machine_type = "e2-medium" + project = module.project.project_id container_image { repository = "gcr.io/deeplearning-platform-release/base-cpu" - tag = "latest" + tag = "latest" } install_gpu_driver = true - boot_disk_type = "PD_SSD" - boot_disk_size_gb = 110 + boot_disk_type = "PD_SSD" + boot_disk_size_gb = 110 - no_public_ip = false + no_public_ip = false no_proxy_access = false network = module.vpc.network.id - subnet = module.vpc.subnets[format("%s/%s", var.region, var.vpc_subnet_name)].id + subnet = module.vpc.subnets[format("%s/%s", var.region, var.vpc_subnet_name)].id } diff --git a/examples/data-solutions/data-playground/outputs.tf b/examples/data-solutions/data-playground/outputs.tf index 0a8426ace7..cdb4002f03 100644 --- a/examples/data-solutions/data-playground/outputs.tf +++ b/examples/data-solutions/data-playground/outputs.tf @@ -19,15 +19,15 @@ output "bucket" { output "project" { description = "Project id" - value = module.project.project_id + value = module.project.project_id } output "vpc" { description = "VPC Network" - value = module.vpc.name + value = module.vpc.name } output "notebook" { description = "Vertex AI notebook" - value = resource.google_notebooks_instance.playground.name + value = resource.google_notebooks_instance.playground.name } From 0830855717526fa2ca25670e7c859df2a24baba9 Mon Sep 17 00:00:00 2001 From: Ayman Farhat Date: Sat, 18 Jun 2022 10:09:30 +0200 Subject: [PATCH 05/16] rename var to ptoject_id, create prefix var, remove extra zone var --- examples/data-solutions/data-playground/main.tf | 6 +++--- .../data-solutions/data-playground/variables.tf | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/data-solutions/data-playground/main.tf b/examples/data-solutions/data-playground/main.tf index ff54a566bd..973932eaa9 100644 --- a/examples/data-solutions/data-playground/main.tf +++ b/examples/data-solutions/data-playground/main.tf @@ -19,9 +19,9 @@ module "project" { source = "../../../modules/project" billing_account = var.billing_account - name = var.project_name + name = var.project_id parent = var.root_node - prefix = "data-playground" + prefix = var.prefix services = [ "stackdriver.googleapis.com", @@ -80,7 +80,7 @@ module "base-gcs-bucket" { resource "google_notebooks_instance" "playground" { name = "data-play-notebook" - location = format("%s-%s", var.region, var.zone) + location = format("%s-%s", var.region, "b") machine_type = "e2-medium" project = module.project.project_id diff --git a/examples/data-solutions/data-playground/variables.tf b/examples/data-solutions/data-playground/variables.tf index dc621429ec..740374803f 100644 --- a/examples/data-solutions/data-playground/variables.tf +++ b/examples/data-solutions/data-playground/variables.tf @@ -23,21 +23,21 @@ variable "location" { default = "europe" } -variable "project_name" { +variable "project_id" { description = "Name for the project." type = string } -variable "region" { - description = "The region where resources will be deployed." +variable "prefix" { + description = "Prefix name for the project" type = string - default = "europe-west1" + default = "data-play" } -variable "zone" { - description = "The zone where resources will be deployed." +variable "region" { + description = "The region where resources will be deployed." type = string - default = "b" + default = "europe-west1" } variable "root_node" { From 85f47d0f6e9d7c9d125da826115e4983a82275ea Mon Sep 17 00:00:00 2001 From: Ayman Farhat Date: Sat, 18 Jun 2022 10:47:40 +0200 Subject: [PATCH 06/16] Adds the option for using an existing project by default --- .../data-solutions/data-playground/main.tf | 8 +++---- .../data-playground/variables.tf | 22 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/examples/data-solutions/data-playground/main.tf b/examples/data-solutions/data-playground/main.tf index 973932eaa9..9e0edb203b 100644 --- a/examples/data-solutions/data-playground/main.tf +++ b/examples/data-solutions/data-playground/main.tf @@ -18,11 +18,11 @@ module "project" { source = "../../../modules/project" - billing_account = var.billing_account name = var.project_id - parent = var.root_node - prefix = var.prefix - + parent = try(var.project_create.parent, null) + billing_account = try(var.project_create.billing_account_id, null) + project_create = var.project_create != null + prefix = var.project_create == null ? null : var.prefix services = [ "stackdriver.googleapis.com", "compute.googleapis.com", diff --git a/examples/data-solutions/data-playground/variables.tf b/examples/data-solutions/data-playground/variables.tf index 740374803f..5a09faa1b5 100644 --- a/examples/data-solutions/data-playground/variables.tf +++ b/examples/data-solutions/data-playground/variables.tf @@ -12,10 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -variable "billing_account" { - description = "Billing account id used as default for new projects." - type = string -} variable "location" { description = "The location where resources will be deployed." @@ -24,12 +20,21 @@ variable "location" { } variable "project_id" { - description = "Name for the project." + description = "Project id, references existing project if `project_create` is null." type = string } +variable "project_create" { + description = "Provide values if project creation is needed, uses existing project if null. Parent format: folders/folder_id or organizations/org_id" + type = object({ + billing_account_id = string + parent = string + }) + default = null +} + variable "prefix" { - description = "Prefix name for the project" + description = "Unique prefix used for resource names. Not used for project if 'project_create' is null." type = string default = "data-play" } @@ -40,11 +45,6 @@ variable "region" { default = "europe-west1" } -variable "root_node" { - description = "The resource name of the parent Folder or Organization. Must be of the form folders/folder_id or organizations/org_id." - type = string -} - variable "vpc_ip_cidr_range" { description = "Ip range used in the subnet deployed in the project." type = string From f74eabb38a13a2b9c1e4f650b9b5f36916d7fcce Mon Sep 17 00:00:00 2001 From: Ayman Farhat Date: Sat, 18 Jun 2022 11:12:41 +0200 Subject: [PATCH 07/16] Bundles all VPC related variables in a single vpc_config variable of type object --- .../data-solutions/data-playground/main.tf | 10 +++---- .../data-playground/variables.tf | 28 ++++++++----------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/examples/data-solutions/data-playground/main.tf b/examples/data-solutions/data-playground/main.tf index 9e0edb203b..9d735fe09b 100644 --- a/examples/data-solutions/data-playground/main.tf +++ b/examples/data-solutions/data-playground/main.tf @@ -45,11 +45,11 @@ module "project" { module "vpc" { source = "../../../modules/net-vpc" project_id = module.project.project_id - name = var.vpc_name + name = var.vpc_config.vpc_name subnets = [ { - ip_cidr_range = var.vpc_ip_cidr_range - name = var.vpc_subnet_name + ip_cidr_range = var.vpc_config.ip_cidr_range + name = var.vpc_config.subnet_name region = var.region secondary_ip_range = {} } @@ -60,7 +60,7 @@ module "vpc-firewall" { source = "../../../modules/net-vpc-firewall" project_id = module.project.project_id network = module.vpc.name - admin_ranges = [var.vpc_ip_cidr_range] + admin_ranges = [var.vpc_config.ip_cidr_range] } ############################################################################### @@ -97,5 +97,5 @@ resource "google_notebooks_instance" "playground" { no_proxy_access = false network = module.vpc.network.id - subnet = module.vpc.subnets[format("%s/%s", var.region, var.vpc_subnet_name)].id + subnet = module.vpc.subnets[format("%s/%s", var.region, var.vpc_config.subnet_name)].id } diff --git a/examples/data-solutions/data-playground/variables.tf b/examples/data-solutions/data-playground/variables.tf index 5a09faa1b5..73039a2f74 100644 --- a/examples/data-solutions/data-playground/variables.tf +++ b/examples/data-solutions/data-playground/variables.tf @@ -45,20 +45,16 @@ variable "region" { default = "europe-west1" } -variable "vpc_ip_cidr_range" { - description = "Ip range used in the subnet deployed in the project." - type = string - default = "10.0.0.0/20" -} - -variable "vpc_name" { - description = "Name of the VPC created in the project." - type = string - default = "data-playground-vpc" -} - -variable "vpc_subnet_name" { - description = "Name of the subnet created in the project." - type = string - default = "default-subnet" +variable "vpc_config" { + description = "Parameters to create a simple VPC for the Data Playground" + type = object({ + ip_cidr_range = string + vpc_name = string + subnet_name = string + }) + default = { + ip_cidr_range = "10.0.0.0/20" + vpc_name = "data-playground-vpc" + subnet_name = "default-subnet" + } } From 44757d323e082e5a32dd83f0f8e8f0583d48652d Mon Sep 17 00:00:00 2001 From: Ayman Farhat Date: Fri, 24 Jun 2022 11:09:12 +0200 Subject: [PATCH 08/16] Add encryption_key usage example + policy_boolean --- examples/data-solutions/data-playground/main.tf | 13 ++++++++++++- .../data-solutions/data-playground/variables.tf | 8 ++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/examples/data-solutions/data-playground/main.tf b/examples/data-solutions/data-playground/main.tf index 9d735fe09b..5bea354dd8 100644 --- a/examples/data-solutions/data-playground/main.tf +++ b/examples/data-solutions/data-playground/main.tf @@ -15,6 +15,9 @@ ############################################################################### # Project # ############################################################################### +locals { + service_encryption_keys = var.service_encryption_keys +} module "project" { source = "../../../modules/project" @@ -36,6 +39,13 @@ module "project" { "notebooks.googleapis.com", "composer.googleapis.com" ] + policy_boolean = { + # "constraints/compute.requireOsLogin" = false + # Example of applying a project wide policy, mainly useful for Composer + } + service_encryption_key_ids = { + storage = [try(local.service_encryption_keys.storage, null)] + } } ############################################################################### @@ -72,12 +82,13 @@ module "base-gcs-bucket" { project_id = module.project.project_id prefix = module.project.project_id name = "base" + encryption_key = try(local.service_encryption_keys.storage, null) # Example assignment of an encryption key } ############################################################################### # Vertex AI Notebook # ############################################################################### - +# TODO: Add encryption_key to Vertex AI notebooks as well resource "google_notebooks_instance" "playground" { name = "data-play-notebook" location = format("%s-%s", var.region, "b") diff --git a/examples/data-solutions/data-playground/variables.tf b/examples/data-solutions/data-playground/variables.tf index 73039a2f74..7b0df7cfc9 100644 --- a/examples/data-solutions/data-playground/variables.tf +++ b/examples/data-solutions/data-playground/variables.tf @@ -45,6 +45,14 @@ variable "region" { default = "europe-west1" } +variable "service_encryption_keys" { # service encription key + description = "Cloud KMS to use to encrypt different services. Key location should match service region." + type = object({ + storage = string + }) + default = null +} + variable "vpc_config" { description = "Parameters to create a simple VPC for the Data Playground" type = object({ From ce447cb91cdea71bbcf30772e9b674983172ae92 Mon Sep 17 00:00:00 2001 From: Ayman Farhat Date: Fri, 24 Jun 2022 16:48:57 +0200 Subject: [PATCH 09/16] Add tests, apply linting and todos for upcoming PRs --- .gitignore | 1 + .../data-solutions/data-playground/main.tf | 11 ++++---- .../data-playground/variables.tf | 4 +-- .../data-playground/__init__.py | 13 ++++++++++ .../data-playground/fixture/main.tf | 24 +++++++++++++++++ .../data-playground/test_plan.py | 26 +++++++++++++++++++ 6 files changed, 72 insertions(+), 7 deletions(-) create mode 100644 tests/examples/data_solutions/data-playground/__init__.py create mode 100644 tests/examples/data_solutions/data-playground/fixture/main.tf create mode 100644 tests/examples/data_solutions/data-playground/test_plan.py diff --git a/.gitignore b/.gitignore index f2ac5f40dd..01b747b916 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ fast/stages/**/globals.auto.tfvars.json cloud_sql_proxy examples/cloud-operations/binauthz/tenant-setup.yaml examples/cloud-operations/binauthz/app/app.yaml +env/ diff --git a/examples/data-solutions/data-playground/main.tf b/examples/data-solutions/data-playground/main.tf index 5bea354dd8..43453f5ae3 100644 --- a/examples/data-solutions/data-playground/main.tf +++ b/examples/data-solutions/data-playground/main.tf @@ -44,7 +44,7 @@ module "project" { # Example of applying a project wide policy, mainly useful for Composer } service_encryption_key_ids = { - storage = [try(local.service_encryption_keys.storage, null)] + storage = [try(local.service_encryption_keys.storage, null)] } } @@ -78,10 +78,10 @@ module "vpc-firewall" { ############################################################################### module "base-gcs-bucket" { - source = "../../../modules/gcs" - project_id = module.project.project_id - prefix = module.project.project_id - name = "base" + source = "../../../modules/gcs" + project_id = module.project.project_id + prefix = module.project.project_id + name = "base" encryption_key = try(local.service_encryption_keys.storage, null) # Example assignment of an encryption key } @@ -89,6 +89,7 @@ module "base-gcs-bucket" { # Vertex AI Notebook # ############################################################################### # TODO: Add encryption_key to Vertex AI notebooks as well +# TODO: Add shared VPC support resource "google_notebooks_instance" "playground" { name = "data-play-notebook" location = format("%s-%s", var.region, "b") diff --git a/examples/data-solutions/data-playground/variables.tf b/examples/data-solutions/data-playground/variables.tf index 7b0df7cfc9..53644d37d5 100644 --- a/examples/data-solutions/data-playground/variables.tf +++ b/examples/data-solutions/data-playground/variables.tf @@ -36,7 +36,7 @@ variable "project_create" { variable "prefix" { description = "Unique prefix used for resource names. Not used for project if 'project_create' is null." type = string - default = "data-play" + default = "dp" } variable "region" { @@ -48,7 +48,7 @@ variable "region" { variable "service_encryption_keys" { # service encription key description = "Cloud KMS to use to encrypt different services. Key location should match service region." type = object({ - storage = string + storage = string }) default = null } diff --git a/tests/examples/data_solutions/data-playground/__init__.py b/tests/examples/data_solutions/data-playground/__init__.py new file mode 100644 index 0000000000..6d6d1266c3 --- /dev/null +++ b/tests/examples/data_solutions/data-playground/__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/data_solutions/data-playground/fixture/main.tf b/tests/examples/data_solutions/data-playground/fixture/main.tf new file mode 100644 index 0000000000..5f36034f14 --- /dev/null +++ b/tests/examples/data_solutions/data-playground/fixture/main.tf @@ -0,0 +1,24 @@ +/** + * 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/data-solutions/data-playground/" + project_id = "sampleproject" + project_create = { + billing_account_id = "123456-123456-123456", + parent = "folders/467898377" + } +} \ No newline at end of file diff --git a/tests/examples/data_solutions/data-playground/test_plan.py b/tests/examples/data_solutions/data-playground/test_plan.py new file mode 100644 index 0000000000..486d2e8993 --- /dev/null +++ b/tests/examples/data_solutions/data-playground/test_plan.py @@ -0,0 +1,26 @@ +# 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. + + +import os +import pytest + + +FIXTURES_DIR = os.path.join(os.path.dirname(__file__), 'fixture') + +def test_resources(e2e_plan_runner): + "Test that plan works and the numbers of resources is as expected." + modules, resources = e2e_plan_runner(FIXTURES_DIR) + assert len(modules) == 4 + assert len(resources) == 23 \ No newline at end of file From 1e14c5c0e45ee0a0346249f007bda6623ca18e56 Mon Sep 17 00:00:00 2001 From: Ayman Farhat Date: Fri, 24 Jun 2022 17:01:42 +0200 Subject: [PATCH 10/16] Update variables in readme --- .../data-solutions/data-playground/README.md | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/data-solutions/data-playground/README.md b/examples/data-solutions/data-playground/README.md index 305f61cf7e..108d882682 100644 --- a/examples/data-solutions/data-playground/README.md +++ b/examples/data-solutions/data-playground/README.md @@ -20,17 +20,17 @@ This sample creates several distinct groups of resources: ## Variables -| name | description | type | required | default | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------ | -------- | ------------------- | -| billing\_account | Billing account id used as default for new projects. | string | ✓ | | -| project\_service\_name | Name for the project. | string | ✓ | | -| root\_node | The resource name of the parent Folder or Organization. Must be of the form folders/folder\_id or organizations/org\_id. | string | ✓ | | -| location | The location where resources will be deployed | string | | europe | -| region | The region where resources will be deployed. | string | | europe-west1 | -| zone | The zone where resources will be deployed. | string | | b | -| vpc\_ip\_cidr\_range | Ip range used in the subnet deployed in the project | string | | 10.0.0.0/20 | -| vpc\_name | Name of the VPC created in the project. | string | | data-playground-vpc | -| vpc\_subnet\_name | Name of the subnet created in the project | string | | default-subnet | +| name | description | type | required | default | +| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ----------- | -------- | ------------ | +| project\_id | Project id, references existing project if \`project\_create\` is null. | string | ✓ | | +| location | The location where resources will be deployed | string | | europe | +| region | The region where resources will be deployed. | string | | europe-west1 | +| project\_create | Provide values if project creation is needed, uses existing project if null. Parent format: folders/folder\_id or organizations/org\_id | object({…}) | | null | +| prefix | Unique prefix used for resource names. Not used for project if 'project\_create' is null. | string | | dp | +| service\_encryption\_keys | Cloud KMS to use to encrypt different services. Key location should match service region. | object({…}) | | null | +| vpc\_config | Parameters to create a simple VPC for the Data Playground | object({…}) | | {...} | +📋 Copy +Clear ## Outputs From ebd6e51e2ff535294bc102004b615a215da3abd4 Mon Sep 17 00:00:00 2001 From: Ayman Farhat Date: Fri, 24 Jun 2022 18:05:23 +0200 Subject: [PATCH 11/16] Fix formatting via fmt --- .../data_solutions/data-playground/fixture/main.tf | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/examples/data_solutions/data-playground/fixture/main.tf b/tests/examples/data_solutions/data-playground/fixture/main.tf index 5f36034f14..8082f997d8 100644 --- a/tests/examples/data_solutions/data-playground/fixture/main.tf +++ b/tests/examples/data_solutions/data-playground/fixture/main.tf @@ -15,10 +15,10 @@ */ module "test" { - source = "../../../../../examples/data-solutions/data-playground/" - project_id = "sampleproject" - project_create = { + source = "../../../../../examples/data-solutions/data-playground/" + project_id = "sampleproject" + project_create = { billing_account_id = "123456-123456-123456", - parent = "folders/467898377" + parent = "folders/467898377" } -} \ No newline at end of file +} From 986dd71430be951259ca664d87efe72e37cd9753 Mon Sep 17 00:00:00 2001 From: Ayman Farhat Date: Fri, 24 Jun 2022 21:14:58 +0200 Subject: [PATCH 12/16] Rename test dir to fix module conflict issue --- .../{data-playground => data_playground}/__init__.py | 0 .../{data-playground => data_playground}/fixture/main.tf | 0 .../{data-playground => data_playground}/test_plan.py | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename tests/examples/data_solutions/{data-playground => data_playground}/__init__.py (100%) rename tests/examples/data_solutions/{data-playground => data_playground}/fixture/main.tf (100%) rename tests/examples/data_solutions/{data-playground => data_playground}/test_plan.py (96%) diff --git a/tests/examples/data_solutions/data-playground/__init__.py b/tests/examples/data_solutions/data_playground/__init__.py similarity index 100% rename from tests/examples/data_solutions/data-playground/__init__.py rename to tests/examples/data_solutions/data_playground/__init__.py diff --git a/tests/examples/data_solutions/data-playground/fixture/main.tf b/tests/examples/data_solutions/data_playground/fixture/main.tf similarity index 100% rename from tests/examples/data_solutions/data-playground/fixture/main.tf rename to tests/examples/data_solutions/data_playground/fixture/main.tf diff --git a/tests/examples/data_solutions/data-playground/test_plan.py b/tests/examples/data_solutions/data_playground/test_plan.py similarity index 96% rename from tests/examples/data_solutions/data-playground/test_plan.py rename to tests/examples/data_solutions/data_playground/test_plan.py index 486d2e8993..1807e3de2b 100644 --- a/tests/examples/data_solutions/data-playground/test_plan.py +++ b/tests/examples/data_solutions/data_playground/test_plan.py @@ -23,4 +23,4 @@ def test_resources(e2e_plan_runner): "Test that plan works and the numbers of resources is as expected." modules, resources = e2e_plan_runner(FIXTURES_DIR) assert len(modules) == 4 - assert len(resources) == 23 \ No newline at end of file + assert len(resources) == 23 From 91f3abdaeae0812dc61c401f19036319ce0d05b5 Mon Sep 17 00:00:00 2001 From: Ayman Farhat Date: Sat, 25 Jun 2022 11:57:02 +0200 Subject: [PATCH 13/16] Add high level diagram and sort vars/outputs by alphabetical --- .../data-solutions/data-playground/README.md | 3 + .../data-playground/diagram.svg | 63 +++++++++++++++++++ .../data-solutions/data-playground/outputs.tf | 12 ++-- .../data-playground/variables.tf | 6 +- 4 files changed, 75 insertions(+), 9 deletions(-) create mode 100644 examples/data-solutions/data-playground/diagram.svg diff --git a/examples/data-solutions/data-playground/README.md b/examples/data-solutions/data-playground/README.md index 108d882682..598a121821 100644 --- a/examples/data-solutions/data-playground/README.md +++ b/examples/data-solutions/data-playground/README.md @@ -2,6 +2,9 @@ This example creates a minimum viable template for a data experimentation project with the needed APIs enabled, basic VPC and Firewall set in place, GCS bucket and an AI notebook to get started. +This is the high level diagram: +![High-level diagram](diagram.svg "High-level diagram") + ## Managed resources and services This sample creates several distinct groups of resources: diff --git a/examples/data-solutions/data-playground/diagram.svg b/examples/data-solutions/data-playground/diagram.svg new file mode 100644 index 0000000000..caa230c930 --- /dev/null +++ b/examples/data-solutions/data-playground/diagram.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + +Architecture + + + + + + + + + + +Data Playground Project + + + +VPC + + + + +Notebook +Vertex AI + + + + +Cloud +Storage + + + + + + + + diff --git a/examples/data-solutions/data-playground/outputs.tf b/examples/data-solutions/data-playground/outputs.tf index cdb4002f03..3c47229fb9 100644 --- a/examples/data-solutions/data-playground/outputs.tf +++ b/examples/data-solutions/data-playground/outputs.tf @@ -17,6 +17,11 @@ output "bucket" { value = module.base-gcs-bucket.url } +output "notebook" { + description = "Vertex AI notebook" + value = resource.google_notebooks_instance.playground.name +} + output "project" { description = "Project id" value = module.project.project_id @@ -25,9 +30,4 @@ output "project" { output "vpc" { description = "VPC Network" value = module.vpc.name -} - -output "notebook" { - description = "Vertex AI notebook" - value = resource.google_notebooks_instance.playground.name -} +} \ No newline at end of file diff --git a/examples/data-solutions/data-playground/variables.tf b/examples/data-solutions/data-playground/variables.tf index 53644d37d5..92f63e2d99 100644 --- a/examples/data-solutions/data-playground/variables.tf +++ b/examples/data-solutions/data-playground/variables.tf @@ -57,12 +57,12 @@ variable "vpc_config" { description = "Parameters to create a simple VPC for the Data Playground" type = object({ ip_cidr_range = string - vpc_name = string subnet_name = string + vpc_name = string }) default = { ip_cidr_range = "10.0.0.0/20" - vpc_name = "data-playground-vpc" subnet_name = "default-subnet" + vpc_name = "data-playground-vpc" } -} +} \ No newline at end of file From 9fe867d5073348efe5b83a92f5576f69bd0ff180 Mon Sep 17 00:00:00 2001 From: Ayman Farhat Date: Sat, 25 Jun 2022 12:04:49 +0200 Subject: [PATCH 14/16] Modify diagram and update main README under data examples with link / summary --- examples/data-solutions/README.md | 6 ++++++ examples/data-solutions/data-playground/diagram.svg | 4 ---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/data-solutions/README.md b/examples/data-solutions/README.md index ec2cfd08d6..961ff07a9b 100644 --- a/examples/data-solutions/README.md +++ b/examples/data-solutions/README.md @@ -32,4 +32,10 @@ This [example](./data-platform-foundations/) implements SQL Server Always On Ava This [example](./cloudsql-multiregion/) creates a [Cloud SQL instance](https://cloud.google.com/sql) with multi-region read replicas as described in the [Cloud SQL for PostgreSQL disaster recovery](https://cloud.google.com/architecture/cloud-sql-postgres-disaster-recovery-complete-failover-fallback) article. +
+ +### Data Playground starter with Cloud Vertex AI Notebook and GCS + + +This [example](./data-playground/) creates a [Vertex AI Notebook](https://cloud.google.com/vertex-ai/docs/workbench/introduction) running under a VPC network and a starter GCS bucket to store inputs and outputs of data experiments.
\ No newline at end of file diff --git a/examples/data-solutions/data-playground/diagram.svg b/examples/data-solutions/data-playground/diagram.svg index caa230c930..9e5cc9de66 100644 --- a/examples/data-solutions/data-playground/diagram.svg +++ b/examples/data-solutions/data-playground/diagram.svg @@ -23,10 +23,6 @@ - - -Architecture - From 66c07affa6c3a397c69cd2dfd2c660719ab1b9f4 Mon Sep 17 00:00:00 2001 From: Ayman Farhat Date: Sat, 25 Jun 2022 12:07:40 +0200 Subject: [PATCH 15/16] Line break --- examples/data-solutions/data-playground/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/data-solutions/data-playground/README.md b/examples/data-solutions/data-playground/README.md index 598a121821..dc12005b8d 100644 --- a/examples/data-solutions/data-playground/README.md +++ b/examples/data-solutions/data-playground/README.md @@ -3,6 +3,7 @@ This example creates a minimum viable template for a data experimentation project with the needed APIs enabled, basic VPC and Firewall set in place, GCS bucket and an AI notebook to get started. This is the high level diagram: + ![High-level diagram](diagram.svg "High-level diagram") ## Managed resources and services From 1e1e677c04b28499b7beba2770c925ed935b6b33 Mon Sep 17 00:00:00 2001 From: Ayman Farhat Date: Sat, 25 Jun 2022 12:20:39 +0200 Subject: [PATCH 16/16] Use png in diagram --- examples/data-solutions/README.md | 2 +- .../data-solutions/data-playground/README.md | 5 +- .../data-playground/diagram.png | Bin 0 -> 27555 bytes .../data-playground/diagram.svg | 59 ------------------ 4 files changed, 2 insertions(+), 64 deletions(-) create mode 100644 examples/data-solutions/data-playground/diagram.png delete mode 100644 examples/data-solutions/data-playground/diagram.svg diff --git a/examples/data-solutions/README.md b/examples/data-solutions/README.md index 961ff07a9b..23fabdcc27 100644 --- a/examples/data-solutions/README.md +++ b/examples/data-solutions/README.md @@ -36,6 +36,6 @@ This [example](./cloudsql-multiregion/) creates a [Cloud SQL instance](https://c ### Data Playground starter with Cloud Vertex AI Notebook and GCS - + This [example](./data-playground/) creates a [Vertex AI Notebook](https://cloud.google.com/vertex-ai/docs/workbench/introduction) running under a VPC network and a starter GCS bucket to store inputs and outputs of data experiments.
\ No newline at end of file diff --git a/examples/data-solutions/data-playground/README.md b/examples/data-solutions/data-playground/README.md index dc12005b8d..fb75969505 100644 --- a/examples/data-solutions/data-playground/README.md +++ b/examples/data-solutions/data-playground/README.md @@ -4,7 +4,7 @@ This example creates a minimum viable template for a data experimentation projec This is the high level diagram: -![High-level diagram](diagram.svg "High-level diagram") +![High-level diagram](diagram.png "High-level diagram") ## Managed resources and services @@ -33,9 +33,6 @@ This sample creates several distinct groups of resources: | prefix | Unique prefix used for resource names. Not used for project if 'project\_create' is null. | string | | dp | | service\_encryption\_keys | Cloud KMS to use to encrypt different services. Key location should match service region. | object({…}) | | null | | vpc\_config | Parameters to create a simple VPC for the Data Playground | object({…}) | | {...} | -📋 Copy -Clear - ## Outputs | Name | Description | diff --git a/examples/data-solutions/data-playground/diagram.png b/examples/data-solutions/data-playground/diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..9da71fd09edc9e7aa171658766a8e0224bf04bbb GIT binary patch literal 27555 zcmeFXbySsIwknZmOJ@~%o zJKsC*`Hefy828@4&UVQ2JS*m!Yp&UI1*s~_V4@MC!NI{{%E?Np!NEOAgM)*oL_q>t zXp0QK0qr`j8rt@1ASZHbTPqVY3oyC8i#3=W>}+NN2j@JS9S?CNZorg!u*1`Ng8G8G zoY*rZip2P;Zj)e3ry|Ofj?mn7e9$CHJiUM&YeWB`af$49b(RJf()7MKBGvGx@nL%R z)Ufk{I`i;NH-k&+ovmnKqu4cv-9RaNQ*+qX%I%GRxDlcIC>ibcjt9uI$zaVP`W})o z#8DgmH`H#dNn*;dR~cys222?psol z?W%~W3k&&8sju**s!MDUh9WWY!MWf`N{II@qjIWPS8xei^{wqlY>~U?bxiT~!spAB z;^T;)w=6wpCzt&v6k9duS0`7;D^Us$zP+(0Pc2&Bow8ityB+Lb%y2c}XTR9KUKsCw znW_?!DBMmqkDyRkepIOVD<$@5TihAmk3=h}@m&SA@Lf)%BjHxg39;6D=JFoY(M$3Q zYB4%DOT&tajnYddJ+oEo4ds_pQRk6sb}U;iML+RUavf3{aU^Lt;w68F>o2KT)lMl; z(1g?EtCZdbJD2JBn+`1!ym>R~fvlMd=hfK!;QWz`KqI^&Ht_lMRf6H?>u;#XQJVwe zYnKIWUrOn@Htp1D_e?O2$hgo&o)91DYC%gB?HZdyk-4mW?go+ zXx}mqmwf$|6Q>TUnPh0y1{zeybrvQV!TUCK4@~rB$c=OiD)6SoR7tCn z<8E*>Kxq2mCXP}dId3DxH=Qylgii(D`{l6CZa(hOfXh$@s%CmS<%AtH$}|KcCQ8cc zx<~K3)*BJK2dxDOCjDdD2lMr>x%=2$7j_GtKdSdQt5QOm3PyYgxhA^fD3ZNCO-yB; zNcU8SCS&~~AX-ps`M$q%5I%+xUaq%uGcfuw;0$7LxfkKZ#eR#8{jk=0=tiZoXm0Bl z65o?tNHXyHiKevPDvw_{_ArN8OK(^H7IWhJ^)lBt^WgEJYDYLw%Idxx>uL_1v0)yyD40KPt%^yU>~nTyH_Y$j$NOgUoaaw7 zo$FYh#~n~1c5ZH2zx-lw6+Dw)A{?N(Z}=s$_pAr5ThaozpPhXTG#;kFC7Dr+@l`}udR3=@9ekQ5&nl>Q`SE7tk==z(>AoY zcivG&NLnjBP%=W3LTAbjhKuESeeWcY8B_An@$6Ei4@NM%WJg=M1f=9y{+ z7El@{o9CA_H+~}G)b`qs+}UG1Y_;pl{K%*|Ye)`gUa4pLqMgtAYkndxXdS(ArvEGI zxrtRGN|Dc=G!snS%$5XrQP4NY+5TAmt^p$-%-pEyGPG2>KGdkVm zc|?rMtc+`>3s^u>dsRe~L3L4`bF`KF!l3c#giPCNbmx~+#9D(kWl%<^`9@yFJ#)%Y zJU@maOg>u@RuLYsiJS1a=1w=o36dKUPZ_J=_s`=Jy^1&72vIR*Eg*uZ2e?IjKXCNsghR3{REhal;$GFvSpuQ{I1=mhJnshQrY(5fA!rDMZ#lRhqR zW}&?1TZNJ79ULKQ7QrWF?|&=tFCH6i&?l1?U3_&-e#Kx}+}ej%IbLnYpUVsr%6(0G zL-n-A#Pl=gNaKOPDF#700>X5RUbI1~0;Z87$R_AF$Z(n?BAaZr?-k>w8Jh&alZ10-e@(?nw9ZwTB(zYT-Kj z-&}DY2z`pwnM+9Uo^*US{A|OEKXV=PB7vD0O|X)?(bq&i@ly8%;=UP_UQyA?B+z!V zs_JmJ`3!CgovK9|_cgP#mn08D*41iY`2{W9kal_&J%m_PmET1Bip{Y|KMDOJ4J$9$ zYm}(3@Q^%#14KPlbrr``XgLhovCe5LtgXNFAg>KGt_b~EZxOk;C;biWOb7Z2hNGjM zTV@;^<}QDbXU5_!MG`J(_KPYG5M{e!rRs#% zIM+veYzTLj8&=+PoJ|)6M0(^N-wggw&*XXbPX%-54PvTQ`ECPN*N72$3^cYzh{8>> zrFCBXW_n)7Zj;GV^wXSQpukNvS1CZ99D?4%pD04z`1020$4@$toowG($+zN!n5X>* z;+a)MSlZ88zO*S$46ZHvo^R@Y^FiS6pS}x1XOctL@+0AX<>}Pb`j*DJJ4QWk>_q_O zcXrinQl?whppV}#eFzDq;_EpI@hZfkKE8Yghft#6$u4||HC*X@dZOLs>Kp?PzeE^0NzCmN|% zJi&kyTNo&A@OqO(>f6V0{vh~eG!(7$Xo_F$_di>X0>?`E<AXP(=*miKX;r60 z(XLLp;k8R1zKlrq*@7v(2jR~C^=V&d3E-K%)10o|-w)t{dQc7~XW)Z!cpGbJznYE7 z$SK_Vbj4_4%RAJEHbornF6Od#Y{$KsMMGa$?@(Gzr50y@u6v^4;_-7^zjZXl;227W zo%tr>IZ?O&&NITNcUIA;-Tv`AbAhjNxi-Ju(?u}~Mr;jsfBdC6DHo_`ygQa={=w4w zDv>Zr{CoOq6@_1J3?jF%$4%?I=?3py&1Uc#4HB^%JDrfSF-VsmKRAV%9B=b;{= zIwLP)#qjkU^ChRWjX%(e^X;FrWuRqUQZDR8zso<~5AK_6YfWpdf5jFPB*;MSg(u`C@E} z7aM*Fr?Q4)a(YZTcBk`pr_La5mqPxWcr>pLh$o}GnU zK-d^jpGP8j;|m0a=15VAgCq$Q>MHrj@#PZsfBeQAaI^R%!hYHnHKA$v{t%h&_dd~= zrv4-wL7M3K8qRJTRK2-R`);B(teySX00!}D6>684kvKdNq6dJ(Z5aAD+`UxSkpt>Fi$1IZ5tpBtq@=2xq~w1DSwLu&=%imU z9C?|gONStL-#I%S*j!*eE0E67ff#w`z)UzvamM2DI1YocPN@P(LwW`{O=uY4hmRbv z$@C90g!^yiQZp3>-(wsGb#e``LzW$u^Oaz~8&!x+5Oruo=}39DzIKM|YHi_k za&?$63dj)CzC2+ms$yq~BRrzEk(#jYdo}9tPQc1$G8jBrOXmfmFJxo0NnR&k)^4Sw z4pdd`LJBT>o()6}5|QUdr1$VsY!M0+Q%Lv%>uv>jL$&viPBFyJE0>Jx7m zp`h$OsSaLoClgZMdwM8_pyfKIU+`osHA^eu{VK}l0upOUVnPM7?mnD{ioZo)$!adc zs%`5HfrmQMW)tGq!m|a!8-5QswQqq-TlMDAEW19!a#ss$MUn`o- zfT-8i42XKQl@tYytRSo)V=F^2t24wJh>GFhgvFe#K}P0advZgtshOn+0n=tg9_p-Bnq`$kp6Pz?f1@6iwJ!5C8xH+k?oRAr_W)g3cn8f8YuN z?~l!Fl;nSg*qe(`YAdOdOIq22$+=m%S=m{noy{CMDMiu9g>8*Z1l6S8{0##55~2KL zZ*MKg#^&VY#OlPwYGrH6#vvdez{bwW#>vS7j9{^Iv9t#{vsl_uJ%acPh7{P&$kxo- z-ptC9{1GO|(8|GHgpv~IC;t!pAl6Ds{{nAm_cs*)dayZztl2nN+1Vfvwtt^tXD{st z0QnoC|8j<%29P{pQv=&sIoKM3r5(YR_Ei54!r16v^Q|3hE&goB*oX~m0fqoW?SNG| z{;{NtoRaFlW;~L>)C^+%XBI&0e~h#@Gx<+q{R6khog2|G6* z52rCVzY!W)QHtJ?N2~9K1Zd96X#{ynNi;9DLk=t@H=rJFu-Cz>1GRIoMgb zIR8w4JQqP=HvnZpkBkZc_|pz-Mo`ig46?Vf)v&U%5TSeoLH;=NU)@T8JQ;)RK~f-l zFaVUDlT(nLSCErOgM(9$LjZVTVh39OZM>DSnTgB)ZuFz{kPH8H=CWpX!1^wKn*I_g zb+FA}Uw?gCnEjC@a`Hb?AqX=1YYTQDN3ii9JONmLbs2pES(<_Y_3<~q{ztpnf58iU zMjS@$0wCbnj0L~|FBtN%2=Hggo7i8laqR_;XJ!L@8Yg;mj3WCB~WRi z((}aFN1S{&k@*Q@2IXY2CTw;xySnCGF|?*%t7XC5{A|k8%BtA4RNr zIK#8YH$>IPzi;92{_XKsTSTVKHtwmOXsQa`Ybhxyj^1 zgC8G%ef<6SyX_xc|5e-n%i;f91pkT5KZYm0P7*{s2A6*>C_CTvuYYgR(W zF|I-<1CRz@Ba81vu3xn|U0b*qd95SjI~RthKFO&owjdMCE&&Mbke#WK3D%R2kvg2` z&joz&p%9oLrbpE9hLG7!1?=wW$E#Ogq)*B}BrM^_CW8{DIm&BWTo6k7OS`PMYa#;! zd$#Bq84LP3tKom(kclQHe0u1odiv27siAHwZV9Iq%KYhb*3yg%vxgmW@g7|X?l=^} z+}~coeCnnsCAHeAULKjG`c47;UdBu1(^l7^Q-V~5BZd_b8+wW920ho7dXotN_SznkK7Xx zIO1mY?0rMU%^?AaPy8$y6$Ly3%EW|(10J*#uVrg#K7`4cXQRQpEdUW|g%e^Ddbv!K z{zd$|UW0=6;Vh;6d9)+L*{$a~Q9K!FB23{;{l*t07GfksC`(L4*`v>Z z{Ce`Slw|1ZYd*LogB9%A+Y_8P!A|mRbX-LwABqJRF9xfoJJL3TX8|3rP2LC!_#lJg zkZ5SKqho%kH#@O~KU^Km9VVN{3$7K39(ihNP909d4$2|jVY0PPt@(*DHdDKNkBsUr zGtjLY=Cxyfat$}NHuuSUdJNA?NxQT}8+|;Ugjp=;wzBXoN;$sEu@rnH^VZJouox!h z5B#Io1JjNeRwzQ7tpaC-wIvS%alZ`W?4y$jj!^mOlzwt~G7c)R><%W^)U-TL`2arg zISDJR8iYb7ix*oX`4*mFR8(-*o+0!7D%IZrr4kYnUBSdsx+<7yQ+bXC9?}h9yM=kb z(;qyNpuA$`0D)a;SUY4_mzSssj-JieT}M zZENU8+-?b!pTg7jBh29#b^|q~YxWyOJQ8q_0XTRb=O+(}8@=?NFSgd|+ z@zAx&Bg12@IavPw_#zj@PeRxF-en-!jsn0pqMOZ>wL#(h@c7m*D~lEfO(zffK}rf& zd~F;pHHOUer%3J4;^f}56ewQnr+W;Ue%Aqy2XgLv1Pjv*wH{yk%NZJ^8qs+H@0iKi z#xg=r_MMX-U;QO0UN%DG#L30&?QtJGyNmgW!>B~XXlZkX8}$#G9^fY?cm|Sec(pvv zjx8lGe!rf%J?*XmCD398P^pQqQBr2WG3tvJ;%$^Pa1?&@H3Cb)+7A!9-k$17xIFB> zkl>0k_(Zrn=hef=+X!VPmTz$j`_libvg5O*WeyaI)xjR;us@LLKWt=G=!Z$fS6-U& zweN0%y0befp5yylDw&ZMulF0j&2?~bUzVGyFl2+>$|2tdLNF>SIq|t%HDb$Zob(a> zq+#?73_qvVsZ0;aa}-lLCyS-0p@VfBiek$)ni~72Co#p*y9_kXP$GZC4;{|w+S&yW zmq&dc-Y~lBa({t?Q@d`iMO@ukLznDMn3ymf-Qp*b4c=YUmo_wfG7}#kLLwAA9v$6t zB6@F3V_B0Zhtut}y}EW$Q~CMqJun@g&(VIhw$S|Zbgd27&j!cm7=HNC$%ABY=iyx7daK18fr2ZOddz*+|DGL7X z#z_PTmUBjra3VyE)kuns85(+f7vxn@>8!Oe1;MALW7}w$@=Babm!#}p)W4>%YCdCy zczPMnbSWj;G|U4W1Fs)1ME9cNe6R3#xYxZv6c2nwCCxYsi**gGe*W z2XvQneKtVz;BQT|*rn)aJa05BQ!#${>kYIP0MXdvz_KZeh> z{~J<|+ft3GaVcm6gBNFTqDBpH0>O=M9U15*55TI0|@ccgmH*Nd8VfsQtq&w1e9-rmzE@E9GN(9^N0 z$*za1mKNdVYg46!)(gwVFqh%x)}duRXJJxWCH&#IUsN;I4ZaRLd}?}n9XKi?9Ud+9 zMB&{XIk$t%&JYebs$+jH%YjddV+H_k1VbUFzandT26_zUrhipcRUMB|g^!Jn%FD}h z;&g?AN79&iE$0Su20w&d&wl>=*~RSs$uO)uQ z{faUYDT-GB<1CcKdwhJH?e@F{x`_!rn3h{8(=EAM=K+-H8#@Jb^p76~3MR~5ma?zr zY+%?n4Q>nGRn-&SsrT1T+uJ+wylc!QcU=?CO&_;+h@6j8ht;&J_7sSo$V*9urEZsI zP2o2~@S8I_UJ9asAL!EGo}h-g`;FvX`64~ESko&th5i0c6`sSCRGx=457z?>(&K-wO*NG*HHf$rIuw+%H=9VbO zOl@(bwGjQ)bCjpft_v-2{BGOEmQ7c*CaGSml|w=SAFP^#Y>Z?O4#yi!;p18k9p1lx zf8^S9yBR}fJ*cV0=`t$S?6l$(XSZ}EBLdwM`j%f%@M!nMZrd(uPa_3F2zkC14IGw9 zK_Qi^#ZD-G_09VRzgtxm^vf6<+z&)2-LDBGZIMno$mTk|5tN=A8W~;=RqLCMb{<(Q zmPY~XEjr~`b1?lH{L|AG341wFzlBjvJ~1w?)v5U`)H2rz=Xr#Bs=Rpih36o5DRTd! zS$r}(y7_O&Z~l?7`S?Jd--~6Ke2&{S>;svx{_#jzKRc?&Frd5DGjClE%g@Q(p+u|% zZo5Vbf#Gx<91+36eX7Az(BcHwm}e2-0V2l^G@*od}J0U#We`_!^KdEdfNbtbJ zTWa;#(nZ4JpO$J}vbMg;jy*hYIKaVhTs$)EpL}8gqvfE059nwkvTEvYIAF^z9#qkY zj?K#A&QVGJYUaqoptzHY_8~1hdkwwmVrR;V8W)WNbUV#z^i_};%S17?%Re;n(qbNo zRvCY4WpBSQFH7W^!R<7MUi&)0$g5Z+?dvQb98Q7)lPu$3=Zd?AwlwxrGC3}{9lCsC z-}6F<6DbW2{r2^KUhz`BvBeakw6d}yE4Lp2fw|xI_E~m^+6xjV-&eAHD@I&;F+Vq# z9+9$0Krmk;L>!$=;yDPF_V7S}LRPH#+3OjWM5<m~I6PGcp@m-VRbZB@`5* zBX9Th-3cF#P%(czrY+|v*R`3UnR7k4sJnT%jsBG>i; zW+soSiprrump@sVkWg%~HodsGxd%INKa2We%!BUHgn7izE2#^w*bO~+a=+sfVh>4y zEN zM|&3;$&TS;z&^eAT{^(O2~gxRuBm)AQ2QI*9r_N^>?~@8k>1IW0-|D^;cyjFX+QW4H+y3zSu~5K;~%<^i%V|Thv{USNA$E?R@(c@x1 zcWi|3?Ech`*W|39r6i5#AUJemf_mj{OG_a+M-2a*e}8zPSTv!M`Jl86IZtiIr@F>X2K zeHk+y9p}cu!4C{9Y~8KDS5)2|&Svl(-P{2#pKU+3W&4(Wq>wQR)4SqyfYX8C1n^bQ zcNtdvV)emOCG+_HZNr}KL_tA87-YO5YpF$P+0{QBh=pm97$JYD&KX7CA69lmfS6y3YL7Ns32`X5r+>=r$Ff6AHq0?$Hq) zliBCbCi_X~a`K8<(2|tf!4K2#YeO4Bb)8*ZBcr43VyPY}p^3aveD2z-y88=y5@tFA z;59b_F-r{A*l&6bPA7o(T6J}ol|{`3=`((T^J2Q9`g^1ZjmBQ7$-rjBQo${Pq^Rc9 zI3Ra`e9i6^y`!W&UfZ!GHtT@8m4|64Gc{+1xVg9oL6KzIYB&<#k?9%}H6EyJ{5s$2 zti1s_s&85%sUN-bAfuY*5!Ze>4i*29wzE&W(dwmZxqOwRw*{~b8Oww3kHX1{(u@ok zG(4Z7S22z~rN712CX4%dT6uZUhQ6zx($$ChB0gtT-YL(O&q&yPVazwbGuIlA)`j%s z4>B?{ulnY`#fj|9^^~as8;rl$Jptk*C$vGGcmm+X2*w*Odh2$~XSH^qa*vZc)@ya} zK!bX8w9CMkcZ5@D#Qct2jGYIAp0*K zbb}I%O-$G~+GiB;#a_aoSMvziKr}{%Eq166L{(;=ELO8)UzLH>MU?}5^780!_x3LD zJZ%H8mS9sKJ6hqF($feD2`?ABA0)tQo^zIbmNnnR-MoWvx+_igOjp;jmb%625d+NB z)YbddPg0Dg_Fp6^(~*WJ0zp!5@13lI0{CRT_;xwhiJjWC%v0eXiw>Ppv}uZHUCT+*-_byaOCBO@G!-rf)ru3!oQA2;rv zMhI+tVxqFa7#ncq)!){U1$0nTi6qaZ3mBV%pX1e@Yt$Hslt0Wb%_ zPO)V!3h*}<2Xq`15fN1WT<+SIv3BZpUSfb;8am0L{(Ad1gpAL>ajD}`-fC<4->hPO z6Rye654uI@FhKNq?Z1Wvo{Reh2#1D-2IuEh6hp%d2la1{?(rgenm1&L{tD0WXH?Pg zPh0&H9U08jZXEF@Rv#w1R!lC;p=gdC$@FIvJUo|odlBbp$Ug7!F7DQy06Un8{cw-2 zL+HFNz0(c$N1 zu_JTC4JceO7d?Eeb=T2=lqkULx6MolF$&+1_rNNb)%MSH$$)1#L<$fy0rXVFX6jGGNc-g$;tI8ztU9yU|A#o ztUqX4uc4C2|8;bKj@Q@MpNaX0et+ozyl?F?>*uK?ahmkE*A#X7oceXfSYtU!*)M>6 zpuk=pW?s@M9Am1!q$Cy0>E+nuy6JCkZa}`@9sbGs{FN^hSb!^t{|+R;q*k-k{>mW! z|EClGnQ8oM+&|m?PD1`yU4LgJaXRzW$>}~QjbuiV0|ymJBXpNLP@eZx;}Hj3ap}hg z{GX4%AAh&~qwBwF`(K7D=QYs?%~X1rr@GB+-tJ}P?=6|%!Yw=@V!B#i&m^Hr9bej*mm7dYDK7NbJ^`rr810G;HUMf z2)xYEXFA@T@MfLFuf$077=?_27}I92R1RxZW-(I$(t+%K?9 z^siWmm8V{LlIWiT$)JdoU%5p|qs;wJ$XV>31u>Xa@$yxiRpJ%h9TpE)Cx7J8|6ff;AF?K zNUT_f6gj(5yrZ|DgM}$5(o!%Wj@h5~m3#i$0VwPGh9=GSvwoWLE7qETUs0xSVjwy< z9BvAZY84(#K3D+ahzQ)16U(*@K}xd0%oo+XD^@4)Ba^csV|#5bX;lMI<^;meqCYUYW;$$mqd57(NkEIbJ2(cdPe+ z*DhzIr&JS81|ch4&{r&*AKoq1dxiQswXqt`B5_9l2FpS7hdcB(xu-rQ@6&DbJ5F1F zj_o3XBKHdii8eSW1Tec%rg{>(YGSJf)kPw!z(P9(rnr(SlYmi~knZw>24XtRIa}cS_ zkF(a-lJxUBvU(0Qy$GW^_g(J9Saj*C&pOy^Hxr1=AeP2^0am{}$?=~UV+|Uuc2>gE zYu5w^kLu~#pJC$X1mU5T-$z~K&3hf5VGoeD$K`K48PV42FzsKU!2cB9Nali*6B*BS z8#omd71Og`G; zaU2D~4K%JY`iBsPLv&V4?@@BcepaEg8C@qa+0=>Pmgt|=g=IIJh{b@P$Bviw8+Fgh zbp6(4?#}>t0ggop^C`k!R*tE(L<)RJyesP%Yhw{CQnnoaMZ%FHwv7QMkmb1*)XJml@%QMo3P5V;6PauQ~3=5@W6!eej zx|TJiV8S)r9X|44D#y*-lep z6Ld#O<5ZVPr>5VOI(v76;`dcVu!bbtCo?w*L42W>A$p%aE4!T3yCPW{Kc_d#Zrvb` z_DGHa*HYVKVDBYFTM|5D$qdV=OHYaEa!q1&%2J~DRiF+C6+!o&y~C@jx#}U{h&gSm zgHseX#Uf+Aebw-dH!4}EqF?lUkAaqzL*NBz_i(LuHhN07h1PSd*lz}D$#Wc0vi&_s z2JIm;MLO3=XFQr&*o3odfL{Jn44GVCKSbQMU$U>y!`w4kYbeDztdW*q37bqAv*$nT z>ipi~N-!9D7NLqtT*+PfS)V|>R#NQ^ucVC9CT|(3IT;|ZhRZkrVLE{*C-%p0`???% z`E+|z3OInJu@{O;tlgwqO3!|aV!4DC4~9-dm24kQmAfN=2|kKmyWZ^tZTSggj>H|Nr+qiTPRo=GL@fa&Z1{HkR`+%#LV zV{-}qdgMHJUO+gmelEqb@TMgXm5X(A95r7kMN-gM`bmk$v_RROrM^PWlNFwxI8&c0z_$;gy->fFX}6cu z3$j-%D50lI*Z;gK2&h&yK&GGN$GrMe2U=lf z@-3~72Us33`)Yc1u>flezx}mJ`iTiY7JnbE%4N?M$pjm`n6g}wz9r7i0@IOFoAs7$ zg=xlxcPwRw+3m3BmsOO@EGc-QqG6x42QsuJv<#00^nITw9{lmSR^YH1$T2|Zg2!S7$^ z>DOtpN0odNl39{-?Av$|)N3Zy-Bk%FIlyfIn9I>p2hhD3BwXb%2PEUQGAk7hqIb+R zfQBI4`8-_*7SWNE`OXJo1Q5|fOav^FRCnjwDjSyOm>gNUijLB#FlfV{ii!uLw|~dm zwrfp%|1bS3j{pFp`6AzTh%i!SVnJklzckI!3lS@Ti_q?KJKr5E_B@qgAE2H}v>f8TPM!fN9_WE5TvXR~G>> zUJFaJm!SZ*h#4+9t8_D%*);TJM)Ff$eg+`ciSM|db%y=NT>vj%vxJBf4~^87IfppT z8>XTU_pCxSmMF-MW5(0dm6RlvSbVO>iWabpiXv*xdU1&s4gatdxYluWww2281rY2D z`+;lF2+_YCZuh2`c5GD6ggq!dGf<5K0z$0 z8EIM794ugt)LDex&%e7aYu`-QIUxM}z-POkt=~=LQKpo;I28<#__tNZlI>eS8=sN2 zboT_AMhS42%G{j3>2?%)V@i+Pd0>7g(bNR#&lsGN&`_KUdZ2iLwq#|#S5c}|D3(!S z?_^9oBB}2MC>ByH`_(2<4d=tVbLEYjPk=4&i6@`ATC4I458j@&R|GuGE_RYIDt{Fe zr_Tb7HT58TI`^Jrs=dsnL7BmOk6p$AI$F_k1^o9{`#b!}thU|Cl(-lbkd^ls9sDLT zDJhMt0UcL<`altmj>g@wu_j=@zk(Uij{x179&@`rKfc#_5o>rjVB-^~nCZOEDh>y?ZMnAQzESPr-c$f~fJb2w?oyXa3qU;o zwdSG6bo@I+uX@y~8^FP#7Uj6gGm3%Uz}S^ed@+EGu6bT@I}u){I4k9-WUd-s%@eSoS^tfu5Kn(f(iu1`Q z8I(P9T+zp{t+^BK&%F{L%ltS2E$#8|-;clB{?YaK$EZ1QHG?vl#dAYFA;DeOZ9*U} z=hs#_i=Ufzs{X)fFI8E&&Y9WU+-{?crc)Iq*HdARtQLL)l3fF!hgMZGtGjD5SbRMzlY)Vv7kfox_JoRhW%&5&)46@0nxVLe8DrC&H}@aU%$_5{`li+2Ne`vn zyH^>aLm?*S&{tyn1v2eSbIT6_KsM-rk;LgYrJHd}wzy7VPL9!9KSI;9v*Ro=~XQ6}s zm_*vupYecVVODlFhxsJ)=vepymgT`hll$o}ncc+}vKrXnjA`niFDec@hU7t7(laW5 zaK&{cWOuqc{CNi_{Pf#K>@jZqq-4i&2Zg=@I9Ua`tn_k3@95P_*5O2cM--fHtY5t^ z0c$pblil2$Vw&ROKRPh6=nmm|rQZDm?`2c4jCq*+l!=Mem!&vF6fj^kED(tsLS;Lzi?r)0TR~M*X z?yA0j-^Vp9>;r+hzRJGviWeZk*iDs!=yC1sy@tN@E1l<2`6=g5hkHS!ZXbZ_9XMqRY)* zO?bVsnHfVOpOwk(bh#^C_Eaco#Na~kd+4i!?Aoj+rQP!o&1=7n(4BGAEme{|(VgwY z24Se37GlG>hV9S{)sd@)1RymY-Es`@dexsb!&9cFr>FN?!cErtez?9`^X*Yo>Dsa{ zvsC8*aQENvyyp-i!HU|X$97}dJ|vr**&M?Zn*Q*%@Q~qAa+}y%bOm*I^F3xK< zICXEFZ0SYg(9#L9jN*7|e4Mm_7_C=D09RLTrI{F}-*pYm<1xEzsK58Pmqn&~^n*t} z#>B)-W^UB!mh`lUkg9}F60jS1FGvf3xy$cdQ44oZ14Dnx)(`Xp=~=hTdTo=vZ)Sg^5SD- zWAU8ez6kZQv0mUbf?*zIzP&3)*8MCszwJo{s8o^#f85{Q?C3?0`FHRAQC5f}QPr7+ zrOgR-b{md5`)96oD`7(QBq5?yR6a1P7Lw8_*jl*)Yu##~QNZJUxSsr-tRY|fCBIjk zkXdd*BBF8#5pY*VLZbaG%+18u*m_vxVq|1QocviUJI3FI%L<1e5C!@O(VsKt_ z38bvTp*|WWjk;!3OHvM*DV1x2$a^42x;4Z))>qP{Ykn=th8~Ov0pX%1n zLpgqTtbv~0vDf4I3c!86EW67p2vI+@M}8z6vgMJDxfd~d)=Vg&@*WOiZr-tRvUG7H zbms3=rwb4hNYsbVF6GtN6Sgd3`?|Hb36lafsCo#QYvcXKG3A?3LTL>R+#Iu%qgOwV zLegD-9n7BfQc#+@!&OUA%-(snD!qB*uN-`u0EjT~S)__GwxkLt@|?NfaK7GwS+72N zdwbhR)N8dTLf8H1=lsHM4ESi;DBOXth?Ja$#t)sf)>|%$7L<@vVgXnNi6tCzd<;^Q zG#f#N(H#IFP>iq)K!8DSj8Q}=;Ozlr)Odfr6CE2XBhTWNk(TED_3M-6m0X8{!a|p=>{#c$>ZzWdp5J{;rMpm>rocd170EpCho@iR zlQ9G+p0vTmK+a|kQRxiubNtX>9bc}5tLy6C*0pb>CnO|jSDIk9Tn$=HH@esvU$uTg zs5>21Xwd6ExBvzXtb~gVwfmtvID8B2d4--XlJw&J>Pdw}x~N#YL@AD^&FI-7ld3E=Ht2~%iDO-(Y8 zK9=otYAokb1$y`voHNH3MF_gg0K$$>PHr;-1D^}1BWxbMtGSQ@gTc8i;gcWkjY*;a zJ`5yI9kV4?J#&m53o3ZODm9|W_*loKoJSh2IQBrkFFv7YA`tC(7#_gSBWHXxx3=u= zv(QlR41Z>&gGAO8;)jdVt9T%z3XdDx?=WieZF8~3D?D6owI3}l{ktDfcp(TiQH%IE z!O>+)Z@gq32Y?uLLip4zeni(%(|pF#WM?ZMpn`&px_(iElc`Qg=e@Y7%#Po=WPP#K zRwTW8nzbNINk{MdK1Q1S*?8}F9Li@XhB`5VbwQ68GVQU_DF(AB>11WO^8Z{iC1&sq z-kg2$d`q$EH{dq7{tA=-Myr2`{IwhW9oNowb|Yp=r;0Sri-v))%S6c4;Rc{-a4?#o zp`on2{PwBQW-nc;@%lh~2tG%~l)jdd5+?d9*41F95^@$6rB`u(Try(Iqib?Rw~j$8 zwPg(24SwZ5&sUy=X+^xm?xvykr|%zKEWSneC~UD?_KJZ$N4t|jKCVwASI6ynezkU1sm9F&{))KD3f=6VBve+CE427F6&YJDF)I@I&B>_Y_nl2Xl**uTX zBcr0OPEsEf6%@WH*Zy&}P92oH+h!`7=>g`sn0F!onA>iBfP$Ad2GCA+E1`UbIxnxT zuI5X$^!10Ae|~dW4x-bpFh<41#Ej?`y*nLK&dvwS_&};y>hbY$PX#6sFT2%(@JH7z z8hHhUvmD09tGhk16hr76YXXc&PxSgafcC(re&7c5hi6-kvDTFDiO! zaCN+PyQBY*QBv}FN$Mr5ZXVx*;%$1C`eOp$7DIMZhqLI@reI%N=WDFUmG-qay*5ZV z$ZZuFC6Y})on1@E)b;d8;DGY39%+AXFMNA;R)Fi>*%XITRKtEfdg;`+;f)o(mMct! z80MueA}itTd%@X6EE2)!50-U^-ao&+sEl}VAS0N$@ikV(L@6uEp(?!^;-^`v7&uRKab@oW##0Iws<`# z_rqW?s-;RvdwXu+P`c+*7s*Iv`t}^1P}^)x-V&)sjQK)fyZPnxrPZ!S{p_8Qn6H@? zHnfH;T09rJWj;L7zqZlfo))<-71~jd80Xf}hAJsV{k)DWnK&p<0D8!#UqzvJO zKi`9OR}PEzFEqI|U=}wx9qd} zehV|Kv~)s&`70P=X3XjDe(NGDyISjq;3J)?@Vgk{1-lcV^?${UL39DR2)zn(uxaC` zyIWQ+<&4XL?!(+2*)a}lU7b?>LDpAmv6a?K5+6OzG;@@bF_#p-*;Uo5zVAFXs(dfvJA_4n7E&zi&I^IMYx479qjfdjDB?bQjk|BKwH#zx^7 zRy}!FS3wRA4&j^K%6e~r_^bV+p0a~_BiGB>#8?5ZTkw5MT9n?;#eevThvpY2<|Vq) z9Ztu_cHq00o00Jq+#h4}OZt_xyv-`^Eya}a(J`7BTj?Q}xjlbiSdTDQGnx_tvB|+= zKY(K^6cWR%(_WxiNJ()Fg;{p297zdMIB5J(NfC~}zII)3I}llL*&tCITV4LfW-^*3 z2RD!;5OuaQbuz#;4EH@ge#Y$1ytFB%;{XCn}A9iX!NhXE#Lm?9B&y-DvMVkSg zQhT%CaJ9zfv&#c4ymK6Hf*CV1F zuoMA1S}xO}th&1R&CO2(l7tBfIlTzj!q2W?>==(aNcxqYl)5^Ojg3umyAi$dOnn)^j*(btnbC3) z){`&RPeQ*5a@+`?BCnR?`@eQ~zkN;?9uy?7ef{g#uX_F)L%6-mYTHkezqyiwFV-|( zZt$>USmXb8@GG6zrcFGqw?5NSE>P-wUNEZgnfYZY_S918!jkc%Eb={hV+&0n$XAGO zfY@;v<~@|i7v9g>5W=9ILwx2E1AhDdeJ})OY-%ba{{e99fSY(v(h3TK2L=X|^#cLb zyF1?y+vbCegNHW(ZV*ikkBoeQghebXAt4dljqnBT=g*(iw6u;}Gq7}ERzd=yKL){Q z_UgJdF!iZVo9xE7=IvQA?&O!BpH$yNm8#fIPDR1&>t_4|sCORB6CkQrP2f|=?k)ofNh88aafO2ln& z@VC%wmF6`G$3G9T@R8y^*^y*l|J(NdUpftb5*1T^Vq|1w_dUMH$kF*mV+L}ZR|+a* zM?d-=)J)A6cXG{sZKGu%`Fpcw>MSfL%}=yAMQo!s3Q`8=_CW}DeY-vCWUPG(c{)Q} zwNhAfXJ$V;&hXEA&BnS*ZMgWhe3tK1Q&T47$?u*!CzlWRicnqd4JQ+JMr|h>4Lx1L zQ$MjW5kM^YsfYYpL?+(j79ME-U0t<-reaG=OL~&OeF`5~sIoxK(5dQea4@~3B<+B} zo(z3Xeg?sn9Akd=mY2q_#~f27266DI*FL+7TwSkj4tTK9b?wZPiIH(LD(&=jl3jIh z8?ARJWR<*5=jD}^s;%$0s3d*vsUpt=(3Vacbq7upt|MQa<{zE!<{-BglI)#;#czJQ zuKG|^P<-p#b#X|!;-{M}N(p?>2H+irovsp~#*<80oEf1+Rmmepc;=@iM(}^4w*7*-6uig3FDXH2py+iz?8uXJ4v$9~x{c1aDHgg@ z#!P83G9l!BK#Zk{CB98J1>krb&IBd!n&21JZWCZvATB;3;#s3VPK4Di@_Z}Y=WNAj z(c0+bCZgf6mD^(>!Q2D1Gy{$Rkd>VtB%MEea~ziZ00-dr-nUukN^0}R!L8h=KY?u+ z&@JS2adzO`M&tg)ZbB5q1J?B*7T-hwQvNlKyx2h=0aAG^=jMpw^#Z&*!_oY{$1b#< zbJwVe-uSCeq^Eqa8G~jtBC5n~XT4fU`KHND2L(1q7W=m)Z_@Bt^RA+_dv0Quh7eY| zP4&~Vg0FyLySxPjj*jSKB}O&FKdzL}lPk@2mx=A7hrq$Y$J`uHkAG*l}^*8%zP<0R3L@UYojuJ>nmx7o@5+Js#Zkhj=aazoE~GD%3L?c4Al$W9EX zG{F4J$irk6W|idjR0(ecY&d$pJeZqOuhApA!f|{2B|8_FzLT)ypC-6h6_K$dgW{wS4mTQ?!3~iKsG1anRjqIk#z*Y!{%QJw35WAi9lm$;HmHv;Euh^e0=od7q__(SsX4G?c%rc;9WP> z_rfA#BM6&gnx)5yQC3bT)Woq@Gx@?!nO-0oF3KqaCR3_6;vb0eMogXWSB$%j*`7ki zT0ed159ANM~n`wRn=*-;+P_DwrsBQRG~JzCzFWw<#|GTXcWpa{&mSBuGF-N zM)l;AD>Ct*N&hMR|D)Y;3`BBm&rYdkZm=@U?e~{b*_M-NMRC{t*mv+aQv9Cnsq! zzbE^CTP*daN09ao=&1EO~ zQuphxlC!llPeZbF0;@+T@$o?bSkxRw^~n=8?$_z)dZO9X+q=8*KbDq83tA6L3j-H{+@ zO%8|(r}eTxuY#1|4W*C`;N09C2_0QHsDxza?%==Xfee22 zKCnC9TWJHpR8UqPdV?b14<~Cs?nm8`IKFSk=kdHMs7T^wkG)a2nF*%D^2*S3fLWTd zK?qF(S#EXTbHKX04I>>%p&r_bM7nfXG5PYnf2NomBNN|FU0pysuo4=x`%3NqzH(xWY=L` zahSFZL{r20UeW1h5Rwsi(LuvS3i1r2ityPnOc6m0oiY6 z`!k*wOC-jfdn6)UAt`pE&PGr&!yA+jFJO2PlJ&t|@TBA(S&Dw%5i{(>s)ToBcMpe( z{8}MEs$D80>n)GJq)u30kz700jdb}HYN{~t^O+fC-L3luTol3+F4r$k7m=Pz=|S=; zDhsP6J=nkBXSaADKSDoi2s!7Uq^VQpK;tj4LSduMAwZmxmKHe_A0XIy`Izj2ZrN4R zs|9?q9AK_L9x9R*J(4&Wcg{CzNCotG1t>8vJp5hNZ~HnQKmS*bkeZEpF;bq9LW555 zERHa$D6EnLR*WSuSXnk7qrIG=jvy0prUAy^`WG_p{m!+qPWL#=#*Z(qcb12X&Z#}h zZ$hCuLu|+5Bg(sYOe(L#MiHVy36pr-PW+L7&Ka?3zsBIatjL$M-?^^k1HCj>0fK&c zEtSq?%Q|5v%-wLx7ZVc`@!1g?_OT}i`HBdphxLR8Q%iv@Z*P45%nq0^lC6wc{avC9 zMqOE18KlqYEe}x%S_+CbEGt1>9H0hzdr8=W*Lc8+Q90oy%v8Lk9T77Y&{k(>P@=p| zAEi7f=x%%I#{Bx3&;+cAarY@-EI`4PDFKAT28-Y6ESp4ywM*!0Y028y*t}4h45Rbz>gq~a z-vBhECKOg-@jh4|!plLhnrPeGuY0&#dL|R_HhieOB_+%R zMKv`As8KarkO7#9GSqMNwzPVARfy=a-nesvV8*Qm3>9|u9F=T0} zq){B51I=~SL!XK8f@4^r#jYz_Xr+Ya-gId;Hn#LSiBkh`ufs)Gbkx-k6Y~oS)^??Z zBqR(dlEJ|9F}{qSBpez>JAuzjiyO|b-%R2=9b&E>(^cH;z}ETt0i2B6w8O85RZYe> zD^o5vd~C+c61u!7LmHv3seX&&uBel@ZOd)GhkEu$5N*2Pr~!1^zZf0E!NvcOnmWn0 zNyE;``JP71&1HG8!g_@3_BguKlC(5bFKAc&H&&jp~qC@9-M z>yNd&l2qP4uVI%hb1z%tzS4WXS7pn=D#`zl%!Ww+>|kV;<==TELwr5Zzn=1H;=Le} zYkzNFG7${>^f+DLtNU8%?HlSM16m>S*7n{N6LEsvLKDT=hTWJqV+T;g zSETKUUtFLn1ARlUI3AFR97t(*emTErMV-!54c2}vI9b?VnXGz(NBtz_G{gB@H*(C} zLRVoqmL~b^=RQ?+4Wp*GH;<0ts20E9*2FOp}SzDL+p14hTE~a33#B|IGGBTuGU0p}3UGsp8-UxnJYSKo;sawpL zdCvX!$41x1er5SwgOBAIeGwvV{jpESZ1L*Uy2D>Rzms5f!ZJ3z8@!-;K6_nN-us+X z-M*m!WiY2?ulBd$c-G*k@rwNvlZMdHk9c}KhqX=7tF3Z}#!&I@hw?xn?DtVDsy|*P zZrEAq9s)Vbs*ot@TP;o-ZiqT_)zs9q#Pnx-L6Nl#PRMq=@=2tIzJ7iXtrzR;uejkU zzqtsxT~M9N^KnM8mWc#GEif~6bMu_$nJ=^+(>QgIYOO3R(c`nyxa z2C|2lRa2AghOS&Zz$htB$z}B?b-5glYf%g|4LJUe zRBCzbM^uv5=s>1i$!AT>61IJ|;vyQ8BC)H+I3N%c73KVUC16Dsb+v^jaFyEa{M6I- z*Ov!Sgh>>#O$?(EL)X{WhYnX>w4c005a?h3o-w@m*y#-Y15G8`m-skqN=vb(AZ#-J z2U;}x7Eh^#*MjY;v!d$wY-|DDyKZm&z|^A&VorvO4JkD}D-)|Vc}~)Z|{(kR1JtwR0WI`4yJxTxy)sE4t(dx0VX&yKJtf4%C?hBc*{abbEbN6V@*P;0lBuD@**QToR{6(=`TDUT0M#tBY8X&$lU%#M2AL;q^k#GO4wr`C^Tk2fy; zOm%T}?GWgqH)pP{sWGMet|?~SH#C&3!-0Ox8ogC*JGQ*K8Vv;d2^|kU*6M`9`J2ix zo(UX%hH)ayx`;~GPhkjix$Uc*{|pIdo5@}-D`na1Iwy^(weBP1B;SiVmREYYklDqSrAgG129C7xScUfxC9| z)o_tfk%xGRhX*5RAFLD&sE0KEg@E<$%TH*jRi7F}{>r0Rz;@OMMziBJ+=MR-9csn(uouxR}6!u!P z0^a|lOk)ohJ$v~m_jy-G*6zBwqi-*X`tEbw&mvJuXNqssh4I=JcFZj;bD_^6`}p`c zR8UVYoHnL&zCVS~uRX++~DFD`+8X01xDO=7n05jr_cqwk2{ zR$(L5cd&yx3h;`w7Q*^ z3XbEIT6*`99&=x^kf&^)A2$xFAB3&TW*YIiE^8e5NWx{!tE|+4<)q1a<55uObru!Y z1SP|A^bQC>g^QouWrNmMR*}(OrD^AQiw;Ym$zmCm6ZmAHG&*>h&gXVeO9WA&bZ%ZA z0F0KMU1636MO;e|u{D(TOoq2f5r2O~6nfd^Xb4Pd^p~5CsokMxTGL4By-OvTt??2A zr$012{JE~q04OicS@SCG?KW3$I_)C1MsW~Y&QA!e0hUs%NT{2X;|e-MY?zhlKVLF>`7);YVCs?M&UmM zvL840N@Q!d+15i13aSmNAfF?$LX{=bAIrmojKU@Mb!1rum3Op@>Zp-^wNTbCb(;U- zV%yzhBClp|U*xtiS@6$w<|POuQHN=NyN0v<&M@c0O=E@rV$;sKog>EVSbh))a=;+| zdW^X3f2lIs^rqL0aRreObF}>EQ@$3qMa}+Ei5moL)I@m&1}1mqyfqM$*il8|ZWM9A=~5Qj zaq_FY0On>;%EFY{GygXV(=IJoQDhBfQ!c6gUw?q=?!T@PsmCJ+sB?%HGTf75^A zm{3ANL4noTGIyfH3b|(;Z30@vEGhFTK0(n5Z@d!o&s&II68-C*uD*}N$ zc`5&>O9v;Dw;WbH;x@zW_}FaK^W!&O-qmCwtuKuFUtT$^X^Xb}fpPgk_cyz@e)m;6 zsej;q1>fah`WeS+DwN^{`0u;Tj1?XMb*w_0YsbrVd9TV~Pe3 zAsRbPR!Tw(eH9`48wi{m_stFkl+}f&l8cg)IrjEtZ-lhwLLn{uZq5LG!&FXwzQ57{ z)L#dWfUkNLK^tKq&WUzt-2jQQG;Qt6O{`-Y6(*OQK zIBEFt|Bm$^-uiE+@jpNH-`?^Mq1L|?TJ|9`4SvH_ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Data Playground Project - - - -VPC - - - - -Notebook -Vertex AI - - - - -Cloud -Storage - - - - - - - -