diff --git a/.github/workflows/node-ci.yml b/.github/workflows/node-ci.yml new file mode 100644 index 000000000..03a70e85f --- /dev/null +++ b/.github/workflows/node-ci.yml @@ -0,0 +1,84 @@ +name: Node.js CI + +on: + push: + paths: + - 'app_node/**' + - '.github/workflows/node-ci.yml' + pull_request: + paths: + - 'app_node/**' + - '.github/workflows/node-ci.yml' + +defaults: + run: + working-directory: ./app_node # Set the default directory of the Node.js project + +jobs: + + lint-and-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code repository + uses: actions/checkout@v2 + + - name: Set up Node.js + uses: actions/setup-node@v2 + with: + node-version: '14' # Change to your preferred Node.js version + + - name: Install dependencies + run: | + npm install + + - name: Lint with ESLint + run: | + npx eslint . + + - name: Run tests + run: | + npx mocha test.js + + security-check: + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + steps: + - name: Checkout code repository + uses: actions/checkout@v2 + + - name: Run Snyk to check for vulnerabilities + uses: snyk/actions/node@master + continue-on-error: true # To make sure that SARIF upload gets called + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + args: --all-projects --sarif + + build-and-push: + needs: [lint-and-test, security-check] + runs-on: ubuntu-latest + + steps: + + - name: Checkout code repository + uses: actions/checkout@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push Docker image + run: | + docker buildx build -t artkochergin/random-node-app:latest . + docker buildx build --push -t artkochergin/random-node-app:latest . + working-directory: ./app_node # Change to your project directory \ No newline at end of file diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml new file mode 100644 index 000000000..9994a0db5 --- /dev/null +++ b/.github/workflows/python-ci.yml @@ -0,0 +1,103 @@ +name: Python CI + +on: + push: + paths: + - 'app_python/**' + - '.github/workflows/python-ci.yml' + pull_request: + paths: + - 'app_python/**' + - '.github/workflows/python-ci.yml' + +defaults: + run: + working-directory: ./app_python # We set the default directory of python project + +jobs: + + lint-and-test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10"] + + steps: + - name: Checkout code repository + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + cache-dependency-path: | + ./app_python/requirements.txt + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Lint with flake8 + run: | + pip install flake8 + flake8 . + + - name: Run unit tests + run: | + python test_app.py + + security-check: + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + steps: + - name: Checkout code repository. + uses: actions/checkout@v3 + + - name: Check for vulnerabilities using Snyk + uses: snyk/actions/python-3.10@master + continue-on-error: true # To make sure that SARIF upload gets called + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + args: + --package-manager=pip + --skip-unresolved + --file=app_python/requirements.txt + + build-and-push: + needs: [lint-and-test, security-check] + runs-on: ubuntu-latest + + steps: + + - name: Checkout code repository + uses: actions/checkout@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + id: login + uses: docker/login-action@v1 + with: + registry: docker.io + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build Docker image + run: | + docker buildx build -t artkochergin/time-python-app:latest . + docker buildx build --push -t artkochergin/time-python-app:latest . + working-directory: ./app_python # Change to your project directory + + - name: Check Docker Hub Login Status + run: echo "Logged in to Docker Hub successfully." + + - name: Logout from Docker Hub + run: docker logout \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..afaa1a62f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +**/.idea +**/venv/ +**/__pycache__/ \ No newline at end of file diff --git a/ansible/ANSIBLE.md b/ansible/ANSIBLE.md new file mode 100644 index 000000000..d8e716990 --- /dev/null +++ b/ansible/ANSIBLE.md @@ -0,0 +1,215 @@ +# Ansible and Docker Deployment +> Deploying Docker on a newly created cloud VM with Ansible. + +## Part 1: Initial Setup + +1. Repository Structure & Installation : + - I organized my repository following the recommended structure. + - Ansible was successfuly installed. + +2. Ansible Role and Playbook : + - I used a recommended ansible role, that I downloaded from `ansible-galaxy` - [geerling.docker](https://github.com/geerlingguy/ansible-role-docker) + - Created a playbook that I used in order to deploy Docker on a server + +## Part 2: Custom Docker Role + +3. Deployement Output : +> Last 50 lines of the ouputs form the deployement command, obtained using `ansible-playbook --diff` command. + +```sh +TASK [geerlingguy.docker : Ensure handlers are notified now to avoid firewall conflicts.] **************************************************** + +TASK [geerlingguy.docker : include_tasks] **************************************************************************************************** +skipping: [server] + +TASK [geerlingguy.docker : Get docker group info using getent.] ****************************************************************************** +skipping: [server] + +TASK [geerlingguy.docker : Check if there are any users to add to the docker group.] ********************************************************* +skipping: [server] + +TASK [geerlingguy.docker : include_tasks] **************************************************************************************************** +skipping: [server] + +PLAY [Deploying custom Docker role] ********************************************************************************************************** + +TASK [Gathering Facts] *********************************************************************************************************************** +ok: [server] + +TASK [docker : include_tasks] **************************************************************************************************************** +included: /Users/artkochergin/devops-course-labs/ansible/roles/docker/tasks/install_pip.yml for server + +TASK [docker : Install pip] ****************************************************************************************************************** +ok: [server] + +TASK [docker : include_tasks] **************************************************************************************************************** +included: /Users/artkochergin/devops-course-labs/ansible/roles/docker/tasks/install_docker.yml for server + +TASK [docker : Add Docker GPG apt Key] ******************************************************************************************************* +ok: [server] + +TASK [docker : Add Docker Repository] ******************************************************************************************************** +ok: [server] + +TASK [docker : Update apt and install docker-ce] ********************************************************************************************* +Suggested packages: + aufs-tools cgroupfs-mount | cgroup-lite +The following packages will be upgraded: + docker-ce +1 upgraded, 0 newly installed, 0 to remove and 19 not upgraded. +changed: [server] + +TASK [docker : include_tasks] **************************************************************************************************************** +included: /Users/artkochergin/devops-course-labs/ansible/roles/docker/tasks/install_compose.yml for server + +TASK [docker : Install Docker Compose using pip] ********************************************************************************************* +ok: [server] + +PLAY RECAP *********************************************************************************************************************************** +server : ok=22 changed=4 unreachable=0 failed=0 skipped=12 rescued=0 ignored=0 +``` + +4. Inventory Details : + +> Inventory Details ouput obtained using `ansible-inventory -i inventory/default-ya-cloud.yaml --list` + +```sh +{ + "_meta": { + "hostvars": { + "server": { + "ansible_host": "158.160.102.54", + "ansible_ssh_private_key_file": "/Users/artkochergin/.ssh/id_ed25519", + "ansible_user": "mainuser" + } + } + }, + "all": { + "children": [ + "ungrouped", + "web_servers" + ] + }, + "web_servers": { + "hosts": [ + "server" + ] + } +} +``` + +5. Best Practices : + - Modules are responsible for one simple & small task. + - Every task has a meaningful name, empty lines are used for readability + - Usage of recommended directory structure. + - Application of `ansible-playbook --check`, in order to check on actions before taking them. + - Usage of dynamic inventory ( from bonus task ) that allows to avoid manual host configuration. + +## Part 3: Deploying Python Application + +```sh +ok: [server] + +TASK [docker : Add Docker Repository] ****************************************** +ok: [server] + +TASK [docker : Update apt and install docker-ce] ******************************* +ok: [server] + +TASK [docker : include_tasks] ************************************************** +included: /Users/artkochergin/devops-course-labs/ansible/roles/docker/tasks/install_compose.yml for server + +TASK [docker : Install Docker Compose using pip] ******************************* +ok: [server] + +TASK [web_app : Wiping containers] ********************************************* +included: /Users/artkochergin/devops-course-labs/ansible/roles/web_app/tasks/0-wipe.yml for server + +TASK [web_app : Check for the existance of /python_app] ************************ +ok: [server] + +TASK [web_app : Check for the existance of Docker Compose File] **************** +ok: [server] + +TASK [web_app : Removing existing Docker Compose containers] ******************* +changed: [server] + +TASK [web_app : Removing existing Docker Compose files] ************************ +changed: [server] + +TASK [web_app : Remove app directory /python_app] ****************************** +ok: [server] + +TASK [web_app : Create directory for the python_app container] ***************** +ok: [server] + +TASK [web_app : Copy Docker Compose] ******************************************* +changed: [server] + +TASK [web_app : Run Docker Compose] ******************************************** +changed: [server] + +TASK [Check if the service is healthy] ***************************************** +ok: [server] + +TASK [Handle the health check result] ****************************************** +skipping: [server] + +PLAY RECAP ********************************************************************* +server : ok=20 changed=4 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0 +``` + +## Part 3: Deploying Node Application + +```sh +ok: [server] + +TASK [docker : Add Docker Repository] ****************************************** +ok: [server] + +TASK [docker : Update apt and install docker-ce] ******************************* +ok: [server] + +TASK [docker : include_tasks] ************************************************** +included: /Users/artkochergin/devops-course-labs/ansible/roles/docker/tasks/install_compose.yml for server + +TASK [docker : Install Docker Compose using pip] ******************************* +ok: [server] + +TASK [web_app : Wiping containers] ********************************************* +included: /Users/artkochergin/devops-course-labs/ansible/roles/web_app/tasks/0-wipe.yml for server + +TASK [web_app : Check for the existance of /my-random-node-app] **************** +ok: [server] + +TASK [web_app : Check for the existance of Docker Compose File] **************** +ok: [server] + +TASK [web_app : Removing existing Docker Compose containers] ******************* +skipping: [server] + +TASK [web_app : Removing existing Docker Compose files] ************************ +skipping: [server] + +TASK [web_app : Remove app directory /my-random-node-app] ********************** +ok: [server] + +TASK [web_app : Create directory for the my-random-node-app container] ********* +ok: [server] + +TASK [web_app : Copy Docker Compose] ******************************************* +changed: [server] + +TASK [web_app : Run Docker Compose] ******************************************** +changed: [server] + +TASK [Check if the service is healthy] ***************************************** +ok: [server] + +TASK [Handle the health check result] ****************************************** +skipping: [server] + +PLAY RECAP ********************************************************************* +server : ok=18 changed=2 unreachable=0 failed=0 skipped=4 rescued=0 ignored=0 + +``` \ No newline at end of file diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 000000000..61d58a222 --- /dev/null +++ b/ansible/ansible.cfg @@ -0,0 +1,3 @@ +[defaults] +inventory = inventory/default-ya-cloud.yml +roles_path=:~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles:roles diff --git a/ansible/inventory/default-aws-cloud.yml b/ansible/inventory/default-aws-cloud.yml new file mode 100644 index 000000000..92901f67b --- /dev/null +++ b/ansible/inventory/default-aws-cloud.yml @@ -0,0 +1,6 @@ +--- +plugin: amazon.aws.aws_ec2 +regions: + - us-west-2 +filtrers: + tag:Name: Webserver diff --git a/ansible/inventory/default-ya-cloud.yml b/ansible/inventory/default-ya-cloud.yml new file mode 100644 index 000000000..aaca1e9be --- /dev/null +++ b/ansible/inventory/default-ya-cloud.yml @@ -0,0 +1,8 @@ +all: + children: + web_servers: + hosts: + server: + ansible_host: 158.160.118.69 + ansible_user: mainuser # Replace with your SSH username + ansible_ssh_private_key_file: /Users/m4k4rich/.ssh/id_ed25519 # Replace with the actual path to your SSH private key file \ No newline at end of file diff --git a/ansible/output-node.txt b/ansible/output-node.txt new file mode 100644 index 000000000..8d92b6e2d --- /dev/null +++ b/ansible/output-node.txt @@ -0,0 +1,50 @@ +ok: [server] + +TASK [docker : Add Docker Repository] ****************************************** +ok: [server] + +TASK [docker : Update apt and install docker-ce] ******************************* +ok: [server] + +TASK [docker : include_tasks] ************************************************** +included: /Users/artkochergin/core-course-labs/ansible/roles/docker/tasks/install_compose.yml for server + +TASK [docker : Install Docker Compose using pip] ******************************* +ok: [server] + +TASK [web_app : Wiping containers] ********************************************* +included: /Users/artkochergin/core-course-labs/ansible/roles/web_app/tasks/0-wipe.yml for server + +TASK [web_app : Check for the existance of /my-random-node-app] **************** +ok: [server] + +TASK [web_app : Check for the existance of Docker Compose File] **************** +ok: [server] + +TASK [web_app : Removing existing Docker Compose containers] ******************* +skipping: [server] + +TASK [web_app : Removing existing Docker Compose files] ************************ +skipping: [server] + +TASK [web_app : Remove app directory /my-random-node-app] ********************** +ok: [server] + +TASK [web_app : Create directory for the my-random-node-app container] ********* +ok: [server] + +TASK [web_app : Copy Docker Compose] ******************************************* +changed: [server] + +TASK [web_app : Run Docker Compose] ******************************************** +changed: [server] + +TASK [Check if the service is healthy] ***************************************** +ok: [server] + +TASK [Handle the health check result] ****************************************** +skipping: [server] + +PLAY RECAP ********************************************************************* +server : ok=18 changed=2 unreachable=0 failed=0 skipped=4 rescued=0 ignored=0 + diff --git a/ansible/output-python.txt b/ansible/output-python.txt new file mode 100644 index 000000000..9fa65a818 --- /dev/null +++ b/ansible/output-python.txt @@ -0,0 +1,50 @@ +ok: [server] + +TASK [docker : Add Docker Repository] ****************************************** +ok: [server] + +TASK [docker : Update apt and install docker-ce] ******************************* +ok: [server] + +TASK [docker : include_tasks] ************************************************** +included: /Users/m4k4rich/core-course-labs/ansible/roles/docker/tasks/install_compose.yml for server + +TASK [docker : Install Docker Compose using pip] ******************************* +ok: [server] + +TASK [web_app : Wiping containers] ********************************************* +included: /Users/m4k4rich/core-course-labs/ansible/roles/web_app/tasks/0-wipe.yml for server + +TASK [web_app : Check for the existance of /python_app] ************************ +ok: [server] + +TASK [web_app : Check for the existance of Docker Compose File] **************** +ok: [server] + +TASK [web_app : Removing existing Docker Compose containers] ******************* +changed: [server] + +TASK [web_app : Removing existing Docker Compose files] ************************ +changed: [server] + +TASK [web_app : Remove app directory /python_app] ****************************** +ok: [server] + +TASK [web_app : Create directory for the python_app container] ***************** +ok: [server] + +TASK [web_app : Copy Docker Compose] ******************************************* +changed: [server] + +TASK [web_app : Run Docker Compose] ******************************************** +changed: [server] + +TASK [Check if the service is healthy] ***************************************** +ok: [server] + +TASK [Handle the health check result] ****************************************** +skipping: [server] + +PLAY RECAP ********************************************************************* +server : ok=20 changed=4 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0 + diff --git a/ansible/playbooks/dev/app_node/main.yml b/ansible/playbooks/dev/app_node/main.yml new file mode 100644 index 000000000..601949628 --- /dev/null +++ b/ansible/playbooks/dev/app_node/main.yml @@ -0,0 +1,42 @@ +--- +- name: Deploying a node web application to our server + hosts: server + become: yes + + vars: + container_name: "random-node-app" + image: "artkochergin/random-node-app" + app_name: "my-random-node-app" + app_port: 3000 + server_ip: 158.160.118.69 + + pre_tasks: + - name: Check for the existance of /{{app_name}} repository. + ansible.builtin.stat: + path: "/{{app_name}}/docker-compose" + register: compose_dir_status + + - name: Stoping Docker compose if the directory exists + command: docker-compose -p {{ app_name }} stop + args: + chdir: "/{{app_name}}/docker-compose" + when: compose_dir_status.stat.exists + + roles: + - web_app + + post_tasks: + - name: Check if the service is healthy + uri: + url: http://{{server_ip}}:{{app_port}} + status_code: 200 # The expected HTTP status code for a healthy service + timeout: 5 # Maximum time to wait for a response in seconds + register: health_check_result + ignore_errors: yes # If the health check fails, the playbook will continue + + - name: Handle the health check result + fail: + msg: "Service is not healthy. Please investigate." + when: health_check_result.status != 200 + +... \ No newline at end of file diff --git a/ansible/playbooks/dev/app_python/main.yml b/ansible/playbooks/dev/app_python/main.yml new file mode 100644 index 000000000..dec31e5e4 --- /dev/null +++ b/ansible/playbooks/dev/app_python/main.yml @@ -0,0 +1,41 @@ +--- +- name: Deploying a python web application to our server + hosts: server + become: yes + + vars: + container_name: "python_time_app" + image: "artkochergin/time-python-app" + app_name: "python_app" + app_port: 8080 + server_ip: 158.160.118.69 + + pre_tasks: + - name: Check for the existance of /{{app_name}} repository. + ansible.builtin.stat: + path: "/{{app_name}}/docker-compose" + register: compose_dir_status + + - name: Stoping Docker compose if the directory exists + command: docker-compose -p {{ app_name }} stop + args: + chdir: "/{{app_name}}/docker-compose" + when: compose_dir_status.stat.exists + + roles: + - web_app + + post_tasks: + - name: Check if the service is healthy + uri: + url: http://{{server_ip}}:{{app_port}} + status_code: 200 # The expected HTTP status code for a healthy service + timeout: 5 # Maximum time to wait for a response in seconds + register: health_check_result + ignore_errors: yes # If the health check fails, the playbook will continue + + - name: Handle the health check result + fail: + msg: "Service is not healthy. Please investigate." + when: health_check_result.status != 200 +... \ No newline at end of file diff --git a/ansible/playbooks/dev/main.yaml b/ansible/playbooks/dev/main.yaml new file mode 100644 index 000000000..7ebda0701 --- /dev/null +++ b/ansible/playbooks/dev/main.yaml @@ -0,0 +1 @@ +# Pinging the hosts, in order to check they're up. diff --git a/ansible/roles/docker/README.md b/ansible/roles/docker/README.md new file mode 100644 index 000000000..3b9f9035f --- /dev/null +++ b/ansible/roles/docker/README.md @@ -0,0 +1,23 @@ +# Docker role description +> The tasks I defined are responsible for adding the Docker GPG apt key, adding the Docker repository to the list of available package sources, and then updating the package cache and installing the docker-ce package on a target Ubuntu Linux system. Also, installing pip and docker-compose. + +## Tasks + +1. install_pip.yml + - install pip + +2. install_docker.yml + - add GPG apt key ( for protection ) + - add Docker repository + - Update cache and install docker-ce + +3. install_compose.yml + - installing docker compose using pip + +## Usage + +In order to apply the configurations to the server, you should run the following cmd : + +```sh +ansible-playbook /Users/m4k4rich/core-course-labs/ansible/playbooks/dev/main.yaml +``` diff --git a/ansible/roles/docker/defaults/main.yml b/ansible/roles/docker/defaults/main.yml new file mode 100644 index 000000000..b89fcf281 --- /dev/null +++ b/ansible/roles/docker/defaults/main.yml @@ -0,0 +1,5 @@ +# Docker version to install +docker_version: "latest" + +# Docker Compose version to install +docker_compose_version: "latest" diff --git a/ansible/roles/docker/handlers/main.yml b/ansible/roles/docker/handlers/main.yml new file mode 100644 index 000000000..c8390040e --- /dev/null +++ b/ansible/roles/docker/handlers/main.yml @@ -0,0 +1,7 @@ +--- +# Used in order to restart services or take +# other actions when specific tasks notify them. +- name: Restart Docker + service: + name: docker + state: restarted \ No newline at end of file diff --git a/ansible/roles/docker/meta/main.yml b/ansible/roles/docker/meta/main.yml new file mode 100644 index 000000000..f984ca89a --- /dev/null +++ b/ansible/roles/docker/meta/main.yml @@ -0,0 +1,16 @@ +galaxy_info: + author: Makar Rabotiaev + description: Installing and deploying Docker + + license: MIT + + min_ansible_version: 2.15.4 + + platforms: + - name: Ubuntu + versions: + - "all" + + galaxy_tags: [] + +dependencies: [] \ No newline at end of file diff --git a/ansible/roles/docker/tasks/install_compose.yml b/ansible/roles/docker/tasks/install_compose.yml new file mode 100644 index 000000000..d6b914004 --- /dev/null +++ b/ansible/roles/docker/tasks/install_compose.yml @@ -0,0 +1,6 @@ +--- +- name: Install Docker Compose using pip + pip: + name: docker-compose + state: present + become: true diff --git a/ansible/roles/docker/tasks/install_docker.yml b/ansible/roles/docker/tasks/install_docker.yml new file mode 100644 index 000000000..26fee7b74 --- /dev/null +++ b/ansible/roles/docker/tasks/install_docker.yml @@ -0,0 +1,15 @@ +--- +- name: Add Docker GPG apt Key + apt_key: + url: https://download.docker.com/linux/ubuntu/gpg + state: present + +- name: Add Docker Repository + apt_repository: + repo: deb https://download.docker.com/linux/ubuntu bionic stable + state: present + +- name: Update apt and install docker-ce + apt: update_cache=yes name=docker-ce state=latest + + diff --git a/ansible/roles/docker/tasks/install_pip.yml b/ansible/roles/docker/tasks/install_pip.yml new file mode 100644 index 000000000..eacaebbe6 --- /dev/null +++ b/ansible/roles/docker/tasks/install_pip.yml @@ -0,0 +1,6 @@ +--- +- name: Install pip + apt: + name: python3-pip + state: present + become: true diff --git a/ansible/roles/docker/tasks/main.yml b/ansible/roles/docker/tasks/main.yml new file mode 100644 index 000000000..9813c4d8d --- /dev/null +++ b/ansible/roles/docker/tasks/main.yml @@ -0,0 +1,4 @@ +--- +- include_tasks: install_pip.yml +- include_tasks: install_docker.yml +- include_tasks: install_compose.yml diff --git a/ansible/roles/web_app/defaults/main.yml b/ansible/roles/web_app/defaults/main.yml new file mode 100644 index 000000000..ed528ab45 --- /dev/null +++ b/ansible/roles/web_app/defaults/main.yml @@ -0,0 +1,5 @@ +container_name: "python_time_app" +image: "m4k4rich/time-python-app" +web_app_full_wipe: true +app_name: "python_app" +app_port: 8080 \ No newline at end of file diff --git a/ansible/roles/web_app/handlers/main.yml b/ansible/roles/web_app/handlers/main.yml new file mode 100644 index 000000000..e69de29bb diff --git a/ansible/roles/web_app/meta/main.yml b/ansible/roles/web_app/meta/main.yml new file mode 100644 index 000000000..ded3284df --- /dev/null +++ b/ansible/roles/web_app/meta/main.yml @@ -0,0 +1,3 @@ +dependencies: + - role: docker + become: true \ No newline at end of file diff --git a/ansible/roles/web_app/tasks/0-wipe.yml b/ansible/roles/web_app/tasks/0-wipe.yml new file mode 100644 index 000000000..e36770de4 --- /dev/null +++ b/ansible/roles/web_app/tasks/0-wipe.yml @@ -0,0 +1,33 @@ +--- +- name: Wipe logic, used to remove the Docker container and all related files + tags: wipe_process + when: web_app_full_wipe + block: + - name: Check for the existance of /{{app_name}} + ansible.builtin.stat: + path: "/{{ app_name }}" + register: web_app_compose_dir + + - name: Check for the existance of Docker Compose File + ansible.builtin.stat: + path: "/{{ app_name }}/docker-compose.yml" + register: compose_status + + - name: Removing existing Docker Compose containers + ansible.builtin.command: docker-compose -p {{app_name}} down --remove-orphans + args: + chdir: "/{{ app_name }}" + when: compose_status.stat.exists == true + + - name: Removing existing Docker Compose files + ansible.builtin.file: + path: "/{{ app_name }}/docker-compose.yml" + state: absent + when: compose_status.stat.exists == true + + - name: Remove app directory /{{ app_name }} + when: web_app_compose_dir.stat.exists + ansible.builtin.file: + path: "/{{ app_name }}" + +... \ No newline at end of file diff --git a/ansible/roles/web_app/tasks/main.yml b/ansible/roles/web_app/tasks/main.yml new file mode 100644 index 000000000..daf7c6042 --- /dev/null +++ b/ansible/roles/web_app/tasks/main.yml @@ -0,0 +1,25 @@ +--- +- name: Deployment logic, used to deploy a python web app. + + tags: deploy_process + + block: + + - name: Wiping containers + ansible.builtin.include_tasks: + file: 0-wipe.yml + + - name: Create directory for the {{app_name}} container + file: + path: "/{{app_name}}" + state: directory + + - name: Copy Docker Compose + template: + src: docker-compose.yml.j2 + dest: "/{{app_name}}/docker-compose.yml" + + - name: Run Docker Compose + shell: "cd /{{app_name}} && docker-compose -p {{app_name}} up -d" + +... \ No newline at end of file diff --git a/ansible/roles/web_app/templates/docker-compose.yml.j2 b/ansible/roles/web_app/templates/docker-compose.yml.j2 new file mode 100644 index 000000000..2efdd80f0 --- /dev/null +++ b/ansible/roles/web_app/templates/docker-compose.yml.j2 @@ -0,0 +1,8 @@ +version: '3.8' +services: + {{ app_name }}: + image: "{{ image }}" + container_name: "{{ container_name }}" + ports: + - {{app_port}}:{{app_port}} + restart: unless-stopped \ No newline at end of file diff --git a/app_node/.eslintrc.yml b/app_node/.eslintrc.yml new file mode 100644 index 000000000..92ea31559 --- /dev/null +++ b/app_node/.eslintrc.yml @@ -0,0 +1,12 @@ +env: + browser: true + commonjs: true + es2021: true +plugins: + - "mocha" +ignorePatterns: + - "**/test.js" +extends: eslint:recommended +parserOptions: + ecmaVersion: latest +rules: {} diff --git a/app_node/DOCKER.md b/app_node/DOCKER.md new file mode 100644 index 000000000..02a44793a --- /dev/null +++ b/app_node/DOCKER.md @@ -0,0 +1,23 @@ +# Containerization Lab + +> In this .md file, I describe how I crafted the dockerfile, what best practices I used. How did I build and test an image. How did I push and pull an image, verifying and validating its functionality. + +## Docker application ( `Quick Guide` ) + + > You can find my image on dockerhub, the link is clickable - [my-node-app](https://hub.docker.com/layers/m4k4rich/my-node-app/dev/images/sha256:ae865650ef996ee89da47f6bda8182234f62f24a43d4210e96e0a2fd9db4af51) + + 1. **How to build?** + + - Clone a repository. + - Change directory to app_python. + - Run ```docker build -t registry/name/tag .``` + + 2. **How to pull?** + + - Login in your dockerhub account. + - Run ```docker pull m4k4rich/my-node-app/dev``` + + 3. **How to run?** + + - Pull or build an image first. + - Run ```docker run -p PORT:8080 m4k4rich/my-node-app/dev``` instead of `PORT` specify which port you want to use** diff --git a/app_node/Dockerfile b/app_node/Dockerfile new file mode 100644 index 000000000..49abeb9c5 --- /dev/null +++ b/app_node/Dockerfile @@ -0,0 +1,20 @@ +# Use an official Node.js runtime as a parent image +FROM node:14-alpine + +# Set the working directory in the container +WORKDIR /app + +# Copy all of the application files to the container +COPY . /app + +# Create user and set ownership and permissions as required +RUN adduser myuser -D && chown -R myuser /app + +# Pick the running user +USER myuser + +# Expose the port on which your Node.js app will run +EXPOSE 3000 + +# Start the Node.js application +CMD ["node", "app.js"] diff --git a/app_node/NODE.md b/app_node/NODE.md new file mode 100644 index 000000000..308eb03fc --- /dev/null +++ b/app_node/NODE.md @@ -0,0 +1,28 @@ +# Node.js Web Application +![Node-CI workflow](https://github.com/m4k4rich/core-course-labs/actions/workflows/node-ci.yml/badge.svg) + +> This is a Node.js application that showcases the current time in Moscow, adhering to best practices and coding standards. It has been thoroughly tested, ensuring that the time updates seamlessly upon page refresh. + +## Framework Selection + +**Node.js is an excellent choice for this task for the following reasons:** + +1. Efficiency and Speed: + - Node.js is known for its event-driven, non-blocking I/O architecture. This makes it highly efficient and responsive, ideal for delivering real-time updates like the current time. + +2. Ecosystem: + - Node.js boasts a rich ecosystem of packages and libraries, enabling me to easily implement the required functionality without reinventing the wheel. + +## Adherence to Best Practices +Code Consistency: + - I ensured that the codebase maintained a consistent style, adhering to the established best practices within the Node.js community. + +## Coding Standards + +> While Node.js doesn't enforce strict coding standards, there are some common conventions and best practices followed by developers when writing Node.js code. + +1. Use const and let: + - In this script, const is used for variables that should not be reassigned (http, server, port), and let is used for variables (randomNumber) that may change their value. + +2. Arrow Functions: + - The script uses arrow functions for defining the callback functions, such as (req, res) => {...} and () => {...}. Arrow functions are commonly used in Node.js for concise and clean syntax. diff --git a/app_node/app.js b/app_node/app.js new file mode 100644 index 000000000..de8e1d23a --- /dev/null +++ b/app_node/app.js @@ -0,0 +1,17 @@ +const http = require('http'); + +const server = http.createServer((req, res) => { + // Generate a random number between 1 and 21 + const randomNumber = Math.floor(Math.random() * 21) + 1; + + // Set the response headers + res.writeHead(200, { 'Content-Type': 'text/plain' }); + + // Send the random number as the response + res.end(`Random Number: ${randomNumber}\n`); +}); + +const port = 3000; +server.listen(port, () => { + console.log(`Server is running on http://localhost:${port}`); +}); diff --git a/app_node/package-lock.json b/app_node/package-lock.json new file mode 100644 index 000000000..7eff16630 --- /dev/null +++ b/app_node/package-lock.json @@ -0,0 +1,2248 @@ +{ + "name": "app_node", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "chai": "^4.3.8", + "chai-http": "^4.4.0", + "eslint": "^8.49.0", + "eslint-plugin-mocha": "^10.1.0", + "mocha": "^10.2.0" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.1.tgz", + "integrity": "sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz", + "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", + "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-VOVRLM1mBxIRxydiViqPcKn6MIxZytrbMpd6RJLIWKxUNr3zux8no0Oc7kJx0WAPIitgZ0gkrDS+btlqQpubpw==", + "dev": true + }, + "node_modules/@types/cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.2.tgz", + "integrity": "sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw==", + "dev": true + }, + "node_modules/@types/superagent": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.13.tgz", + "integrity": "sha512-YIGelp3ZyMiH0/A09PMAORO0EBGlF5xIKfDpK74wdYvWUs2o96b5CItJcWPdH409b7SAXIIG6p8NdU/4U2Maww==", + "dev": true, + "dependencies": { + "@types/cookiejar": "*", + "@types/node": "*" + } + }, + "node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.8.tgz", + "integrity": "sha512-vX4YvVVtxlfSZ2VecZgFUTU5qPCYsobVI2O9FmwEXBhDigYGQA6jRXCycIs1yJnnWbZ6/+a2zNIF5DfVCcJBFQ==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^4.1.2", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chai-http": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/chai-http/-/chai-http-4.4.0.tgz", + "integrity": "sha512-uswN3rZpawlRaa5NiDUHcDZ3v2dw5QgLyAwnQ2tnVNuP7CwIsOFuYJ0xR1WiR7ymD4roBnJIzOUep7w9jQMFJA==", + "dev": true, + "dependencies": { + "@types/chai": "4", + "@types/superagent": "4.1.13", + "charset": "^1.0.1", + "cookiejar": "^2.1.4", + "is-ip": "^2.0.0", + "methods": "^1.1.2", + "qs": "^6.11.2", + "superagent": "^8.0.9" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/charset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/charset/-/charset-1.0.1.tgz", + "integrity": "sha512-6dVyOOYjpfFcL1Y4qChrAoQLRHvj2ziyhcm0QJlhOcAhykL/k1kTUPbeo+87MNRTRdk2OIIsIXbuF3x2wi5EXg==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz", + "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.49.0", + "@humanwhocodes/config-array": "^0.11.11", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-mocha": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-10.1.0.tgz", + "integrity": "sha512-xLqqWUF17llsogVOC+8C6/jvQ+4IoOREbN7ZCHuOHuD6cT5cDD4h7f2LgsZuzMAiwswWE21tO7ExaknHVDrSkw==", + "dev": true, + "dependencies": { + "eslint-utils": "^3.0.0", + "rambda": "^7.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", + "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", + "dev": true, + "dependencies": { + "flatted": "^3.2.7", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", + "integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==", + "dev": true, + "dependencies": { + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0", + "qs": "^6.11.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/hexoid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", + "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-2.0.0.tgz", + "integrity": "sha512-9MTn0dteHETtyUx8pxqMwg5hMBi3pvlyglJ+b79KOCca0po23337LbVV2Hl4xmMvfw++ljnO0/+5G6G+0Szh6g==", + "dev": true, + "dependencies": { + "ip-regex": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/keyv": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", + "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loupe": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", + "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.0" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "dev": true, + "dependencies": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/rambda": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/rambda/-/rambda-7.5.0.tgz", + "integrity": "sha512-y/M9weqWAH4iopRd7EHDEQQvpFPHj1AA3oHozE9tfITHUtTR7Z9PSlIRRG2l1GuW7sefC1cXFfIcF+cgnShdBA==", + "dev": true + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/superagent": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz", + "integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==", + "dev": true, + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.1.2", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=6.4.0 <13 || >=14" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/app_node/package.json b/app_node/package.json new file mode 100644 index 000000000..c2c263d45 --- /dev/null +++ b/app_node/package.json @@ -0,0 +1,12 @@ +{ + "scripts": { + "test": "mocha" + }, + "devDependencies": { + "chai": "^4.3.8", + "chai-http": "^4.4.0", + "eslint": "^8.49.0", + "eslint-plugin-mocha": "^10.1.0", + "mocha": "^10.2.0" + } +} diff --git a/app_node/test.js b/app_node/test.js new file mode 100644 index 000000000..a07dc745d --- /dev/null +++ b/app_node/test.js @@ -0,0 +1,41 @@ +const chai = require('chai'); +const chaiHttp = require('chai-http'); +const http = require('http'); +const expect = chai.expect; + +chai.use(chaiHttp); + +describe('Random Number Server', () => { + let server; + + before((done) => { + // Create and start the server before running tests + server = http.createServer((req, res) => { + const randomNumber = Math.floor(Math.random() * 21) + 1; + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end(`Random Number: ${randomNumber}\n`); + }); + const port = 3000; + server.listen(port, () => { + console.log(`Server is running on http://localhost:${port}`); + done(); + }); + }); + + after(() => { + // Close the server after all tests have run + server.close(); + }); + + it('should generate a random number between 1 and 21 exactly once', (done) => { + chai.request(server) + .get('/') + .end((err, res) => { + expect(err).to.be.null; + expect(res).to.have.status(200); + const randomNumber = parseInt(res.text.split(' ')[2]); + expect(randomNumber).to.be.within(1, 21); + done(); + }); + }); +}); diff --git a/app_python/.dockerignore b/app_python/.dockerignore new file mode 100644 index 000000000..5d4f1d892 --- /dev/null +++ b/app_python/.dockerignore @@ -0,0 +1,3 @@ +.gitignore +Dockerfile +.dockerignore \ No newline at end of file diff --git a/app_python/CI.md b/app_python/CI.md new file mode 100644 index 000000000..c02b4d680 --- /dev/null +++ b/app_python/CI.md @@ -0,0 +1,13 @@ +# Best CI Practices + +1. Usage of secrets. + - In order to retrieve sensitive information I use github secrets, which allows me to safely interact with Docker and Snyk api. + +2. Usage of dependend jobs. + - Before building and pushing docker container, the process will need to wait until the security check and lint and test checks are completed. + +3. Security Check. + - I implemented a security check using Snyk. + +4. Caching Mechanism. + - In order to increase the speed of installations I have implement ```actions/setup-python``` action used to cache all the necessary ependencies. \ No newline at end of file diff --git a/app_python/DOCKER.md b/app_python/DOCKER.md new file mode 100644 index 000000000..6db34eb8e --- /dev/null +++ b/app_python/DOCKER.md @@ -0,0 +1,57 @@ +# Containerization Lab + +> In this .md file, I describe how I crafted the dockerfile, what best practices I used. How did I build and test an image. How did I push and pull an image, verifying and validating its functionality. + +## Docker application ( `Quick Guide` ) + + > You can find my image on dockerhub, the link is clickable - [my-python-app](https://hub.docker.com/layers/m4k4rich/my-python-app/dev/images/sha256:02eb0a9d5e9925897024fd996ee796a9324dfe035fa59b2b23286f78eecfcb67) + + 1. **How to build?** + + - Clone a repository. + - Change directory to app_python. + - Run ```docker build -t registry/name/tag .``` + + 2. **How to pull?** + + - Login in your dockerhub account. + - Run ```docker pull m4k4rich/my-python-app/dev``` + + 3. **How to run?** + + - Pull or build an image first. + - Run ```docker run -p PORT:8080 m4k4rich/my-python-app/dev``` instead of `PORT` specify which port you want to use** + +## Dockerizing application ( `Thorough Explantion` ) + +**Here are the steps I took in order to successfuly dockerize an app :** + +1. Creating dockerfile and building an image : + + > Inside the `app_python` folder, I crafted a `Dockerfile` which followed and implement Docker best practices. + + - `Non-Root User` : enforced a non root user for improved security + + - `Grouped Commands` : grouped commands to reduce number of layers + + - `Small Base Image` : python:alpine only contains the required packages for the + application to run + + - `Official Image` : python:alpine is an official and well-maintained Python image. + + - `Version Tagging` : tagged our application `dev` + + - `.dockerignore` : used to exclude all the files which are not useful + + - `hadolint` : linter used for quality assurance + + ![Screenshot](images/building.png) + +2. Pushing, Pulling, Running and Verifying Image : + > I loged into docker hub, taged an image and pushed it inside. + + ![Screenshot](images/pushing.png) + + > After removing the existing image, I pulled the image from the docker hub, ran it, and validated its functionality. + + ![Screenshot](images/pulling.png) diff --git a/app_python/Dockerfile b/app_python/Dockerfile new file mode 100644 index 000000000..264841d7c --- /dev/null +++ b/app_python/Dockerfile @@ -0,0 +1,22 @@ +# Use an official Python runtime as a parent image +FROM python:3-alpine3.15 + +# Set the working directory in the container +WORKDIR /app + +# Copy all of the application files in the container +COPY . /app + +# Create user and set ownership and permissions as required +# Install any needed packages specified in requirements.txt +RUN adduser myuser -D && chown -R myuser /app \ + && pip install --no-cache-dir -r requirements.txt + +# Pick the running user +USER myuser + +# Expose the port of the Flask app +EXPOSE 8080 + +# Start the Flask app +CMD ["python", "./app.py"] diff --git a/app_python/PYTHON.md b/app_python/PYTHON.md new file mode 100644 index 000000000..5dd5f2260 --- /dev/null +++ b/app_python/PYTHON.md @@ -0,0 +1,102 @@ +# Python Web Application +![Python-CI workflow](https://github.com/m4k4rich/core-course-labs/actions/workflows/python-ci.yml/badge.svg) + +> This is a python app that displays a current time in Moscow, it implements best practices and follows coding standarts. It was tested and the time updates upon page refreshing. + +## Framework choice + +> For this task I choose to use Flask as a framework. + +**Flask is a good framework for this task because:** + +1. `Simplicity` : + - It is minimalistic and doesn't come with unnecessary features. For a small application like this, simplicity is an advantage. + +2. `Flexibility` : + - There is no rigid structure, that allows me to organize the project as I see fit, More precisely I was able to use `html`, and did not have the need to learn any custom markup language instead. + +## Best practices + +1. `PEP 8`: + - I followed PEP 8 guidelines. + +2. `Zen of Python`: + - In order to uphold best practices I followed 'Zen of Python' + + +## Coding standards + +> PEP 8 coding standads were used. Below the description of some coding standards that I used. + +1. `Tabs or spaces`: + - I used spaces as it it a preferred indentation method. + +2. `Maximum Line Length`: + - My maximum line length did not exceed 79 characters. + +3. `Imports`: + - Import are on separate lines. + +4. `Whitespace in expressions and statements`: + - All extraneous whitespace are avoided. + +# Unit Tests for Flask Application + +This document describes the unit tests for the Flask application that generates and displays the current time in the 'Europe/Moscow' timezone. + +## Test Coverage + +The unit tests cover the following aspects of the Flask application: + +1. **HTTP Status Code**: The test checks if the HTTP status code of the response is 200, indicating a successful response. + +2. **Template Usage**: The test ensures that the 'time.html' template is used for rendering the response. The response is taken, and the absolute difference between displayed time and actual time is checked. In case if it is higher than 5 seconds, the exceptions is thrown. + +You can extend the test suite to cover additional aspects of the application, such as checking the time format, verifying the correct timezone, or testing edge cases. + +## Unit Testing Best Practices + + 1. Isolation of Tests + - Each test method (`test_display_time_status_code` and `test_display_time_difference`) focuses on testing a specific aspect of the application's functionality. This ensures that each test is isolated and independent, making it easier to pinpoint issues when failures occur. + + 2. Test Setup with `setUp` + - The `setUp` method is utilized to set up any common resources or configurations needed for the tests. In this case, it initializes a test client for the Flask application, ensuring that the client is available for all test methods. + ```python + def setUp(self): + self.app = app.test_client() + ``` + + 3. Clear and Descriptive Test Method Names + - The test method names are clear and descriptive, indicating what aspect of the application's functionality is being tested. For example, test_display_time_status_code tests the HTTP status code, and test_display_time_difference tests the time difference calculation + + 4. Assertions for Expected Outcomes + - The unittest library's assert methods are used to make assertions about expected outcomes. For instance, self.assertEqual(response.status_code, 200) checks if the HTTP status code is 200, indicating a successful response. + + 5. Printing Informative Messages + - Informative messages are printed to the console upon test success or failure. For example, the absolute time difference is printed for the test_display_time_difference test. + ```python + print("\nThe absolute time difference is : " + str(abs(time_difference))) + ``` + 6. Main Guard for Test Execution + - The if ```__name__ == '__main__'```: block ensures that the tests are executed when the script is run directly. This allows running the tests without the need for external test runners. + ```python + if __name__ == '__main__': + unittest.main() + ``` + +## Running the Unit Tests + +To run the unit tests, follow these steps: + +1. Ensure that you have the necessary Python packages installed. You can use `pip` to install them if needed: + +```bash +pip install Flask unittest +``` + +2. Run the test script. + +```bash +python test_app.py +``` + diff --git a/app_python/app.py b/app_python/app.py new file mode 100644 index 000000000..a8e9039c3 --- /dev/null +++ b/app_python/app.py @@ -0,0 +1,17 @@ +from flask import Flask, render_template +import datetime +import pytz + +app = Flask(__name__) + + +@app.route('/') +def display_time(): + moscow_tz = pytz.timezone('Europe/Moscow') + current_time = datetime.datetime.now(moscow_tz) + formatted_time = current_time.strftime('%Y-%m-%d %H:%M:%S') + return render_template('time.html', time=formatted_time) + + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=8080, debug=True) diff --git a/app_python/images/building.png b/app_python/images/building.png new file mode 100644 index 000000000..8e9724545 Binary files /dev/null and b/app_python/images/building.png differ diff --git a/app_python/images/pulling.png b/app_python/images/pulling.png new file mode 100644 index 000000000..28b472c05 Binary files /dev/null and b/app_python/images/pulling.png differ diff --git a/app_python/images/pushing.png b/app_python/images/pushing.png new file mode 100644 index 000000000..f43c76786 Binary files /dev/null and b/app_python/images/pushing.png differ diff --git a/app_python/requirements.txt b/app_python/requirements.txt new file mode 100644 index 000000000..81407b818 --- /dev/null +++ b/app_python/requirements.txt @@ -0,0 +1,10 @@ +beautifulsoup4==4.12.2 +blinker==1.6.2 +click==8.1.7 +Flask==2.3.3 +itsdangerous==2.1.2 +Jinja2==3.1.2 +MarkupSafe==2.1.3 +pytz==2023.3.post1 +soupsieve==2.5 +Werkzeug==2.3.7 diff --git a/app_python/templates/time.html b/app_python/templates/time.html new file mode 100644 index 000000000..261296957 --- /dev/null +++ b/app_python/templates/time.html @@ -0,0 +1,11 @@ + + + + Current Time in Moscow + + +

