From 215e99338eb7a697fd3d120ccad83c693f7c14f7 Mon Sep 17 00:00:00 2001 From: Eduardo Valdes Jr <1084551+emvaldes@users.noreply.github.com> Date: Wed, 26 Aug 2020 20:48:19 -0700 Subject: [PATCH] Initializing Repository --- .github/templates/manage-terraform.shell | 47 ++ .github/workflows/terraform-controller.yaml | 277 ++++++++ .github/workflows/terraform-restore.yaml | 204 ++++++ .gitignore | 36 + README.md | 703 +++++++++++++++++++- _config.yml | 1 + action.functions | 383 +++++++++++ action.yaml | 450 +++++++++++++ configs/dev-configs.tfvars | 9 + configs/prod-configs.tfvars | 9 + configs/uat-configs.tfvars | 9 + main.tf | 291 ++++++++ modules/s3/main.tf | 55 ++ modules/s3/outputs.tf | 8 + modules/s3/variables.tf | 4 + outputs.tf | 39 ++ terraform.tfvars | 47 ++ variables.tf | 65 ++ website/corporate.jpg | Bin 0 -> 41686 bytes website/index.html | 0 workspace | 4 + 21 files changed, 2639 insertions(+), 2 deletions(-) create mode 100644 .github/templates/manage-terraform.shell create mode 100644 .github/workflows/terraform-controller.yaml create mode 100644 .github/workflows/terraform-restore.yaml create mode 100644 .gitignore create mode 100644 _config.yml create mode 100644 action.functions create mode 100644 action.yaml create mode 100644 configs/dev-configs.tfvars create mode 100644 configs/prod-configs.tfvars create mode 100644 configs/uat-configs.tfvars create mode 100644 main.tf create mode 100644 modules/s3/main.tf create mode 100644 modules/s3/outputs.tf create mode 100644 modules/s3/variables.tf create mode 100644 outputs.tf create mode 100644 terraform.tfvars create mode 100644 variables.tf create mode 100644 website/corporate.jpg create mode 100644 website/index.html create mode 100644 workspace diff --git a/.github/templates/manage-terraform.shell b/.github/templates/manage-terraform.shell new file mode 100644 index 0000000..eac31b6 --- /dev/null +++ b/.github/templates/manage-terraform.shell @@ -0,0 +1,47 @@ +#!/usr/bin/env bash ; + +read -p "Enter Target-Profile [ e.g.: default ] ?: " aws_default_profile ; +read -p "Enter Target-Region [ e.g.: us-east-1 ] ?: " aws_default_region ; + +export terraform_restore="{{ console.Restore_Folder }}" ; + +mkdir -p ${terraform_restore} ; +cd ${terraform_restore} ; + +git clone {{ console.Remote_Origin }}.git ${terraform_restore} ; +git checkout -b restore {{ console.Commit_SHAID }} ; + +[[ -d {{ github.workspace }} ]] && export HOME="{{ github.workspace }}" ; +cp -pr ${HOME}/.ssh ${terraform_restore} ; + +export AWS_PROFILE="${aws_default_profile}"; +export AWS_DEFAULT_REGION="${aws_default_region}"; + +echo -e "\nFetching Terraform components ... \n"; +aws --profile ${AWS_PROFILE} \ + --region ${AWS_DEFAULT_REGION} \ + s3 cp s3://{{ console.S3Bucket_Name }}/{{ console.Remote_Path }} \ + ${terraform_restore} \ + --recursive \ + ; + +echo -e "\nDisplaying Terraform file-structure ...\n" ; +tree -FCla --prune -I .git $(pwd) ; + +if [[ -f ${terraform_restore}/terraform.tfstate.d/dev/terraform.tfplan ]]; then + echo -e "\nInitializing Terraform ... \n" ; + eval {{ console.Verbosity }} \ + terraform init ; echo -e ; + echo -e "\nTerraform Create|Select Workspace [{{ console.Target_Workspace }}] ... \n" ; + eval {{ console.Verbosity }} \ + terraform workspace select {{ console.Target_Workspace }} || terraform workspace new {{ console.Target_Workspace }} ; + echo -e "\nTerraform Listing Workspaces ... \n" ; + eval {{ console.Verbosity }} \ + terraform workspace list ; + echo -e "\nExecuting Terraform ..." ; + eval {{ console.Verbosity }} \ + TF_VAR_region=${AWS_DEFAULT_REGION} \ + terraform {{ console.Terraform_Action }} ; + else echo -e "\nWarning: Unable to download Terraform components! \n" ; + exit 1 ; +fi ; diff --git a/.github/workflows/terraform-controller.yaml b/.github/workflows/terraform-controller.yaml new file mode 100644 index 0000000..eae4d2c --- /dev/null +++ b/.github/workflows/terraform-controller.yaml @@ -0,0 +1,277 @@ +name: GitHub Actions - Terraform Controller +on: + +####---------------------------------------------------------------------------- + workflow_dispatch: + name: Manual Deployment + description: 'Triggering Manual Deployment' + inputs: + accesskey: + description: 'Target Access Key-ID' + required: false + default: '' + account: + description: 'Target AWS Account' + required: false + default: '' + destroy-terraform: + description: 'Terraform Destroy Request' + required: false + default: true + keypair-name: + description: 'Private Key-Pair Name' + required: false + default: '' + keypair-secret: + description: 'Private Key-Pair Secret' + required: false + default: '' + region: + description: 'Target AWS Region' + required: false + default: '' + secretkey: + description: 'Target Secret Access-Key' + required: false + default: '' + workspace: + description: 'Terraform Workspace' + required: false + default: 'dev' + ####---------------------------------------------------------------------- + # logLevel: + # description: 'Log level' + # required: true + # default: 'warning' + # tags: + # description: 'Terraform Controller' +####---------------------------------------------------------------------------- + push: + branches: [ master ] + paths: + - action.yaml +####---------------------------------------------------------------------------- +env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_DEFAULT_ACCOUNT: 738054984624 ## ${{ secrets.AWS_DEFAULT_ACCOUNT }} + AWS_DEFAULT_PROFILE: default ## ${{ secrets.AWS_DEFAULT_PROFILE }} + AWS_DEFAULT_REGION: us-east-1 ## ${{ secrets.AWS_DEFAULT_REGION }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + ## Terraform Operations: Deploy, Destroy + BACKUP_TERRAFORM: ${{ secrets.BACKUP_TERRAFORM }} + DEPLOY_TERRAFORM: ${{ secrets.DEPLOY_TERRAFORM }} + DESTROY_TERRAFORM: ${{ secrets.DESTROY_TERRAFORM }} + ## DEVOPS_ASSUMEROLE_POLICY + ## DEVOPS_BOUNDARIES_POLICY + ## DEVOPS_ACCESS_POLICY + DEVOPS_ACCESS_ROLE: ${{ secrets.DEVOPS_ACCESS_ROLE }} + DEVOPS_ACCOUNT_NAME: devops ## ${{ secrets.DEVOPS_ACCOUNT_NAME }} + DYNAMODB_DEFAULT_REGION: us-east-1 ## ${{ secrets.DYNAMODB_DEFAULT_REGION }} + ## INSPECT_DEPLOYMENT + PRIVATE_KEYPAIR_FILE: .ssh/id_rsa ## ${{ secrets.PRIVATE_KEYPAIR_FILE }} + PRIVATE_KEYPAIR_NAME: devops ## ${{ secrets.PRIVATE_KEYPAIR_NAME }} + PRIVATE_KEYPAIR_SECRET: ${{ secrets.PRIVATE_KEYPAIR_SECRET }} + PROVISION_TERRAFORM: ${{ secrets.PROVISION_TERRAFORM }} + S3BUCKET_CONTAINER: pipelines + S3BUCKET_DEFAULT_REGION: us-east-1 ## ${{ secrets.S3BUCKET_DEFAULT_REGION }} + TARGET_WORKSPACE: dev ## ${{ secrets.TARGET_WORKSPACE }} + ## UPDATE_PYTHON_LATEST + ## UPDATE_SYSTEM_LATEST + ## + terraform_input_params: '' +####---------------------------------------------------------------------------- +jobs: + terraform-controller: + runs-on: ubuntu-latest + steps: +####---------------------------------------------------------------------------- + - name: checkout + uses: actions/checkout@v2 +####---------------------------------------------------------------------------- + ## Environment Variables + - name: Environment Variables + id: environment-variables + run: | + ####------------------------------------------------------------------ + ## Parsing GitHub Action - Workflow dispatch (limited to 10 input-params) + echo -e "Processing File|Input-based Parameters ... [ 1-10 ]\n" ; + ####------------------------------------------------------------------ + eval "echo '::set-env name=TARGET_WORKSPACE::$( + cat ${{ github.workspace }}/workspace \ + | grep -vxE '[[:blank:]]*([#;].*)?' \ + | tr -d "[:space:]" + )'" ; + ####------------------------------------------------------------------ + eval "echo '::set-env name=SESSION_TIMESTAMP::$(date +"%y%m%d%H%M%S")'" ; + echo '::set-env name=AWS_ACCESS_KEY_ID::${{ secrets.AWS_ACCESS_KEY_ID }}' + echo '::set-env name=AWS_SECRET_ACCESS_KEY::${{ secrets.AWS_SECRET_ACCESS_KEY }}' + ####------------------------------------------------------------------ + custom_workspace="${{ github.event.inputs.workspace }}" ; + if [[ (${#custom_workspace} -gt 0) && (${custom_workspace} != '') ]]; then + echo -e " Target Workspace [input-based]: '${custom_workspace}'" ; + eval "echo '::set-env name=TARGET_WORKSPACE::${custom_workspace}'" ; + fi ; + ####------------------------------------------------------------------ + cloud_region="${{ github.event.inputs.region }}" ; + if [[ (${#cloud_region} -gt 0 ) && (${cloud_region} != '') ]]; then + echo -e " Target Cloud Region [input-based]: '${cloud_region}'" ; + eval "echo '::set-env name=AWS_DEFAULT_REGION::${cloud_region}'" ; + fi ; + ####------------------------------------------------------------------ + cloud_account="${{ github.event.inputs.account }}" ; + if [[ (${#cloud_account} -gt 0 ) && (${cloud_account} != '') ]]; then + echo -e " Target Cloud Account [input-based]: '${cloud_account}'" ; + eval "echo '::set-env name=AWS_DEFAULT_ACCOUNT::${cloud_account}'" ; + fi; + ####------------------------------------------------------------------ + access_keyid="${{ github.event.inputs.accesskey }}" ; + if [[ (${#access_keyid} -gt 0 ) && (${access_keyid} != '') ]]; then + echo -e " Target Access Key-ID [input-based]: '${access_keyid}'" ; + eval "echo '::set-env name=AWS_ACCESS_KEY_ID::${access_keyid}'" ; + fi; + ####------------------------------------------------------------------ + secret_keyid="${{ github.event.inputs.secretkey }}" ; + if [[ (${#secret_keyid} -gt 0 ) && (${secret_keyid} != '') ]]; then + echo -e " Target Secret Key-ID [input-based]: '${secret_keyid}'" ; + eval "echo '::set-env name=AWS_SECRET_ACCESS_KEY::${secret_keyid}'" ; + fi; + ####------------------------------------------------------------------ + keypair_name="${{ github.event.inputs.keypair-name }}" ; + if [[ (${#keypair_name} -gt 0 ) && (${keypair_name} != '') ]]; then + echo -e " Private Key-Pair Name [input-based]: '${keypair_name}'" ; + eval "echo '::set-env name=PRIVATE_KEYPAIR_NAME::${keypair_name}'" ; + fi; + ####------------------------------------------------------------------ + keypair_secret="${{ github.event.inputs.keypair-secret }}" ; + if [[ (${#keypair_secret} -gt 0 ) && (${keypair_secret} != '') ]]; then + private_keypair_secret="$(echo -e "${keypair_secret}" | sed -e "s|;$||" | tr ';' '\n')"; + echo -e "Private Key-Pair Secret [input-based]: \n'***'" ; + eval "echo '::set-env name=PRIVATE_KEYPAIR_SECRET::${private_keypair_secret}'" ; + fi; + ####------------------------------------------------------------------ + destroy_terraform="${{ github.event.inputs.destroy-terraform }}" ; + if [[ (${#destroy_terraform} -gt 0 ) && (${destroy_terraform} != true) ]]; then + echo -e " Destroy Terraform [input-based]: \n'${destroy_terraform}'" ; + eval "echo '::set-env name=DESTROY_TERRAFORM::${destroy_terraform}'" ; + fi; +####---------------------------------------------------------------------------- + ## System Requirements + - name: System Requirements + uses: emvaldes/system-requirements@master + id: system-requirements + with: + install-awscli-tool: true + install-custom-tools: 'netcat' + install-default-tools: true + install-terraform-cli: latest + update-operating-system: ${UPDATE_SYSTEM_LATEST} + update-python-version: ${UPDATE_PYTHON_LATEST} + continue-on-error: false +####---------------------------------------------------------------------------- + ## Installed Packages + - name: Installed Packages + id: installed-packages + shell: bash + run: | + ####------------------------------------------------------------------ + jq --version; + tree --version; + aws --version; + terraform --version; +####---------------------------------------------------------------------------- + ## Terraform Parameters + - name: Terraform Parameters + id: terraform-parameters + shell: bash + run: | + ####------------------------------------------------------------------ + remote_origin="$(git config --get remote.origin.url)" ; + route53_record="${remote_origin##*\/}" ; + oIFS="${IFS}" ; IFS=$'\n' ; + declare -a custom_params=( + custom_timestamp="${SESSION_TIMESTAMP}" + custom_engineer='Eduardo Valdes' + custom_contact='emvaldes@hotmail.com' + custom_listset='["ami-abc123","ami-def456"]' + custom_mapset='{"us-east-1":"ami-abc123","us-east-2":"ami-def456"}' + route53_record="${SESSION_TIMESTAMP}.${route53_record}" + ) ; + ## echo -e "\nListing Encoding entries: ..." ; + ## for xitem in ${custom_params[@]}; do + ## encrypted=$(echo -en ${xitem} | base64 -w0 | tr -d '\n\r') ; + ## decrypted=$(echo -en "${encrypted}" | base64 --decode) ; + ## echo -e "${encrypted} -> ${decrypted}" ; + ## done ; + eval "echo '::set-env name=terraform_input_params::$( + for xitem in ${custom_params[@]}; do + echo -en "`echo -en ${xitem} | base64 -w0 | tr -d '\n\r'`_" ; + done | sed -e 's|\(.*\)\(\_\)$|\1|' ; + )'" ; + IFS="${oIFS}" ; + eval "echo '::set-env name=terraform_input_tfvars::configs/${TARGET_WORKSPACE}-configs.tfvars'" ; + continue-on-error: false +####---------------------------------------------------------------------------- + ## Requesting Credentials + - name: Requesting Credentials + uses: emvaldes/generate-credentials@master + id: request-credentials + with: + aws-access-key-id: ${AWS_ACCESS_KEY_ID} + aws-default-account: ${AWS_DEFAULT_ACCOUNT} + aws-default-profile: ${AWS_DEFAULT_PROFILE} + aws-default-region: ${AWS_DEFAULT_REGION} + aws-secret-access-key: ${AWS_SECRET_ACCESS_KEY} + devops-access-role: ${DEVOPS_ACCESS_ROLE} + devops-account-name: ${DEVOPS_ACCOUNT_NAME} + session-timestamp: "DevOpsPipeline--${SESSION_TIMESTAMP}" + continue-on-error: false +####---------------------------------------------------------------------------- + ## Provisioning Access + - name: Provisioning Access + uses: emvaldes/configure-access@master + id: provision-access + with: + private-keypair-file: ${PRIVATE_KEYPAIR_FILE} + private-keypair-secret: "${PRIVATE_KEYPAIR_SECRET}" + continue-on-error: false +####---------------------------------------------------------------------------- + ## Provision Terraform + - name: Provision Terraform + uses: ./ + id: provision-terraform + with: + provision-terraform: ${PROVISION_TERRAFORM} + terraform-input-params: "${terraform_input_params}" + terraform-input-tfvars: "${terraform_input_tfvars}" +## Terraform Log-levels: TRACE, DEBUG, INFO, WARN or ERROR + terraform-loglevel: false + continue-on-error: false +####---------------------------------------------------------------------------- + ## Deploy Terraform + - name: Deploy Terraform + uses: ./ + id: deploy-terraform + with: + deploy-terraform: ${DEPLOY_TERRAFORM} +## Terraform Log-levels: TRACE, DEBUG, INFO, WARN or ERROR + terraform-loglevel: false + continue-on-error: false +####---------------------------------------------------------------------------- + ## Backup Terraform + - name: Backup Terraform + uses: emvaldes/provision-terraform@master + id: backup-terraform + with: + backup-terraform: ${BACKUP_TERRAFORM} + continue-on-error: false +####---------------------------------------------------------------------------- + ## Destroy Terraform + - name: Destroy Terraform + uses: ./ + id: destroy-terraform + with: + destroy-terraform: ${DESTROY_TERRAFORM} +## Terraform Log-levels: TRACE, DEBUG, INFO, WARN or ERROR + terraform-loglevel: false + continue-on-error: false +###---------------------------------------------------------------------------- diff --git a/.github/workflows/terraform-restore.yaml b/.github/workflows/terraform-restore.yaml new file mode 100644 index 0000000..698a832 --- /dev/null +++ b/.github/workflows/terraform-restore.yaml @@ -0,0 +1,204 @@ +name: GitHub Actions - Terraform Restore +on: + +####---------------------------------------------------------------------------- + workflow_dispatch: + name: Manual Deployment + description: 'Triggering Manual Deployment' + inputs: + region: + description: 'Target AWS Region' + required: true + default: 'us-east-1' + credentials: + description: 'AWS Temporary Credentials' + required: true + default: '' + keypair-name: + description: 'Private Key-Pair Name' + required: true + default: 'devops' + keypair-secret: + description: 'Private Key-Pair Secret' + required: true + default: '' + restore-project: + description: 'Terraform Restore Project' + required: true + default: 'terraform-controller' + restore-shaindex: + description: 'Terraform Restore Index' + required: true + default: '' + ####---------------------------------------------------------------------- + # logLevel: + # description: 'Log level' + # required: true + # default: 'warning' + # tags: + # description: 'Terraform Restore' +####---------------------------------------------------------------------------- + # push: + # branches: [ master ] + # paths: + # - action.yaml +####---------------------------------------------------------------------------- +env: + ## AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + ## AWS_DEFAULT_ACCOUNT: ${{ secrets.AWS_DEFAULT_ACCOUNT }} + ## AWS_DEFAULT_PROFILE: ${{ secrets.AWS_DEFAULT_PROFILE }} + ## AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }} + ## AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + ## Terraform Operations: Deploy, Destroy + ## BACKUP_TERRAFORM: ${{ secrets.BACKUP_TERRAFORM }} + ## DEPLOY_TERRAFORM: ${{ secrets.DEPLOY_TERRAFORM }} + ## DESTROY_TERRAFORM: ${{ secrets.DESTROY_TERRAFORM }} + ## DEVOPS_ASSUMEROLE_POLICY + ## DEVOPS_BOUNDARIES_POLICY + ## DEVOPS_ACCESS_POLICY + ## DEVOPS_ACCESS_ROLE: ${{ secrets.DEVOPS_ACCESS_ROLE }} + ## DEVOPS_ACCOUNT_NAME: ${{ secrets.DEVOPS_ACCOUNT_NAME }} + DYNAMODB_DEFAULT_REGION: us-east-1 ## ${{ secrets.DYNAMODB_DEFAULT_REGION }} + ## INSPECT_DEPLOYMENT + PRIVATE_KEYPAIR_FILE: .ssh/id_rsa ## ${{ secrets.PRIVATE_KEYPAIR_FILE }} + ## PRIVATE_KEYPAIR_NAME: ${{ secrets.PRIVATE_KEYPAIR_NAME }} + ## PRIVATE_KEYPAIR_SECRET: ${{ secrets.PRIVATE_KEYPAIR_SECRET }} + ## PROVISION_TERRAFORM + S3BUCKET_CONTAINER: pipelines + S3BUCKET_DEFAULT_REGION: us-east-1 ## ${{ secrets.S3BUCKET_DEFAULT_REGION }} + ## TARGET_WORKSPACE: pipelines/${{ secrets.TARGET_WORKSPACE }} + ## UPDATE_PYTHON_LATEST + ## UPDATE_SYSTEM_LATEST + ## +####---------------------------------------------------------------------------- +jobs: + terraform-restore: + runs-on: ubuntu-latest + steps: +####---------------------------------------------------------------------------- + - name: checkout + uses: actions/checkout@v2 +####---------------------------------------------------------------------------- + ## Environment Variables + - name: Environment Variables + id: environment-variables + run: | + ####------------------------------------------------------------------ + ## Parsing GitHub Action - Workflow dispatch (limited to 10 input-params) + echo -e "Processing File|Input-based Parameters ... [ 1-10 ]\n" ; + ####------------------------------------------------------------------ + eval "echo '::set-env name=SESSION_TIMESTAMP::$(date +"%y%m%d%H%M%S")'" ; + ####------------------------------------------------------------------ + cloud_region="${{ github.event.inputs.region }}" ; + if [[ (${#cloud_region} -gt 0 ) || (${cloud_region} != '') ]]; then + echo -e " Target Cloud Region [input-based]: '${cloud_region}'" ; + eval "echo '::set-env name=AWS_DEFAULT_REGION::${cloud_region}'" ; + fi ; + ####------------------------------------------------------------------ + declare -a credentials=($( + echo -e ${{ github.event.inputs.credentials }} \ + | sed -e 's|\([[:space:]]\)\{1,\}| |g' \ + -e 's|\(\[\)\(.*\)\(\]\)\(.*\)$|\1default\3\4|' \ + -e 's| = |*=*|g' + )) ; + if [[ (${#credentials[@]} -gt 0 ) || (${credentials[@]} != '') ]]; then + echo -e " Temporary Credentials [input-based]: \n" ; + for xline in ${credentials[@]}; do + echo -e ${xline} ; + done | sed -e 's|\(\*\)\(\=\)\(\*\)| = |g' ; echo -e ; + eval "echo '::set-env name=TEMPORARY_CREDENTIALS::${credentials[@]}'" ; + fi ; + ####------------------------------------------------------------------ + keypair_name="${{ github.event.inputs.keypair-name }}" ; + if [[ (${#keypair_name} -gt 0 ) || (${keypair_name} != '') ]]; then + echo -e " Private KeyPair Name [input-based]: '${keypair_name}'" ; + eval "echo '::set-env name=PRIVATE_KEYPAIR_NAME::${keypair_name}'" ; + fi ; + ####------------------------------------------------------------------ + keypair_secret="${{ github.event.inputs.keypair-secret }}" ; + if [[ (${#keypair_secret} -gt 0 ) || (${keypair_secret} != '') ]]; then + private_keypair_secret="$( + echo -e "${keypair_secret}*" \ + | sed -e 's|\([[:space:]]\)\{1,\}|*|g' + )" ; + ## echo "::add-mask::${private_keypair_secret}" ; + echo -e "Private KeyPair Secret [input-based]: '***'"; + eval "echo \"::set-env name=PRIVATE_KEYPAIR_SECRET::${private_keypair_secret[@]}\"" ; + fi ; + ####------------------------------------------------------------------ + restore_project="${{ github.event.inputs.restore-project }}" ; + if [[ (${#restore_project} -gt 0) || (${restore_project} != false) ]]; then + echo -e " Restore Repository [input-based]: '${restore_project}'" ; + eval "echo '::set-env name=RESTORE_PROJECT::${restore_project}'" ; + else echo -e "\nWarning: Target Restore Project is invalid! " ; + exit 1 ; + fi ; + ####------------------------------------------------------------------ + restore_shaindex="${{ github.event.inputs.restore-shaindex }}" ; + if [[ (${#restore_shaindex} -gt 0) || (${restore_shaindex} != false) ]]; then + echo -e " Restore SHA Index [input-based]: '${restore_shaindex}'" ; + eval "echo '::set-env name=RESTORE_SHAINDEX::${restore_shaindex}'" ; + else echo -e "\nWarning: Target Restore Point is invalid! " ; + exit 2; + fi ; +####---------------------------------------------------------------------------- + ## System Requirements + - name: System Requirements + uses: emvaldes/system-requirements@master + id: system-requirements + with: + install-awscli-tool: true + install-default-tools: true + install-terraform-cli: latest + update-operating-system: true + update-python-version: true + continue-on-error: false +####---------------------------------------------------------------------------- + ## Installed Packages + - name: Installed Packages + id: installed-packages + shell: bash + run: | + jq --version; + tree --version; + aws --version; + terraform --version; +####---------------------------------------------------------------------------- + ## Requesting Credentials + - name: Requesting Credentials + uses: emvaldes/generate-credentials@master + id: request-credentials + with: + temporary-credentials: true + # aws-access-key-id: ${AWS_ACCESS_KEY_ID} + # aws-default-account: ${AWS_DEFAULT_ACCOUNT} + # aws-default-profile: ${AWS_DEFAULT_PROFILE} + # aws-default-region: ${AWS_DEFAULT_REGION} + # aws-secret-access-key: ${AWS_SECRET_ACCESS_KEY} + # devops-access-role: ${DEVOPS_ACCESS_ROLE} + # devops-account-name: ${DEVOPS_ACCOUNT_NAME} + # session-timestamp: "DevOpsPipeline--${SESSION_TIMESTAMP}" + continue-on-error: false +####---------------------------------------------------------------------------- + ## Provisioning Access + - name: Provisioning Access + uses: emvaldes/configure-access@master + id: provision-access + with: + private-keypair-file: ${PRIVATE_KEYPAIR_FILE} + private-keypair-secret: "${PRIVATE_KEYPAIR_SECRET}" + continue-on-error: false +####---------------------------------------------------------------------------- + ## Restore Terraform + - name: Restore Terraform + uses: ./ + id: restore-terraform + with: + restore-terraform: true + restore-region: ${AWS_DEFAULT_REGION} + restore-project: ${RESTORE_PROJECT} + restore-shaindex: ${RESTORE_SHAINDEX} +## Terraform Log-levels: TRACE, DEBUG, INFO, WARN or ERROR + terraform-loglevel: INFO + continue-on-error: false +###---------------------------------------------------------------------------- diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..19f0ca6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log + +# Ignore any .tfvars files that are generated automatically for each Terraform run. Most +# .tfvars files are managed as part of configuration and so should be included in +# version control. +# +# example.tfvars + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negated pattern +# +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Custom exluded paths +.terraform +terraform.tfstate.d + +# Excluding the generated Terraform Plans +*.tfplan diff --git a/README.md b/README.md index edc9af7..cce2659 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,701 @@ -# assumerole -AWS IAM Assume Role As A Service (ARaaS) +# Provision Terraform - Infrastructure As Code (IaC) +GitHub Actions - Provision Terraform - Infrastructure As Code (IaC) + +![GitHub Actions - Terraform Controller](https://github.com/emvaldes/terraform-controller/workflows/GitHub%20Actions%20-%20Terraform%20Controller/badge.svg) + +```console +$ treee ~/Repos/devops/github/modules/terraform-controller/ +├── .github/ +│   ├── templates/ +│   │   └── manage-terraform.shell +│   └── workflows/ +│   ├── terraform-controller.yaml +│   └── terraform-restore.yaml +├── .gitignore +├── LICENSE +├── README.md +├── _config.yml +├── action.functions +├── action.yaml +├── configs/ +│   ├── dev-configs.tfvars +│   ├── prod-configs.tfvars +│   └── uat-configs.tfvars +├── main.tf +├── modules/ +│   └── s3/ +│   ├── main.tf +│   ├── outputs.tf +│   └── variables.tf +├── outputs.tf +├── terraform.tfvars +├── variables.tf +├── website/ +│   ├── corporate.jpg +│   └── index.html +└── workspace + +7 directories, 22 files +``` + +```yaml +workflow_dispatch: + name: Manual Deployment + description: 'Triggering Manual Deployment' + inputs: + accesskey: + description: 'Target Access Key-ID' + required: false + default: '' + account: + description: 'Target AWS Account' + required: false + default: '' + destroy-terraform: + description: 'Terraform Destroy Request' + required: false + default: true + keypair-name: + description: 'Private Key-Pair Name' + required: false + default: '' + keypair-secret: + description: 'Private Key-Pair Secret' + required: false + default: '' + region: + description: 'Target AWS Region' + required: false + default: '' + secretkey: + description: 'Target Secret Access-Key' + required: false + default: '' + workspace: + description: 'Terraform Workspace' + required: false + default: 'dev' +``` + +```yaml +env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_DEFAULT_ACCOUNT: ${{ secrets.AWS_DEFAULT_ACCOUNT }} + AWS_DEFAULT_PROFILE: ${{ secrets.AWS_DEFAULT_PROFILE }} + AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + ## Terraform Operations: Deploy, Destroy + BACKUP_TERRAFORM: ${{ secrets.BACKUP_TERRAFORM }} + DEPLOY_TERRAFORM: ${{ secrets.DEPLOY_TERRAFORM }} + DESTROY_TERRAFORM: ${{ secrets.DESTROY_TERRAFORM }} + ## DEVOPS_ASSUMEROLE_POLICY + ## DEVOPS_BOUNDARIES_POLICY + ## DEVOPS_ACCESS_POLICY + DEVOPS_ACCESS_ROLE: ${{ secrets.DEVOPS_ACCESS_ROLE }} + DEVOPS_ACCOUNT_NAME: ${{ secrets.DEVOPS_ACCOUNT_NAME }} + DYNAMODB_DEFAULT_REGION: ${{ secrets.DYNAMODB_DEFAULT_REGION }} + ## INSPECT_DEPLOYMENT + PRIVATE_KEYPAIR_FILE: ${{ secrets.PRIVATE_KEYPAIR_FILE }} + PRIVATE_KEYPAIR_NAME: ${{ secrets.PRIVATE_KEYPAIR_NAME }} + PRIVATE_KEYPAIR_SECRET: ${{ secrets.PRIVATE_KEYPAIR_SECRET }} + PROVISION_TERRAFORM: ${{ secrets.PROVISION_TERRAFORM }} + TARGET_WORKSPACE: ${{ secrets.TARGET_WORKSPACE }} + ## UPDATE_PYTHON_LATEST + ## UPDATE_SYSTEM_LATEST + ## + S3BUCKET_CONTAINER: pipelines + terraform_input_params: '' +``` + +GitHub Secrets (Required): + +```bash +AWS_ACCESS_KEY_ID Service-Account AWS Access Key-Id (e.g.: AKIA2...VT7DU). +AWS_DEFAULT_ACCOUNT The AWS Account number (e.g.: 123456789012). +AWS_DEFAULT_PROFILE The AWS Credentials Default User (e.g.: default). +AWS_DEFAULT_REGION The AWS Default Region (e.g.: us-east-1) +AWS_SECRET_ACCESS_KEY Service-Account AWS Secret Access Key (e.g.: zBqDUNyQ0G...IbVyamSCpe) +BACKUP_TERRAFORM Enable|Disable (true|false) backing-up terraform plan/state +DEPLOY_TERRAFORM Enable|Disable (true|false) deploying terraform infrastructure +DESTROY_TERRAFORM Enable|Disable (true|false) destroying terraform infrastructure +DEVOPS_ACCESS_POLICY Defines the AWS IAM Policy: DevOps--Custom-Access.Policy +DEVOPS_ACCESS_ROLE Defines the AWS IAM Role: DevOps--Custom-Access.Role +DEVOPS_ACCOUNT_NAME A placeholder for the Deployment Service Account name (devops). +DEVOPS_ASSUMEROLE_POLICY Defines the AWS IAM Policy: DevOps--Assume-Role.Policy +DEVOPS_BOUNDARIES_POLICY Defines the AWS IAM Policy: Devops--Permission-Boundaries.Policy +DYNAMODB_DEFAULT_REGION Single-Region tables are used (e.g.: us-east-1) +INSPECT_DEPLOYMENT Enable|Disable (true|false) inspecting deployment +PRIVATE_KEYPAIR_FILE Terraform AWS KeyPair (location: ~/.ssh/id_rsa). +PRIVATE_KEYPAIR_NAME Terraform AWS KeyPair (e.g.: devops). +PRIVATE_KEYPAIR_SECRET Terraform AWS KeyPair (PEM, Private file) +PROVISION_TERRAFORM Enable|Disable (true|false) the provisioning of the terraform-toolset +S3BUCKET_CONTAINER Identifies where the deployment will be stored +TARGET_WORKSPACE Identifies which is your default (current) environment +UPDATE_PYTHON_LATEST Enable|Disable (true|false) updating Python version +UPDATE_SYSTEM_LATEST Enable|Disable (true|false) updating operating system +``` + +```bash +$ amazon-assumerole default--devops devops DevOpsPipeline ; +``` + +```bash +[default--devops] +aws_access_key_id = ASIA2XV4BKOYHQQXMIU7 +aws_secret_access_key = ez33R69zA8125yt48t2QYBv202d5AFLlxUaMe41o +aws_session_token = IQoJb3JpZ2luX2VjECYaCXVzLWVhc3QtMSJGMEQCIHyVn5cxkd9zpgoPQ7WufaDD2FcNDjxZRAIdVLgixH6mAiApRkDjzvSernVBDnXB04gcmPsqo2sdo7ysxTpVN+iaISqqAghOEAAaDDczODA1NDk4NDYyNCIMaKoey/WvyA5YyqejKocCuWFNIPeijL2GczxgAMiQ3BoNzDBH65TRJVK1ybwESF+AQ5KLj5bZxsLh9DVqhpTLxA6XTJ5Mgjdqvvv2HTvimZSK0aIy3IBEtvvEK2w63VR/c81IKIltx3Naq48OUqJX54T4Qy/Af5QlF/Ho59YNHnjmSwY6smkLik5UXwLubn5Ne/vCYIWVw4L1JOHI5AozBnvNgBmkgcsS6eWy2g4y0x+7RPQVIOCWrJINRqGCYws9uQfXT96d379JWbmXCiaiAPbld0T88pRb/lsMe72YUeIc1+9COTJFUk70mvD1nG9Z9/he4c/KnYFw4fAf4mQvefWViRn8lasGWEmzaFKip6X5MEZLl6cw/sTO+wU6ngGJzj+vaUiLbDjw8mWfa2bIEM8DD1mrf0z/KkOjYsjdQmT8xoENS0h3OIOV6H0f9f5vtyAYNz78YMpH2Tkd4oXKjirQQDgTVNG+GdD8VZ49TWMzu1rHTebj5pJIoRVjEhI1dWBdn25qWVmiqL/Ghe95fPFrIhGhCcvtqL1tcRp4cr8jx+0zhsk+uhjQHO/XNip4/Mwd6JzdZdW/+IM8xA== +x_principal_arn = arn:aws:iam::123456789012:user/devops +x_security_token_expires = 2020-09-29T22:09:18+00:00 +``` + +```json +{ + "UserId": "AROA2XV4BKOYLAX3Z52SQ:DevOpsPipeline-20200929140916", + "Account": "123456789012", + "Arn": "arn:aws:sts::123456789012:assumed-role/DevOps--Custom-Access.Role/DevOpsPipeline-20200929140916" +} +``` + +```bash +$ amazon-credentials default--devops ; +``` + +```console +$ terraform init ; + +Initializing modules... +- bucket in modules/s3 +Downloading terraform-aws-modules/vpc/aws 2.15.0 for vpc... +- vpc in .terraform/modules/vpc + +Initializing the backend... + +Initializing provider plugins... +- Finding latest version of hashicorp/aws... +- Finding latest version of hashicorp/random... +- Finding latest version of hashicorp/template... +- Installing hashicorp/random v2.3.0... +- Installed hashicorp/random v2.3.0 (signed by HashiCorp) +- Installing hashicorp/template v2.1.2... +- Installed hashicorp/template v2.1.2 (signed by HashiCorp) +- Installing hashicorp/aws v3.8.0... +- Installed hashicorp/aws v3.8.0 (signed by HashiCorp) + +The following providers do not have any version constraints in configuration, +so the latest version was installed. + +To prevent automatic upgrades to new major versions that may contain breaking +changes, we recommend adding version constraints in a required_providers block +in your configuration, with the constraint strings suggested below. + +* hashicorp/aws: version = "~> 3.8.0" +* hashicorp/random: version = "~> 2.3.0" +* hashicorp/template: version = "~> 2.1.2" + +Terraform has been successfully initialized! + +You may now begin working with Terraform. Try running "terraform plan" to see +any changes that are required for your infrastructure. All Terraform commands +should now work. + +If you ever set or change modules or backend configuration for Terraform, +rerun this command to reinitialize your working directory. If you forget, other +commands will detect it and remind you to do so if necessary. +``` + +```console +$ terraform workspace new dev ; + +Created and switched to workspace "dev"! + +You're now on a new, empty workspace. Workspaces isolate their state, +so if you run "terraform plan" Terraform will not see any existing state +for this configuration. +``` + + +```bash +$ terraform plan \ + -var="region=${AWS_DEFAULT_REGION}" \ + -var="aws_access_key=${AWS_ACCESS_KEY_ID}" \ + -var="aws_secret_key=${AWS_SECRET_ACCESS_KEY}" \ + -var="private_keypair_file=${HOME}/.ssh/domains/default/private/default" \ + -var="private_keypair_name=devops" \ + -var-file="configs/dev-configs.tfvars" \ + -out terraform.tfstate.d/dev/terraform.tfplan + ; +``` + +```console +Refreshing Terraform state in-memory prior to plan... +The refreshed state will be used to calculate this plan, but will not be +persisted to local or remote state storage. + +data.template_file.public_cidrsubnet[0]: Refreshing state... +data.aws_elb_hosted_zone_id.main: Refreshing state... +data.aws_ami.aws-linux: Refreshing state... +data.aws_availability_zones.available: Refreshing state... + +------------------------------------------------------------------------ + +An execution plan has been generated and is shown below. +Resource actions are indicated with the following symbols: +``` + +```yaml + + create + +Terraform will perform the following actions: + + # aws_elb.web will be created + + resource "aws_elb" "web" { + ... + } + + # aws_instance.nginx[0] will be created + + resource "aws_instance" "nginx" { + ... + } + + # aws_s3_bucket_object.graphic will be created + + resource "aws_s3_bucket_object" "graphic" { + ... + } + + # aws_security_group.elb-sg will be created + + resource "aws_security_group" "elb-sg" { + ... + } + + # aws_security_group.nginx-sg will be created + + resource "aws_security_group" "nginx-sg" { + ... + } + + # random_integer.rand will be created + + resource "random_integer" "rand" { + ... + } + + # module.bucket.aws_iam_instance_profile.instance_profile will be created + + resource "aws_iam_instance_profile" "instance_profile" { + ... + } + + # module.bucket.aws_iam_role.allow_instance_s3 will be created + + resource "aws_iam_role" "allow_instance_s3" { + ... + } + + # module.bucket.aws_iam_role_policy.allow_s3_all will be created + + resource "aws_iam_role_policy" "allow_s3_all" { + ... + } + + # module.bucket.aws_s3_bucket.web_bucket will be created + + resource "aws_s3_bucket" "web_bucket" { + ... + } + + # module.vpc.aws_internet_gateway.this[0] will be created + + resource "aws_internet_gateway" "this" { + ... + } + + # module.vpc.aws_route.public_internet_gateway[0] will be created + + resource "aws_route" "public_internet_gateway" { + ... + } + + # module.vpc.aws_route_table.public[0] will be created + + resource "aws_route_table" "public" { + ... + } + + # module.vpc.aws_route_table_association.public[0] will be created + + resource "aws_route_table_association" "public" { + ... + } + + # module.vpc.aws_subnet.public[0] will be created + + resource "aws_subnet" "public" { + ... + } + + # module.vpc.aws_vpc.this[0] will be created + + resource "aws_vpc" "this" { + ... + } + +Plan: 16 to add, 0 to change, 0 to destroy. + +------------------------------------------------------------------------ + +This plan was saved to: terraform.tfstate.d/dev/terraform.tfplan + +To perform exactly these actions, run the following command to apply: + terraform apply "terraform.tfstate.d/dev/terraform.tfplan" +``` + +```bash +$ terraform apply "terraform.tfstate.d/dev/terraform.tfplan" ; + +random_integer.rand: Creating... +random_integer.rand: Creation complete after 0s [id=36748] +module.bucket.aws_iam_role.allow_instance_s3: Creating... +module.vpc.aws_vpc.this[0]: Creating... +module.bucket.aws_s3_bucket.web_bucket: Creating... +module.bucket.aws_iam_role.allow_instance_s3: Creation complete after 1s [id=terraform-dev-36748_allow_instance_s3] +module.bucket.aws_iam_role_policy.allow_s3_all: Creating... +module.bucket.aws_iam_instance_profile.instance_profile: Creating... +module.bucket.aws_iam_role_policy.allow_s3_all: Creation complete after 0s [id=terraform-dev-36748_allow_instance_s3:terraform-dev-36748_allow_all] +module.bucket.aws_iam_instance_profile.instance_profile: Creation complete after 1s [id=terraform-dev-36748_instance_profile] +module.vpc.aws_vpc.this[0]: Creation complete after 4s [id=vpc-08601ba0c7d611f63] +module.vpc.aws_route_table.public[0]: Creating... +module.vpc.aws_internet_gateway.this[0]: Creating... +module.vpc.aws_subnet.public[0]: Creating... +aws_security_group.elb-sg: Creating... +aws_security_group.nginx-sg: Creating... +module.vpc.aws_route_table.public[0]: Creation complete after 1s [id=rtb-00bc8c353eb56e391] +module.vpc.aws_subnet.public[0]: Creation complete after 2s [id=subnet-07ccd80b42959356e] +module.vpc.aws_route_table_association.public[0]: Creating... +module.vpc.aws_internet_gateway.this[0]: Creation complete after 2s [id=igw-01679506397fbf208] +module.vpc.aws_route.public_internet_gateway[0]: Creating... +module.vpc.aws_route_table_association.public[0]: Creation complete after 0s [id=rtbassoc-07f9d9a7a503e7d7c] +module.vpc.aws_route.public_internet_gateway[0]: Creation complete after 1s [id=r-rtb-00bc8c353eb56e3911080289494] +aws_security_group.elb-sg: Creation complete after 4s [id=sg-06ec7a84be35e7914] +aws_security_group.nginx-sg: Creation complete after 4s [id=sg-0d296ac58a656dc62] +module.bucket.aws_s3_bucket.web_bucket: Still creating... [10s elapsed] +module.bucket.aws_s3_bucket.web_bucket: Creation complete after 11s [id=terraform-dev-36748] +aws_s3_bucket_object.graphic: Creating... +aws_instance.nginx[0]: Creating... +aws_s3_bucket_object.graphic: Creation complete after 1s [id=/website/corporate.jpg] +aws_instance.nginx[0]: Still creating... [10s elapsed] +... +aws_instance.nginx[0]: Still creating... [30s elapsed] +aws_instance.nginx[0]: Provisioning with 'file'... +aws_instance.nginx[0]: Still creating... [40s elapsed] +... +aws_instance.nginx[0]: Still creating... [1m40s elapsed] +aws_instance.nginx[0]: Provisioning with 'file'... +aws_instance.nginx[0]: Provisioning with 'file'... +aws_instance.nginx[0]: Provisioning with 'remote-exec'... +aws_instance.nginx[0] (remote-exec): Connecting to remote host via SSH... +aws_instance.nginx[0] (remote-exec): Host: 52.3.228.143 +aws_instance.nginx[0] (remote-exec): User: ec2-user +aws_instance.nginx[0] (remote-exec): Password: false +aws_instance.nginx[0] (remote-exec): Private key: true +aws_instance.nginx[0] (remote-exec): Certificate: false +aws_instance.nginx[0] (remote-exec): SSH Agent: true +aws_instance.nginx[0] (remote-exec): Checking Host Key: false +aws_instance.nginx[0]: Still creating... [1m50s elapsed] +aws_instance.nginx[0]: Still creating... [2m0s elapsed] +aws_instance.nginx[0] (remote-exec): Connecting to remote host via SSH... +aws_instance.nginx[0] (remote-exec): Host: 52.3.228.143 +aws_instance.nginx[0] (remote-exec): User: ec2-user +aws_instance.nginx[0] (remote-exec): Password: false +aws_instance.nginx[0] (remote-exec): Private key: true +aws_instance.nginx[0] (remote-exec): Certificate: false +aws_instance.nginx[0] (remote-exec): SSH Agent: true +aws_instance.nginx[0] (remote-exec): Checking Host Key: false +aws_instance.nginx[0] (remote-exec): Connected! +aws_instance.nginx[0] (remote-exec): Loaded plugins: priorities, update-motd, +aws_instance.nginx[0] (remote-exec): : upgrade-helper +aws_instance.nginx[0] (remote-exec): Resolving Dependencies +aws_instance.nginx[0] (remote-exec): --> Running transaction check +aws_instance.nginx[0] (remote-exec): ---> Package nginx.x86_64 1:1.18.0-1.40.amzn1 will be installed +aws_instance.nginx[0] (remote-exec): --> Processing Dependency: libprofiler.so.0()(64bit) for package: 1:nginx-1.18.0-1.40.amzn1.x86_64 +aws_instance.nginx[0] (remote-exec): --> Running transaction check +aws_instance.nginx[0] (remote-exec): ---> Package gperftools-libs.x86_64 0:2.0-11.5.amzn1 will be installed +aws_instance.nginx[0] (remote-exec): --> Processing Dependency: libunwind.so.8()(64bit) for package: gperftools-libs-2.0-11.5.amzn1.x86_64 +aws_instance.nginx[0] (remote-exec): --> Running transaction check +aws_instance.nginx[0] (remote-exec): ---> Package libunwind.x86_64 0:1.1-10.8.amzn1 will be installed +aws_instance.nginx[0] (remote-exec): --> Finished Dependency Resolution +aws_instance.nginx[0] (remote-exec): Dependencies Resolved +aws_instance.nginx[0] (remote-exec): ======================================== +aws_instance.nginx[0] (remote-exec): Package Arch Version +aws_instance.nginx[0] (remote-exec): Repository Size +aws_instance.nginx[0] (remote-exec): ======================================== +aws_instance.nginx[0] (remote-exec): Installing: +aws_instance.nginx[0] (remote-exec): nginx x86_64 1:1.18.0-1.40.amzn1 +aws_instance.nginx[0] (remote-exec): amzn-updates 602 k +aws_instance.nginx[0] (remote-exec): Installing for dependencies: +aws_instance.nginx[0] (remote-exec): gperftools-libs +aws_instance.nginx[0] (remote-exec): x86_64 2.0-11.5.amzn1 +aws_instance.nginx[0] (remote-exec): amzn-main 570 k +aws_instance.nginx[0] (remote-exec): libunwind x86_64 1.1-10.8.amzn1 +aws_instance.nginx[0] (remote-exec): amzn-main 72 k +aws_instance.nginx[0] (remote-exec): Transaction Summary +aws_instance.nginx[0] (remote-exec): ======================================== +aws_instance.nginx[0] (remote-exec): Install 1 Package (+2 Dependent packages) +aws_instance.nginx[0] (remote-exec): Total download size: 1.2 M +aws_instance.nginx[0] (remote-exec): Installed size: 3.0 M +aws_instance.nginx[0] (remote-exec): Downloading packages: +aws_instance.nginx[0] (remote-exec): (1/3): libunwind-1 | 72 kB 00:00 +aws_instance.nginx[0] (remote-exec): (2/3): gperftools- | 570 kB 00:00 +aws_instance.nginx[0] (remote-exec): (3/3): nginx- 100% | 1.2 MB --:-- ETA +aws_instance.nginx[0] (remote-exec): (3/3): nginx-1.18. | 602 kB 00:00 +aws_instance.nginx[0] (remote-exec): ---------------------------------------- +aws_instance.nginx[0] (remote-exec): Total 2.0 MB/s | 1.2 MB 00:00 +aws_instance.nginx[0] (remote-exec): Running transaction check +aws_instance.nginx[0] (remote-exec): Running transaction test +aws_instance.nginx[0] (remote-exec): Transaction test succeeded +aws_instance.nginx[0] (remote-exec): Running transaction +aws_instance.nginx[0] (remote-exec): Installing : libunwin [ ] 1/3 +... +aws_instance.nginx[0] (remote-exec): Installing : libunwind-1.1-10.8 1/3 +aws_instance.nginx[0] (remote-exec): Installing : gperftoo [ ] 2/3 +... +aws_instance.nginx[0] (remote-exec): Installing : gperftoo [######## ] 2/3 +aws_instance.nginx[0] (remote-exec): Installing : gperftools-libs-2. 2/3 +aws_instance.nginx[0] (remote-exec): Installing : 1:nginx- [ ] 3/3 +... +aws_instance.nginx[0] (remote-exec): Installing : 1:nginx- [######## ] 3/3 +aws_instance.nginx[0] (remote-exec): Installing : 1:nginx-1.18.0-1.4 3/3 +aws_instance.nginx[0] (remote-exec): Verifying : 1:nginx-1.18.0-1.4 1/3 +aws_instance.nginx[0] (remote-exec): Verifying : gperftools-libs-2. 2/3 +aws_instance.nginx[0] (remote-exec): Verifying : libunwind-1.1-10.8 3/3 +aws_instance.nginx[0] (remote-exec): Installed: +aws_instance.nginx[0] (remote-exec): nginx.x86_64 1:1.18.0-1.40.amzn1 +aws_instance.nginx[0] (remote-exec): Dependency Installed: +aws_instance.nginx[0] (remote-exec): gperftools-libs.x86_64 0:2.0-11.5.amzn1 +aws_instance.nginx[0] (remote-exec): libunwind.x86_64 0:1.1-10.8.amzn1 +aws_instance.nginx[0] (remote-exec): Complete! +aws_instance.nginx[0] (remote-exec): Starting nginx: [ OK ] +aws_instance.nginx[0]: Still creating... [2m10s elapsed] +aws_instance.nginx[0] (remote-exec): Collecting s3cmd +aws_instance.nginx[0] (remote-exec): Downloading https://files.pythonhosted.org/packages/26/44/19e08f69b2169003f7307565f19449d997895251c6a6566ce21d5d636435/s3cmd-2.1.0-py2.py3-none-any.whl (145kB) +aws_instance.nginx[0] (remote-exec): +aws_instance.nginx[0] (remote-exec): 7% |██▎ | 10kB 39.4MB/s eta 0:00:01 +... +aws_instance.nginx[0] (remote-exec): 100% |████████████████████████████████| 153kB 2.9MB/s +aws_instance.nginx[0] (remote-exec): Collecting python-magic (from s3cmd) +aws_instance.nginx[0] (remote-exec): Downloading https://files.pythonhosted.org/packages/59/77/c76dc35249df428ce2c38a3196e2b2e8f9d2f847a8ca1d4d7a3973c28601/python_magic-0.4.18-py2.py3-none-any.whl +aws_instance.nginx[0] (remote-exec): Requirement already satisfied: python-dateutil in /usr/lib/python2.7/dist-packages (from s3cmd) +aws_instance.nginx[0] (remote-exec): Requirement already satisfied: six in /usr/lib/python2.7/dist-packages (from python-dateutil->s3cmd) +aws_instance.nginx[0] (remote-exec): Installing collected packages: python-magic, s3cmd +aws_instance.nginx[0] (remote-exec): Successfully installed python-magic-0.4.18 s3cmd-2.1.0 +aws_instance.nginx[0] (remote-exec): You are using pip version 9.0.3, however version 20.2.3 is available. +aws_instance.nginx[0] (remote-exec): You should consider upgrading via the 'pip install --upgrade pip' command. +aws_instance.nginx[0] (remote-exec): download: 's3://terraform-dev-36748/website/corporate.jpg' -> './corporate.jpg' [1 of 1] +aws_instance.nginx[0] (remote-exec): 41686 of 41686 100% in 0s 351.27 KB/s +aws_instance.nginx[0] (remote-exec): 41686 of 41686 100% in 0s 350.64 KB/s done +aws_instance.nginx[0] (remote-exec): upload: '/var/log/nginx/access.log' -> 's3://terraform-dev-36748/nginx/i-0c77a6dfacc236aba/access.log' [1 of 3] +aws_instance.nginx[0] (remote-exec): 0 of 0 0% in 0s 0.00 B/s +aws_instance.nginx[0] (remote-exec): 0 of 0 0% in 0s 0.00 B/s done +aws_instance.nginx[0] (remote-exec): upload: '/var/log/nginx/access.log-20200930.gz' -> 's3://terraform-dev-36748/nginx/i-0c77a6dfacc236aba/access.log-20200930.gz' [2 of 3] +aws_instance.nginx[0] (remote-exec): 20 of 20 100% in 0s 21.73 KB/s +aws_instance.nginx[0] (remote-exec): 20 of 20 100% in 0s 750.10 B/s done +aws_instance.nginx[0] (remote-exec): upload: '/var/log/nginx/error.log' -> 's3://terraform-dev-36748/nginx/i-0c77a6dfacc236aba/error.log' [3 of 3] +aws_instance.nginx[0] (remote-exec): 0 of 0 0% in 0s 0.00 B/s +aws_instance.nginx[0] (remote-exec): 0 of 0 0% in 0s 0.00 B/s done +aws_instance.nginx[0] (remote-exec): remote copy: 'access.log-20200930.gz' -> 'error.log-20200930.gz' +aws_instance.nginx[0] (remote-exec): Done. Uploaded 20 bytes in 1.0 seconds, 20.00 B/s. +aws_instance.nginx[0]: Creation complete after 2m13s [id=i-0c77a6dfacc236aba] +aws_elb.web: Creating... +aws_elb.web: Creation complete after 6s [id=dev-nginx-elb-36748] + +Apply complete! Resources: 16 added, 0 changed, 0 destroyed. + +The state of your infrastructure has been saved to the path +below. This state is required to modify and destroy your +infrastructure, so keep it safe. To inspect the complete state +use the `terraform show` command. + +State path: terraform.tfstate + +Outputs: + +aws_elb_public_dns = dev-nginx-elb-36748-1893652761.us-east-1.elb.amazonaws.com +cname_record_url = http://prototype.emvaldes.name +custom_contact = Updated - emvaldes@yahoo.com +custom_engineer = Updated - DevOps Team +custom_listset = Updated - Proving Nothing +custom_mapset = Updated - Testing Something +custom_timestamp = Updated - Today Is A Good Day To ... +filebased_parameters = Updated - Development Parameters loaded from a custom parameter file +resources_index = 36748 +``` + +```bash +$ host dev-nginx-elb-36748-1893652761.us-east-1.elb.amazonaws.com; + +dev-nginx-elb-36748-1893652761.us-east-1.elb.amazonaws.com has address 107.21.103.47 +dev-nginx-elb-36748-1893652761.us-east-1.elb.amazonaws.com has address 18.205.134.116 +``` + +```bash +$ terraform destroy ; + +random_integer.rand: Refreshing state... [id=36748] +data.template_file.public_cidrsubnet[0]: Refreshing state... [id=a34d80f7b68bf2b583681f727bae85f5a202ee100ad60ab3b3d351b9ce929539] +data.aws_availability_zones.available: Refreshing state... [id=2020-09-30 00:10:06.441389 +0000 UTC] +data.aws_ami.aws-linux: Refreshing state... [id=ami-032930428bf1abbff] +module.bucket.aws_iam_role.allow_instance_s3: Refreshing state... [id=terraform-dev-36748_allow_instance_s3] +data.aws_elb_hosted_zone_id.main: Refreshing state... [id=Z35SXDOTRQ7X7K] +module.bucket.aws_s3_bucket.web_bucket: Refreshing state... [id=terraform-dev-36748] +module.vpc.aws_vpc.this[0]: Refreshing state... [id=vpc-08601ba0c7d611f63] +module.bucket.aws_iam_instance_profile.instance_profile: Refreshing state... [id=terraform-dev-36748_instance_profile] +module.bucket.aws_iam_role_policy.allow_s3_all: Refreshing state... [id=terraform-dev-36748_allow_instance_s3:terraform-dev-36748_allow_all] +aws_security_group.elb-sg: Refreshing state... [id=sg-06ec7a84be35e7914] +aws_security_group.nginx-sg: Refreshing state... [id=sg-0d296ac58a656dc62] +module.vpc.aws_subnet.public[0]: Refreshing state... [id=subnet-07ccd80b42959356e] +module.vpc.aws_internet_gateway.this[0]: Refreshing state... [id=igw-01679506397fbf208] +module.vpc.aws_route_table.public[0]: Refreshing state... [id=rtb-00bc8c353eb56e391] +module.vpc.aws_route.public_internet_gateway[0]: Refreshing state... [id=r-rtb-00bc8c353eb56e3911080289494] +module.vpc.aws_route_table_association.public[0]: Refreshing state... [id=rtbassoc-07f9d9a7a503e7d7c] +aws_s3_bucket_object.graphic: Refreshing state... [id=/website/corporate.jpg] +aws_instance.nginx[0]: Refreshing state... [id=i-0c77a6dfacc236aba] +aws_elb.web: Refreshing state... [id=dev-nginx-elb-36748] + +An execution plan has been generated and is shown below. +Resource actions are indicated with the following symbols: +``` + +```yaml + - destroy + +Terraform will perform the following actions: + + # aws_elb.web will be destroyed + - resource "aws_elb" "web" { + ... + } + + # aws_instance.nginx[0] will be destroyed + - resource "aws_instance" "nginx" { + ... + } + + # aws_s3_bucket_object.graphic will be destroyed + - resource "aws_s3_bucket_object" "graphic" { + ... + } + + # aws_security_group.elb-sg will be destroyed + - resource "aws_security_group" "elb-sg" { + ... + } + + # aws_security_group.nginx-sg will be destroyed + - resource "aws_security_group" "nginx-sg" { + ... + } + + # random_integer.rand will be destroyed + - resource "random_integer" "rand" { + ... + } + + # module.bucket.aws_iam_instance_profile.instance_profile will be destroyed + - resource "aws_iam_instance_profile" "instance_profile" { + ... + } + + # module.bucket.aws_iam_role.allow_instance_s3 will be destroyed + - resource "aws_iam_role" "allow_instance_s3" { + ... + } + + # module.bucket.aws_iam_role_policy.allow_s3_all will be destroyed + - resource "aws_iam_role_policy" "allow_s3_all" { + ... + } + + # module.bucket.aws_s3_bucket.web_bucket will be destroyed + - resource "aws_s3_bucket" "web_bucket" { + ... + } + + # module.vpc.aws_internet_gateway.this[0] will be destroyed + - resource "aws_internet_gateway" "this" { + ... + } + + # module.vpc.aws_route.public_internet_gateway[0] will be destroyed + - resource "aws_route" "public_internet_gateway" { + ... + } + + # module.vpc.aws_route_table.public[0] will be destroyed + - resource "aws_route_table" "public" { + ... + } + + # module.vpc.aws_route_table_association.public[0] will be destroyed + - resource "aws_route_table_association" "public" { + ... + } + + # module.vpc.aws_subnet.public[0] will be destroyed + - resource "aws_subnet" "public" { + ... + } + + # module.vpc.aws_vpc.this[0] will be destroyed + - resource "aws_vpc" "this" { + ... + } + +Plan: 0 to add, 0 to change, 16 to destroy. + +Changes to Outputs: + - aws_elb_public_dns = "dev-nginx-elb-36748-1893652761.us-east-1.elb.amazonaws.com" -> null + - cname_record_url = "http://prototype.emvaldes.name" -> null + - custom_contact = "Updated - emvaldes@yahoo.com" -> null + - custom_engineer = "Updated - DevOps Team" -> null + - custom_listset = "Updated - Proving Nothing" -> null + - custom_mapset = "Updated - Testing Something" -> null + - custom_timestamp = "Updated - Today Is A Good Day To ..." -> null + - filebased_parameters = "Updated - Development Parameters loaded from a custom parameter file" -> null + - resources_index = 36748 -> null + +Do you really want to destroy all resources in workspace "dev"? + Terraform will destroy all your managed infrastructure, as shown above. + There is no undo. Only 'yes' will be accepted to confirm. + + Enter a value: yes + +module.vpc.aws_route_table_association.public[0]: Destroying... [id=rtbassoc-07f9d9a7a503e7d7c] +aws_s3_bucket_object.graphic: Destroying... [id=/website/corporate.jpg] +module.vpc.aws_route.public_internet_gateway[0]: Destroying... [id=r-rtb-00bc8c353eb56e3911080289494] +aws_elb.web: Destroying... [id=dev-nginx-elb-36748] +aws_s3_bucket_object.graphic: Destruction complete after 1s +module.vpc.aws_route.public_internet_gateway[0]: Destruction complete after 0s +module.vpc.aws_internet_gateway.this[0]: Destroying... [id=igw-01679506397fbf208] +module.vpc.aws_route_table_association.public[0]: Destruction complete after 1s +module.vpc.aws_route_table.public[0]: Destroying... [id=rtb-00bc8c353eb56e391] +module.vpc.aws_route_table.public[0]: Destruction complete after 1s +aws_elb.web: Destruction complete after 6s +aws_security_group.elb-sg: Destroying... [id=sg-06ec7a84be35e7914] +aws_instance.nginx[0]: Destroying... [id=i-0c77a6dfacc236aba] +module.vpc.aws_internet_gateway.this[0]: Still destroying... [id=igw-01679506397fbf208, 10s elapsed] +aws_instance.nginx[0]: Still destroying... [id=i-0c77a6dfacc236aba, 10s elapsed] +aws_security_group.elb-sg: Still destroying... [id=sg-06ec7a84be35e7914, 10s elapsed] +module.vpc.aws_internet_gateway.this[0]: Still destroying... [id=igw-01679506397fbf208, 20s elapsed] +aws_security_group.elb-sg: Destruction complete after 19s +aws_instance.nginx[0]: Still destroying... [id=i-0c77a6dfacc236aba, 20s elapsed] +module.vpc.aws_internet_gateway.this[0]: Still destroying... [id=igw-01679506397fbf208, 30s elapsed] +aws_instance.nginx[0]: Still destroying... [id=i-0c77a6dfacc236aba, 30s elapsed] +module.vpc.aws_internet_gateway.this[0]: Still destroying... [id=igw-01679506397fbf208, 40s elapsed] +aws_instance.nginx[0]: Destruction complete after 36s +module.bucket.aws_iam_role_policy.allow_s3_all: Destroying... [id=terraform-dev-36748_allow_instance_s3:terraform-dev-36748_allow_all] +module.bucket.aws_iam_instance_profile.instance_profile: Destroying... [id=terraform-dev-36748_instance_profile] +module.vpc.aws_subnet.public[0]: Destroying... [id=subnet-07ccd80b42959356e] +aws_security_group.nginx-sg: Destroying... [id=sg-0d296ac58a656dc62] +module.bucket.aws_s3_bucket.web_bucket: Destroying... [id=terraform-dev-36748] +module.vpc.aws_internet_gateway.this[0]: Destruction complete after 43s +module.bucket.aws_iam_role_policy.allow_s3_all: Destruction complete after 1s +aws_security_group.nginx-sg: Destruction complete after 1s +module.vpc.aws_subnet.public[0]: Destruction complete after 1s +module.vpc.aws_vpc.this[0]: Destroying... [id=vpc-08601ba0c7d611f63] +module.bucket.aws_iam_instance_profile.instance_profile: Destruction complete after 2s +module.bucket.aws_iam_role.allow_instance_s3: Destroying... [id=terraform-dev-36748_allow_instance_s3] +module.vpc.aws_vpc.this[0]: Destruction complete after 1s +module.bucket.aws_iam_role.allow_instance_s3: Destruction complete after 1s +module.bucket.aws_s3_bucket.web_bucket: Destruction complete after 8s +random_integer.rand: Destroying... [id=36748] +random_integer.rand: Destruction complete after 0s + +Destroy complete! Resources: 16 destroyed. +``` diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..3397c9a --- /dev/null +++ b/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-architect \ No newline at end of file diff --git a/action.functions b/action.functions new file mode 100644 index 0000000..651db70 --- /dev/null +++ b/action.functions @@ -0,0 +1,383 @@ + +## https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html +function configure_terraform_template () { + ## echo -e "\nManaged Scripts: ${managed_scripts[@]}" ; + if [[ ${#managed_scripts[@]} -eq 0 ]]; then + echo -e "\nWarning: Managed Scripts set is not defined! \n" ; + exit 1 ; + else if [[ ! -e ${manage_terraform_template} ]]; then + if [[ ${result} -gt 0 ]]; then + echo -e "\nWarning: Manage Terraform template was not found! \n" ; + exit 2 ; + fi ; + fi ; + ## Note: Disregarding these configuration entries ... + ## -e "s|{{ console.Default_Profile }}|${aws_default_profile}|g" \ + ## -e "s|{{ console.S3Bucket_Region }}|${s3bucket_default_region}|g" \ + for managed_script in ${managed_scripts[@]}; do + cat /dev/null > ${managed_script} ; + cat ${manage_terraform_template} \ + | sed -e "s|{{ console.Restore_Folder }}|${restore_folder}|g" \ + -e "s|{{ console.Remote_Origin }}|${remote_origin/https:/git:}|g" \ + -e "s|{{ console.Commit_SHAID }}|${commit_shaid}|g" \ + -e "s|{{ console.S3Bucket_Name }}|${terraform_s3bucket}|g" \ + -e "s|{{ console.Remote_Path }}|${target_location}|g" \ + -e "s|{{ console.Verbosity }}|${terraform_verbosity}|g" \ + -e "s|{{ console.Target_Workspace }}|${target_workspace}|g" \ + -e "s|{{ github.workspace }}|${github_workspace}|g" \ + > ${managed_script}; + done ; + sed -i -e "s|{{ console.Terraform_Action }}|apply -auto-approve \${terraform_restore}/terraform.tfstate.d/${target_workspace}/terraform.tfplan|g" ${restore_script} ; + sed -i -e "s|{{ console.Terraform_Action }}|destroy -auto-approve|g" ${destroy_script} ; + fi ; + return 0 ; + } ; + +function setup_dynamodb_github () { + awscli_command="$( + echo aws --profile ${aws_default_profile} \ + --region ${dynamodb_default_region} \ + dynamodb list-tables + )" ; + echo -en "> ${awscli_command}\n" ; + eval "${awscli_command}" ; + table_exists="$( + eval ${awscli_command} \ + | jq '.TableNames[]|select(.=="'${dynamodb_github_table}'")' \ + --raw-output + )" ; + if [[ ${#table_exists} -eq 0 ]]; then + echo -e "\nWarning: Creating GitHub-Pipelines DynamoDB Table ...\n" ; + curl --silent ${usercontent_repository}/service/dynamodb/github/pipelines/create.shell \ + | sed -e "s|{{ console.Profile }}|${aws_default_profile}|g" \ + -e "s|{{ console.Region }}|${aws_default_region}|g" \ + -e "s|{{ console.Table_Name }}|${dynamodb_github_table}|g" \ + | bash - ; + awscli_command="$( + echo aws --profile ${aws_default_profile} \ + --region ${dynamodb_default_region} \ + dynamodb wait table-exists \ + --table-name ${dynamodb_github_table} + )" ; + echo -en "> ${awscli_command}\n" ; + eval "${awscli_command}" ; + fi; unset table_exists ; + dynamodb_github="${usercontent_repository}/service/dynamodb/github/pipelines/template.json" ; + dynamodb_github_template="${github_workspace}/${dynamodb_github_table}.json" ; + cat /dev/null > ${dynamodb_github_template} ; + wget --quiet --output-document=${dynamodb_github_template} ${dynamodb_github} ; + sed -i -e "s|{{ console.Repo_SHAIndex }}|${commit_shaid}|g" \ + -e "s|{{ console.Repository }}|${github_repository}|g" \ + -e "s|{{ console.Date_Time }}|${target_timestamp}|g" \ + -e "s|{{ console.Author_Name }}|${github_author_name}|g" \ + -e "s|{{ console.Repo_Branch }}|${github_repository_branch}|g" \ + -e "s|{{ console.Author_Contact }}|???|g" \ + -e "s|{{ console.Organization }}|${github_repository_owner}|g" \ + -e "s|{{ console.Repo_Version }}|???|g" \ + ${dynamodb_github_template} ; + ## cat ${dynamodb_github_template} | jq '.' ; echo -e ; + awscli_command="$( + echo aws --profile ${aws_default_profile} \ + --region ${dynamodb_default_region} \ + dynamodb put-item \ + --table-name ${dynamodb_github_table} \ + --item file://${dynamodb_github_template} + )" ; + echo -en "\n> ${awscli_command}\n" ; + eval "${awscli_command}" ; + echo -e "\nListing DynamoDB GitHub Table-Record: ...\n" + awscli_command="$( + echo aws --profile ${aws_default_profile} \ + --region ${dynamodb_default_region} \ + dynamodb get-item \ + --table-name ${dynamodb_github_table} \ + --key "'{ \"shaindex\": { \"S\": \"${commit_shaid}\" }, \"repository\": { \"S\": \"${github_repository}\" } }'" + )" ; + echo -en "> ${awscli_command}\n" ; + eval "${awscli_command}" ; + return 0 ; + } ; + +function setup_dynamodb_terraform () { + awscli_command="$( + echo aws --profile ${aws_default_profile} \ + --region ${dynamodb_default_region} \ + dynamodb list-tables + )" ; + table_exists="$( + eval ${awscli_command} \ + | jq '.TableNames[]|select(.=="'${dynamodb_terraform_table}'")' \ + --raw-output + )" ; + echo -en "\n> ${awscli_command}\n" ; + eval "${awscli_command}" ; + if [[ ${#table_exists} -eq 0 ]]; then + echo -e "\nWarning: Creating Terraform-Pipelines DynamoDB Table ...\n" ; + curl --silent ${usercontent_repository}/service/dynamodb/terraform/pipelines/create.shell \ + | sed -e "s|{{ console.Profile }}|${aws_default_profile}|g" \ + -e "s|{{ console.Region }}|${aws_default_region}|g" \ + -e "s|{{ console.Table_Name }}|${dynamodb_terraform_table}|g" \ + | bash - ; + awscli_command="$( + echo aws --profile ${aws_default_profile} \ + --region ${dynamodb_default_region} \ + dynamodb wait table-exists \ + --table-name ${dynamodb_terraform_table} + )" ; + echo -en "> ${awscli_command}\n" ; + eval "${awscli_command}" ; + fi; unset table_exists ; + dynamodb_terraform="${usercontent_repository}/service/dynamodb/terraform/pipelines/template.json" ; + dynamodb_terraform_template="${github_workspace}/${dynamodb_terraform_table}.json" ; + cat /dev/null > ${dynamodb_terraform_template} ; + wget --quiet --output-document=${dynamodb_terraform_template} ${dynamodb_terraform} ; + declare -a terraform_variables=(); + oIFS="${IFS}"; IFS='_' ; + for param in ${input_params[@]}; do + terraform_variables+=("{ \"S\": \"$( + echo -en ${param} \ + | base64 --decode \ + | sed -e 's|"|\\\\"|g' \ + -e "s|^\(.*\)\(=\)\(.*\)$|\1\2'\3'|g" + )\" },") ; + done ; IFS="${oIFS}" ; + sed -i -e "s|{{ console.Repo_SHAIndex }}|${commit_shaid}|g" \ + -e "s|{{ console.Repository }}|${github_repository}|g" \ + -e "s|{{ console.Date_Time }}|${target_timestamp}|g" \ + -e "s|{{ console.Restore_Location }}|${target_location}|g" \ + -e "s|{{ console.Custom_Configs }}|${target_custom_tfvars}|g" \ + -e "s|{{ console.Target_LongID }}|???|g" \ + -e "s|{{ console.Target_ShortID }}|${target_workspace}|g" \ + -e "s|{{ console.Identity_Access }}|${DEVOPS_ACCESS_ROLE}|g" \ + -e "s|{{ console.Identity_Account }}|${aws_default_account}|g" \ + -e "s|{{ console.Identity_Profile }}|${DEVOPS_ACCOUNT_NAME}|g" \ + -e "s|{{ console.Identity_UserID }}|???|g" \ + -e "s|{{ console.Source_Pipeline }}|${target_container}|g" \ + -e "s|{{ console.Custom_Variables }}|$( + echo ${terraform_variables[@]} | sed -e 's|,$||' + )|g" \ + ${dynamodb_terraform_template} ; + ## cat ${dynamodb_terraform_template} | jq '.' ; echo -e ; + awscli_command="$( + echo aws --profile ${aws_default_profile} \ + --region ${dynamodb_default_region} \ + dynamodb put-item \ + --table-name ${dynamodb_terraform_table} \ + --item file://${dynamodb_terraform_template} + )" ; + echo -en "\n> ${awscli_command}\n" ; + eval "${awscli_command}" ; + echo -e "\nListing DynamoDB Terraform Table-Record: ..." + awscli_command="$( + echo aws --profile ${aws_default_profile} \ + --region ${dynamodb_default_region} \ + dynamodb get-item \ + --table-name ${dynamodb_terraform_table} \ + --key "'{ \"shaindex\": { \"S\": \"${commit_shaid}\" }, \"repository\": { \"S\": \"${github_repository}\" } }'" + )" ; + echo -en "\n> ${awscli_command}\n" ; + eval "${awscli_command}" ; + return 0 ; + } ; + +function update_dynamodb_terraform () { + IFS="${IFS}" ; IFS=':' ; + declare -a outputs=($( + terraform output \ + | cut -d '=' -f1 \ + | tr '[[:space:]]' ':' \ + | sed -e 's|\(:\)\{1,\}$||g' -e 's|\(:\)\{1,\}|:|g' + )); IFS="${oIFS}" ; + declare -a terraform_outputs=(); + for xitem in ${outputs[@]}; do + output="$( + terraform output ${xitem} | sed -e 's|"|\\\\"|g' + )" ; + terraform_outputs+=("{ \"S\": \"${xitem}='${output}'\" },") ; + done ; + pwd; + attribute_values="attribute-values.json" ; + custom_outputs="{ \":outputs\": { \"L\": [ $( + echo ${terraform_outputs[@]} \ + | sed -e 's|"|\"|g' \ + -e 's|^\(.*\)\(\,\)$|\1|' + ) ] } }" ; + echo "${custom_outputs[@]}" | jq '.' > ${attribute_values}; + echo -e "\nAWS DynamoDB Expression Attribute-Values: \n" ; + cat ${attribute_values} | jq '.'; echo -e ; + awscli_command="$( + echo aws --profile ${aws_default_profile} \ + --region ${dynamodb_default_region} \ + dynamodb update-item \ + --table-name ${dynamodb_terraform_table} \ + --key "'{ \"shaindex\": { \"S\": \"${commit_shaid}\" }, \"repository\": { \"S\": \"${github_repository}\" } }'" \ + --update-expression \"SET outputs = :outputs\" \ + --expression-attribute-values file://${attribute_values} + )" ; + ## echo -en "> "; $(echo -e ${awscli_command} | sed -e "s|\([[:space:]]\)\{1,\}| |g") ; + echo -en "> ${awscli_command}\n" ; + eval "${awscli_command}" ; + return 0 ; + } ; + +function backup_terraform_state () { + ## tracking_process ${FUNCNAME} "${@}" ; + oIFS="${IFS}" ; + for xitem in "${@}"; do + IFS='='; set `echo -e "${xitem}" | sed -e '1s|^\(-\)\{1,\}||'` + [[ ${1#*\--} = "source-dataset" ]] && export source_dataset="${2}" ; + [[ ${1#*\--} = "restore-path" ]] && export target_restorepath="${2}" ; + [[ ${1#*\--} = "s3bucket-container" ]] && export s3bucket_container="${2}" ; + ## [[ ${1#*\--} = "dry-run" ]] && export dry_run="${2}" ; + [[ ${1#*\--} = "verbose" ]] && export verbose='true' ; + [[ ${1#*\--} = "help" ]] && export display_help='true' ; + done; IFS="${oIFS}" ; + ## Define custom-parameter(s): + [[ ${#source_dataset} -eq 0 ]] && export target_builset='false' ; + [[ ${#target_restorepath} -eq 0 ]] && export target_restorepath='' ; + [[ ${#s3bucket_container} -eq 0 ]] && export s3bucket_container='' ; + ## [[ ${#dry_run} -eq 0 ]] && export dry_run='false' ; + [[ ${#verbose} -eq 0 ]] && export verbose='false' ; + if [[ ${source_dataset} != false ]]; then + echo -e "\nWorking Directory: $(pwd)" ; + export target_s3path="$( + echo -e "${s3bucket_container}/${target_restorepath}" \ + | sed -e 's|\(/\)\{2,\}|/|g' + )" ; + echo -e "\nS3Bucket Path: ${target_s3path}" ; + echo -e "\nTransferring Terraform State @ Local-Storage ... ${terraform_s3bucket}" ; + awscli_command="$( + echo aws --profile ${aws_default_profile} \ + --region ${s3bucket_default_region} \ + s3 cp ${source_dataset} s3://${terraform_s3bucket}/${target_s3path} \ + --recursive --only-show-errors + )" ; + echo -en "\n> ${awscli_command}\n" ; + eval "${awscli_command}" ; + echo -e "\nListing Terraform State @ AWS S3 Bucket ... ${terraform_s3bucket}" ; + awscli_command="$( + echo aws --profile ${aws_default_profile} \ + --region ${s3bucket_default_region} \ + s3 ls s3://${terraform_s3bucket}/${target_s3path} \ + --recursive + )" ; + echo -en "\n> ${awscli_command}\n" ; echo -e ; + eval "${awscli_command}" ; + else echo -e "\nWarning: Target Buildset is invalid! \n" ; + exit 1 ; + fi ; + return 0 ; + }; + +function backup_terraform_configs () { + terraform_modules="${github_workspace}/.terraform" ; + echo -e "\nCreating Target Build-Set Container ... [ ${target_buildset} ]" ; + mkdir -p ${target_buildset}/scripts ; + echo -e "\nTransferring terraform configurations to container ...\n" ; + cp -r ${terraform_tfstate} ${target_buildset}/ | cut -d ' ' -f2- ; + touch ${target_buildset}/${commit_shaid} ; + cp ${restore_script} ${target_buildset}/scripts/ ; + cp ${destroy_script} ${target_buildset}/scripts/ ; + tree ${target_buildset} ; + backup_terraform_state --source-dataset="${target_buildset}" \ + --s3bucket-container="${S3BUCKET_CONTAINER}/${target_buildset}" \ + ; + return 0 ; + } ; + +function restore_terraform_state () { + restore_timestamp="$(date +"%y%m%d%H%M%S")"; + if [[ "${#restore_shaindex}" -eq 0 ]]; then + echo -e "\nWarning: Git SHA Index is invalid! \n"; + exit 1 ; + fi ; +##------------------------------------------------------------------------------ + awscli_command=" + aws --profile ${aws_default_profile} \ + --region ${dynamodb_default_region} \ + dynamodb get-item \ + --table-name ${dynamodb_github_table} \ + --key '{ \"shaindex\": { \"S\": \""${restore_shaindex}"\" }, \"repository\": { \"S\": \""${restore_project}"\" } }' \ + --query 'Item.{organization:organization.S,repository:repository.S}' + " ; + echo -en "> ${awscli_command}\n" | sed -e 's#\([[:space:]]\)\{2,\}# #g' ; + eval $( + aws --profile ${aws_default_profile} \ + --region ${dynamodb_default_region} \ + dynamodb get-item \ + --table-name ${dynamodb_github_table} \ + --key '{ "shaindex": { "S": "'${restore_shaindex}'" }, "repository": { "S": "'${restore_project}'" } }' \ + --query 'Item.{organization:organization.S,repository:repository.S}' \ + | jq -r "to_entries|map(\"export \(.key)=\(.value|tostring)\")|.[]" + ) ; + if [[ ( ${#organization} -eq 0 ) || ( ${organization} == 'null' ) ]]; then + echo -e "\nWarning: Source Organization is invalid! \n"; + exit 2; + fi; + if [[ ( ${#repository} -eq 0 ) || ( ${repository} == 'null' ) ]]; then + echo -e "\nWarning: Source Repository is invalid! \n"; + exit 3; + fi; +##------------------------------------------------------------------------------ + awscli_command=" + aws --profile ${aws_default_profile} \ + --region ${dynamodb_default_region} \ + dynamodb get-item \ + --table-name ${dynamodb_terraform_table} \ + --key '{ \"shaindex\": { \"S\": \""${restore_shaindex}"\" }, \"repository\": { \"S\": \""${restore_project}"\" } }' \ + --query 'Item.{location:location.S,workspace:environment.M.target.S}' + " ; + echo -en "> ${awscli_command}\n" | sed -e 's#\([[:space:]]\)\{2,\}# #g' ; + eval $( + aws --profile ${aws_default_profile} \ + --region ${dynamodb_default_region} \ + dynamodb get-item \ + --table-name ${dynamodb_terraform_table} \ + --key '{ "shaindex": {"S": "'${restore_shaindex}'"}, "repository": { "S": "'${restore_project}'" } }' \ + --query 'Item.{location:location.S,workspace:environment.M.target.S}' \ + | jq -r "to_entries|map(\"export target_\(.key)=\(.value|tostring)\")|.[]" + ) ; + if [[ ( ${#target_workspace} -eq 0 ) || ( ${target_workspace} == 'null' ) ]]; then + echo -e "\nWarning: Restore Environment is invalid! \n"; + exit 4; + else echo -e "Restore Environment: '${target_workspace}'" ; + fi; + if [[ ( ${#target_location} -eq 0 ) || ( ${target_location} == 'null' ) ]]; then + echo -e "\nWarning: Restore Location is invalid! \n"; + exit 5; + else echo -e "\nTarget Location: '${target_location}'" ; + fi; +##------------------------------------------------------------------------------ + echo -e "\nRestoring Terraform State @ ${organization}/${repository} -> ${restore_shaindex} [${target_workspace}] = ${target_location} :\n" ; + export target_path="${github_workspace}"; + export restore_folder="${target_path}/restore/${restore_shaindex:0:7}" ; + mkdir -p ${restore_folder} ; + export restore_script="${target_path}/restore.shell" ; + declare -a managed_scripts=(${restore_script}) ; + configure_terraform_template ; + terraform_planfile="\${terraform_restore}/terraform.tfstate.d/${target_workspace}/terraform.tfplan"; + terraform_action="apply -auto-approve ${terraform_planfile}"; + sed -i -e "s|{{ console.Terraform_Action }}|${terraform_action}|g" \ + -e "s|^\(export \)\(AWS_PROFILE=\)\(.*\)$|\1\2'${aws_default_profile}';|g" \ + -e "s|^\(export \)\(AWS_DEFAULT_REGION=\)\(.*\)$|\1\2'${aws_default_region}';|g" \ + ${restore_script} ; + sed -i -e '/^read -p.*$/d' ${restore_script} ; + sed -i -e '/./b' \ + -e :n \ + -e 'N;s/\n$//;tn' \ + ${restore_script} ; + echo -e "Restore Terraform Script:" ; + cat ${restore_script}; + echo -e ; + bash ${restore_script} ; + cd ${restore_folder} ; + echo -e "\nExecuting Terraform Show ...\n" ; + terraform_states="${restore_folder}/terraform.tfstate.d/${target_workspace}" ; + terraform show -no-color | tee ${terraform_states}/terraform.tfstate.yaml ; + echo -e "\nBacking-Up Terraform Restore-State ..." ; + target_restorepath="restored/${AWS_DEFAULT_ACCOUNT}/${AWS_DEFAULT_REGION}/${restore_timestamp}/terraform.tfstate.d/${target_workspace}" ; + backup_terraform_state --source-dataset="${terraform_states}" \ + --restore-path="${target_location}/${target_restorepath}" \ + ; + return 0 ; + } ; diff --git a/action.yaml b/action.yaml new file mode 100644 index 0000000..4598735 --- /dev/null +++ b/action.yaml @@ -0,0 +1,450 @@ +name: Terraform Controller +description: 'Terraform Controller (DevOps)' + +####---------------------------------------------------------------------------- +inputs: + aws-access-key-id: + description: 'Terraform AWS Access Key-ID' + required: false + default: false + aws-default-account: + description: 'Terraform AWS Default Account' + required: false + default: false + aws-default-profile: + description: 'Terraform AWS Default Profile' + required: false + default: false + aws-default-region: + description: 'Terraform AWS Default Region' + required: false + default: false + aws-secret-access-key: + description: 'Terraform AWS Secret Access Key' + required: false + default: false + backup-terraform: + description: 'Backup Terraform Infrastructure' + required: false + default: false + controller-functions: + description: 'Terraform-Controller functions' + required: false + default: 'action.functions' + deploy-terraform: + description: 'Deploy Terraform Infrastructure' + required: false + default: false + destroy-terraform: + description: 'Destroy Terraform Infrastructure' + required: false + default: false + dynamodb-default-region: + description: 'Define AWS DynamoDB Default Region' + required: false + default: 'us-east-1' + dynamodb-github-table: + description: 'Define AWS DynamoDB GitHub Table' + required: false + default: 'github-pipelines' + dynamodb-terraform-table: + description: 'Define AWS DynamoDB Terraform Table' + required: false + default: 'terraform-pipelines' + manage-terraform-script: + description: 'Manage-Terraform template (script)' + required: false + default: 'manage-terraform.shell' + private-keypair-file: + description: 'Terraform Private KeyPair-File' + required: false + default: .ssh/id_rsa + private-keypair-name: + description: 'Terraform Private Key-Name' + required: false + default: false + provision-terraform: + description: 'Provision Terraform Provisioning' + required: false + default: false + repository--manage-profiles: + description: 'Manage-Profile GitHub Repository' + required: false + default: 'emvaldes/manage-profiles' + repository--terraform-controller: + description: 'Terraform-Controller GitHub Repository' + required: false + default: 'emvaldes/terraform-controller' + restore-account: + description: 'Restore Terraform AWS Account' + required: false + default: false + restore-index: + description: 'Restore Terraform SHA-Index' + required: false + default: false + restore-keypair-file: + description: 'Restore Terraform KeyPair-File' + required: false + default: .ssh/id_rsa + restore-project: + description: 'Restore Terraform Project' + required: false + default: false + restore-region: + description: 'Restore Terraform AWS Region' + required: false + default: false + restore-shaindex: + description: 'Restore Terraform SHA Index' + required: false + default: '' + restore-terraform: + description: 'Restore Terraform Infrastructure' + required: false + default: false + restore-workspace: + description: 'Restore Terraform Workspace' + required: false + default: false + route53-subdomain: + description: 'Define Route53 Sub-Domain' + required: false + default: false + s3bucket-nameset: + description: 'Default AWS S3 Bucket Name-Prefix' + default: 'terraform--states--' + s3bucket-default-region: + description: 'Default AWS S3 Default Region' + default: 'us-east-1' + target-container: + description: 'Define Terraform Container' + required: false + default: false + target-timestamp: + description: 'Define Terraform Time-Stamp' + required: false + default: false + target-workspace: + description: 'Define Terraform Workspace' + required: false + default: false + terraform-config: + description: 'Define Terraform Configuration' + required: false + default: false + terraform-input-params: + description: 'Define Terraform Input Parameters' + required: false + default: false + terraform-input-tfvars: + description: 'Define Terraform Input Variables' + required: false + default: false + terraform-planfile: + description: 'Define Terraform Planfile Name' + required: false + default: 'terraform' + terraform-tfstate: + description: 'Define Terraform System-State' + required: false + default: false + upgrade-providers: + description: 'Upgrade Terraform Providers' + default: false + validate-formatting: + description: 'Validate Terraform Formatting' + default: true + terraform-loglevel: + description: 'Define Terraform Loglevel' + required: false + default: false +####---------------------------------------------------------------------------- +runs: + using: "composite" + steps: + ####------------------------------------------------------------------------ + - name: Terraform Controller + id: terraform-controller + shell: bash + run: | + ####-------------------------------------------------------------------- + completion="Skipping ...! " ; + ####-------------------------------------------------------------------- + github_usercontent="https://raw.githubusercontent.com" ; + usercontent_repository="${github_usercontent}/${{ inputs.repository--manage-profiles }}/master" ; + usercontent_controller="${github_usercontent}/${{ inputs.repository--terraform-controller }}/master" ; + ####-------------------------------------------------------------------- + manage_terraform_script="${{ inputs.manage-terraform-script }}" ; + manage_terraform_template="${{ github.workspace }}/${manage_terraform_script}" ; + wget --quiet --output-document=${manage_terraform_template} ${usercontent_controller}/.github/templates/${manage_terraform_script} ; + ####-------------------------------------------------------------------- + wget --quiet --output-document=./${{ inputs.controller-functions }} ${usercontent_controller}/${{ inputs.controller-functions }} ; + source ${{ inputs.controller-functions }} ; + ####-------------------------------------------------------------------- + remote_origin="$(git config --get remote.origin.url)" ; + github_repository="${remote_origin##*\/}" ; + commit_shaid="$(git rev-parse HEAD)" ; + ####-------------------------------------------------------------------- + github_workspace="${{ github.workspace }}"; + ####-------------------------------------------------------------------- + dynamodb_default_region="${{ inputs.dynamodb-default-region }}" ; + dynamodb_github_table="${{ inputs.dynamodb-github-table }}" ; + dynamodb_terraform_table="${{ inputs.dynamodb-terraform-table }}" ; + ####-------------------------------------------------------------------- + s3bucket_default_region="${{ inputs.s3bucket-default-region }}" ; + ####-------------------------------------------------------------------- + route53_subdomain="${{ inputs.route53-subdomain }}" ; + [[ "${route53_subdomain}" == false ]] && { + route53_subdomain="${github_repository}" ; + } ; + target_workspace="${{ inputs.target-workspace }}" ; + [[ "${target_workspace}" == false ]] && { + target_workspace="${TARGET_WORKSPACE}" ; + } ; + target_timestamp="${{ inputs.target-timestamp }}" ; + [[ "${target_timestamp}" == false ]] && { + target_timestamp="${SESSION_TIMESTAMP}" ; + [[ ${#target_timestamp} -eq 0 ]] && { + echo -e "\nWarning: Session Time-Stamp is invalid! \n" ; + exit 1; + } ; + } ; + ####-------------------------------------------------------------------- + custom_filepath="${target_timestamp:0:2}/${target_timestamp:2:2}/${target_timestamp:4:2}/${target_timestamp:6:6}" ; + target_buildset="${target_workspace}/${custom_filepath}/$(git rev-parse --short HEAD)" ; + ####-------------------------------------------------------------------- + terraform_tfstate="${{ github.workspace }}/terraform.tfstate.d" ; + terraform_tfplan="${terraform_tfstate}/${target_workspace}/${{ inputs.terraform-planfile }}.tfplan" ; + ####-------------------------------------------------------------------- + terraform_verbosity="${{ inputs.terraform-loglevel }}" ; + [[ "${terraform_verbosity}" != false ]] && { + terraform_verbosity="TF_LOG=${{ inputs.terraform-logLevel }}" ; + } || { + terraform_verbosity='' ; + } ; + ####-------------------------------------------------------------------- + terraform_loglevel="${{ inputs.terraform-loglevel }}" ; + ####-------------------------------------------------------------------- + aws_default_profile="${{ inputs.aws-default-profile }}" ; + [[ "${aws_default_profile}" == false ]] && { + aws_default_profile="${AWS_DEFAULT_PROFILE}" ; + } ; + aws_default_account="${{ inputs.aws-default-account }}" ; + [[ "${aws_default_account}" == false ]] && { + aws_default_account="${AWS_DEFAULT_ACCOUNT}" ; + } ; + aws_default_region="${{ inputs.aws-default-region }}" ; + [[ "${aws_default_region}" == false ]] && { + aws_default_region="${AWS_DEFAULT_REGION}" ; + } ; + aws_access_key_id="${{ inputs.aws-access-key-id }}" ; + [[ "${aws_access_key_id}" == false ]] && { + aws_access_key_id="${AWS_ACCESS_KEY_ID}" ; + } ; + aws_secret_access_key="${{ inputs.aws-secret-access-key }}" ; + [[ "${aws_secret_access_key}" == false ]] && { + aws_secret_access_key="${AWS_SECRET_ACCESS_KEY}" ; + } ; + ####-------------------------------------------------------------------- + s3bucket_nameset="${{ inputs.s3bucket-nameset }}" ; + terraform_s3bucket="${s3bucket_nameset}${aws_default_account}" ; + ####-------------------------------------------------------------------- + target_container="${{ inputs.target-container }}" ; + [[ "${target_container}" == false ]] && { + target_container="${S3BUCKET_CONTAINER}" ; + [[ ${#target_container} -eq 0 ]] && { + target_container="environments" ; + } ; + } ; + ####-------------------------------------------------------------------- + target_location="${target_container}/${target_buildset}"; + scripts_location="s3://${terraform_s3bucket}/${target_location}/scripts" ; + ####-------------------------------------------------------------------- + restore_script_location="${scripts_location}/restore.shell" ; + destroy_script_location="${scripts_location}/destroy.shell" ; + ####-------------------------------------------------------------------- + keypair_file="${{ inputs.private-keypair-file }}" ; + [[ "${keypair_file}" == false ]] && { + keypair_file="${PRIVATE_KEYPAIR_FILE}" ; + } ; + target_keypair_file="$( + echo ${keypair_file} | sed -e "s|${{ github.workspace }}/||g" + )" ; + keypair_name="${{ inputs.private-keypair-name }}" ; + [[ "${keypair_name}" == false ]] && { + keypair_name="${PRIVATE_KEYPAIR_NAME}" ; + } ; + [[ ${#keypair_name} -eq 0 ]] && { + echo -e "\nWarning: Invalid Key-Pair Name! \n" ; + exit 1 ; + } ; + ####-------------------------------------------------------------------- + target_path='/tmp/terraform' ; + restore_folder="${target_path}/${target_timestamp}" ; + mkdir -p ${restore_folder} ; + ####-------------------------------------------------------------------- + destroy_script="${target_path}/destroy.shell" ; + restore_script="${target_path}/restore.shell" ; + ####-------------------------------------------------------------------- + declare -a managed_scripts=(${restore_script} ${destroy_script}) ; + configure_terraform_template ; + ####-------------------------------------------------------------------- + if [[ "${{ inputs.provision-terraform }}" == true ]]; then + if [[ ${{ inputs.upgrade-providers }} == true ]]; then + echo -e "\nUpgrading Terraform Providers On-Demand ..." ; + eval ${terraform_verbosity} \ + terraform 0.13upgrade -yes ; + fi; + eval ${terraform_verbosity} \ + terraform init ; + eval ${terraform_verbosity} \ + terraform workspace new ${target_workspace} ; + if [[ ${{ inputs.validate-formatting }} == true ]]; then + eval ${terraform_verbosity} \ + terraform fmt -check ; + fi; + eval ${terraform_verbosity} \ + terraform validate ; + completion="Completed! " ; + fi; + ####-------------------------------------------------------------------- + if [[ "${{ inputs.provision-terraform }}" == true ]]; then + input_params="${{ inputs.terraform-input-params }}" ; + [[ "${input_params}" == false ]] && input_params='' ; + oIFS="${IFS}"; IFS='_' ; + if [[ ${#input_params[@]} -gt 0 ]]; then + echo -e "Terraform Custom Parameters:" ; + for encoded in ${input_params[@]}; do + decoded="$(echo -en ${encoded} | base64 --decode)" ; + echo -e "${encoded} -> ${decoded}" ; + done ; echo -e ; + fi ; + IFS="${oIFS}" ; + input_tfvars="${{ inputs.terraform-input-tfvars }}" ; + [[ "${input_tfvars}" == false ]] && input_tfvars='' ; + if [[ ${#input_tfvars} -gt 0 ]]; then + tfvars_file="${{ github.workspace }}/${input_tfvars}" ; + custom_message="Custom Terraform Variables file [${input_tfvars}]" ; + if [[ -e ${tfvars_file} ]]; then + echo -e "Listing ${custom_message}:" ; + ls -al ${tfvars_file} ; echo -e ; + else echo -e "Notice: ${custom_message} does not exit! \n" ; + fi ; + fi ; + echo -e "Terraform Output Plan: ${terraform_tfplan}\n" ; + custom_message="Private-KeyPair File: [${keypair_file}]" ; + if [[ -e ${keypair_file} ]]; then + echo -e "Listing ${custom_message}:" ; + ls -al ${keypair_file} ; echo -e ; + else echo -e "Warning: ${custom_message} does not exit! \n" ; + exit 1 ; + fi ; + echo -e "Private-KeyPair File: ${keypair_file}" ; echo -e ; + echo -e "Private-KeyPair Name: ${keypair_name}" ; echo -e ; + target_custom_tfvars="$( + echo ${tfvars_file} | sed -e "s|${{ github.workspace }}/||g" + )" ; + custom_tfvars=''; + [[ ${#target_custom_tfvars} -gt 0 ]] && custom_tfvars="-var-file=\"${target_custom_tfvars}\"" ; + target_terraform_tfplan="$( + echo ${terraform_tfplan} | sed -e "s|${{ github.workspace }}/||g" + )" ; + declare -a terraform_parameters=(); + terraform_parameters+=("-var=\"region=${aws_default_region}\"") ; + terraform_parameters+=("-var=\"aws_access_key=${aws_access_key_id}\"") ; + terraform_parameters+=("-var=\"aws_secret_key=${aws_secret_access_key}\"") ; + terraform_parameters+=("-var=\"private_keypair_file=${target_keypair_file}\"") ; + terraform_parameters+=("-var=\"private_keypair_name=${keypair_name}\"") ; + oIFS="${IFS}"; IFS='_' ; + for param in ${input_params[@]}; do + terraform_parameters+=("-var=\"$(echo -en ${param} | base64 --decode)\"") ; + done ; + IFS="${oIFS}" ; + echo -e "Listing Terraform Plan command:\n" ; + echo ${terraform_verbosity} \ + terraform plan \ + ${terraform_parameters[*]} \ + ${custom_tfvars} \ + -out ${target_terraform_tfplan} ; echo -e ; + + eval ${terraform_verbosity} \ + terraform plan \ + ${terraform_parameters[*]} \ + ${custom_tfvars} \ + -out ${target_terraform_tfplan} ; + completion="Completed! " ; + fi ; + ####-------------------------------------------------------------------- + if [[ "${{ inputs.provision-terraform }}" == true ]]; then + github_author_name="${{ github.actor }}" ; + github_repository_branch="${{ github.ref }}" ; + github_repository_owner="${{ github.repository_owner }}" ; + setup_dynamodb_github ; + setup_dynamodb_terraform ; + completion="Completed! " ; + fi ; + ####-------------------------------------------------------------------- + if [[ "${{ inputs.deploy-terraform }}" == true ]]; then + custom_message="Terraform Input Plan: [${terraform_tfplan}]" ; + if [[ -e ${terraform_tfplan} ]]; then + echo -e "Listing ${custom_message}:" ; + ls -al ${terraform_tfplan} ; echo -e ; + else echo -e "Notice: ${custom_message} does not exit! \n" ; + exit 1 ; + fi ; + eval ${terraform_verbosity} \ + terraform apply -auto-approve -compact-warnings ${terraform_tfplan} ; + completion="Completed! " ; + fi ; + ####-------------------------------------------------------------------- + if [[ "${{ inputs.deploy-terraform }}" == true ]]; then + update_dynamodb_terraform ; + echo -e "\nHow-To Destroy Terraform Infrastructure State:" ; + echo -e "-> ${destroy_script_location}\n" ; + cat ${destroy_script} ; + completion="Completed! " ; + fi ; + ####-------------------------------------------------------------------- + if [[ "${{ inputs.terraform-config }}" == true ]]; then + eval ${terraform_verbosity} \ + terraform show \ + | tee ${terraform_tfstate}/${target_workspace}/terraform.show ; + completion="Completed! " ; + fi ; + ####-------------------------------------------------------------------- + if [[ "${{ inputs.terraform-tfstate }}" == true ]]; then + tree ${terraform_tfstate} ; + target_state="$( + find ${terraform_tfstate} -type f -name terraform.tfstate | head -n1 + )" ; + echo -e "\nDisplaying Terraform State: ${target_state}" ; + cat ${target_state} ; + completion="Completed! " ; + fi ; + ####-------------------------------------------------------------------- + if [[ "${{ inputs.backup-terraform }}" == true ]]; then + backup_terraform_configs ; + completion="Completed! " ; + fi ; + ####-------------------------------------------------------------------- + if [[ "${{ inputs.destroy-terraform }}" == true ]]; then + eval ${terraform_verbosity} \ + terraform destroy -auto-approve ; + echo -e "\nHow-To Restore Terraform Infrastructure State:\n" ; + echo -e "-> ${restore_script_location}\n" ; + cat ${restore_script} ; + completion="Completed! " ; + fi ; + ####-------------------------------------------------------------------- + if [[ "${{ inputs.restore-terraform }}" == true ]]; then + restore_shaindex="${{ inputs.restore-shaindex }}" ; + restore_region="${{ inputs.restore-region }}" ; + restore_project="${{ inputs.restore-project }}" ; + restore_s3bucket="${{ inputs.s3bucket-nameset }}${aws_default_account}" ; + restore_terraform_state ; + completion="Completed! " ; + fi ; + ####-------------------------------------------------------------------- + echo -e "\n${completion}\n" ; + ####-------------------------------------------------------------------- diff --git a/configs/dev-configs.tfvars b/configs/dev-configs.tfvars new file mode 100644 index 0000000..d9f6609 --- /dev/null +++ b/configs/dev-configs.tfvars @@ -0,0 +1,9 @@ +custom_timestamp = "Updated - Today Is A Good Day To ..." +custom_engineer = "Updated - DevOps Team" +custom_contact = "Updated - emvaldes@yahoo.com" +custom_listset = "Updated - Proving Nothing" +custom_mapset = "Updated - Testing Something" + +filebased_parameters = "Updated - Development Parameters loaded from a custom parameter file" + +billing_code_tag = "1234567890" diff --git a/configs/prod-configs.tfvars b/configs/prod-configs.tfvars new file mode 100644 index 0000000..6842075 --- /dev/null +++ b/configs/prod-configs.tfvars @@ -0,0 +1,9 @@ +custom_timestamp = "Production - Today Is A Good Day To ..." +custom_engineer = "Production - DevOps Team" +custom_contact = "Production - emvaldes@yahoo.com" +custom_listset = "Production - Proving Nothing" +custom_mapset = "Production - Testing Something" + +filebased_parameters = "Production - Development Parameters loaded from a custom parameter file" + +billing_code_tag = "5432109876" diff --git a/configs/uat-configs.tfvars b/configs/uat-configs.tfvars new file mode 100644 index 0000000..2782e34 --- /dev/null +++ b/configs/uat-configs.tfvars @@ -0,0 +1,9 @@ +custom_timestamp = "Completed - Today Is A Good Day To ..." +custom_engineer = "Completed - DevOps Team" +custom_contact = "Completed - emvaldes@yahoo.com" +custom_listset = "Completed - Proving Nothing" +custom_mapset = "Completed - Testing Something" + +filebased_parameters = "Completed - Development Parameters loaded from a custom parameter file" + +billing_code_tag = "0987654321" diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..0ac3178 --- /dev/null +++ b/main.tf @@ -0,0 +1,291 @@ +################################################################################## +# PROVIDERS +################################################################################## + +provider "aws" { + region = var.region +} + +################################################################################## +# DATA +################################################################################## + +data "aws_availability_zones" "available" {} + +data "aws_ami" "aws-linux" { + most_recent = true + owners = ["amazon"] + + filter { + name = "name" + values = ["amzn-ami-hvm*"] + } + + filter { + name = "root-device-type" + values = ["ebs"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } +} + +data "template_file" "public_cidrsubnet" { + count = var.subnet_count[terraform.workspace] + + template = "$${cidrsubnet(vpc_cidr,8,current_count)}" + + vars = { + vpc_cidr = var.network_address_space[terraform.workspace] + current_count = count.index + } +} + +################################################################################## +# RESOURCES +################################################################################## + +data "aws_elb_hosted_zone_id" "main" {} + +# resource "aws_route53_record" "testing" { +# zone_id = var.zone_id +# name = local.route53_record +# type = "A" +# alias { +# name = aws_elb.web.dns_name +# zone_id = data.aws_elb_hosted_zone_id.main.id +# evaluate_target_health = true +# } +# } + +#Random ID +resource "random_integer" "rand" { + min = 10000 + max = 99999 +} + +## Standardize a fixed-sufix for all entities +## Usage: terraform output resources_index +locals { + env_index = random_integer.rand.result +} + +# NETWORKING # +module "vpc" { + source = "terraform-aws-modules/vpc/aws" + name = "${local.env_name}-vpc-${local.env_index}" + version = "2.15.0" + + cidr = var.network_address_space[terraform.workspace] + azs = slice(data.aws_availability_zones.available.names, 0, var.subnet_count[terraform.workspace]) + public_subnets = data.template_file.public_cidrsubnet[*].rendered + private_subnets = [] + + tags = local.common_tags + +} + +# SECURITY GROUPS # +resource "aws_security_group" "elb-sg" { + name = "nginx_elb_sg-${local.env_index}" + vpc_id = module.vpc.vpc_id + + #Allow HTTP from anywhere + ingress { + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + #allow all outbound + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = merge(local.common_tags, { Name = "${local.env_name}-elb-sg-${local.env_index}" }) + +} + +# Nginx security group +resource "aws_security_group" "nginx-sg" { + name = "nginx_sg-${local.env_index}" + vpc_id = module.vpc.vpc_id + + # SSH access from anywhere + ingress { + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + # HTTP access from the VPC + ingress { + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = [var.network_address_space[terraform.workspace]] + } + + # outbound internet access + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = merge(local.common_tags, { Name = "${local.env_name}-nginx-sg-${local.env_index}" }) + +} + +# LOAD BALANCER # +resource "aws_elb" "web" { + name = "${local.env_name}-nginx-elb-${local.env_index}" + + subnets = module.vpc.public_subnets + security_groups = [aws_security_group.elb-sg.id] + instances = aws_instance.nginx[*].id + + listener { + instance_port = 80 + instance_protocol = "http" + lb_port = 80 + lb_protocol = "http" + } + + health_check { + healthy_threshold = 2 + unhealthy_threshold = 2 + timeout = 5 + target = "TCP:80" + interval = 30 + } + + tags = merge(local.common_tags, { Name = "${local.env_name}-elb-${local.env_index}" }) + +} + +# INSTANCES # +resource "aws_instance" "nginx" { + count = var.instance_count[terraform.workspace] + ami = data.aws_ami.aws-linux.id + instance_type = var.instance_size[terraform.workspace] + subnet_id = module.vpc.public_subnets[count.index % var.subnet_count[terraform.workspace]] + vpc_security_group_ids = [aws_security_group.nginx-sg.id] + key_name = var.private_keypair_name + iam_instance_profile = module.bucket.instance_profile.name + depends_on = [module.bucket] + + connection { + type = "ssh" + host = self.public_ip + user = "ec2-user" + private_key = file(var.private_keypair_file) + + } + + provisioner "file" { + content = < + + ${local.corporate_title} + + +