Current Time in Moscow:

+

{{ time }}

+

Refresh the page to update the time.

+ + diff --git a/app_python/test_app.py b/app_python/test_app.py new file mode 100644 index 000000000..ac7f89052 --- /dev/null +++ b/app_python/test_app.py @@ -0,0 +1,54 @@ +import unittest +from app import app +import datetime +import pytz +from bs4 import BeautifulSoup + + +class TestDisplayTime(unittest.TestCase): + + def setUp(self): + self.app = app.test_client() + + def test_display_time_status_code(self): + response = self.app.get('/') + self.assertEqual(response.status_code, 200) + + def test_display_time_difference(self): + response = self.app.get('/') + self.assertEqual(response.status_code, 200) + + # Use BeautifulSoup to parse the HTML response + soup = BeautifulSoup( + response.get_data(as_text=True), + 'html.parser' + ) + + # Find the

element containing the time + time_paragraph = soup.find('p') + + # Extract the time string from the

element + app_time_str = time_paragraph.text.strip() + + # Parse the time string into a datetime object (offset-aware) + app_time = datetime.datetime.strptime( + app_time_str, '%Y-%m-%d %H:%M:%S' + ) + app_time = pytz.timezone('Europe/Moscow').localize(app_time) + + # Get the current time in Moscow (offset-aware) + current_time_moscow = datetime.datetime.now( + pytz.timezone('Europe/Moscow') + ) + + # Calculate the time difference + time_difference = app_time - current_time_moscow + print("\nThe absolute time difference is: " + + str(abs(time_difference))) + self.assertLessEqual( + abs(time_difference.total_seconds()), 5 + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/monitoring/LOGGING.md b/monitoring/LOGGING.md new file mode 100644 index 000000000..f1430b8ea --- /dev/null +++ b/monitoring/LOGGING.md @@ -0,0 +1,44 @@ + +# Lab 7: Monitoring and Logging + +## Task 1: Logging Stack Setup + +### 1. Introduction & Logging Stack Components + +The goal of this task is to prepare monitoring and visualising environment for the python and node app, as a network of connected containers (application containers & grafana & loki with promtail ). Then I will configure Loki, in order for it to monitor logs from all the running containers. And finally I wil use Grafana dashboard to visualize the scraped data. + +In order to visualiase and analyse logs, I will use **Grafana** it provides a nice UI for creating and cutomizing dashboards with panels. Also, it visualizes **metrics** and **logs** collected by a monitoring solution ( in our case it's going to be **Prometheus** and **Grafana Loki**). + +In order to monitor my apps, I will use **Grafana Loki** - this is a monitoring solution like Prometheus, but focused on application logs. Those logs are collected by clients, instead of general metrics. + +### 2. Demo + +All I need to do to get everything started is just to run `docker-compose up`, and verify that all containers are running. + +- Verify that the python application is running at *http://localhost:8080* +![Python App](img/python-app.png) + +- Verify that the node application is running at *http://localhost:3001* +![Node App](img/node-app.png) + +- Verify that Grafana UI is running at *http://localhost:8081* +![Grafana UI](img/grafana.png) + +1) After that I added data source, using the required url *http://loki:3100* + +The final result of seeing the container logs will look like this : + +![Grafana Logs](img/grafana_results.png) + +### 3. Dashboards + +Now, we can finally make use of all the data collected by Loki. We can visualise the data using dashboard and also we can expor the data as a reusable JSON. + +### 4. Best Practices + +- Utilize official documentation from Grafana and Loki for guidance. +- Craft logs that are detailed and adhere to a standardized format. +- Employ log rotation strategies to optimize memory and disk space usage. +- Develop straightforward dashboards with clear, meaningful titles. +- When setting up alert configurations, aim for activation only when necessary. +- Minimize dashboard reloading to alleviate network load and enhance efficiency. diff --git a/monitoring/config/loki.yml b/monitoring/config/loki.yml new file mode 100644 index 000000000..98f6383a1 --- /dev/null +++ b/monitoring/config/loki.yml @@ -0,0 +1,27 @@ +# https://github.com/grafana/loki/blob/main/cmd/loki/loki-docker-config.yaml +--- +auth_enabled: false + +server: + http_listen_port: 3100 + +common: + path_prefix: /loki + storage: + filesystem: + chunks_directory: /loki/chunks + rules_directory: /loki/rules + replication_factor: 1 + ring: + kvstore: + store: inmemory + +schema_config: + configs: + - from: 2020-10-24 + store: boltdb-shipper + object_store: filesystem + schema: v11 + index: + prefix: index_ + period: 24h \ No newline at end of file diff --git a/monitoring/config/prometheus.yml b/monitoring/config/prometheus.yml new file mode 100644 index 000000000..8f7ad8366 --- /dev/null +++ b/monitoring/config/prometheus.yml @@ -0,0 +1,24 @@ +# https://github.com/prometheus/prometheus/blob/main/documentation/examples/prometheus.yml +--- +global: + scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. + evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. + # scrape_timeout is set to the global default (10s). + +scrape_configs: + # The job name is added as a label `job=` to any timeseries scraped from this config. + - job_name: "prometheus" + + # metrics_path defaults to '/metrics' + # scheme defaults to 'http'. + + static_configs: + - targets: ["localhost:9090"] + + - job_name: "loki" + static_configs: + - targets: [loki:3100] + + - job_name: "app" + static_configs: + - targets: [loki:5000] \ No newline at end of file diff --git a/monitoring/config/promtail.yml b/monitoring/config/promtail.yml new file mode 100644 index 000000000..2763f91d3 --- /dev/null +++ b/monitoring/config/promtail.yml @@ -0,0 +1,55 @@ +# https://github.com/grafana/loki/blob/main/clients/cmd/promtail/promtail-docker-config.yaml +# https://gist.github.com/ruanbekker/c6fa9bc6882e6f324b4319c5e3622460 +--- +server: + http_listen_address: 0.0.0.0 + http_listen_port: 9080 + +positions: + filename: /tmp/positions.yaml + +clients: + - url: http://loki:3100/loki/api/v1/push + +scrape_configs: + - job_name: system + static_configs: + - targets: + - localhost + labels: + job: varlogs + __path__: /var/log/*log + + - job_name: containers + static_configs: + - targets: + - localhost + labels: + job: containerlogs + __path__: /var/lib/docker/containers/*/*log + + pipeline_stages: + - json: + expressions: + output: log + stream: stream + attrs: + - json: + expressions: + tag: + source: attrs + - regex: + expression: (?P(?:[^|]*[^|])).(?P(?:[^|]*[^|])).(?P(?:[^|]*[^|])).(?P(?:[^|]*[^|])) + source: tag + - timestamp: + format: RFC3339Nano + source: time + - labels: + tag: + stream: + image_name: + container_name: + image_id: + container_id: + - output: + source: output \ No newline at end of file diff --git a/monitoring/docker-compose.yml b/monitoring/docker-compose.yml new file mode 100644 index 000000000..13b80dfe3 --- /dev/null +++ b/monitoring/docker-compose.yml @@ -0,0 +1,56 @@ +--- +version: "3.6" + +networks: + loki: + +services: + app: + image: artkochergin/app-python-time:arm64-tag + ports: + - "8080:8080" + networks: + - loki + + app2: + image: artkochergin/app-node-random:arm64-tag + ports: + - "3001:3000" + networks: + - loki + + prometheus: + image: prom/prometheus:latest + ports: + - "9090:9090" + volumes: + - ./config:/mnt/config + command: --config.file=/mnt/config/prometheus.yml + networks: + - loki + + loki: + image: grafana/loki:latest + ports: + - "3100:3100" + volumes: + - ./config:/mnt/config + command: -config.file=/mnt/config/loki.yml + networks: + - loki + + promtail: + image: grafana/promtail:2.5.0 + volumes: + - /var/lib/docker/containers:/var/lib/docker/containers + - ./config:/mnt/config + command: -config.file=/mnt/config/promtail.yml + networks: + - loki + + grafana: + image: grafana/grafana:latest + ports: + - "8081:3000" + networks: + - loki \ No newline at end of file diff --git a/monitoring/img/grafana.png b/monitoring/img/grafana.png new file mode 100644 index 000000000..c63032ecb Binary files /dev/null and b/monitoring/img/grafana.png differ diff --git a/monitoring/img/grafana_results.png b/monitoring/img/grafana_results.png new file mode 100644 index 000000000..df77529f6 Binary files /dev/null and b/monitoring/img/grafana_results.png differ diff --git a/monitoring/img/node-app.png b/monitoring/img/node-app.png new file mode 100644 index 000000000..8e8fc885b Binary files /dev/null and b/monitoring/img/node-app.png differ diff --git a/monitoring/img/python-app.png b/monitoring/img/python-app.png new file mode 100644 index 000000000..93637a50a Binary files /dev/null and b/monitoring/img/python-app.png differ diff --git a/terraform/TF.md b/terraform/TF.md new file mode 100644 index 000000000..3d6c702ba --- /dev/null +++ b/terraform/TF.md @@ -0,0 +1,536 @@ +# Infrastructure as Code Lab + +> All the necessary files for each task are distributed to required folders. There is a main.tf, variables.tf and outputs.tf. Below, there is a result from running required commands after initialising, planning and applying terraform. + +## Best Practices Applied : + +> In this module I describe all the best practices I used throughout completing the task. + + 1. *Modularization* : I have organized infrastructure code into separate modules for Docker, Yandex Cloud, GitHub, and GitHub teams. This promotes reusability and maintainability. + + 2. *Module Descriptions* : Each module has a meaningful and concise description (e.g., module "nginx_server"). This helps in understanding the purpose of each module. + + 3. *Variable Usage* : I'm using variables like var.github_token to make my code more flexible and avoid hardcoding values directly into configuration. + + 4. *Source Paths* : I am specifying source paths for each module using relative paths (e.g., source = "./docker"). + + 5. *Explicit Dependency* : There's an explicit dependency relationship between modules. For example, the github-teams module depends on the github module. + + 6. *Descriptive Names* : I'm using descriptive names for resources, like container_name = "my-nginx-app". This makes it easier to understand the purpose of each resource. + + 8. *Consistent Naming Convention* : I tried to keep my naming conventions consistent. + + 9. *Provider Configuration* : I configure my Terraform providers properly, including authentication and region settings. + +## Task 1: Docker Infrastructure Using Terraform + +#### terraform state list + +```sh +terraform state list +``` + +``` +docker_container.nginx +docker_image.nginx +``` + +#### terraform state show + +```sh +terraform state show docker_container.nginx + +# docker_container.nginx: +resource "docker_container" "nginx" { + attach = false + command = [ + "nginx", + "-g", + "daemon off;", + ] + container_read_refresh_timeout_milliseconds = 15000 + cpu_shares = 0 + entrypoint = [ + "/docker-entrypoint.sh", + ] + env = [] + hostname = "b98f7cec04c9" + id = "b98f7cec04c9fef0005afa52be4005e3e2d6db9267d0e4b7fa82c2d9f9aa8292" + image = "sha256:2a4fbb36e96607b16e5af2e24dc6a1025a4795520c98c6b9ead9c4113617cb73" + init = false + ipc_mode = "private" + log_driver = "json-file" + logs = false + max_retry_count = 0 + memory = 0 + memory_swap = 0 + must_run = true + name = "my-nginx-app" + network_data = [ + { + gateway = "172.17.0.1" + global_ipv6_address = "" + global_ipv6_prefix_length = 0 + ip_address = "172.17.0.2" + ip_prefix_length = 16 + ipv6_gateway = "" + mac_address = "02:42:ac:11:00:02" + network_name = "bridge" + }, + ] + network_mode = "default" + privileged = false + publish_all_ports = false + read_only = false + remove_volumes = true + restart = "no" + rm = false + runtime = "runc" + security_opts = [] + shm_size = 64 + start = true + stdin_open = false + stop_signal = "SIGQUIT" + stop_timeout = 0 + tty = false + wait = false + wait_timeout = 60 + + ports { + external = 8000 + internal = 80 + ip = "0.0.0.0" + protocol = "tcp" + } +} +``` + +```sh +terraform state show docker_image.nginx + +# docker_image.nginx: +resource "docker_image" "nginx" { + id = "sha256:2a4fbb36e96607b16e5af2e24dc6a1025a4795520c98c6b9ead9c4113617cb73nginx:latest" + image_id = "sha256:2a4fbb36e96607b16e5af2e24dc6a1025a4795520c98c6b9ead9c4113617cb73" + keep_locally = false + name = "nginx:latest" + repo_digest = "nginx@sha256:32da30332506740a2f7c34d5dc70467b7f14ec67d912703568daff790ab3f755" +} +``` + +#### log with the applied changes + +```sh +❯ terraform apply +var.container_name + Enter a value: random-container + +var.image_name + Enter a value: nginx:latest + +docker_image.nginx: Refreshing state... [id=sha256:2a4fbb36e96607b16e5af2e24dc6a1025a4795520c98c6b9ead9c4113617cb73nginx:latest] +docker_container.nginx: Refreshing state... [id=0b8770d6fa197cbd5d43f01a4d19ad3165d74a9908c92e9db41a3c9dd5613e2f] + +Note: Objects have changed outside of Terraform + +Terraform detected the following changes made outside of Terraform since the last "terraform apply" which may have affected this plan: + + # docker_container.nginx has been deleted + - resource "docker_container" "nginx" { + - id = "0b8770d6fa197cbd5d43f01a4d19ad3165d74a9908c92e9db41a3c9dd5613e2f" -> null + name = "random-container-name" + # (16 unchanged attributes hidden) + + # (1 unchanged block hidden) + } + + +Unless you have made equivalent changes to your configuration, or ignored the relevant attributes using ignore_changes, the following plan may include actions to undo or respond to these changes. + +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + +Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + + create + +Terraform will perform the following actions: + + # docker_container.nginx will be created + + resource "docker_container" "nginx" { + + attach = false + + bridge = (known after apply) + + command = (known after apply) + + container_logs = (known after apply) + + container_read_refresh_timeout_milliseconds = 15000 + + entrypoint = (known after apply) + + env = (known after apply) + + exit_code = (known after apply) + + hostname = (known after apply) + + id = (known after apply) + + image = "sha256:2a4fbb36e96607b16e5af2e24dc6a1025a4795520c98c6b9ead9c4113617cb73" + + init = (known after apply) + + ipc_mode = (known after apply) + + log_driver = (known after apply) + + logs = false + + must_run = true + + name = "random-container" + + network_data = (known after apply) + + read_only = false + + remove_volumes = true + + restart = "no" + + rm = false + + runtime = (known after apply) + + security_opts = (known after apply) + + shm_size = (known after apply) + + start = true + + stdin_open = false + + stop_signal = (known after apply) + + stop_timeout = (known after apply) + + tty = false + + wait = false + + wait_timeout = 60 + + + ports { + + external = 8000 + + internal = 80 + + ip = "0.0.0.0" + + protocol = "tcp" + } + } + +Plan: 1 to add, 0 to change, 0 to destroy. + +Changes to Outputs: + + container_id = (known after apply) + +Do you want to perform these actions? + Terraform will perform the actions described above. + Only 'yes' will be accepted to approve. + + Enter a value: yes + +docker_container.nginx: Creating... +docker_container.nginx: Creation complete after 1s [id=8afb3b8aa35610fa13dadd8d09a22cbb889b6650e32ac9100c6cec7c7c2d4544] + +Apply complete! Resources: 1 added, 0 changed, 0 destroyed. + +Outputs: + +container_id = "8afb3b8aa35610fa13dadd8d09a22cbb889b6650e32ac9100c6cec7c7c2d4544" +image_id = "sha256:2a4fbb36e96607b16e5af2e24dc6a1025a4795520c98c6b9ead9c4113617cb73nginx:latest" +``` + +#### terraform output + +``` +❯ terraform output +container_id = "8afb3b8aa35610fa13dadd8d09a22cbb889b6650e32ac9100c6cec7c7c2d4544" +image_id = "sha256:2a4fbb36e96607b16e5af2e24dc6a1025a4795520c98c6b9ead9c4113617cb73nginx:latest" +``` + +## Task 2: Yandex Cloud Infrastructure Using Terraform + +#### terraform state list + +```sh +❯ terraform state list +yandex_compute_instance.vm-1 +yandex_compute_instance.vm-2 +yandex_vpc_network.network-1 +yandex_vpc_subnet.subnet-1 +``` +#### terraform state show + +```sh +❯ terraform state show yandex_vpc_network.network-1 +# yandex_vpc_network.network-1: +resource "yandex_vpc_network" "network-1" { + created_at = "2023-09-27T03:08:02Z" + default_security_group_id = "enpgp3p6f2e4gavut1vf" + folder_id = "b1g1aqrohbot4fl3e1vb" + id = "enp7ckctka4ce3i3lnlh" + labels = {} + name = "network1" + subnet_ids = [] +} + +❯ terraform state show yandex_vpc_subnet.subnet-1 +# yandex_vpc_subnet.subnet-1: +resource "yandex_vpc_subnet" "subnet-1" { + created_at = "2023-09-27T03:08:05Z" + folder_id = "b1g1aqrohbot4fl3e1vb" + id = "e9b2qbs7k3kul1bo22qo" + labels = {} + name = "subnet1" + network_id = "enp7ckctka4ce3i3lnlh" + v4_cidr_blocks = [ + "192.168.10.0/24", + ] + v6_cidr_blocks = [] + zone = "ru-central1-a" +} + +❯ terraform state show yandex_compute_instance.vm-1 +# yandex_compute_instance.vm-1: +resource "yandex_compute_instance" "vm-1" { + created_at = "2023-09-27T03:08:08Z" + folder_id = "b1g1aqrohbot4fl3e1vb" + fqdn = "fhm6n20l4geif7u8e72r.auto.internal" + id = "fhm6n20l4geif7u8e72r" + name = "my-vm-1" + network_acceleration_type = "standard" + platform_id = "standard-v1" + status = "running" + zone = "ru-central1-a" + + boot_disk { + auto_delete = true + device_name = "fhm39e712ct1njgagot2" + disk_id = "fhm39e712ct1njgagot2" + mode = "READ_WRITE" + + initialize_params { + block_size = 4096 + image_id = "fd82sqrj4uk9j7vlki3q" + size = 8 + type = "network-hdd" + } + } + + metadata_options { + aws_v1_http_endpoint = 1 + aws_v1_http_token = 2 + gce_http_endpoint = 1 + gce_http_token = 1 + } + + network_interface { + index = 0 + ip_address = "192.168.10.17" + ipv4 = true + ipv6 = false + mac_address = "d0:0d:6b:88:15:24" + nat = true + nat_ip_address = "158.160.109.49" + nat_ip_version = "IPV4" + security_group_ids = [] + subnet_id = "e9b2qbs7k3kul1bo22qo" + } + + placement_policy { + host_affinity_rules = [] + } + + resources { + core_fraction = 100 + cores = 2 + gpus = 0 + memory = 2 + } + + scheduling_policy { + preemptible = false + } +} +``` + +#### log with the applied changes + +```sh +❯ terraform apply + +Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + + create + +Terraform will perform the following actions: + + # yandex_compute_instance.vm-1 will be created + + resource "yandex_compute_instance" "vm-1" { + + created_at = (known after apply) + + folder_id = (known after apply) + + fqdn = (known after apply) + + gpu_cluster_id = (known after apply) + + hostname = (known after apply) + + id = (known after apply) + + name = "my-vm-1" + + network_acceleration_type = "standard" + + platform_id = "standard-v1" + + service_account_id = (known after apply) + + status = (known after apply) + + zone = (known after apply) + + + boot_disk { + + auto_delete = true + + device_name = (known after apply) + + disk_id = (known after apply) + + mode = (known after apply) + + + initialize_params { + + block_size = (known after apply) + + description = (known after apply) + + image_id = "fd82sqrj4uk9j7vlki3q" + + name = (known after apply) + + size = (known after apply) + + snapshot_id = (known after apply) + + type = "network-hdd" + } + } + + + network_interface { + + index = (known after apply) + + ip_address = (known after apply) + + ipv4 = true + + ipv6 = (known after apply) + + ipv6_address = (known after apply) + + mac_address = (known after apply) + + nat = true + + nat_ip_address = (known after apply) + + nat_ip_version = (known after apply) + + security_group_ids = (known after apply) + + subnet_id = (known after apply) + } + + + resources { + + core_fraction = 100 + + cores = 2 + + memory = 2 + } + } + + # yandex_compute_instance.vm-2 will be created + + resource "yandex_compute_instance" "vm-2" { + + created_at = (known after apply) + + folder_id = (known after apply) + + fqdn = (known after apply) + + gpu_cluster_id = (known after apply) + + hostname = (known after apply) + + id = (known after apply) + + name = "my-vm-2" + + network_acceleration_type = "standard" + + platform_id = "standard-v1" + + service_account_id = (known after apply) + + status = (known after apply) + + zone = (known after apply) + + + boot_disk { + + auto_delete = true + + device_name = (known after apply) + + disk_id = (known after apply) + + mode = (known after apply) + + + initialize_params { + + block_size = (known after apply) + + description = (known after apply) + + image_id = "fd82sqrj4uk9j7vlki3q" + + name = (known after apply) + + size = (known after apply) + + snapshot_id = (known after apply) + + type = "network-hdd" + } + } + + + network_interface { + + index = (known after apply) + + ip_address = (known after apply) + + ipv4 = true + + ipv6 = (known after apply) + + ipv6_address = (known after apply) + + mac_address = (known after apply) + + nat = true + + nat_ip_address = (known after apply) + + nat_ip_version = (known after apply) + + security_group_ids = (known after apply) + + subnet_id = (known after apply) + } + + + resources { + + core_fraction = 100 + + cores = 4 + + memory = 4 + } + } + + # yandex_vpc_network.network-1 will be created + + resource "yandex_vpc_network" "network-1" { + + created_at = (known after apply) + + default_security_group_id = (known after apply) + + folder_id = (known after apply) + + id = (known after apply) + + labels = (known after apply) + + name = "network1" + + subnet_ids = (known after apply) + } + + # yandex_vpc_subnet.subnet-1 will be created + + resource "yandex_vpc_subnet" "subnet-1" { + + created_at = (known after apply) + + folder_id = (known after apply) + + id = (known after apply) + + labels = (known after apply) + + name = "subnet1" + + network_id = (known after apply) + + v4_cidr_blocks = [ + + "192.168.10.0/24", + ] + + v6_cidr_blocks = (known after apply) + + zone = "ru-central1-a" + } + +Plan: 4 to add, 0 to change, 0 to destroy. + +Changes to Outputs: + + external_ip_address_vm_1 = (known after apply) + + external_ip_address_vm_2 = (known after apply) + + internal_ip_address_vm_1 = (known after apply) + + internal_ip_address_vm_2 = (known after apply) + +Do you want to perform these actions? + Terraform will perform the actions described above. + Only 'yes' will be accepted to approve. + + Enter a value: yes + +yandex_vpc_network.network-1: Creating... +yandex_vpc_network.network-1: Creation complete after 8s [id=enp7ckctka4ce3i3lnlh] +yandex_vpc_subnet.subnet-1: Creating... +yandex_vpc_subnet.subnet-1: Creation complete after 1s [id=e9b2qbs7k3kul1bo22qo] +yandex_compute_instance.vm-1: Creating... +yandex_compute_instance.vm-2: Creating... +yandex_compute_instance.vm-2: Still creating... [10s elapsed] +yandex_compute_instance.vm-1: Still creating... [10s elapsed] +yandex_compute_instance.vm-2: Still creating... [20s elapsed] +yandex_compute_instance.vm-1: Still creating... [20s elapsed] +yandex_compute_instance.vm-2: Still creating... [30s elapsed] +yandex_compute_instance.vm-1: Still creating... [30s elapsed] +yandex_compute_instance.vm-2: Still creating... [40s elapsed] +yandex_compute_instance.vm-1: Still creating... [40s elapsed] +yandex_compute_instance.vm-2: Creation complete after 40s [id=fhmn7k6epn41im89c8tp] +yandex_compute_instance.vm-1: Creation complete after 41s [id=fhm6n20l4geif7u8e72r] + +Apply complete! Resources: 4 added, 0 changed, 0 destroyed. + +Outputs: + +external_ip_address_vm_1 = "158.160.109.49" +external_ip_address_vm_2 = "158.160.115.243" +internal_ip_address_vm_1 = "192.168.10.17" +internal_ip_address_vm_2 = "192.168.10.22" +``` + +#### terraform output + +```sh +❯ terraform output +external_ip_address_vm_1 = "158.160.109.49" +external_ip_address_vm_2 = "158.160.115.243" +internal_ip_address_vm_1 = "192.168.10.17" +internal_ip_address_vm_2 = "192.168.10.22" +``` + +## Task 3: Terraform for GitHub +I imported existing repository, using *imports.tf* . +And also created github infrastructure, the results are visible on the github, here is a link to the created repository - [My Core Course Labs ](https://github.com/m4k4rich/my-core-course-labs) + +## Bonus Task : Adding Teams + +Using terraform I created an organization called - +*InnoDevopsCourseOrg*, added several teams to the repository with different levels of access. There is a team of admins, a team of people why are able to push and the team of people who are able to pull! Here is a link to the repo, where you can see all the teams - [gh-teams-terraform](https://github.com/InnoDevopsCourseOrg/gh-teams-terraform) \ No newline at end of file diff --git a/terraform/cloud/main.tf b/terraform/cloud/main.tf new file mode 100644 index 000000000..0e1d69016 --- /dev/null +++ b/terraform/cloud/main.tf @@ -0,0 +1,82 @@ +terraform { + required_providers { + yandex = { + source = "yandex-cloud/yandex" + } + } + required_version = ">= 0.13" +} + +provider "yandex" { + service_account_key_file = "/Users/m4k4rich/core-course-labs/terraform/cloud/authorized_key.json" + cloud_id = "b1g5ltf518ub13g9v269" + folder_id = "b1g1aqrohbot4fl3e1vb" + zone = var.zone +} + +resource "yandex_compute_instance" "vm-1" { + name = "my-vm-1" + + resources { + cores = 2 + memory = 2 + } + + boot_disk { + initialize_params { + image_id = "fd82sqrj4uk9j7vlki3q" + } + } + + network_interface { + subnet_id = yandex_vpc_subnet.subnet-1.id + nat = true + } +} + +resource "yandex_compute_instance" "vm-2" { + name = "my-vm-2" + + resources { + cores = 4 + memory = 4 + } + + boot_disk { + initialize_params { + image_id = "fd82sqrj4uk9j7vlki3q" + } + } + + network_interface { + subnet_id = yandex_vpc_subnet.subnet-1.id + nat = true + } +} + +resource "yandex_vpc_network" "network-1" { + name = "network1" +} + +resource "yandex_vpc_subnet" "subnet-1" { + name = "subnet1" + zone = "ru-central1-a" + network_id = yandex_vpc_network.network-1.id + v4_cidr_blocks = ["192.168.10.0/24"] +} + +output "internal_ip_address_vm_1" { + value = yandex_compute_instance.vm-1.network_interface.0.ip_address +} + +output "internal_ip_address_vm_2" { + value = yandex_compute_instance.vm-2.network_interface.0.ip_address +} + +output "external_ip_address_vm_1" { + value = yandex_compute_instance.vm-1.network_interface.0.nat_ip_address +} + +output "external_ip_address_vm_2" { + value = yandex_compute_instance.vm-2.network_interface.0.nat_ip_address +} \ No newline at end of file diff --git a/terraform/cloud/outputs.tf b/terraform/cloud/outputs.tf new file mode 100644 index 000000000..e69de29bb diff --git a/terraform/cloud/variables.tf b/terraform/cloud/variables.tf new file mode 100644 index 000000000..48678ffc7 --- /dev/null +++ b/terraform/cloud/variables.tf @@ -0,0 +1,4 @@ +variable "zone" { + default = "ru-central1-a" + type = string +} \ No newline at end of file diff --git a/terraform/docker/main.tf b/terraform/docker/main.tf new file mode 100644 index 000000000..bf24a3170 --- /dev/null +++ b/terraform/docker/main.tf @@ -0,0 +1,24 @@ +terraform { + required_providers { + docker = { + source = "kreuzwerker/docker" + version = "~> 3.0.1" + } + } +} + +provider "docker" {} + +resource "docker_image" "nginx" { + name = var.image_name + keep_locally = false +} + +resource "docker_container" "nginx" { + image = docker_image.nginx.image_id + name = var.container_name + ports { + internal = 80 + external = var.container_ports_external + } +} diff --git a/terraform/docker/outputs.tf b/terraform/docker/outputs.tf new file mode 100644 index 000000000..8bdd0fbb2 --- /dev/null +++ b/terraform/docker/outputs.tf @@ -0,0 +1,9 @@ +output "image_id" { + description = "My image ID" + value = docker_image.nginx.id +} + +output "container_id" { + description = "My Container ID" + value = docker_container.nginx.id +} \ No newline at end of file diff --git a/terraform/docker/variables.tf b/terraform/docker/variables.tf new file mode 100644 index 000000000..0953ed857 --- /dev/null +++ b/terraform/docker/variables.tf @@ -0,0 +1,12 @@ +variable "image_name" { + type = string +} + +variable "container_name" { + type = string +} + +variable "container_ports_external" { + type = number + default = 8000 +} \ No newline at end of file diff --git a/terraform/github/main.tf b/terraform/github/main.tf new file mode 100644 index 000000000..86e700d29 --- /dev/null +++ b/terraform/github/main.tf @@ -0,0 +1,35 @@ +terraform { + required_providers { + github = { + source = "integrations/github" + version = "~> 4.0" + } + } +} + +provider "github" { + token = var.github_token +} + +resource "github_repository" "devops_course_labs" { + name = "my-core-course-labs" + description = "Solutions for the DevOps course" + visibility = "public" +} + +# make the default branch main +resource "github_branch_default" "main" { + repository = github_repository.devops_course_labs.name + branch = "master" +} + +resource "github_branch_protection" "default" { + repository_id = github_repository.devops_course_labs.id + pattern = github_branch_default.main.branch + require_conversation_resolution = true + enforce_admins = true + + required_pull_request_reviews { + required_approving_review_count = 1 + } +} \ No newline at end of file diff --git a/terraform/github/variables.tf b/terraform/github/variables.tf new file mode 100644 index 000000000..f2e4ce7c9 --- /dev/null +++ b/terraform/github/variables.tf @@ -0,0 +1,5 @@ +variable "github_token" { + type = string + description = "Required to provide `GITHUB_TOKEN`" + sensitive = true +} \ No newline at end of file diff --git a/terraform/imports.tf b/terraform/imports.tf new file mode 100644 index 000000000..e0bfb80fd --- /dev/null +++ b/terraform/imports.tf @@ -0,0 +1,4 @@ +import { + to = module.github.github_repository.devops_course_labs + id = "my-core-course-labs" +} \ No newline at end of file diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 000000000..d19308fbd --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,20 @@ +module "nginx_server" { + source = "./docker" + image_name = "nginx:latest" + container_name = "my-nginx-app" +} + +module "yandex-cloud" { + source = "./cloud" +} + +module "github" { + source = "./github" + github_token = var.github_token +} + +module "github-teams" { + source = "./teams" + github_organization = "main-devops-org" + github_token = var.github_token +} \ No newline at end of file diff --git a/terraform/teams/main.tf b/terraform/teams/main.tf new file mode 100644 index 000000000..9caba2ec5 --- /dev/null +++ b/terraform/teams/main.tf @@ -0,0 +1,50 @@ +terraform { + required_providers { + github = { + source = "integrations/github" + version = "~> 4.0" + } + } +} + +provider "github" { + owner = var.github_organization + token = var.github_token +} + + +resource "github_repository" "gh_teams_terraform" { + name = "gh-teams-terraform" + description = "terraform with gh" + visibility = "public" +} + +resource "github_team" "team_pull" { + name = "pullies" +} + +resource "github_team" "team_maintain" { + name = "pushies" +} + +resource "github_team" "team_admins" { + name = "admins" +} + +resource "github_team_repository" "team_a_access" { + repository = github_repository.gh_teams_terraform.name + team_id = github_team.team_pull.id + permission = "pull" +} + +resource "github_team_repository" "team_b_access" { + repository = github_repository.gh_teams_terraform.name + team_id = github_team.team_maintain.id + permission = "maintain" +} + +resource "github_team_repository" "team_c_access" { + repository = github_repository.gh_teams_terraform.name + team_id = github_team.team_admins.id + permission = "admin" +} \ No newline at end of file diff --git a/terraform/teams/variables.tf b/terraform/teams/variables.tf new file mode 100644 index 000000000..2c450ce5d --- /dev/null +++ b/terraform/teams/variables.tf @@ -0,0 +1,10 @@ +variable "github_organization" { + type = string + default = "InnoDevopsCourseOrg" +} + +variable "github_token" { + type = string + description = "Specifies the GitHub PAT token or `GITHUB_TOKEN`" + sensitive = true +} \ No newline at end of file diff --git a/terraform/variable.tf b/terraform/variable.tf new file mode 100644 index 000000000..f2e4ce7c9 --- /dev/null +++ b/terraform/variable.tf @@ -0,0 +1,5 @@ +variable "github_token" { + type = string + description = "Required to provide `GITHUB_TOKEN`" + sensitive = true +} \ No newline at end of file