+ ${local.corporate_title} +

+ + + +EOF + + destination = "/home/ec2-user/index.html" + } + + provisioner "remote-exec" { + inline = [ + "sudo yum install nginx -y", + "sudo service nginx start", + "sudo cp /home/ec2-user/.s3cfg /root/.s3cfg", + "sudo cp /home/ec2-user/nginx /etc/logrotate.d/nginx", + "sudo pip install s3cmd", + "## s3cmd get s3://${module.bucket.bucket.id}/website/index.html . --force", + "s3cmd get s3://${module.bucket.bucket.id}/website/${local.corporate_image} . --force", + "sudo cp /home/ec2-user/index.html /usr/share/nginx/html/index.html", + "sudo cp /home/ec2-user/${local.corporate_image} /usr/share/nginx/html/${local.corporate_image}", + "sudo logrotate -f /etc/logrotate.conf" + + ] + } + + tags = merge(local.common_tags, { Name = "${local.env_name}-nginx-${local.env_index}-${count.index + 1}" }) +} + +# S3 Bucket config# +module "bucket" { + name = local.s3_bucket_name + source = "./modules/s3" + # source = "terraform-aws-modules/s3-bucket/aws" + common_tags = local.common_tags +} +# Error: Unsupported argument +# on resources.tf line 245, in module "bucket": +# 245: name = local.s3_bucket_name +# An argument named "name" is not expected here. +# Error: Unsupported argument +# on resources.tf line 248, in module "bucket": +# 248: common_tags = local.common_tags +# An argument named "common_tags" is not expected here. + + +# resource "aws_s3_bucket_object" "website" { +# bucket = module.bucket.bucket.id +# key = "/website/index.html" +# source = "./website/index.html" +# } + +resource "aws_s3_bucket_object" "graphic" { + bucket = module.bucket.bucket.id + key = "/website/${local.corporate_image}" + source = "./website/${local.corporate_image}" +} diff --git a/modules/s3/main.tf b/modules/s3/main.tf new file mode 100644 index 0000000..1a911a4 --- /dev/null +++ b/modules/s3/main.tf @@ -0,0 +1,55 @@ +# S3 Bucket config# +resource "aws_iam_role" "allow_instance_s3" { + name = "${var.name}_allow_instance_s3" + + assume_role_policy = <YWM$1YwKRZAs`3ChH~;|tjf=IbH#`;q z;Oge%rKKQ4WngGTg}efQ0}ulc0L%bkOKWcrX-!R4!2gwQ=3up9G|m2^>;GEq|6G7( zW9w}VEBXuD&8$7Vd;kDA6PWz^t&hix>;RMTtepRm-@nLSFau%vi7&GKzjDbxb^euG z|EZ&+B@3%F0+Xfe|2Ns{f0O^$bzxNi2-Z9=?EhQxvGs@94*&pV0DzZg`mgr?KCA!c zArQb!;Dx=5lKz)NHiIU1bSny9X672Nys{MoA43@t>;@lfl~q@H~B7E$t1Q zd_Cm+gw<67jI6W_^Z?Jx09gPkjL=Zg-n>D>#KOSB#K*(I!NaE_B!+EN+>~^bu*A*A z&&tHg&!#ReC@8LOq^hWD#+9@@QQHd#OqgUS6G18^4DK#-v zvd~kds?jy%lULJGVo^i{z*(RovdRt9Ai?9aL~!6+bAY}upt=$tA-jKrt76C z8=G>l`z9|P((Gxc&ub6$!k4V$QsIJ7u~F&J<;u`PH35~14TxRX&AM`<)WjM5DLB~K z6>&N`X-tDAbGs)spOuFVm7H1FXxfz0(vjR`N~y6X7_{I?rFM{UJUwY>wW;U^5M!oH zv~ezZra@^WBe63Vyw*b(GZ({xs*OW#n;C@oVl}L<@sLu&)R3@pK|o*$Eq|%gtY)gs zN4W-1M@GUNMnbk?8X8qr1}p|FM!HG^UHLecwB(q)l3AM-X#@NeTbBJp{ny2|p3TmD z3Nq8efmOO2(gC^zOyG$De4fH~9i9Vw&j9Vw&o#z)1}joHbjf9Mm90E7o-}kyP(2z3 zpsXi-P8bdiu7@*M0NADw4V@=cr^Jw$$}>?aOD82Bs--8FD1}bL7fPoLs;Cra)lLFA z=yd$5)r*_pa%ow;b8YkZZFwNo*|QJ!qr9^0U@lWFQs z8t2YTlCRHE%)>8ccq@{2KDKyV$7^b$u}hp0gVXRp-X;@+^53t*VHtWJt46 zj~)t7ga%KHl^7NVz)OsEH$KR9cO)7OOFHySI-;eWcBxaBm)1d>XC=_@Rc$Y(>!)ZE@pe@_Q57TPX$8w-X6nk_l>(ia2Y!D0d4-->iflE-vp9o zCeKxfOWVq8fA6*RpA@0qTN8wN6v=fifh=v1lDYs$DYy*nlTX>0I_c@6ns>&t+9{v1@zDx--5R;FP`QQm~s(Sdqd+>p|TG6=`{p!b^W7e$TU zmA$2|Y^*p_UuU($#SMc*?dOLe=fbOv40ykltKWI=>CW`#$S)p@-H@$5__m6#v5&>& zK7O6;BLupI`y6^V`0y+@aDT~prCvIg?4QH1Ii*Vc}`LU(KoS23}xFm{pkef;CDu*Vp;6p z5go&bYnB`3<{{PUmk|CI`{1{e0prpg9Z6HY7xRYtCWlA&V_(<_pQ0DWH+hxmVe=J7SS{B zJIE7ina=b4)2Y8Osz0(iI9zXYxw3nFakFTeHf42i{tbSorB-w_d~L_D`Mbo|7D
D?W0YrO zs`Xetr1~OGpp*cp4H!sEe&sYIG0BA{(L>1ehmS9VOzy^mLS)AyNAIrlI*W$_HTXJ0 zM2iYUV&_VcCI}fzGeqJGXn%dvMnTEtu8F4-Z?Hby0 zYJhr9H8>66Dt#;8J>4NjFXNKy%3(9~=Psb%ZlpdAdr-7M^1y$! zvdw16&W2Bhmeu!aOuw4#D@wai5G$X42s@CM+gYE!P5_!bqgAJ?+#;Y)YF#r+s+-zI z(q2mJxsvTQSQcq2)V_OIIlRA@j{2_EWw^z^m7n0y`QSePH}gdIcSebTclqueWo^6P zd&eZZe|VQ?PQAN#SZ{r*=OA00dN4o6^Hzu= zrg3+>27{^e=>6A$TdqKpe4m{Nei4J6P_&14~pGr zE#kMGg6vhcsxz$5xpDNS$cb7PnCAWtLFX+lGt6ftAU)GvE~xmF>(yt(w)3|0>t!B> z;jxcU(vCQ4*GP7&xQpqnOY5VkP~I=bBZ0%|f|S0%jF_i@)$%AiPV2!jk#D1k^SRz0 z!xGKq@?|#s>j7@NyL(4pBr3a?y^k1qzo_}$iHo=t-HR+$Yb{mB8CA~ zKYq1PqHReN9iMf&PfjYrxQ&Bf;JwB0YVWTM{6&c;SKgtjc?knkGyM}y__3s zGhW4I^KD;#3lWchD+@`{-j~of94^m3Iomt9xMpm5|HtOQYvc&I=2LZFul=XA=$uc@ zlTb7DbeWNQ3UWh%nbOByGeQU>BHH^J8e_1=o^b7zxpnli)#70 z4Yl4J->-FcYX7)81D#&m(=?*KiThp2W}#>WSVD6n62xbHPLWV0VZ=tb{*rlch>U3LU%yZ>zFkzJS zbH^C#bU6}pD_fa9s2S;KAYOw)0t`*aM6gr6ON0)E%04JM7{$|+$?KGWtx(6(2qvnI zaw1&Kw9Um;qQ6i!j+7JMY!@CY-)#QMTg{q}&pOFl?&V5vOw)Id2w484iyLG;nmmd1 zd-vcSCBpp0L+Qweqw!Db%IW#3<)oI8r$PC-7p8b!@2{aTZgF@6qxCzDB41U~>IPJ_ zJdPlQ(PcI*1Pnq`XgMRrbEl9e*ONHFo3h zI?jmztGHFY7h#s)ZAu(KO-nMDUMSjKDH=nY@uT{a*Ai_L_dO7HYpVu zlrFn$4MexuYIgN8IXqJ;yO9!M=Dwh4Ga~Kiq`TST{jX9UMsdYxHZTIso9GJpbX~3) z2Oew^D<%L|Ww@IW4G%q+&J<#n&494YCQd3!7Q8XJtDSsLsY#>F=B7HL+B|(ujWJqJ z{hW$*1w{En*B6_w{Uw*dzv%?5XZJ5}b2YNAN?^7|CIV`#0K6_a?5EJ;YV5l%Il zN>(*fHrPB(26}a_y3nrXV#mVG9XD!0csAD8;24YpP74VGE~AhT{`zb|Pt0wnVaG z)k{;;q1 zBeLZ7awhvSZ-laY%c-BpHDLZwxL?xzQw4N=fAl<}JCB8iL^)&hZ8jSlJahf@$>O`W zkZrb*?ZiTl$vloFxJ@&9X`5O{-?7G%e~c%O7>HgpNlikS-230~Dolj`&nbK;00jVG z2fQqoFaZFe|H=RV`TyYwAi(fH2NnPx9v%(>{uOME0JZ}50{;Qn2sqS;ShzIYRJ2?? zQqp*uyfPMaNPLB`MFAApQUM$i{NIN(7M)71PL^jtVx?B;d*5e(XbAdn28YS__???J zubtrx&Ts#m{&q8I3#v2uCyT>X09NBgJP8RiH!4+zDXF_&>r7Mr%bKwSYx8em28X$G z#+~m{r^ty`sUJ+ea@ta7nqS@QGl0?SiS-%q@zeeC|J40os$uL&6xY$){m^~$d*l=8 z$%_q3JELXO0XzAI?+s%n3kujwTKxWP_Y5#KSIZoqjG;;!m0dnT^KbL|W8MpEa_46L z4pyae{>6r0hBb4}{Mn;+jV}UUwwhgvVa=4|i_mD8Jm)oSOqM8}g;PjY<@tchAKSQ~ z979c}v8rbKa%2CtY=9YmUETcOjDJ+U==>J5V#NKkW1+A?k7_ncZa&ws<~xWiEC89# z8gC`Mr~cba-%X$JV{dkJ$v>B5NKyJ%_{WIO2;b?>yBeJOzbaA(}9c&8{=q-|?ojJ+*kbOf~6bG-T@Qp~~-) zA72!)p1--t5(=Fm7MHgxEtAx00c@P>JOeb@Bx`jH5O!^^T7cBXJ3rp68LlFjm4c13 z{D~$F9FMjv-rQzyWBV?uJp**Cy~)cu-Z=ko-1tcTsfy2ogpro!@Q~kK(^a2kVV=4r z#DatB{$qWFUxCA>ZU50V&gVZd+#%JRXeKCkzc4^}(w-p$q7tGxfkq9R`l|?mDG`J_ zzB6`tS=Z4seTy)~R$`GvF4qdQh4(~pF^XRSj`eElsi&wsMNAhau_^9G>~dx8eN=x_nCSC zkwQyu*VdCb4khZJkqjHQuz~#yK+NY%7ex5m5)@U&y+_$1!T8%LUy8bzrYi)zP~uiUEAPEO7Lq&2dnd%BDTcUYPKAQVCW`Nv6)@AQ z{hx9F?^xzM`ueyXnhEhL9Wnc>HM2?nrt+X5b-uGI$OV$*LyfItlEQ2Mho$r5e z%PF!vOxPHvPHomt1pofcunl*!YBc=iV&hV1%FvnK6WjDn?n!3tNURdGjKpS(%HB8l zjf;bIXVveTt_pR!nz5gRe*y-0u2w6Q8}(5MzMp z&Fl9j>*G4N5?lh7Di?1r<}AK?*8esQHodUjJnjTUA8*I6eR8NNt+$!kvQ&!)WF>hu2&NM684;iNo$;~M zinXhzb5jNVb>_CWwRL~xR_{yHAVkC!-Uk_q7b$ix5ypnU&$dxkk2O(enCI9#=1=4D z3$YH$xL6E>YqdipXf5y@9`?pIs!(-5M94!WaV`+2-O{9PEzcvc(V~_gVMFXY`BF45 zs8MpY_hVT-T40d8`H|Pejjia-Y;h*vd|eFQqAaO^v~J6;AKgj5=0V|?Ci3GvSF5r= z`rqs~xfnzGe)W&sOg{q>$Qqf5KdS2I4bE!X3|t4b7heQ&wg|S!ZijChCQwYp{6wf3 zylIr}S#n_si=>iF`s01%6pAKM0lvIDl=V4m+|A6Adj^>7mr~-66>uvmBavG~n6u%( z`aqDU7atb;GwXex!{Td9LYasB;^n$p5#M`-Jz*!va>ZbM?^aKzVN#fE?xMqvIgZz|ufx`HcQoK3b|}zApIhH$ zvFrqwLwK*>^(Z4rdXT4c+rfDCul7Qf9R;?Hnqz@+?J>>wjX9WB%Hwn_Gds==yd1brSOG*6K$OQFNt3 z$yZMdw?2^VvX=ZzJqP8>5DyGF4rZe*ni2|~e)k*-dQd`F-MwS?mQNms2K6BvQyy^P zs}o^G?H%4;$g(p8=!{E+$!?NS(|6(hv3tX_l|bQ68D_tJNEdh0 zyFrl;!wGQ0=o$g-A4av=odz{KzMhx9VVa<;fFG56Oz2-C6ws6gE5K7w5lZ$capIz2 z6-HzSSZ!A-B@x0x*=?DHWzM04qn_e`8K2xW)u490SH2O7CzG}1z#w6b3`yN?AA`OG zXeB>nbi+JnhTz`GIHJ8R>6}MMb35AzyyM7Fy{tkwS!Sx%QMJ+sBN`3#&Zc01;ab)HB1w zK671kL)^*mH(8{N7`;WdpE&3#2YY0q!@UpZ$x_xS6NFLz&7qKnm7 zyy&^Z01;)1D{5tZrYZd5E$f1%6|$*KHA(z+dcFpJ;VCQJ^QD@Q*?QJ#hI_st%Hd?& z;TF0fd<-vb+8wL_9t{F1^)s(mU0(Jd{KN7KWj^A8^l-u_h%5gZo&4s@9kokYa!1Vt zR=79(VUxH9w$=TZxjgptMd`OK6ZxY1u@i6gn#wnJjU(Z2_3<9r_*N62x@s(+P0YRG zxhT5Kk2wQ_>g@JjE3``tE?d5tUI)xcSnqx$%|JaR*ds4kauwor&cP)F5w_F@5<$cM z@M~i*!0B5b8sy2=sAi6gjT|hOyL$Z=`O=s`OUwMeoQ4UPvZM(6F_lL;+3Yf4`y(~( z8p*dpw_DEU;fy=G9p&Fm1JhdX5RiF|@HHvKH3Zt;Nlhib*k`eVzUDUhvG^`P4XJWwR~%2dGzvUs5Y}SGP~v zZ@{xvynyn@sMQ-2|IqwI0ql>`@y=RhpT|8=%9+*Ynp8;^+ zVIY_R8-{#gF!<&00)bypFg83j7B>zq0+lqEB@c+kGxQ(u3;T{P3Af{5@(C&}U zDZi|z0pqlRo!NYjiQ&}PD0E=DBxbBhlval%ZzEKd+C*k8T&-#0F9QS8FIt3#{3=@Zk zw>K$4)-}0^fex6NBIBZamSXclcB450T4`yiU?ps9Y!d!nX5dH!I-O4V#8Rp1M5>I; zG#;M=7z{pFJr~17_;DtN2~V<_TFUGvl;p$_IVB&JW~+LDK26b^YIV;_AU~10sLY_J zl@Si+)7bQ>7#9d=4agD5%vmV5_IMOwS2S$0nwE2kPh4!Yg38EjG@Ps7O=6=sxD4~@ zHZVkKR-{-@*@+aVPWmunKd>!1wBSqVzdpSCg?bi~ctN3tFS|3yIaTDc^hlIz1Yk1+{e)B~lQhwi4%WNfi6M041RaQ~3&KsF;PoDw1;CdKP%2b)3cFG0D_c_>!W`nG*QGfG0UT=#tT%=XO}+6 zb~v|>wnv@miA*2~FTCl=mUq2k&t&>Tx{#D0bChl7%xtNUEISI?Q6Xwb)d(U{^uj?c z1M^jq%6J?{Mn=~|`RJ93eR@OeLBqSf4A6*PGzv^!!s}YqX zmGwssO_R1}G~z*VQx%;WuuEjw3hP~O{Cm{075iDccT>L}4>k^AQg1G9PAroH+0FW< zxCeO>&?1*yKxG_>R}&>X&7`D`m?ClYd^%dhTY7S2?miu?Pi`iDro)i~77m#=vqJfja$M7Lo?;MF*pTtda?6HFbcU>5nYTbt1bcj`d8+3UZYg;Ds}u zJ19vk5Fa%}rVp*#VOb!n#kcS7*Hn-I-C!pU4Go_hjObrQYZMS!wZ!VBgEMAJ=p@%` z-p1kMqv4}QUu4y>;L5A+-`~EG97Mfxj=C+C2gbx`m6!>zxZ>5osMMB~BYfd9YB&uy zj>z63n}t-yp)8Js9|bmS!QeP-teeIa*u-GpoIm$2>%_hd#tcseZdzbR>%s5S?-?nr z?u`gXCZ7Dpy~Rgvc~@ZU__lFv{2h$tU#`3xfBS?8bB!$jxJEccn0Evq|JOC*AYkEg z!(1bX#uAZ-SIWaP>>u|C|Kc9`??_LSwNkgenAPQ(m1AN$oOc>SPzFYma;Q-TU<8!m z!A?p_X|f4H1M#G4Y$?Osrj8c0Saq@(7zgu5WT0L$>>ul%LS%-$NiP(WgROSE*wE0B z3cK;`Aw4~T(D>Hg5CD_c&Y@rVjUJX5|jE`375|;^>~u zcw_|F(USrhA@sA=e!UMSeAI_5p9a-9Y?xvJmtU?aO^p3nBfH+hxnuUc*}}!y!bHd2 zcoDcP5sQPe99;f-d?^%Mp7{9qgt)*s5*iwzTu{%ZR%4RkKV*<*PY|D+7(8a|1`&Bo zU}D=OFn&j%{D;SJJ1V7E7n7iKI9ZrCU4g-SkZhsRJ`GYe&Pu3rL7~xAY)8Z}PIhH6 zT_$`SP%fZgklvRoJB6d^(65GGw(u>^yphjErNNvOjlsMteJnlpTZoJ<1aI+Y@e&O- zReH}ep!hhd7;o9madPZ-4@J&-Hx1`X9}$WVpiD+Dj$tGe4Z89{5~(0ptpQ+Oj3mnyIp)=waCpQ+!eNd|nEA(mbiErMR5EF3R;j z<|IC!LmFe{2j5-E^+RMljP3CJOTvp8&q-DFP$4dEB&D(4c8;`5H#W-*5%x}6(b1{) z4dfCGbE`fGNKy;|UVlT0!M9&hbE4f4- z&w+W-dQ|y^MkAvuEiy*VcRP2wZ$w`wPXdLS9w8(x{>AphayeYl@5(W^d}4lL z!nKBM` z6g3BrctB|%+a?5khtvW7b6G9C;JvW@MZHT z^NQgF4}imyP{7g+Y0(1KC9xvlcE%g8G8Kl$%o+O>HT-d)xM`MEBarB9xJB)F%W`&n z-{4nK%V$L=ZG6kA1L>y3hk`|~{1K0Liv{XfayU3RjNQfYQ4v5PCY^YtXliMwc~M#7 z`Fw5WUb=9Ij2=b8BBbfi_DgZ`SHs@mF`}I-%gJuSeJ^5$@z)A*Ph;G~HieIgc^Uo= zlOI2fCRD#pwH$)t*LdGy%mv-~c)6G}(lG1J>gQkyQ>gqAhL( z@&p#XX)+t7gqSj6piTM>gFrS7pTlIaOkNR6hnyu=gIYpGim-sq5|Eg?k?qrBq{lOW z2ge^WSuBXQF%*PZZC<}S!lOD!{YOB3GNtJRW$p}YCfELGWJXrUpiwj1-ovd5Bb1Aj8wHHWol)LdE?{QyDcWu#gO=-Z5b$deTpkEGZ zk-`2;jUs~;br-~7dyw#ZLxL%smiEPc!VG*FXbO0q3OLwg>P+3G>iJ?I@vQafK%rbk z*sa4NJC~OUqGo$cfwsp#0ee$S@tn3N0p~x=AYV*_f~N0nba5|4O!2JvrAQ%~g3kYi zgU?|CXajc6c{Bgy2AcqcVIPR#;1LlKkdP5yJ`w#VHpbzWrr`luB6x;TX(kob<4VbB zS^YfY;569gn;c++`cW4S(H2Wa}l-?ZViV3#wHWK+K^sYrI zH9$j8+-0YA{MgR`g-a{5D2zSt!Jj&d*h)kyl;1#ls6AO<@=))u2@S+V9qFz=no4@} z>idyY4zKtEBg9=~Cd@xqs@z3B7D}nm0`8yi6l?vebEH2J#`b)_qv}30yd(QLhWunu*X8`j%8T^hs>qFH) zUu4-;Kz`;je)!zval=mWS{L534M78eF`v3wfSrhvy?-!^Hndh}^{>R}TP(S?p8+)G z4Q1kxgNX#$k*w2xhH%NZj$KS`B-BPKy8HT9H@u92!5`zsF?_9cy){0Eua6r7T9 zT@DE5f=eSJz+yr($FZ}Hya~s0-0$l4P8EJ$k%nhYCb!_L=$6<2op{9D4$kgW)!WR$ z=h%bn@`?S5x@7{c^SP2VM1+=wQibEnj2D0f!ukJ+A4Pg-j09xP@jfx31nVn3&Fk$xI^|Q0F z@26RG{-d^DI$(s(bvvJYI*FYKnSecxayp~n;2zgnW%Cpr%|F} z?Al*np6}G|Z8eG00f?hHYh|M}!|v0jH?Ow-2Z765jM9UYq!O{jPd0pcp%wm^lRFE~W-mEx} z%ridY;CM>^Dub_$mG7-^FaqpHP$ka9`^rC?t5#fv%W4Nw=#8=|_<=sR47HJIv-GS1 zRACl{8GbrVa$4mB+xNMtKOalf?e|r*_m1C+{`mk&cD9sD;N>g_5mz*ugKD2*w@!k~ z>D46qjz&rTMyH*V>1&(jf6x_m8EKxb&Q{o9iESR`e{#)A>AAAbH;OJjn-S65_MNGU zPaC5ke^nkF-Un*)(r0uDF3a!k*1a8eQldTdMw9ovVczht9krY$oKxAwU(x;L-CY+# zK+)<|%#XG|qQ9ZnBIjV^2}M+4nibRXZiFjp9bZWCnlFhgH_Lg&upgt-U;z zkj~{&i-Hcs!+AF6H_h51{&Cnh`KrO>Iy+xRbbX^GIX#>8?DZ<{(a1{`?^rxMJ?cIT z7qwtk@lFDB2V7NczgxwW>1(CtWVMkiGyk1Ut=kdhtjsu@u9y?n3s)NkX_NlpKNoUt zQu%zGBUI^=Ty1;)xwd-7o_QEKZ!N5&<|Ws7Q;`+W?@-#vPYc!ERoWW5^?9^Cpda@h8ooNIeD{t?((=4P zVv&uC*=IBNNjH+*(@f)LA7flU>IVaj{tA4Jx6aIF)}EQfYjyf_D_lGCZ8AN1P{V}t z)33~iK8#xV-4F=|quX}2TY1i`>Ibx{fUNX(hFjK7Q)M1G>@#4h4 z=aqUZI~NXBk+qRd#dHMfC}kDp)#ScrJ}EZ3SeXmUjFv270;9SNMini#g_mDc_Kmw! zb*eUawaq{^!<=TNVrrkjcNPEKd$UmFpOhg{s-_C; z%DyN+XQ?)0MV&r$wA@h=?4UKrKGm!`)Yc{AZOiC0tT>EPAD5L>f}?VIWOel2ze=BusHj zhM_Bvfc)vwO60YYXo}^hq#$tpo9NCSU^`hb!#AC*;z*air^%2?1#%}l84=4_Z67R7{XuV0joFswSIwi+hM@`@Y2bsb6NK$Iu_ z6s=rbB$*9;DsxBYq-NPq`$|y2B-u1rrJX~9*T@cLuOv+@6L~yHc0|sqX0TK0+e7=V z<#e8rlw32R46+pLRmR!T&=&9|A zS&O^`Ip2LQ!rUY9X5lPP?+e;ZPQjM2i!iS=w_7=z_0ZETo%Kw+kYS*)yDVg#;edv{ z&NQMsRWH1m(b>dTTc=1Il>Qy{J!&{7PQ3mFO-J$-uXZQ8Pld03%PMelip5(Xa+6U8 z$?=e5%b5L7T2)rbTj@D;w<=YwH$94b%}P|E*$jikUx-%u zjG5JKo8wdQt|+z5?PB!#l9U_lN~``Hb_obaM!_3jG5UhM)7Ec=)y7ZzC;o^$Vm@Yw zDLQ9rYhXMBZgf-jDt#KVDkqY&oo3y`YbR!UMbm2QqIB?iB9#3|tF=UK>G#`n;IgRY6GX=4jb4B(%$K=v|Dhz4FVekcSWSX{zYo(DCp24;NFG_i@-@S`9xXc zI?zYki7UNlI{KLoohpMMb{`mDB`oZ2<{?(jzr#8b?(~=7%xraX38c+V$!r?*H2HQm zQPAmp;ux_S{n0q%#$I694Ce7Puh8EzI(*iRl`wyR;_{5n6er7ZFyN z2=ZAUdi8tF65hC<5>Ld^XWYF$OL3vs4~}@q4et}lKKEU5)GhmVUUbVwqgPG{R<{pt z4kyWbu&kY|L-~Db8V{-_h18Mc5LVJo~TzNg4cC+EmA7=3>t3wc=)dili|90CG7@_+s|28Rux=9UIoV!^OL5?4|E&&iFm zbE*Fz0658)#kmJKJdivr?NgYALStyL_d)WoNIixLx(|hvX9YLofjn%lOnl7JV0G4* zps)gL6xgvnFAt>`mNk*UbASEorzsxD7q-hERy+eX>%bvDpa<)Lc(PS795F&$2}S*6 z!HI5Bl-&JGLJtYGi<7_RJjV`JMBYUycqBBv2lu@1AF&eJiWhq1pM0?emX5xwolHH6 za`#h~P-=Hd{=ENv{r7<^1PNB=^N$rULt;3l8|SP}@Q!0RBJhCBzhTX0fJ{)H2XmFf zk@x*%cD!F*_k6tD1)c%;i)3f}o_(}?wSlZJ_W&vKVQP(GYK44}Aq|AzWSDNO+?i_K z2YKSXB>A4>7(SrDB&BTpi&e1nAZeU18N^u}&rMnMo|6)Jc@7F`0tM<9^;3fS$vm%w zdj7G0>snGJ9)Zjg{D=?JLfCcw41nL41Nm_3UYqJeAGnV$&w?KA#odeJB|ktR&=}5` ze~f`%S3qMVL3(UzPgo$Do@m0NK3Z}X9nowl)KqqPs?1pACJURPl|Y!zf0CS4jQ=wr ztvI&Nn!9g}mpEP`ip(=V@@>9Z*qU|T<@ZvVa6lBt)Dn|mbLMYJ2N|`33rZaZlyF2X zP2TU7EA}DEr`Fgovxn05Z(@P8X+(!TL1IH&6ItKU>toA`fZ7{~O zb25>N4nB)-7eX?O5w@gEb!g*4{&h)lfE!epEO5a2FlDK_H6oNqWjc08!8wCYvwwsTI(uFh@6`EWft=9flB#RQFr5lqds5U4M(tX#lbYfL4b2KcHuU=_1 zy_$S_23UHt+bbF{S3F3mo2YUHQkIYTWi18KO&0gZk05_v$v?$HVY;d(g-Z#Jf;EXSS zZMCYfuo(n?h8cmlej4TM^?b8*^+tVnhCur_q9R^_0@)CVV2um{Bx3<3L%4|RRfqGa zsiXiu(k@Z05ws6pEmA#6P;;na=6zfD|7gX6m6F0pG^HJ%V39CiIY(|3l^lYzpS%)8 zF`@Wl;xU9vHtfl>M+>n1#zWt-P>$on=~Mly3qk)#Rvaog`3Wcl3|*ID3|E7m%pb%4 zJ+-(fX72!*>hOW}SlBRv9zr3NJq80l8#u^25MYN49+t&9#<}Fyx2-UAHKwNx znftI!)D~$o5C5==2uA5u%$>rZet!i;&WHm4AFNh$Qh&gnr;~HyB`FWecCo>q?V+`| zs-A2D0>n`=OkkTyrLr(_- z3p5$Ut$$O7VepaY#XB%T58G)8vC+d#$y2%~@XWD&O+CYDam{7f5TyYR`1g?jBy1J` z^`I5Ui!i!4sfBQLnOy?zA5WKnn#XmiK0h>3r3c)fn{SK}>K`()bK{!QrO0Uen9SsY zsY20l+86%FU8G2ft0Hq<#hdSRXUep zy1Ud(a9J)XQ0gX&R~JvdU?o`~)j%fIYi@&>;}$YEpN~49Ybd?HVokiiuyT8#j1S_U z?W4ULBmQAJ1typA4v0Of(o8M`6m7FQ&?a7B|HMK!25b0-MVe5#HBhrHvO`40hBt~d z-(WGtp^Px3Vmg{W%F{tiCSy|;~3VJLM8I-KSkaQ}3N_{dOlJhVmnNQ(2^{(JL-x>r*C??<6frrG zC>GJw6A?|VGv5;5(i77L*~}ITQ5pTq+b{!GB~KLCk{UmW5stN<5nvZ&n){=WomO&5 zUAmwoFx>O6^Iq*~5E@6uEX<8(!X}ySghE{+4}c^X5un!d{%gjYIAOF;6^UOEducasSgt!dcgemK@jO1C#JAlJ^>a;P59lLa!YM3V$5cHIw za7I1H9H}`g8H7b{Dm(_v>tWesZDG0}$oJ)&NL+C2g4~7j;QYiNUs#VNxeUrFx6zHt zGm#{^C=mIAHFGYbEWSIvCyfd#Rzxd1D5}cFfTs? zsH6vpUqN*!IVkB02__mXcwYl&zaDW@mwjh~ck0{x`4Plw)0}Z;J{V5iYkPuX6A<=C zff;@?w_Z_toy&{r(7gXERy5+S6x-sxjGNr6*Z!}vC%E9O?ck`&DeT;6AM^bH{Zk+V3fsL3k$jMi3Jo_nSV|@Ux*gixQM2D(uh5{)Q9 zmy$P1Ws{D4n!<&CWV4w!){&LaUnKVH4w54XAHI!)-hEJ8xI{LK`g+Bc3E~qH#dO`J z8^9viMN!2P{$Z)VL`0YPv7pUDM$>}xlnUcf5tG&)tjH5PHYAzb9h}a3*Hamasw2y4 z@R=)-Rr1)!M@m2rD`2_bALCdvT#Bt*5y9FwYA};3STlaaN_bWD3AulH0v2Xm!onky zUTpC2fya96G@XIj8MPJnZw|j;ss-pUPe!)X4`kw?qM3#WX@oT%DZ!l|dWPO6zTZn0 zh)|$iKj(0fmWAa;vBIV~#zd*87d~B+UL*bnVg#+z*OgFX{Gbm`ye2XZnT$DOp!1@! z@VUtxkH3PJrVfM}$%2*QR0SCAgtRdJQ^Tk*pKzmZb~%Ln{m|&@Ez!ckUv%ozSfwrLioMZ@@$i_dO^Ujg<^ENukhJ z;TQt8M9%A8nS`0O7W;;_GB=eu?FCQ|4T%>9vUbz3W2i~N{EmkxXMI`?r zAo523TVFnvn!F8B2-^_H|3lqd|3&qE(Zf@~&_g#2Db3IzH8cX!4bt5uDKInCP!f`Y zG)Sp*DCp3sG@_!Uh$u))`^@M4ed2F;=H9vYH9yY1XP>k8+H0+IaJT#^n#$!5yLv`; z|7HE0B3LM9F?t>cT_yk{{p$H6!|A3vCJw(2)rHnLu8JF8dA(^Btpp zA|<|EkT66#G!j^7lO66zt}GVrHDd5p_a4VUP1Jpoz%5-I@*4dztCuo;Nj>%Q+HbJ-{q>; zhF_&vaT2f}mWme#h8JhS)cpnsz|S=1EJmE-kDuEg>l!gip>ubyK9X^BD`r%rlo#VP z1^!cQR^rx!WP|8V;34MOkg9Nvp-5ZaS^W5c%VIrtXPoTGbA<{ek{a*0i$AU3xHn z9fo%RofwUpeV!j0my;)$K?blOuulXLdV?{$Ffn!^GCd;xxBVr~M_|RBJ%N3#94hkp z8u>6Obu~_smOr+C+xs8j?aYL*yEJ4$y+wCu7A{RG4+P29mk$)JMn*N^VMrKU8WD7Y zN{7>HE?O2IG>u<}erO)dz|p|PtkmGQI`!$mdfVIb%F!ke_(0=Y8GuEp_8lGT^2yl=K5^lmedL`-J12KlTdKr%UE3vW0l=yf4_ARGJ6MeB z@-rhSv%uVie(`(>epe`+r>K~X-T^7Gh@Y@hA0Wx*aHMY1x+dZDpX+rZTRXvq-7#7 zJEG%#I12V_ov3XLI!GAaE~w%dr-P_9nAj&5*W;Us6GlF9CB%z<+Fcsc8P3x)DA)Bd z&O7+PNY~=eR7<6r4HZ*Ax%jDGh}2^qVBRU8LQRL5v#WU^)#I@IIsO4dwQGd$fI>jP=zY6h0dOL%D{1Ak+(wYOTVc>Gb=1=PA8q z;fKiXNcnyY8XI&^A9j+Q9JU>k6q&^p#>!#ghn&=l2O0QQF}vh z$n%3rF17FT+W7V{D_~11b3cHTy8jY8_g)cNPA_mIHAjV|?HV3jafZ&{B#=qm7nzt$ z(u5z)h*ts2g&9{t+nyT$*Q*e=GT5Jg0H{IVEyU9neka0dOt2Y|&KNJ03s3C7#!W$%mNCF>yND@a{3SR4jsU8H!rYU~AzQ+l#?e?a zGDmnwJ(7Mpc<9!j!VPFQOU+`mfK#P%tbVUfR2IJ%LG zJd~5do zufDTCe+y++Pjd%(1WZv66Z!W%MFUM1q;**waYd>Y8HGwS@`FBkIZx58TFD_v7p}R| znd89ZM=|;mh4nw02SF@#Z-@=F!GzoZ>g(OR(7G!!j$C!3n3_5q9etscZ339a6Bz5r z3Yq7dJaB#p_EswxfD4fR0{9RjGn}Ns_xLE532Ez|>Fr71u4!mhhQhliGG1*RGq+NZ zC~woiY}v}ZO!yB{d-Ol&nle4zbXSV?6kPKRGH! z?pkW&u`muUDnSU2bjuN=E7SK8L9!|+$FHEy>*4Txn*z@AWs1wZ%BPNx2aU~qSBP~X6#AAN&zML-TVc&d>xsP!D9=im;f6KXh8zkDg%{L;bux*wFi=ik zF1uo}yo*dfVAdCH)__%B@CIbP1H>P>4MqW*Fk;<_EQyPkC^vRh0PoBe=PGsx^UQ0t zC;|Q;BExAm+*m$}yjLrkw`Iza@x>(iKO~9Pr1JDXAPw1)%iRz+HNWF!tS5nZC7>0gnP4DF3wtBA#{W2lxos4y#K`%vF%;rlrmd-1oAjyNMOY)d~bljO_4AeQ+CZxrRcS>KOTZ z?4@j-+yk9LHc8r>HH|>lb*^JEQq|M;dnyrvdsNa6IrP_H+LsVx8>g`Zfg=*s> zfFJ?T8U!#IVR2T0#+3MuQ{~{1c&mzW-YllWDT&}^aJzUQi(Hu7EiLg7fC|Dpr!qng zgSeJ0@Mmq^4WyxBTU%iKzsnDNQ5rNlGqNp?fAcKDs#L+z`geF+pThHCWKdNg5h5Mq zay3O>>_J{b;Pj*esux1Om9CT3wxF#}DWn|)%JXY+&?fCwFqkzm&Ee#TFcVxRS51bE zcRoaia7C5Nl~U8QJ)eRs=VhMx&_wp6K&YesM6kq^zKCxzy!nHwXT5R%^Sg8N$U`AzZlEh%U1kQ0eY zp~sWGNZHB>e#-Ar`%Aq}# zD&k;63~h;%xnY4ukW`|qT+0Z{FnxN}1iQu92^zU7%3c7tCmd47p~oLag7$~+2W27PREGT*(s z7|wyMiw$Zpn>Vow8$osn) zgahLX-6qnnkoOzm#MgeJ*(7BCgkRVDX2lPl=%Mmedl7FY>g?~Unp=VUzJYD%5HGE= z)`@7B3Dis6jlc9#hICLEKT1I0@G(oJ0C!$>0e1fuHyPQ#b)p3%yzm@ws`u zF5KU<84m35y*^7PVENL^gX~puB6z7BdhRZ_`78KR&a%7U#t86a@IsT1K?*=VM&}S| zg*YZ1v%mD#eo?uys<>cMs4>97odmrla$GoijfGoHw%_5ii?HHnM|s`t8)p zGAt?3qGdJcu4|-9APE*RZhjkd(DFz9kjiAncASe2215b8z!*SF&p`eHD9hEoLXEIi z6M!dPv&$R`1Hi3Gz(>h1a2J>%Ge;s(K{nNgY-<4cGe=PJVVc+9bTXPW?6TGBb;1v8 z*z<+1jGJg;4DYQr&x_k9REwjmZ!_0^E;#aL$z#|p%ftt8{{H~&d1Ip&EKl_UhUN%w zM(84^& zGJ@;dbENSsVM%gw;a@A~?DEH|-Dkv&c(#$n<(7ruoH6 zkCutPF<*akQQ-BV(Tpcpi>>&ec6O;hX`BYgCU+c&Dk7O7*wlioVRvQXA<*l-nq+M( zV6+#+s`vIEpvW|g?D*Q5Bp_qs3IYH5(^0WaYYlb~y0vok3LXI#d;u9&&||QY~M^kzvRP0@Gxg#k^`kTy%sbeB1(Z zAyu@w?qKZ!dJ&;>E$PzBs{t4FVt-De*o<@~`CJ^s2tU;x9cYb?1zeHQ9Pyci-%%@% z9G=?s4u;43y0EJF%o?SQAe}OP*TxL!u!+C@AOXIsje~ZTQR#SlE{{Mx*7m@#JQ!l~ z4$FHcMv!LeaLkYw5AaqEoU4FOD);bf&4NMlRM6;Z%}Joz#QkS3L`K{|7mOqJRu)AF zaoVsuMaR22%pqjMS)jW}WIABKs#A-ei>wp2*Gq44rcykRf{cXf6?TE>9mEmpsj$1X zpUH57geUMpXK6AtRJvQ=cQ7S8co!w%8T#Bdht}Wq;|4)^TKZ>y&E&wfEDJdZuJ-?dc$zA>O-L{Hr>}L$2Dc!qVGd zE)`_Z==^^@y_bYxj1_Au$2r?Xo0_yav9G21E z?5o(tc=p&C1@NSbeBkgW=mxAfT98H<@RhK`0@IjOlXYFe0A&QCRC={PdJTj@GqT6t z)}jVZr0A|)u9O`aZP?lZn!0!fq{cS@fVWx1Lec7>t^H>i6L@>?d*ttoEx`XwIty+*kQWHJcG? zs-zR2$ujsk3jr8gJK}v24eA6U=>Bv>T5|n`9w+4*>T@yl{114m7wHo{$B6XPwDtDg(wh|M|NubF((TwuYQk^ATFjR>|ArTumf@+top>2al{DBtTh+} z`~z{i3Ic-O!E$`v&QN0wk1si2Ro=3kw&2IabR7;V7gTkL+{%ooQiCxfrBzmN+3HVI z-e*>u(GRc?u7jj+hCm+YNDlzg-RRDC0sVAQHKpI1fjJh3vIx5=Pp3qeUI^?JWjoA; zvR*3SHCPb9PHac_s{2(0IaG=y*_s1JrhCB0g-kBIw^bNFVKESiNT^mczUvym@4EKe z<+1XAJqi5CWOY`4iO-0|4_Pa)#h(dZPZ>A^V&L$swMUf-*vI&GjAaOP_U*8ijs~)4E6~&`UXXBt+R6#ht(oa6BDJgJ`O^$h`lTM0`nq>Z8j>$?M!No*| zL_#;u|Bh-h=$%g-tPxpNrq$|uL2-;@_|&9+$ob{m1)s37BHzY3HS~m&vib`pjNFd> zfYb~;Wz3kVw^K>Qtp~=dKx4!<@Cf>kGWExDx6HehDH1!zaGjK1V3Z|@?1k3diV%q6 z8o-m+Pcg*-0?a+pk1E6AA@-_ok^gnyR)^w%nu?5+tx@Q|H==kVz^&e?!^^iSrH|z< zGTWV%@xhu|tNna>diBPfwQjWHQc!O5r}3!L^CzMXqsJW2KPxUTv7TJ~QO}&Y?EY@Z zo{ax{|EDLfoJ=!c@@RA+hJt55g)u#VgG*WC(a^UD&7DW19C6xKE!>%fp1l=2!((~x z4l~P8mExMT4io$d#z6}YNa=+gwEvvQX3pW!bYsn>OM%$p0(v6R^LOWR~(gep8q297Ge*M?5nWG|Ko*y!S}SKFYFQmZs1k=#SidhMXx{PBLDu zEmQ6|`Cz9b8>&?s?hzKG4J;WSaw3bf<8e7zPiM~hx~9yVk<&#Z&D|_sn&M8ANY(V6 zy6a*@mMUX&8{e4TSK9FB7C++NR`v)z{JZ0Mp5ep#h;cJrdvSCYe{g7X+&q?Kbyvn; z@LY4P43>x~&)LE+Bj2PvuRgeNL|l!2lR2!?{s+JX;3Yr*c?ADw8w4AAmKq~rPNJ)= z|!;w%^)C8r8RR4 zE>0kQQ%vz2`Ry{Wt#6l{0n5!kAva+*3gsycQkZ7c9_qftj>_q(UUl5&FSRg6!=44s zPf7q>Bp3?c8(#W#%CZ-}J0JgMM`GC7@?MTM`(w+KpC^=lFWehwh^*dTdVd9KJy*K+ zcze(B<^CE)Z`{vWDO!&2hMxoLW0yrGqwh-pu(e7S8+noVkZc1-M6K99?%yK%2P=O! zD%M!twoh-pGC}a;&%6*mRiA0oZt(qNKQZ#T#_uW;S`IO;OCNTH+Wr9^J~q|+NqoAKyg)qVwKp;LDf?m_g3!AeB3)F z#C}+;(%bLrWJ`j))b}{=W(PftQuI*%2UtnRw$`_M*o(d??U{H=FdlOKK=|skVYW*D zxMohZ4`aUyK2m@4!jQ1WfQzm7kHk9p;i1X?7U1G_blZ~!4He4=p6hHAWA9oN47bcX zZ&ar4$4&PNMR^r+t$5_7QU;gC=#m_0y%0DH3;R4qi`jIL+VxSotmj(&=6xZ$AZz2% zJTRu@{qBA1&}^E;3=ftQPn=l2V=R4#{R@Lk!go9Mr|Fdaw^Z(QX(5*ISFEzhB}x&i zSugK;*s>i~fq%-NF8=}AMC0^Va=3bAD4<+*{)v3yyn?=wsLij-d+%N{p54Z2p8(h; zoq{W{K4lyxQ@QW=2MOPf8oPvDjWW2%?((;Msu-uiVY2*+y`{HqD+>pu1pYu^im0M4vznn2wr6&&WWdCwr z{R8wr{{u2CdaO-nn#XPvmtptQHe!fFKks4pBZD46ekOWg^R_-a|0msW(L7X(V}+ZrAiLu-Z00Uwx!4Rqm>j>5yj=r2YB2gbC5}9X zoCA2k4jDiMTyG2JjriM_LMS)czvd0;BTR$(GcwZQEWjwv<1G*l4WT2x!OYAoNvVad zPtk91l&dfH?@~}ouo1UES0(4>&zj-HsB(g0P@KT|ZvB%eb}i6H~e z4?x6FNq|eY)n}q}O^}Ynf@D5T#fRsC2?hwnQ|gLD^ffNm*oINf+0`+EatO|hbA0lX zy!qpj6gaTLlmo+oVcvz)I#B^zh$G5|UW2_(m}8S8TqifYUlC-$%O5^Af*8{&;GSC zs4L@wBm6GG2@MVj$6NT+bu)8<57gGE2f0F_#kAL%GGN(B_KYK_61w~`i8!25>?7v#*^3?>s5f1z*F>D=~T*z1o5tQsJm8_C}VKu3c{L4Tvy&zRDUo^$dP6ad3OLae$azX^CXO&NCq^`rJteqHZHMtP3f+k_{G}Rb zjDNcW+T-Ec(*ZnB(^`cY}bMx}%BV6H3iYL{ZXg${ypv zU-tET{nc`T`y^yUUTSJwgo&h0>S~)W;ITl35Uzrq3UP0OglGm}8M=i6tZ*##?jq zkcGG9el5Mq5L62FZ3h7O=X&xVzz-kr>{S16xZ^DX|Mvj`{-1#VKi&!d69>aSIZu8p z9&}e^K2n&T?`O8994AU|jbQ43Vo}TN_2Pa&(mw!|_lOEWku@3Vw0(P-9x8)(Njit9 zj=fLWy5+*-c2tUr658d9FI9t`VqOfh(i{+d+*&<^^Gpi24veX%94-FQ7|fnwB^*;h zjU4s{Y-zlAj_B<>-8Gw}`E+K>9!tjs;d_s%Zc7UNK!5Lv`+dA#?pAP0Hu02GQyPac z;^vH@zDBVSC519x6V`T&f2W<*kkNcf#^O=D08ZenzS~ka;#J7I6%1NF6=VHk@u=xB0av;GN+!U?9nifM?Aqa`%7}P0!>8X$a+%HT1F^6W z;#hXTv`c2nt4xsz9@Dt~;&ZZ0_Foc5M+*@=$wccZk(4=nPtI)=bBMy?NTu@$eu!Vh z8jh`v#xuo=sC#aIi9qbDqwFuDXp)%wMywz z9UueAsnQ1Fy-(0;N4_u4Vt5WxRDx$j`eIeJ0ZrP#f@G8%_0hw5Z?|1l7#N*=l78&0 zs0X<(6C*=H8SJQg`HbgoggPu1DYqNZvn@MguR^V2u4Q=tsS?JIV$D5?9dhoHDMwkI zX(G1epbajM{F=5IpyLMg;+%Rs<% zl#LO(OxgZ2m~a%BoOH#I7D-B;o~jq?Y&@ixIQ}<%eLp3TObwmET!gT3zlI3<)Wxxz+8s zgXNLqapA&dQqgVW>f7-wzR&lRx-dm}*E|Bl?%P+e%o(sNd&L~!1I;AJ_|C?0#wq)2 zU`SmVeSdW+%-i*=)Amy?Ai?DRv+&^zF=_=UGI^~;B~=`77JD!3X%t2!K|gb&^uzg! zO?J!NJ1Ld!>|S*gPvcj!?b9+wQ%4!0tPq%1Pd@g3KoMzg*eKz&@bjIdzD5hl6**r0 z*$maSj^@j6+>{VT`-*b_hzNYdP%Mc`A4YMpU~bc_6!kk{C0!<(>=NIqqf50(`y*rz z?{>Jr^f&KHdr+0aZ2!2S*9)EKyK=Rxv<^EJTRE1F9`rwP$8GjM;Vzn5zlFEHr#HnR z#O`mNKR;=v+Y8HAm7oa)dIVFBx$5!NB66%HKW^!8?q>G6HOkSeXJ`YSrV82djF*rp zRqW(FCVq5pMg-*jar+ma`Vk};yVywYYxNI+6fgq+cvY^?qCgmJ{k>Qw4q=TiJ~HuB zjsOh5K?&Zm<$pZ%|8D=Aa{k9ehdBkIvn$|-|1r@K{~r+@mXDvT(3|WL@m2=`x3sv( zl~-Q+(CSXy_bYlQ%Ryv>t^aya^+nt{Z;<3+gtuzU8aPJJoLe+Rj#gX z(d0v6oG!7p#z;-py+F-UVJG{VWK-&~xIvb}Un3OP#)AA&2gWN^3+08my1ZsDCVEVI zE$dBE47d$sg%;FXIcXLecTEtZCMmWlizB^53c2}Z-2#!GG3VdxZ5P_F^}^S>QvLyU z3JUg#c3sAm$E}!jD9X8-nNd->ztIov-6{-C9}PS^=LoXtOd?pk$=gtNSfz`;^W#c# ztF$#U5gIc;&I0QE?h*sPg+y z@Uvzy1bcZ-rfE(TukJg#BeBNZ!>ErLy#YZnn!`V+q7N^Mkf+Kal6k*yy}v|KoMYXT zrVB3)V*dfMBk~IhG8?bf4UJ2btV?>^X-_3?^hVv?G{tHCEjzSXy6Xa{>m%&Cy7g8P z7}Cxrk-2;1lDGYAy81mU&x5~CMwP`%@iGO3uuUos@LCn^r=;;di*Rxs94QF?(f{dT8=CjyDQUrE-|< zAA3!CEKnDQIMBU9B`^n!-?MfAexme9(;nC6X?j!t+L~0n_8~ydI_^x7pWoxzpc&xf z>jIRQ%4=?8z=p$Y-E4~X87Gy>)L=J0vnVLB%Jy|Xy1jFC@Ja`k8;2`PzV0q4V?MYL z+iob>_xwp2`c)-X=I{048!?KXvG^cibU~aIU=9psDC?7BH&u(RJtx)UO0@S3V@2I+ zAd{~JJBVvjWbIGE72dMXU@e%T{r2m6Gy?QuGa{W^lXAO_jis@U03H0g@WxRt;O0; z?MCXeNG5yjMC#fMrT!9|Io|SX^U4(bD%cv66Y~4v@|&~5v5Bx*Ni*{6OEX4MKWw?N zkRowLU2yBhZ)QYvPa(4BLut8P`^~OJ6#I`a)RR^pFV_AVw*QSAAU#+9D4Ld)*VYpd zeEbiZwL`%jVd&+QdwEKU5iyU79@g3cW+M}3 z2)6h{f5~QTGm_EP0LEF}aHpd5G$*9f0g@16N9oA-Z)?=J{br+PwP6wqtXCf@v93<3 zwGa4L&Q|!*bdzRkXZ)QlLsStp6Pg6esQyyst8Dkb|ep#c=|*;=hV+B?qY(j@zeHQH2u=E^hEl z{~7PF3j7Od`Ug1rD?x^Fd6qA;ZA@zJspi5;gtVE-G&b|7Hsz_3bIeEUiZ(+&>90f| zBn1PSo@*T(vO`}=NaWv!Y4wAv3<${m^WAdN%+<_^50@vT^xM1k3|cq3b0_zHle%Gp z(oMTjs^`@nESd#{k~R}tT*eNn9%`5z zJ$UxU$+ppJF)4MeCz&&3=4V%S2wNRTke@`?YQyS!qd2H zf8HMB2YQ+vL9ss?Ou?-Te`pYVG1CPCe}TKPFkS?&-U8Fjkwv8qTD7OS2`^1g`{u#i z73iYd8@12L&lDva{t3(J7DNFvSK@`$Y3j*TPckIbB=yyQ<))LfK=NrDhIomFS`4OgTsl_aUEh*zg_!p*`AuV#E-tOJI=IxaQFBGrppoHJjWvyb>!3t_; z(*?tl$3k=wnK$lF_Oq~&T>e}=1>UD#_?K>+mPDK9zIwc1e%ABnI23*_(F#`b? z#5|?SQ5(Hpxr2sq-9p~ArzKnh7lha3WIv+|MnS)T?SU0{Qv)>g?ZnmG%rd^D)sY02@XK&?Ef)8w@x?enuu9Jc;a@n%^*D3{KAOk5R1NZCb>O@ICz1F7S zZ-+l1)BjX0;frE3PBFI`+f-6{gx%9L-UXa_X%cLk%+dsHv$@TPv=Bq0=G-C9h?g+v`W>{{Jy=MkzVs@ zK0whaW?Q53mkp-nq^kVZUHLsJ-<}D~qoi7A6RFXrp{wlQ?2zq&>hf$a8!KJuE9>6p z4ZfJH+^LO;8{5fC$<OTiOqYKvt^v(V~QdZYj7~PvM~OuYXC5 zYOEkNX+}Ub(l^a--g50Cm~pdmRzE@e2}8|nsy^m6#c6@VbS5WDq(sl-`Khu$2(q~3 z>=wC}yH8N$+{j^PHRuOIcgR;o-D-1(X;RK~TOsfViOx@RR{%mHmJkD^W+8F>)!Suj z)qR8R!rAu@&&XGhmPD#X=X0x{q{NTKjlmB?d#a^0ea*{b#>)EdKzRS>l zu3MH{LaVi?weX+eoSi{}g0<9GO=0SF9q-N{<~0^bHrsb1Ho5KNK|)53|qfGHqY9*FuS5zfk_>olr!q( zCZHr{EP|a*(UwtF*aj?V(dpf-Q!Mm8+KO4n%!F&?AYA^dhQz1fmV(S!+Jh!Hw-JVw zcR@NivdPCZOufoA&`0XK>{En}!mXB_L_@~{0>p-OEN(S&Mm_4jlQ>UaaOhzm!9zbA z^R;N&VRYpExyL$Im_(SA^$v1pz{%UW(fSiuUqtbZfC&XmPM`Q2S1~J4tGi|J%mXu* zLGec}jd72^_ai!_vxn1`#Y(CsKb3Vz_Ncbsip+cOIjXp5(RtEu%TV8KFs#5iq19)W z(0xi7*?#ZIl)2r3uoZH-^NoZzdP;3wmsmAy`^K(2b)X6IBl>YJsUE3;fYrbsjL7_g z$y|sP`eCk2lS}x|B1)52^sAs$E8eiM%$F3{q5OO!)-Nc9ZTM$F7^pRs^M%AQ^)je* z!p6+4NDVc2wh#rdbPHpHCH4oB8Tr0!Ait;eEp@Bx@=<>NReHE2y~oP*JMwLhPT75T zj}@=ZoRwjJJMyPiODOr{_i%87& z`Y!E6@jrl|Z+0MP;ytl2o2u&pz1L4PA(X~5-&GstqHOZz3x(6xbSnG=I{h`&W{cta9}q!3>&rP~E2`97n%u`bQM{E_aNV!#U$LuBbvA{(6?S{VwvPLi zl_>Fkqs?&{J8PkrgAL^KB1l|1-~EFfDv0W*>a*`X;k7L9!E&Ge=CD~bgN2468mcl6 zzDBRy>5NDtR3p}s45fkNOpscIS8$Gz8P~zhmcKX9b%i8vR!Ig0~aEP^d$}%k!naWI}VA9iPkR@d25`JcJJMmYU zdiL-~&_jArM3L3?@U{n201cBhv1wX~+xM%2q`v#z38nQJ-?)?Erz0-F4!&?#M_~<% z&_B|k1Q&A4nm*wb)a#zg+`&~48zBoVKZleY^TQk<`!KNRnj}N3_L!J zO^2a&>il(;vmdIy_TOt#Uh^u^T%x|Tr*^Ms{^W(Shn#i;~D6B_CB4QrkXPa!|nfzJc=&eo>6 zs;G85!5tk@T86xjxK#6Wp%cQy$NMEP#gBnzxQ^x*f;PBfEMc1HyVj?!D3u`N1FlWN zg7O0DWpHsv9n0wuH9_BlI#y%MkBO*-(S&N#R?~$F{^Q%Ba3Ly=h-&SvuyM%?j$qk> z{Ra<*uQ2;=3OR>=mFHgm7?j+nU73il`}!>mk**Fx8zSqGf-6G&^R;VqYW-fljFQ+6 zR7FOLEqkBVNu@}4OZLw`-8LNx+QQ^TF^3N3%I9!eDK7e|_zE%l-A(TSbo^xY?wXP3 z>XCch){8eJttU^N9^Y3=Z)Qk^lG4H@IzQ#uZ<+i=?Fx)M^)GaS_3u81NbYWD>fl0w z(Sg_rKq&o7g6~{$G6*PPMLjU4?{f-fyL@mu~U8fd-!U3ih zxwkK!XFrFhwR%f>GBGd)k6;pWb6+lhpYBN^Y$ix+Q0(Ic0t}Sa%h}?R6rg&H285z% zq73_Yo>B_Gb2sXg)hFeBD18x?FI(DD#6TDrODPa z0^BpOpMMsZ5oFh(NXfXg!|z2w1PN1ft*WV6-}6W3FU~tQoE=#ar`2zxZ0gyC8-wRX z?4;Utp+;8ZsF4WxB-evtGp++Oyub+#a2SZOjPHbS76vF$VAnuK4@iX9lGtiBKXWUJ zB7JlP%PW5~eO(jp9c1dyXs%3rxnx!zZr<(N944|L_v<6fjLz+~lLbHFEAa3j3eNE< zhI~j#7geM1$b3At;58nA38&QYp`og~F|6Z3z+J!cLlj~emSo1rr5#UqlvR@>SY@zi4OI4(k&HgjOP4?MBl`}MN zUcJT&U}GsroaA6$ju}G%C$$3$`vNmQI;hXlRJFr;C$`4oD#`cqyMnO zw_3PvJ5iKa=$`#x1eZqnuVR7so%D;L!O5RQ1G6Pk7WX};%w=gqKdhDYlvC~1D781rFT9;AInmys{@yPSH zK+NW+9NRD)VUGE0Vp3^zyrb`Hqil#*W2^Jh0=6xr|3g4pUGt23#6u!ZDUpiNYME@z zcXZAH%N4avjg$!ZAArmgv<2TFHg9J(kZ|I~oFa&Ril;!W)ea^IpFK?jOneQSORP`9 zGVs>XH{rr|hP-#Kt&N{FN0Npt4p$Ml5;7kBwy1&d-2rSIJg8|HO)5$xmGNK0ia468 z=k&iRfA>&K(^mv&f1ad$p8)D4+=ZC!QK!Xs1tERhK_+}`*O7W-1NsALj45eL76M4Wm2jKlS+#IsoQ>;;7sezM)&;nZ=`6jAGc zJ&ENZdwMiC6eh9n#$5Lko<`JywI>q*Sf8Y!z-ZJN!iBs^Qz_@u$%vGdKKag@4;fQT ztzl$1IS6N$&d!&}H7dsmBnyFdXyUI?<36OBr^?j_F2`bF(dn{_6qDT9M45s7*{n;Z z)UINN2DfORd+m($@>PvOFCYFgf`g&ht0Z3kI$+p_7w9!bLNj+HR|D6Zu?fU!usJRF z@O?xhf8nhCVswx$i+7`leRNZ-IdxsFOv>$o*$4gH+XesRyk5X;y!5UIAS1U|$f%fH z!qx(_V(z)Ga>_iI_wk}9=B`P+KoAwF-ErkE_XTzLLA~Wlgu9q{iau{RvA9DXBIwbA`-6g^iY_ zNcOPzop$HlwWeG_k@;sZZ-L4tUjr&U(CL_oAxR*{lcK*EAxNoyqxTrlLcgq2g?wdv z_tbN0emXAOb7YKEn36lOrI3N5tt4jY-#5#e$lZ55DBL!l4bF2L85kN-kGpmzpJDtq zYs`&n)M!av&RJ>KdS45e!f#_BSIN&@>B2^$&=^g9DSIs5*7>I!neEy74^Ue3^`~8r z`q;X7q^7R?Tu(ueZUfHgVWfbDj@ygo*!|_!ZVcA;wIsN1_O<12x1L4M)<3wZ*YiXk zPGjL@jjpfMUO|`OuF?+}1!)M;XL;4t>a-#><(*H&N#>lpc4dZjUyAcjue^E4V0@}A zHHy78nL7+3w(k{D#@<2aCix5_`taLPQNc}CAKi}#ohQ%d>Ld-Be4U7;fHX_=j8Ey? z)r|$)NgMHxJ$UHc#Ag_jJutvr=lQyu)K_AV^Nt#{o!y)^%BB<6&Tl{m@zWH2!F&x1 zR`^p_7uXw>FMQvr*g&Uchot|#s{yr#1)abMCkc^NK8zD)s^-VNVKukbQ{puQk>)Hm zeGnfwLG)a@v-}4M4BI>a({xYTOz3CQb+4~#-<%~IYs=zQ%Ty7pT5EZ;)w?TM1c+?& z!D+G36buLIM^d~(Cz;U6r%?nlrBur|WaA%rI)TKyeDk3pOwxPyv?P#54MVdF(5FMK z(oqLO5(s;l-4nLiXj@d<;<{`m_1kYOdsx~OIo5Qho73?5)9H5%@G1Y5-H-?Mn~g^w zj>$E3-4q8!9(mu{<$dola#8XqG9PtyNOHh}`7G~6;`JdWNazzwmDTA!u^U(Z2PnNL z;A`TG(K@wU8}%{Wx_pN})NhE3SmjH{LFj|NvT@5!IqimOYH81gBCmjlh>6aLI{E_n z1=j(*xgub z$WzMLb1qQmI`7wLX^oO@0Ck3Qg%Vec?$9%>_Jvm(&g_-cSlH&khihjk1AdX~Vhz^$ zbb8K*9t;cS($yhPYB0_~!S`X|h`iy(+OO}@&HRy|P$w;A*5BPB5gT%%f5~;b#-;G9 znL2Pe$rwtL} z=t=4G_Gu(Y0gc;LQ-_=7;~)4Yi6;4RgX-r+>P9Xr8_tMMfW;_ z5{o`CsquZ{pO-d$j4Od8k!|-+ri*pFp6s%hgv^>1iii zHI0O&Jdl>b{edv3-lVZ(y{J6Z(LqC&E*G>{wAfLc`d*#j{|f3G735Y9OWAmqz!lg= zu_cE&S4q?zP^8&Ks_qCBBAG7+2kcjQnQf}6F|UlG;Ef35DgqD{FDOf6aMl<@fC zF=C#g%=mQ}%=HEg7%u!b0}y+;if+?8=hRfKrIB~7oiJ9A4YfN0@XUwH1HhpHz);QBokR%%NXn}Q+ltpTP=Rc5Vg`tSwJy`btr4J%!$p-90xgkQWgVs(st6Z^ zi?Be$cF-!t3@6%aTCfop(d;3x33MBmcDttYmo*;KC_%LdRv``&z(6YJ6{8sAa6*=Y z8&=B^V`^lsSod`ztxQ=s4Q&M7Mlic6_vSiTQXenI&T5PO3KpLZ?Rw~tdkjZlE=*Iwd*D{3=gSY_hw zMHXIGW(@(_u^b$^UV4crm9zpCb;sUV7|l~kwubi1J1~JD#%ST;c;+!&67ZmE#+Q=0 z%mJlz2J;i7L&{)v0;&WwplCMOLLOQJlnCCzSRqjt3z`cpq=b!{j-28Jt; z3|lDjEyOoylu01pwt~l?qvmOLObq90f&+vHAk4eSn^4+Iyp_x$=T4VS4`w9F6wY!8 zMOk?%6&Mn&MFEw^&W|nN&?pfy#_K_gS&+GB)qo`G(V?J4%D}ApkHG=P>x5zCJ3_#2 zsKBx#A}(wI^s#KptQ^5Cod_$?7Z-zK(|{I^7aWrXdfJL$Z&ewX*?EDh(S|hY`fi4A zkCS8!Aw`8{x&fDUE)`&st(6)i51#?UeAh=@%d;T4S|3LYZ&=`nfLA1Ktm0eSE){O$ zEBKt!(NS33XZY|5ie--Ap5-^gL#j1>!5RclCOM@*z@0M?cP-nX=4lq0O!lA;+dN5mrJ zA~F@VWdiVLHCGfxgjkHQB1YZS8Ggbm!r~kwa{CaWGZ!*(K_dzhfOqJY`X$|=PH%4{|U3L@dz2KX8-&;f&%N>G-&%N3aw_hFrQH8L~M_Qn^e zQ>wreYec13Q%b^LB%q*R4V9FFih~CD5t1~%sua?w1tl(+Sf~$Oy-92+@F^NoVRi%( z85osRFx}BF6mM$Oq1FK8_AECvBc_*P(YmN*j4UcV3v`{bv?74GG0j%k925mB7#9Bk z>4L^h;4O3=KrB|?cPr{wI*IoZ^%E;A8p@9Gqr=QcI+^t=JIna}!f`#+xNh2dhJ_Og z>=NR`kW-z9P!7Zv^k-F2t6@e&wz_b#g(H<%uoDk>jS_)qU}?FEN`fjVqlO@$jV+^2 zN2r5TbhtVD#5h+X=q^<{Q#lS+;@$IV4N;P&nJplO;F)_WBq{_^B9yVF=$5jYn=w5K z(tt&xGoGqs;$2Ti0T0?_D}l5`Xod?cVlGpI9Hjyx9LN()m*Ap;DiGoa^}&kZPzXEL z1P60k&^A0C*qH`WGF_g;n87IX(_}9j;_Z}+1VXv=n*wkuWT(%&NM;zI~15TBzElTXgOK1{Q zl^e9s%hT@!cxh}sFEY_0imWw@y^kv98_iY7jUqUSOLs$|jlqeb8aU;Q#1!pH#>9+I z*(IA3DGz{}C8<0?LJbo!?zSP>ZUi>R3@Qo7)0UJ9tAZyij8(|6m4gadHq=2E05xs` zUK@pgyeixu*F#fnHYWst^)j)$>?3o?-9;|wwh+|>1BzcPz}S*B#({3W^$dc8QPqQS zqNhd`oKe6*pYPMyiGX6t4gx^})h?}wBI8iigA`z(foM#I8Z`lq02U5_preu+RzW#* ztTnpX0MHq3tU-Ynr1s;6>cv#7V-AD^bVP}ZB5osxl$k3cz{~{~HFIxFw456`!<>gq zWJ|$a4!~CBNGKty2T8G9^%zJ`itmF$2~V|T&}dh22m-lw8VjJ8;w1_ga;w$*fEhKM z-raR_yn7?j@PDao#_?Un-h5vhd1siZ_u>g};xxrGYupvmsbe{=+R-esbLgGX97`qdI3WaM*gR;9bW40u2-lm*euBn?yD%l`n( zUz2jLxZ}mtQa&0vm$0uT_B>JfGcjb4!fyQ*7!na?#D46xa0T)K{!s0|7@72xz3 zfavrEMe|o&$FMI4*H9|9jbje)SeItVKqFSEY(|=mw^3dT8JQ5&rWzg=27ui((O4J< zgXrY61PyL5jSb7nQB<_jiX{gIsUVyt50uO_0RuHD0I^@CT8zVF6B7K1RLRiNF%5|- zJjf!u2}FjWtEe)%DXYO@xV35&(-!5=r_qWHP2@;x>L!4U9tJ5>gv%>iS+>Li>wK_d z`>*;pGI{jyZb7t68Ya67|SB zFcIXA$il_2yeRASh}oM|g7CMB1>#!Z5Ghr>)J`n6*-gWatCf}zsD!i^h#*zLd^@Za ze7TQTs~1aL2V_)Ll|@E@eN-wqmX)_$-x0k)fQ1OOf~M1NtB<4CIF}lairH~UzUBBW z%Zn}q0)9fJ5{Q9(N-oF^%pACCguuB+Brcac7ll^pT;=M;uc9V|tl(Cf9^Is0M!{u{ zHbH8jObfenVn=f9tb}DgI)ter019@MsY+PV^gFQvkg|-N?$&V(sX!1{)bG&@20$5C zX`gT`&S+p=w`Dz&^&&tUIWu%tS*M>}14L$&gf0Ieo&M`=Ri9m+J7Xj~;< zZQukhj7N-&w6%38p4h@3%eL7tw}_syLh-GeL=>u&bQ+AX5GCR(QHTjNrXdVQW^N@& ztrGcwwgIXV?Y*KzLQ<hM+1=s+MN;DzglYmGK!t*!LWk{I6t%Qt78R*LGCNdN0tS&^({NK|9^BNjw{I%K ztFCTl3d?26@0X~Q*~W^0i;a||5{7#YS+{0kd01bJn9Mf}-9y z4XBC0C98_#ul%7<1zVB4FGL@D5lMl#KzvJkj9{r%Y?9Ptj90L5X?8OI0O$m=R47?Z zR;oPe%;1BK05rFBHZ9JQFacchg9JenV++FE?G&V|B!q0d5tam6$Xj8QQ>qXG%7Hcr zCWC2K%@7O2Ut9oQFK=WQp?UIGLUIVsujp1!vHDEnyZJ(TH8RYMKdc+GzHFR+Psz~Mbp}=qjLoq zWGk0p*AQT{&=IbalqRZ5t=6>O$QeKZI1LWYtM+3Uh%JCk+si7Dg}mTQH0HXS;1QcD zqK8#a{$<2U$H2VC7ZG(IH8OSM{{U!I#B3!|vpmKEQwlF--^pMCYFclU7MKo2Uqxcn zO$CgC)q2uh0H`fC4T5VaG!7SP;YH?N1t=>+*&{FrqPAWR2x!;U)n06m1$At?dc9)H zL0DT)Bs69jHufl?8AC`=(|}hUu{B9@G^qNHXC+y;zKt@JG)rAz}1E;fiY8Yl~R=s&~IqcVxhaUASEu#y28gOmr@o099q{-;hLlhfJ)2UQ`rD%F?*<9 z0<5z7g%*^x0t6Zt00|fhVVG_uI ztc#)x1yRby&GPyHh@91_Q|M5!^wHdkvfqN9kjgJLdu3L{(!~nPM#2QII@l%-T8{_? zBB>VNA`;(Jf*ymJ$iqC4s4Wh4j1(ZDaH|w|Xn;}*v_+;0w*<{m-a1ZFU@AzZ0h&t*J`70GK#))j$NDZzS{(*Y|qsH?aQ*`Z~5sGWIVZAWO3Y1%8(Ab16L|QYB z#^R*PB>}`SMS5Wj<3j}&3VSNO5oMs7t1Hz+igPK{N8G2vN{C82i1#SWq5D7l8DtUw zz>4Uftj4^k-XV$r^HT{#8-6;uV2E9_e0|iTMUG$gu3z$J7o6#Zicm7Xt{)(rD1e}8DJ&H2IuDtv@r*mr4Pt0PVeN|?G$E3ZjNyshQ8rbs*5Ugdm?^!b*qIxY za)f$&L40n?3*BZhW&&pwQrdAv36t?K<`dC(^2;X-Dbi$%mK91?u@Kdpb(|Xx^aHtpa5u&W>4}^WhBOXmqQ0EUSiXX zDBfG{Di*@9qs9G!h=Xd#F5VZZepK0$UaArq=5=c<*C+yFg{|6WIuNwQ&@Oq_k{k-) zCLey;cVSUuiZ+_5k3cG>Ty2btjn#p{)>u(iUKYLCP$L6RrX;4pX^sLBB1lwLR3>Vb zE&_^J zLJcB%CG#!ZQ5CB5tK7CPsPtwqzid>uxo>_yh+8S_ne{1;vrvFixr1HMjwm5AjWVtS z(KpjUA9GU%WAAd%sL_G3ic?lr)L36JuXzg_7tg;?L;QE#@bs(qBYdfw-)O&|z7{EqkalYfQAV>W2ijQ3Bg;!5+Ovpj2!R z>T}c`6b8*(B}FQp@Dn2QdHDo2BQ`^B+jvdhk)l8h;5zJ%t;4!V39iEfBNxX_332Wb zm3Mukgj8B_Yh@PWfl{ByszFr|1$5jJ3iuY~@pQH*T~oOEQ39HX^=JMWeZzMSIVOJe z%j^9R-qZ?bHK*QDg2grn`}M>YR5Wrgr$lgUiGfOW1O-E#Cs2nL%gvValMq^I2LAvd z0C9P1{TM=w1E<%?xs88i>ky`W!vo^wMIPc4k(JU>E2&3}KuY-%RIWT-mzP1sqWm}S z64L#`T~um{w!5+9lQcv!AaApYN*jAM?##v6*p|}xqjL)l+dJp8xo$yceU2UDY{6QE z#~aI@-ggDSSzk4KmnTcY)@G7u~_!?(A z8Imr;M7LuoS&#{8vAPN|kTgu_7wenlDgv?=3vkXs+|NyKbw0^dO*3kT?k$v-j43Pn z<_ugU&Qb3!{{SNA^_?@6fU^r-E4bnWdRjb z19^qlMU;BRrbwO?%=I<0>c&Ge`a-5ac!}h_nG$$r`^xF*Tpeh<|Zan#aVLBVtACqtgNrE z79|t;56O@C+`n+v?w{C%F+<%&p+Jl{6mu?4Y1`Y?s1tC z+#v0MRM~|r7kD&7IRtTZ`>rK>K;RelUwM`7?q`kM#y1LUetAQYVfee!~EtQmIMC)$2`O= zz<^+|wZ=iT)*Y)C)Kz3rFI7lhn#qMgh$@J<36qti(E){{Ur5 zh;X4dC}mmP;j@mmc*GnOCcW zQu}!?+zShf;V*iF?q4JrBjVU!buL`FWU%Cy1}*AmvR}D={9dyPGzwqBZ~Ng_%`D07 z{{Ur>L&A}NlKYl=mzSu%e5IU4lKLga7Dda;^W(~U#LCCke8u?v7cuZ4@>>T|{u@jg zSxH&&xkGS3k@iaN72*lz2`SugN-#bjp9UF}=IV4`fNo`8Wq6Bg{{XQnZQ{=1CF}%g zGsF@S6YR28%=0PkTfZ0AjU^#IB+By@_bJ@3odHa}mDe08^xlvC{w`>m6KH+C~GIC7%{&;B1_)BHJpw#v<@o!OH zWu?Bqq|ytE7(Uf;um$191|AJ=04gjch_eJL`w4oWM@ZPpzwT8%jn8u!2-=l-l!8Gy z31>bMiCOF=dpuR{CpnengpM!$CI0}~F8Y>uvRUpa9Qv2iV3+g7lFtqQ0ANr0WcvRA zWqHh#3>*v!wFXIw4UySvCjP!qu z^*@aEzl`C32=spvu0PE7*X*D7(a*b{%dE>>74(TEht^$ye_-7Y zdX;v5$7X%ajREN_?tPN3L-u?B074eJ59q<02k9(u{>vQy01V8tfci@{Rq2<6=Fo$y zGCHnh-N7e6+L+L#y3XQNX)D+IOP4L%EiLi1wpvM&J*n