diff --git a/.github/workflows/app_nodejs.yml b/.github/workflows/app_nodejs.yml new file mode 100644 index 0000000000..3748265fce --- /dev/null +++ b/.github/workflows/app_nodejs.yml @@ -0,0 +1,136 @@ +name: Nodejs CI + +on: + push: + paths: + - "app_nodejs/**" + - ".github/workflows/app_nodejs.yml" + pull_request: + paths: + - "app_nodejs/**" + - ".github/workflows/app_nodejs.yml" + +jobs: + dependencies: + name: Install Dependencies + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set Up Node.js + uses: actions/setup-node@v4 + with: + node-version: "18" + cache: "npm" + cache-dependency-path: app_nodejs/package-lock.json + + - name: Cache Node Modules + uses: actions/cache@v3 + with: + path: app_nodejs/node_modules + key: ${{ runner.os }}-node-modules-${{ hashFiles('app_nodejs/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node-modules- + + - name: Install Dependencies + run: npm install + working-directory: app_nodejs + + lint: + name: Lint Code + runs-on: ubuntu-latest + needs: dependencies + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set Up Node.js + uses: actions/setup-node@v4 + with: + node-version: "18" + + - name: Restore Cached Node Modules + uses: actions/cache@v3 + with: + path: app_nodejs/node_modules + key: ${{ runner.os }}-node-modules-${{ hashFiles('app_nodejs/package-lock.json') }} + + - name: Run ESLint + run: npx eslint . + working-directory: app_nodejs + + snyk: + name: Snyk Security Scan + runs-on: ubuntu-latest + needs: lint + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set Up Node.js + uses: actions/setup-node@v4 + with: + node-version: "18" + + - name: Restore Cached Node Modules + uses: actions/cache@v3 + with: + path: app_nodejs/node_modules + key: ${{ runner.os }}-node-modules-${{ hashFiles('app_nodejs/package-lock.json') }} + + - name: Run Snyk Security Scan + uses: snyk/actions/node@master + with: + args: --skip-unresolved --severity-threshold=high app_nodejs/ + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + + test: + name: Run Tests + runs-on: ubuntu-latest + needs: snyk + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set Up Node.js + uses: actions/setup-node@v4 + with: + node-version: "18" + + - name: Restore Cached Node Modules + uses: actions/cache@v3 + with: + path: app_nodejs/node_modules + key: ${{ runner.os }}-node-modules-${{ hashFiles('app_nodejs/package-lock.json') }} + + - name: Run Jest Tests + run: npm test -- --ci --coverage + working-directory: app_nodejs + + docker: + name: Build & Push Docker Image + runs-on: ubuntu-latest + needs: test + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set Up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ vars.DOCKER_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build & Push Docker Image + uses: docker/build-push-action@v6 + with: + push: true + tags: "${{ vars.DOCKER_USERNAME }}/app_nodejs:latest" + context: ./app_nodejs + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/app_python.yml b/.github/workflows/app_python.yml new file mode 100644 index 0000000000..354c4e5632 --- /dev/null +++ b/.github/workflows/app_python.yml @@ -0,0 +1,157 @@ +name: Python CI + +on: + push: + paths: + - "app_python/**" + - ".github/workflows/app_python.yml" + pull_request: + paths: + - "app_python/**" + - ".github/workflows/app_python.yml" + +jobs: + dependencies: + name: Dependencies + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Cache Venv with Dependencies + uses: actions/cache@v3 + with: + path: app_python/venv + key: ${{ runner.os }}-venv-${{ hashFiles('app_python/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-venv- + + - name: Install Dependencies + run: | + python -m venv venv + source venv/bin/activate + pip install -r requirements.txt + working-directory: app_python + + + lint: + name: Lint + runs-on: ubuntu-latest + needs: dependencies + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set Up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Restore Cached venv + uses: actions/cache@v3 + with: + path: app_python/venv + key: ${{ runner.os }}-venv-${{ hashFiles('app_python/requirements.txt') }} + + - name: Run Flake8 Linter + run: | + source venv/bin/activate + flake8 --exclude=venv,__pycache__,.git --max-line-length=100 . + working-directory: app_python + + snyk: + name: Snyk Security Scan + runs-on: ubuntu-latest + needs: lint + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Run Snyk Security Scan + uses: snyk/actions/python-3.10@master + with: + args: --skip-unresolved app_python/ + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + + + run: + name: Run + runs-on: ubuntu-latest + needs: snyk + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set Up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Restore Cached venv + uses: actions/cache@v3 + with: + path: app_python/venv + key: ${{ runner.os }}-venv-${{ hashFiles('app_python/requirements.txt') }} + + - name: Start Application + run: | + source venv/bin/activate + nohup python app.py > app.log 2>&1 & + working-directory: app_python + + test: + name: Test + runs-on: ubuntu-latest + needs: run + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set Up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Restore Cached venv + uses: actions/cache@v3 + with: + path: app_python/venv + key: ${{ runner.os }}-venv-${{ hashFiles('app_python/requirements.txt') }} + + - name: Run Tests + run: | + source venv/bin/activate + pytest --disable-warnings --maxfail=3 + working-directory: app_python + + docker: + runs-on: ubuntu-latest + needs: test + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set Up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ vars.DOCKER_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build & Push + uses: docker/build-push-action@v6 + with: + push: true + tags: "${{ vars.DOCKER_USERNAME }}/app_python:latest" + context: ./app_python + cache-from: type=gha + cache-to: type=gha,mode=max + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..e43b0f9889 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/ansible/.gitignore b/ansible/.gitignore new file mode 100644 index 0000000000..bda703f8a9 --- /dev/null +++ b/ansible/.gitignore @@ -0,0 +1,4 @@ +key.json +fact_cache +inventory/yandex-cloud-token +plugins/inventory/__pycache__/ diff --git a/ansible/ANSIBLE.md b/ansible/ANSIBLE.md new file mode 100644 index 0000000000..2412f223a1 --- /dev/null +++ b/ansible/ANSIBLE.md @@ -0,0 +1,691 @@ +# Ansible + +## Best practices + +1. Names for tasks + + Every task has a descriptive name for better readability and debugging. + +2. Using Handlers + + Use handlers to restart services only when needed, reducing unnecessary downtime. + +3. Checked syntax + + Validated syntax with `ansible-playbook --syntax-check` before deploying to production for errors reducing. + +## Docker with `ansible-galaxy` + +Playbook to run existing Ansible role: + +```yml + --- + - name: Install Docker + hosts: all + become: true + + roles: + - geerlingguy.docker +``` + +As in the task requirements the inventory file is named `default_aws_ec2.yml`, at first I created file with this name and named host +`aws_instance`. Later it was renamed as `yc_instance` and vm from previous lab with terraform was used. Output of +`ansible-playbook -i inventory/default_yandex_cloud.yml playbooks/dev/main.yaml`: + +```bash + PLAY [Install Docker] + ************************************************************************************************************************************************ + + TASK [Gathering Facts] + *********************************************************************************************************************************************** + ok: [aws_instance] + + TASK [geerlingguy.docker : Load OS-specific vars.] + ******************************************************************************************************************* + ok: [aws_instance] + + TASK [geerlingguy.docker : include_tasks] + **************************************************************************************************************************** + skipping: [aws_instance] + + TASK [geerlingguy.docker : include_tasks] + **************************************************************************************************************************** + included: + /Users/ilsianasibullina/.ansible/roles/geerlingguy.docker/tasks/setup-Debian.yml + for aws_instance + + TASK [geerlingguy.docker : Ensure apt key is not present in trusted.gpg.d] + ******************************************************************************************* + ok: [aws_instance] + + TASK [geerlingguy.docker : Ensure old apt source list is not present in + /etc/apt/sources.list.d] + ********************************************************************* + ok: [aws_instance] + + TASK [geerlingguy.docker : Ensure the repo referencing the previous + trusted.gpg.d key is not present] + **************************************************************** + ok: [aws_instance] + + TASK [geerlingguy.docker : Ensure old versions of Docker are not + installed.] + ***************************************************************************************** + ok: [aws_instance] + + TASK [geerlingguy.docker : Ensure dependencies are installed.] + ******************************************************************************************************* + changed: [aws_instance] + + TASK [geerlingguy.docker : Ensure directory exists for /etc/apt/keyrings] + ******************************************************************************************** + ok: [aws_instance] + + TASK [geerlingguy.docker : Add Docker apt key.] + ********************************************************************************************************************** + changed: [aws_instance] + + TASK [geerlingguy.docker : Ensure curl is present (on older systems + without SNI).] + *********************************************************************************** + skipping: [aws_instance] + + TASK [geerlingguy.docker : Add Docker apt key (alternative for older + systems without SNI).] + ************************************************************************** + skipping: [aws_instance] + + TASK [geerlingguy.docker : Add Docker repository.] + ******************************************************************************************************************* + changed: [aws_instance] + + TASK [geerlingguy.docker : Install Docker packages.] + ***************************************************************************************************************** + skipping: [aws_instance] + + TASK [geerlingguy.docker : Install Docker packages (with downgrade + option).] + ***************************************************************************************** + changed: [aws_instance] + + TASK [geerlingguy.docker : Install docker-compose plugin.] + *********************************************************************************************************** + skipping: [aws_instance] + + TASK [geerlingguy.docker : Install docker-compose-plugin (with downgrade + option).] + *********************************************************************************** + ok: [aws_instance] + + TASK [geerlingguy.docker : Ensure /etc/docker/ directory exists.] + **************************************************************************************************** + skipping: [aws_instance] + + TASK [geerlingguy.docker : Configure Docker daemon options.] + ********************************************************************************************************* + skipping: [aws_instance] + + TASK [geerlingguy.docker : Ensure Docker is started and enabled at boot.] + ******************************************************************************************** + ok: [aws_instance] + + TASK [geerlingguy.docker : Ensure handlers are notified now to avoid + firewall conflicts.] + **************************************************************************** + + RUNNING HANDLER [geerlingguy.docker : restart docker] + **************************************************************************************************************** + changed: [aws_instance] + + TASK [geerlingguy.docker : include_tasks] + **************************************************************************************************************************** + skipping: [aws_instance] + + TASK [geerlingguy.docker : Get docker group info using getent.] + ****************************************************************************************************** + skipping: [aws_instance] + + TASK [geerlingguy.docker : Check if there are any users to add to the + docker group.] + ********************************************************************************* + skipping: [aws_instance] + + TASK [geerlingguy.docker : include_tasks] + **************************************************************************************************************************** + skipping: [aws_instance] + + PLAY RECAP + *********************************************************************************************************************************************************** + aws_instance : ok=15 changed=5 unreachable=0 + failed=0 skipped=11 rescued=0 ignored=0 +``` + +## Custom Docker role + +The output of command `ansible-playbook -i inventory/default_yandex_cloud.yml playbooks/dev/main.yaml`: + +```bash + PLAY [Install Docker] + ************************************************************************************************************************************************ + + TASK [Gathering Facts] + *********************************************************************************************************************************************** + [WARNING]: Platform linux on host yc_instance is using the discovered Python interpreter at /usr/bin/python3.12, but future installation of another + Python + interpreter could change the meaning of that path. See https://docs.ansible.com/ansible-core/2.18/reference_appendices/interpreter_discovery.html + for more + information. + ok: [yc_instance] + + TASK [docker : Install required packages] + **************************************************************************************************************************** + ok: [yc_instance] + + TASK [docker : Add Docker GPG key] + *********************************************************************************************************************************** + ok: [yc_instance] + + TASK [docker : Add Docker repository] + ******************************************************************************************************************************** + ok: [yc_instance] + + TASK [docker : Install Docker] + *************************************************************************************************************************************** + ok: [yc_instance] + + TASK [docker : Enable and start Docker service] + ********************************************************************************************************************** + ok: [yc_instance] + + TASK [docker : Download Docker Compose] + ****************************************************************************************************************************** + changed: [yc_instance] + + TASK [docker : Verify Docker Compose installation] + ******************************************************************************************************************* + ok: [yc_instance] + + TASK [docker : Display installed Docker Compose version] + ************************************************************************************************************* + ok: [yc_instance] => { + "msg": "Docker Compose version v2.32.4" + } + + TASK [docker : Add current user to the docker group] + ***************************************************************************************************************** + changed: [yc_instance] + + PLAY RECAP + *********************************************************************************************************************************************************** + yc_instance : ok=10 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 + + ``` + + The output of command `ansible-playbook -i inventory/default_yandex_cloud.yml playbooks/dev/main.yaml --check --diff`: + + ```bash + + PLAY [Install Docker] + ************************************************************************************************************************************************ + + TASK [Gathering Facts] + *********************************************************************************************************************************************** + ok: [yc_instance] + + TASK [docker : Install required packages] + **************************************************************************************************************************** + ok: [yc_instance] + + TASK [docker : Add Docker GPG key] + *********************************************************************************************************************************** + ok: [yc_instance] + + TASK [docker : Add Docker repository] + ******************************************************************************************************************************** + ok: [yc_instance] + + TASK [docker : Install Docker] + *************************************************************************************************************************************** + ok: [yc_instance] + + TASK [docker : Enable and start Docker service] + ********************************************************************************************************************** + ok: [yc_instance] + + TASK [docker : Download Docker Compose] + ****************************************************************************************************************************** + ok: [yc_instance] + + TASK [docker : Verify Docker Compose installation] + ******************************************************************************************************************* + skipping: [yc_instance] + + TASK [docker : Display installed Docker Compose version] + ************************************************************************************************************* + ok: [yc_instance] => { + "msg": "" + } + + TASK [docker : Add current user to the docker group] + ***************************************************************************************************************** + ok: [yc_instance] + + PLAY RECAP + *********************************************************************************************************************************************************** + yc_instance : ok=9 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0 + +``` + +The output of command `ansible-inventory -i inventory/default_yandex_cloud.yml --list`: + +```bash + { + "_meta": { + "hostvars": { + "yc_instance": { + "ansible_host": "158.160.85.1", + "ansible_ssh_private_key_file": "~/.ssh/id_ed25519", + "ansible_user": "ubuntu" + } + } + }, + "all": { + "children": [ + "ungrouped" + ] + }, + "ungrouped": { + "hosts": [ + "yc_instance" + ] + } + } +``` + +The output of command `ansible-inventory -i inventory/default_yandex_cloud.yml --graph`: + +```bash + @all: + |--@ungrouped: + | |--yc_instance + +``` + +## Bonus Task + +### Dynamic inventory with Yandex cloud + +Output of `ansible-inventory -i inventory/yacloud_compute.yml --list` + +```bash + { + "_meta": { + "hostvars": { + "terraform-vm-1": { + "ansible_host": "51.250.107.29" + } + } + }, + "all": { + "children": [ + "ungrouped", + "yacloud" + ] + }, + "yacloud": { + "hosts": [ + "terraform-vm-1" + ] + } + } + +``` + +### Secure Docker Configuration + +Output of running ansible docker role with secure docker configuration: + +```bash + PLAY [Install Docker] + **************************************************************************************************************************************************************************** + + TASK [Gathering Facts] + *************************************************************************************************************************************************************************** + ok: [yc_instance] + + TASK [docker : Update apt package index] + ********************************************************************************************************************************************************* + changed: [yc_instance] + + TASK [docker : Install required packages] + ******************************************************************************************************************************************************** + changed: [yc_instance] + + TASK [docker : Add Docker GPG key] + *************************************************************************************************************************************************************** + changed: [yc_instance] + + TASK [docker : Add Docker repository] + ************************************************************************************************************************************************************ + changed: [yc_instance] + + TASK [docker : Install Docker] + ******************************************************************************************************************************************************************* + changed: [yc_instance] + + TASK [docker : Enable and start Docker service] + ************************************************************************************************************************************************** + ok: [yc_instance] + + TASK [docker : Download Docker Compose] + ********************************************************************************************************************************************************** + changed: [yc_instance] + + TASK [docker : Verify Docker Compose installation] + *********************************************************************************************************************************************** + ok: [yc_instance] + + TASK [docker : Display installed Docker Compose version] + ***************************************************************************************************************************************** + ok: [yc_instance] => { + "msg": "Docker Compose version v2.32.4" + } + + TASK [docker : Add current user to the docker group] + ********************************************************************************************************************************************* + changed: [yc_instance] + + TASK [docker : Secure Docker Configuration - Disable Root Access] + ******************************************************************************************************************************** + changed: [yc_instance] + + RUNNING HANDLER [docker : Restart Docker] + ******************************************************************************************************************************************************** + changed: [yc_instance] + + PLAY RECAP + *************************************************************************************************************************************************************************************** + yc_instance : ok=13 changed=9 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +## Ansible web_app CD + +### Task 1 - Simple Ansible Role + +In this task I defined variables and added tasks to pull image and start container using ansible. This is the output of running playbook with web_app role with +command `ansible-playbook -i inventory/yacloud_compute.yml playbooks/dev/main.yaml`: + +```bash + PLAY [Install Docker] ******************************************************************************************************* + + TASK [Gathering Facts] ****************************************************************************************************** + ok: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [docker : Update apt package index] ************************************************************************************ + changed: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [docker : Install required packages] *********************************************************************************** + ok: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [docker : Add Docker GPG key] ****************************************************************************************** + ok: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [docker : Add Docker repository] *************************************************************************************** + ok: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [docker : Install Docker] ********************************************************************************************** + ok: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [docker : Enable and start Docker service] ***************************************************************************** + ok: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [docker : Download Docker Compose] ************************************************************************************* + ok: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [docker : Verify Docker Compose installation] ************************************************************************** + ok: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [docker : Display installed Docker Compose version] ******************************************************************** + ok: [compute-vm-2-2-20-ssd-1739036897134] => { + "msg": "Docker Compose version v2.32.4" + } + + TASK [docker : Add current user to the docker group] ************************************************************************ + ok: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [docker : Secure Docker Configuration - Disable Root Access] *********************************************************** + ok: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [web_app : Pull the latest Docker image] ******************************************************************************* + changed: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [web_app : Run the Web App container] ********************************************************************************** + changed: [compute-vm-2-2-20-ssd-1739036897134] + + PLAY RECAP ****************************************************************************************************************** + compute-vm-2-2-20-ssd-1739036897134 : ok=14 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 + +``` + +### Task 2 + +After applying first three steps of best practices I tried to run specific tag to escape of running same steps^ which have already been done, every time. The +command I used `ansible-playbook -i inventory/yacloud_compute.yml playbooks/dev/main.yaml --tags deploy`: + +```bash + PLAY [Install Docker] + **************************************************************************************************************************************************** + + TASK [Gathering Facts] + *************************************************************************************************************************************************** + ok: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [web_app : Pull the latest Docker image] + **************************************************************************************************************************** + ok: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [web_app : Run the Web App container] + ******************************************************************************************************************************* + ok: [compute-vm-2-2-20-ssd-1739036897134] + + PLAY RECAP + *************************************************************************************************************************************************************** + compute-vm-2-2-20-ssd-1739036897134 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +Outut of running wipe tasks independently using `wipe` tag in command `ansible-playbook -i inventory/yacloud_compute.yml playbooks/dev/main.yaml --tags wipe`: + +```bash + + PLAY [Install Docker] + **************************************************************************************************************************************************** + + TASK [Gathering Facts] + *************************************************************************************************************************************************** + ok: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [web_app : Stop and remove the web app container] + ******************************************************************************************************************* + ok: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [web_app : Remove Docker image] + ************************************************************************************************************************************* + ok: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [web_app : Remove application data directory] + *********************************************************************************************************************** + ok: [compute-vm-2-2-20-ssd-1739036897134] + + PLAY RECAP + *************************************************************************************************************************************************************** + compute-vm-2-2-20-ssd-1739036897134 : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 + +``` + +Output of running ansible with docker-compose template `ansible-playbook playbooks/dev/main.yaml -i inventory/yacloud_compute.yml`: + +```bash + PLAY [Install Docker] + **************************************************************************************************************************************************** + + TASK [Gathering Facts] + *************************************************************************************************************************************************** + ok: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [docker : Update apt package index] + ********************************************************************************************************************************* + changed: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [docker : Install required packages] + ******************************************************************************************************************************** + ok: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [docker : Add Docker GPG key] + *************************************************************************************************************************************** + ok: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [docker : Add Docker repository] + ************************************************************************************************************************************ + ok: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [docker : Install Docker] + ******************************************************************************************************************************************* + ok: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [docker : Enable and start Docker service] + ************************************************************************************************************************** + ok: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [docker : Download Docker Compose] + ********************************************************************************************************************************** + ok: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [docker : Verify Docker Compose installation] + *********************************************************************************************************************** + ok: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [docker : Display installed Docker Compose version] + ***************************************************************************************************************** + ok: [compute-vm-2-2-20-ssd-1739036897134] => { + "msg": "Docker Compose version v2.32.4" + } + + TASK [docker : Add current user to the docker group] + ********************************************************************************************************************* + ok: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [docker : Disable Root Access] + ************************************************************************************************************************************** + ok: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [web_app : Stop and remove the web app container] + ******************************************************************************************************************* + changed: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [web_app : Remove Docker image] + ************************************************************************************************************************************* + changed: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [web_app : Remove application data directory] + *********************************************************************************************************************** + changed: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [web_app : Ensure Web App Directory Exists] + ************************************************************************************************************************* + changed: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [web_app : Generate docker-compose.yml file from template] + ********************************************************************************************************** + changed: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [web_app : Pull the latest Docker image] + **************************************************************************************************************************** + changed: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [web_app : Ensure Docker service is running] + ************************************************************************************************************************ + ok: [compute-vm-2-2-20-ssd-1739036897134] + + TASK [web_app : Create and start the services] + *************************************************************************************************************************** + changed: [compute-vm-2-2-20-ssd-1739036897134] + + PLAY RECAP + *************************************************************************************************************************************************************** + compute-vm-2-2-20-ssd-1739036897134 : ok=20 changed=8 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 + +``` + +### Bonus Task - Lab6 + +Output of running playbook for app_python with command `ansible-playbook -i inventory/yacloud_compute.yml playbooks/dev/app_python/main.yaml +--tags deploy`: + +```bash + PLAY [Deploy Web App] + ****************************************************************************************************************************************************************************** + + TASK [Gathering Facts] + ***************************************************************************************************************************************************************************** + ok: [compute-vm-2-2-20-ssd-1739276511287] + + TASK [web_app : Ensure Web App Directory Exists] + *************************************************************************************************************************************************** + ok: [compute-vm-2-2-20-ssd-1739276511287] + + TASK [web_app : Generate docker-compose.yml file from template] + ************************************************************************************************************************************ + ok: [compute-vm-2-2-20-ssd-1739276511287] + + TASK [web_app : Pull the latest Docker image] + ****************************************************************************************************************************************************** + changed: [compute-vm-2-2-20-ssd-1739276511287] + + TASK [web_app : Ensure Docker service is running] + ************************************************************************************************************************************************** + ok: [compute-vm-2-2-20-ssd-1739276511287] + + TASK [web_app : Create and start the services] + ***************************************************************************************************************************************************** + changed: [compute-vm-2-2-20-ssd-1739276511287] + + PLAY RECAP + ***************************************************************************************************************************************************************************************** + compute-vm-2-2-20-ssd-1739276511287 : ok=6 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +Output of running playbook for app_nodejs with command `ansible-playbook -i inventory/yacloud_compute.yml playbooks/dev/app_nodejs/main.yaml +--tags deploy`: + +```bash + PLAY [Deploy Web App] + ****************************************************************************************************************************************************************************** + + TASK [Gathering Facts] + ***************************************************************************************************************************************************************************** + ok: [compute-vm-2-2-20-ssd-1739276511287] + + TASK [web_app : Ensure Web App Directory Exists] + *************************************************************************************************************************************************** + changed: [compute-vm-2-2-20-ssd-1739276511287] + + TASK [web_app : Generate docker-compose.yml file from template] + ************************************************************************************************************************************ + changed: [compute-vm-2-2-20-ssd-1739276511287] + + TASK [web_app : Pull the latest Docker image] + ****************************************************************************************************************************************************** + changed: [compute-vm-2-2-20-ssd-1739276511287] + + TASK [web_app : Ensure Docker service is running] + ************************************************************************************************************************************************** + ok: [compute-vm-2-2-20-ssd-1739276511287] + + TASK [web_app : Create and start the services] + ***************************************************************************************************************************************************** + changed: [compute-vm-2-2-20-ssd-1739276511287] + + PLAY RECAP + ***************************************************************************************************************************************************************************************** + compute-vm-2-2-20-ssd-1739276511287 : ok=6 changed=4 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 0000000000..19450fdca9 --- /dev/null +++ b/ansible/ansible.cfg @@ -0,0 +1,9 @@ +[defaults] +inventory = ./inventory/ +remote_user = ubuntu +playbook_dir = ./playbooks +roles_path = ./roles +inventory_plugins = ./plugins/inventory +host_key_checking = false +enable_plugins = host_list, script, auto, yaml, ini, toml, yacloud_compute + diff --git a/ansible/inventory/yacloud_compute.yml b/ansible/inventory/yacloud_compute.yml new file mode 100644 index 0000000000..c14bd64ad1 --- /dev/null +++ b/ansible/inventory/yacloud_compute.yml @@ -0,0 +1,6 @@ +--- +plugin: yacloud_compute +yacloud_token_file: "./inventory/yandex-cloud-token" +yacloud_clouds: cloud-ilsya-nasibullina +yacloud_folders: default +ansible_python_interpreter: /usr/bin/python3 diff --git a/ansible/playbooks/dev/app_nodejs/main.yaml b/ansible/playbooks/dev/app_nodejs/main.yaml new file mode 100644 index 0000000000..2f7837bd01 --- /dev/null +++ b/ansible/playbooks/dev/app_nodejs/main.yaml @@ -0,0 +1,11 @@ +- name: Deploy Web App + hosts: all + become: true + roles: + - role: docker + - role: web_app + vars: + web_app_name: "app_nodejs" + web_app_external_port: 3000 + web_app_internal_port: 3000 + web_app_dir: "~/app_nodejs" diff --git a/ansible/playbooks/dev/app_python/main.yaml b/ansible/playbooks/dev/app_python/main.yaml new file mode 100644 index 0000000000..259129e3b6 --- /dev/null +++ b/ansible/playbooks/dev/app_python/main.yaml @@ -0,0 +1,11 @@ +- name: Deploy Web App + hosts: all + become: true + roles: + - role: docker + - role: web_app + vars: + web_app_name: "app_python" + web_app_external_port: 5001 + web_app_internal_port: 5000 + web_app_dir: "~/app_python" diff --git a/ansible/plugins/inventory/yacloud_compute.py b/ansible/plugins/inventory/yacloud_compute.py new file mode 100755 index 0000000000..28915c112f --- /dev/null +++ b/ansible/plugins/inventory/yacloud_compute.py @@ -0,0 +1,172 @@ +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = """ + name: yacloud_compute + plugin_type: inventory + short_description: Yandex.Cloud compute inventory source + requirements: + - yandexcloud + extends_documentation_fragment: + - inventory_cache + - constructed + description: + - Get inventory hosts from Yandex Cloud + - Uses a YAML configuration file that ends with C(yacloud_compute.(yml|yaml)). + options: + plugin: + description: Token that ensures this is a source file for the plugin. + required: True + choices: ['yacloud_compute'] + yacloud_token: + description: Oauth token for yacloud connection + yacloud_token_file: + description: File with oauth token for yacloud connection + yacloud_clouds: + description: Names of clouds to get hosts from + type: list + default: [] + yacloud_folders: + description: Names of folders to get hosts from + type: list + default: [] + yacloud_group_label: + description: VM's label used for group assignment + type: string + default: "" + +""" + +EXAMPLES = """ +""" + +from ansible.errors import AnsibleError +from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable +from ansible.utils.display import Display +from ansible.module_utils._text import to_native + +try: + import yandexcloud + from yandex.cloud.compute.v1.instance_service_pb2_grpc import InstanceServiceStub + from yandex.cloud.compute.v1.instance_service_pb2 import ListInstancesRequest + from google.protobuf.json_format import MessageToDict + from yandex.cloud.resourcemanager.v1.cloud_service_pb2 import ListCloudsRequest + from yandex.cloud.resourcemanager.v1.cloud_service_pb2_grpc import CloudServiceStub + from yandex.cloud.resourcemanager.v1.folder_service_pb2 import ListFoldersRequest + from yandex.cloud.resourcemanager.v1.folder_service_pb2_grpc import ( + FolderServiceStub, + ) +except ImportError: + raise AnsibleError("The yacloud dynamic inventory plugin requires yandexcloud") + +display = Display() + + +class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable): + NAME = "yacloud_compute" + + def verify_file(self, path): + if super(InventoryModule, self).verify_file(path): + if path.endswith(("yacloud_compute.yml", "yacloud_compute.yaml")): + return True + display.debug( + "yacloud_compute inventory filename must end with 'yacloud_compute.yml' or 'yacloud_compute.yaml'" + ) + return False + + def _get_ip_for_instance(self, instance): + interfaces = instance["networkInterfaces"] + for interface in interfaces: + address = interface["primaryV4Address"] + if address: + if address.get("oneToOneNat"): + return address["oneToOneNat"]["address"] + else: + return address["address"] + return None + + def _get_clouds(self): + all_clouds = MessageToDict(self.cloud_service.List(ListCloudsRequest()))[ + "clouds" + ] + if self.get_option("yacloud_clouds"): + all_clouds[:] = [ + x for x in all_clouds if x["name"] in self.get_option("yacloud_clouds") + ] + self.clouds = all_clouds + + def _get_folders(self): + all_folders = [] + for cloud in self.clouds: + all_folders += MessageToDict( + self.folder_service.List(ListFoldersRequest(cloud_id=cloud["id"])) + )["folders"] + + if self.get_option("yacloud_folders"): + all_folders[:] = [ + x + for x in all_folders + if x["name"] in self.get_option("yacloud_folders") + ] + + self.folders = all_folders + + def _get_all_hosts(self): + self.hosts = [] + for folder in self.folders: + hosts = self.instance_service.List( + ListInstancesRequest(folder_id=folder["id"]) + ) + dict_ = MessageToDict(hosts) + + if dict_: + self.hosts += dict_["instances"] + + def _init_client(self): + file = self.get_option("yacloud_token_file") + if file is not None: + token = open(file).read().strip() + else: + token = self.get_option("yacloud_token") + if not token: + raise AnsibleError( + "token it empty. provide either `yacloud_token_file` or `yacloud_token`" + ) + sdk = yandexcloud.SDK(token=token) + + self.instance_service = sdk.client(InstanceServiceStub) + self.folder_service = sdk.client(FolderServiceStub) + self.cloud_service = sdk.client(CloudServiceStub) + + def _process_hosts(self): + group_label = str(self.get_option("yacloud_group_label")) + + for instance in self.hosts: + if group_label and group_label in instance["labels"]: + group = instance["labels"][group_label] + else: + group = "yacloud" + + self.inventory.add_group(group=group) + if instance["status"] == "RUNNING": + ip = self._get_ip_for_instance(instance) + if ip: + self.inventory.add_host(instance["name"], group=group) + self.inventory.set_variable( + instance["name"], "ansible_host", to_native(ip) + ) + + def parse(self, inventory, loader, path, cache=True): + super(InventoryModule, self).parse(inventory, loader, path) + + self._read_config_data(path) + self._init_client() + + self._get_clouds() + self._get_folders() + + self._get_all_hosts() + self._process_hosts() diff --git a/ansible/roles/docker/README.md b/ansible/roles/docker/README.md new file mode 100644 index 0000000000..8e2c3d4e50 --- /dev/null +++ b/ansible/roles/docker/README.md @@ -0,0 +1,25 @@ +# Docker Role + +This role installs and configures Docker and Docker Compose. + +## Requirements + +- **Ansible** 2.9+ +- **Supported OS**: Ubuntu 22.04+ + +## Role Variables + +- `compose_version`: The version of Docker-compose to install. +- `ansible_user`: Username in VM. + +## Example Playbook + +The playbook for role + +```yaml + - name: Install Docker + hosts: all + become: true + roles: + - docker +``` diff --git a/ansible/roles/docker/defaults/main.yml b/ansible/roles/docker/defaults/main.yml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/ansible/roles/docker/defaults/main.yml @@ -0,0 +1 @@ +--- diff --git a/ansible/roles/docker/handlers/main.yml b/ansible/roles/docker/handlers/main.yml new file mode 100644 index 0000000000..0a307c98d6 --- /dev/null +++ b/ansible/roles/docker/handlers/main.yml @@ -0,0 +1,5 @@ +- name: Restart Docker + systemd: + name: docker + state: restarted + diff --git a/ansible/roles/docker/tasks/install_compose.yml b/ansible/roles/docker/tasks/install_compose.yml new file mode 100644 index 0000000000..859c33ca56 --- /dev/null +++ b/ansible/roles/docker/tasks/install_compose.yml @@ -0,0 +1,20 @@ +--- +- name: Install Docker Compose + block: + - name: Download Docker Compose + get_url: + url: "https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64" + dest: "/usr/local/bin/docker-compose" + mode: '0755' + + - name: Verify Docker Compose installation + command: docker-compose --version + register: compose_version + changed_when: false + + - name: Display installed Docker Compose version + debug: + msg: "{{ compose_version.stdout }}" + tags: + - compose + diff --git a/ansible/roles/docker/tasks/install_docker.yml b/ansible/roles/docker/tasks/install_docker.yml new file mode 100644 index 0000000000..1150c264dd --- /dev/null +++ b/ansible/roles/docker/tasks/install_docker.yml @@ -0,0 +1,44 @@ +--- +- name: Install Docker and Dependencies + block: + - name: Update apt package index + ansible.builtin.apt: + update_cache: true + + - name: Install required packages + apt: + name: + - apt-transport-https + - ca-certificates + - curl + - software-properties-common + state: present + update_cache: yes + + - name: Add Docker GPG 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 jammy stable" + state: present + + - name: Install Docker + apt: + name: + - docker-ce + - docker-ce-cli + - containerd.io + state: present + update_cache: yes + + - name: Enable and start Docker service + systemd: + name: docker + enabled: yes + state: started + tags: + - install + diff --git a/ansible/roles/docker/tasks/main.yml b/ansible/roles/docker/tasks/main.yml new file mode 100644 index 0000000000..466677352b --- /dev/null +++ b/ansible/roles/docker/tasks/main.yml @@ -0,0 +1,32 @@ +- name: Setup Docker Environment + block: + - name: Install Docker + import_tasks: install_docker.yml + + - name: Install Docker Compose + import_tasks: install_compose.yml + + - name: Add current user to the docker group + user: + name: "{{ ansible_user }}" + groups: docker + append: yes + tags: + -setup + +- name: Secure Docker Configuration + block: + - name: Disable Root Access + copy: + dest: /etc/docker/daemon.json + content: | + { + "no-new-privileges": true, + "userns-remap": "default" + } + owner: root + group: root + mode: '0644' + notify: Restart Docker + tags: + - security diff --git a/ansible/roles/web_app/README.md b/ansible/roles/web_app/README.md new file mode 100644 index 0000000000..79f2db389b --- /dev/null +++ b/ansible/roles/web_app/README.md @@ -0,0 +1,52 @@ +# Web App Role + +This role deploys a web application using Docker and Docker Compose. + +## Requirements + +- Ansible 2.9+ + +- Ubuntu 22.04+ + +- Docker and Docker Compose installed + +## Role Variables + +docker_username: Docker username to pull docker image. + +docker_image: Docker image for the web app. + +docker_image_tag: Tag of the Docker image (`latest`). + +web_app_name: Name of the web application container (`app_python`). + +web_app_external_port: External port for accessing the web app. + +web_app_internal_port: Internal port inside the container. + +web_app_dir: Directory for application data (`~/app_python`). + +web_app_full_wipe: Whether to remove all related Docker resources before deployment (default: false). + +## Example Playbook + +```bash +- hosts: all + roles: + - role: web_app +``` + +## Usage + +To run the whole playbook with automated docker installation and dynamic inventory use command: + +```bash +ansible-playbook playbooks/dev/main.yaml -i inventory/yacloud_compute.yml +``` + +To run a specific stage use flag --tags: + +```bash +ansible-playbook playbooks/dev/main.yaml -i inventory/yacloud_compute.yml --tags +``` + diff --git a/ansible/roles/web_app/defaults/main.yml b/ansible/roles/web_app/defaults/main.yml new file mode 100644 index 0000000000..57bfd6ecb0 --- /dev/null +++ b/ansible/roles/web_app/defaults/main.yml @@ -0,0 +1,9 @@ +--- +web_app_name: "app_python" +docker_username: "ilsiia" +docker_image: "{{ docker_username }}/{{ web_app_name }}" +docker_image_tag: "latest" +web_app_internal_port: 5000 +web_app_external_port: 5001 +web_app_dir: "~/app_python" +web_app_full_wipe: true diff --git a/ansible/roles/web_app/handlers/main.yml b/ansible/roles/web_app/handlers/main.yml new file mode 100644 index 0000000000..cd21505a47 --- /dev/null +++ b/ansible/roles/web_app/handlers/main.yml @@ -0,0 +1,2 @@ +--- + diff --git a/ansible/roles/web_app/meta/main.yml b/ansible/roles/web_app/meta/main.yml new file mode 100644 index 0000000000..cb7d8e0460 --- /dev/null +++ b/ansible/roles/web_app/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - role: docker 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 0000000000..e019b85d85 --- /dev/null +++ b/ansible/roles/web_app/tasks/0-wipe.yml @@ -0,0 +1,18 @@ +--- +- name: Wipe Web App Container and Related Files + block: + - name: Stop and remove the web app container + docker_container: + name: "{{ web_app_name }}" + state: absent + + - name: Remove Docker image + docker_image: + name: "{{ docker_image }}" + tag: "{{ docker_image_tag }}" + state: absent + + - name: Remove application data directory + file: + path: "{{ web_app_dir }}" + state: absent diff --git a/ansible/roles/web_app/tasks/main.yml b/ansible/roles/web_app/tasks/main.yml new file mode 100644 index 0000000000..b6e4954f43 --- /dev/null +++ b/ansible/roles/web_app/tasks/main.yml @@ -0,0 +1,53 @@ +--- +- name: Perform Wipe + when: web_app_full_wipe | default(false) | bool + import_tasks: 0-wipe.yml + tags: + - wipe + +- name: Deploy Web Application + block: + - name: Ensure Web App Directory Exists + file: + path: "{{ web_app_dir }}" + state: directory + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: '0755' + + - name: Generate docker-compose.yml file from template + template: + src: docker-compose.yml.j2 + dest: "{{ web_app_dir }}/docker-compose.yml" + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: '0644' + + - name: Pull the latest Docker image + docker_image: + name: "{{ docker_image }}" + tag: "{{ docker_image_tag }}" + source: pull + - name: Ensure Docker service is running + service: + name: docker + enabled: true + state: started + + - name: Create and start the services + community.docker.docker_compose_v2: + project_src: "{{ web_app_dir }}" + remove_orphans: true + state: present + +# - name: Run the Web App container +# docker_container: +# name: "{{ web_app_name }}" +# image: "{{ docker_image }}" +# state: started +# restart_policy: always +# ports: +# - "{{ web_app_external_port }}:{{ web_app_internal_port }}" + tags: + - deploy + 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 0000000000..3a1ca2e2c8 --- /dev/null +++ b/ansible/roles/web_app/templates/docker-compose.yml.j2 @@ -0,0 +1,7 @@ +services: + web_app: + image: "{{ docker_image }}:{{ docker_image_tag }}" + container_name: "{{ web_app_name }}" + restart: always + ports: + - "{{ web_app_external_port }}:{{ web_app_internal_port }}" diff --git a/app_nodejs/.dockerignore b/app_nodejs/.dockerignore new file mode 100644 index 0000000000..898af96211 --- /dev/null +++ b/app_nodejs/.dockerignore @@ -0,0 +1,7 @@ +.git +.gitignore +**.md +node_modules/ +**.png +tests/ +eslint.config.mjs diff --git a/app_nodejs/.gitignore b/app_nodejs/.gitignore new file mode 100644 index 0000000000..0722fea5fa --- /dev/null +++ b/app_nodejs/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.DS_Store +Thumbs.db diff --git a/app_nodejs/CI.md b/app_nodejs/CI.md new file mode 100644 index 0000000000..f87e8e5e83 --- /dev/null +++ b/app_nodejs/CI.md @@ -0,0 +1,20 @@ +# CI Pipeline + +## Best Practices + +1. Structured Workflow Organization + + Each job is independent and runs only if the previous stage succeeds, ensuring early failure detection. + +2. Workflow Trigger Optimization + + The pipeline triggers only when changes occur in the app_nodejs/ directory or CI workflow files. + +3. Efficient Dependency Management + + The workflow uses Node.js caching (cache: "npm") to store dependencies, reducing installation time. + +4. Automated Deployment to Docker Hub + + The pipeline builds and pushes the latest Docker image only after successful tests. + diff --git a/app_nodejs/DOCKER.md b/app_nodejs/DOCKER.md new file mode 100644 index 0000000000..beb56c374e --- /dev/null +++ b/app_nodejs/DOCKER.md @@ -0,0 +1,30 @@ +# Docker Best Practices for Node.js App + +## Best Practices Implemented + +1. **Non-Root User**: + + - Added a non-root user (`nonrootuser`) for better security. + +2. **Efficient Layer Caching**: + + - Copied only `package.json` and `package-lock.json` for dependency installation to utilize Docker layer caching effectively. + +3. **Clean Installations**: + + - Used `--only=production` to avoid installing unnecessary development dependencies. + +4. **.dockerignore Usage**: + - Excluded unnecessary files like `node_modules`, `.git`, and other files. + +In this particular case, a multi-stage build would not provide significant benefits because the application is relatively simple and does not require a +complex build process or large dependencies that need to be separated. + +## Distroless Image + +The Distroless image for the Node.js app is 132MB, which is 100MB smaller than the previous image (223MB). This +reduction in size is due to the Distroless image excluding unnecessary OS components, leaving only the essential +runtime required to execute the application. The smaller size improves security by reducing the attack surface, +speeds up deployments, and optimizes resource usage. + +![Docker Image Size](./nodejs_images_size.png) diff --git a/app_nodejs/Dockerfile b/app_nodejs/Dockerfile new file mode 100644 index 0000000000..1a12174870 --- /dev/null +++ b/app_nodejs/Dockerfile @@ -0,0 +1,18 @@ +FROM node:18-slim AS build + +RUN adduser nonrootuser + +USER nonrootuser + +WORKDIR /app_nodejs + +COPY --chown=nonrootuser:nonrootuser package*.json app.js ./ + +RUN npm install --only=production + +COPY --chown=nonrootuser:nonrootuser public/ ./public/ + +EXPOSE 3000 + +CMD ["node", "app.js"] + diff --git a/app_nodejs/NODEJS.md b/app_nodejs/NODEJS.md new file mode 100644 index 0000000000..af2a211058 --- /dev/null +++ b/app_nodejs/NODEJS.md @@ -0,0 +1,55 @@ +# Node.js Web Application + +## Framework Choice + +I chose **Express.js** because: + +1. It is minimalistic and efficient for web applications. +2. Built-in routing support. + +## Best Practices + +1. Organized the app into modular components. +2. Used `moment` for timezone handling, which simplifies date and time management. + +## Coding Standards + +1. Code is written in a modular and readable way. +2. Variable names are descriptive and named in lowerCamelCase. + +## Testing + +1. Manualy verified that the app endpoint correctly displays Abu Dabi time. +2. Manualy tested app stability by loading the HTML page in the browser for multiple concurrent times. + +## Unit Testing + +The unit tests are written using **Jest** and **Supertest** to test the Express API endpoint `/current-time`. The following test cases have been covered: + +1. **Valid Response Format** + + - Ensures the `/current-time` endpoint returns a JSON response containing a valid time in `HH:mm:ss` format. + +2. **Correct Abu Dhabi Time (UTC+4)** + + - Verifies that the API returns the expected time with the correct UTC offset. + +3. **404 Not Found for Invalid Routes** + - Checks if the server returns a `404` status for unknown routes. + +### Best Practices in Unit Tests + +1. Comprehensive Test Coverage + + - Covered **normal, edge, and failure scenarios** to ensure robustness. + +2. Mocking Dependencies for Consistency + + - Used **Jest mocks** to control the output of `moment.js`, ensuring consistent test results regardless of the system time. + +3. Proper Error Handling & HTTP Status Codes + + - Implemented error handling in `app.js` to gracefully handle internal failures and unknown routes. + +4. Maintainable & Readable Tests + - Used **clear test names** and grouped tests using `describe()` for better structure. diff --git a/app_nodejs/README.md b/app_nodejs/README.md new file mode 100644 index 0000000000..c611bb367b --- /dev/null +++ b/app_nodejs/README.md @@ -0,0 +1,118 @@ +# Node.js Web Application + +[![Nodejs CI](https://github.com/IlsiyaNasibullina/S25-core-course-labs/actions/workflows/app_nodejs.yml/badge.svg)](https://github.com/IlsiyaNasibullina/S25-core-course-labs/actions/workflows/app_nodejs.yml) + +## Overview + +This web application displays the current time in Abu Dabi. It is built using Node.js and Express, with a client-side HTML page that dynamically fetches the time. + +## Installation + +### Prerequisites + +- Node.js installed +- npm package manager installed + +### Steps + +1. Clone the repository. +2. Install dependencies using command: + + ```bash + npm install + ``` + +3. Run the application: + + ```bash + node app.js + ``` + +4. Open browser and go to: + ` http://localhost:3000` + +## DOCKER + +### Build + +To build the Docker image: + +```bash + docker build --no-cache -t app_nodejs:latest . +``` + +### Pull + +To pull the Docker image: + +```bash + docker pull ilsiia/app_nodejs:latest +``` + +### Run + +To run the Docker image: + +```bash + docker run -p 3000:3000 ilsiia/app_nodejs:latest +``` + +Or if port 3000 on your machine is in use, replace the port 5000 with the free port like that: + +```bash + docker run -p :3000 ilsiia/app_nodejs:latest +``` + +## Distroless Image Version + +### Pull Distroless Image + +To pull the distroless image: + +```bash + docker pull ilsiia/nodejs_app:distroless +``` + +### Run Distroless Image + +To run the distroless image: + +```bash + docker run -p 3000:3000 ilsiia/nodejs_app:distroless +``` + +If port 3000 is not free on your machine change it with free one: + +```bash + docker run -p :3000 ilsiia/nodejs_app:distroless +``` + +### Build Distroless Image + +To build the distroless image: + +```bash + docker build -t nodejs_app:distroless -f distroless.Dockerfile . +``` + +## Unit Tests + +To execute the test suite, run: + +```bash + npm test +``` + +## CI Pipeline + +This project includes a GitHub Actions CI pipeline to automate testing and deployment. The workflow follows these stages: + +- Dependencies - Installs required dependencies. +- Lint - Checks code for style and syntax issues. +- Snyk - Checks for security vulnerabilities. +- Run - Starts the application. +- Test - Runs unit tests to verify functionality. +- Docker - Builds and pushes the Docker image to Docker Hub. + +The CI pipeline is triggered on pushes and pull requests for the app_nodejs/ directory. + diff --git a/app_nodejs/app.js b/app_nodejs/app.js new file mode 100644 index 0000000000..4b37b4220d --- /dev/null +++ b/app_nodejs/app.js @@ -0,0 +1,49 @@ +const express = require("express"); +const moment = require("moment"); +const client = require("prom-client"); + +const app = express(); +const collectDefaultMetrics = client.collectDefaultMetrics; +collectDefaultMetrics(); + +const requestCounter = new client.Counter({ + name: "app_requests_total", + help: "Total number of requests", +}); + +const currentTimeGauge = new client.Gauge({ + name: "current_time", + help: "Current time in Abu Dhabi timezone", +}); + +app.use(express.static("public")); + +app.get("/current-time", (req, res) => { + try { + requestCounter.inc(); + const abuDabiTime = moment().utcOffset("+04:00").format("HH:mm:ss"); + currentTimeGauge.set(Date.now()); + res.json({ time: abuDabiTime }); + } catch (error) { + console.error(error) + res.status(500).json({ error: "Internal Server Error" }); + } +}); + +app.get("/metrics", async (req, res) => { + res.set("Content-Type", client.register.contentType); + res.end(await client.register.metrics()); +}); + +app.use((req, res) => { + res.status(404).json({ error: "Not Found" }); +}); + +if (require.main === module) { + const PORT = 3001; + app.listen(PORT, () => { + console.log(`Server running on http://localhost:${PORT}`); + }); +} + +module.exports = app; diff --git a/app_nodejs/distroless.Dockerfile b/app_nodejs/distroless.Dockerfile new file mode 100644 index 0000000000..ebf79cd4ef --- /dev/null +++ b/app_nodejs/distroless.Dockerfile @@ -0,0 +1,22 @@ +FROM node:18-alpine AS build + +WORKDIR /app_nodejs + +COPY package*.json ./ + +COPY app.js ./ + +COPY public/ ./public/ + +RUN npm install --omit=dev + +FROM gcr.io/distroless/nodejs18-debian12:nonroot AS final + +COPY --from=build /app_nodejs /app_nodejs + +WORKDIR /app_nodejs + +EXPOSE 3000 + +CMD ["app.js"] + diff --git a/app_nodejs/eslint.config.mjs b/app_nodejs/eslint.config.mjs new file mode 100644 index 0000000000..6c9fb5e798 --- /dev/null +++ b/app_nodejs/eslint.config.mjs @@ -0,0 +1,25 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import pluginJest from "eslint-plugin-jest"; + +/** @type {import('eslint').Linter.Config[]} */ +export default [ + {files: ["**/*.js"], languageOptions: {sourceType: "commonjs"}}, + {languageOptions: { globals: globals.browser }}, + pluginJs.configs.recommended, + { + languageOptions: { + globals: globals.jest, + }, + plugins: { + jest: pluginJest, + }, + rules: { + "jest/no-disabled-tests": "warn", + "jest/no-focused-tests": "error", + "jest/no-identical-title": "error", + "jest/prefer-to-have-length": "warn", + "jest/valid-expect": "error", + }, + } +]; diff --git a/app_nodejs/nodejs_images_size.png b/app_nodejs/nodejs_images_size.png new file mode 100644 index 0000000000..b3e912d62f Binary files /dev/null and b/app_nodejs/nodejs_images_size.png differ diff --git a/app_nodejs/package-lock.json b/app_nodejs/package-lock.json new file mode 100644 index 0000000000..2d1b47b8e7 --- /dev/null +++ b/app_nodejs/package-lock.json @@ -0,0 +1,5689 @@ +{ + "name": "app_nodejs", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "app_nodejs", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "express": "^4.21.2", + "jest": "^29.7.0", + "moment": "^2.30.1", + "prettier": "^3.4.2", + "prom-client": "^15.1.3", + "supertest": "^7.0.0" + }, + "devDependencies": { + "@eslint/js": "^9.19.0", + "@types/jest": "^29.5.14", + "eslint": "^9.19.0", + "eslint-plugin-jest": "^28.11.0", + "globals": "^15.14.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz", + "integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.7.tgz", + "integrity": "sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA==", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.5", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.7", + "@babel/parser": "^7.26.7", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.26.7", + "@babel/types": "^7.26.7", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/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==", + "license": "MIT" + }, + "node_modules/@babel/generator": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", + "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.5", + "@babel/types": "^7.26.5", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz", + "integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz", + "integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", + "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", + "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.7.tgz", + "integrity": "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.5", + "@babel/parser": "^7.26.7", + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/traverse/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==", + "license": "MIT" + }, + "node_modules/@babel/types": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz", + "integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "license": "MIT" + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/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, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array/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, + "license": "MIT" + }, + "node_modules/@eslint/core": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", + "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", + "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.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": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/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, + "license": "Python-2.0" + }, + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/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, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/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, + "license": "MIT" + }, + "node_modules/@eslint/js": { + "version": "9.19.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz", + "integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", + "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.10.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "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, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", + "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.13.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.0.tgz", + "integrity": "sha512-ClIbNe36lawluuvq3+YYhnIN2CELi+6q8NpnM7PYp4hBn/TatfboPgVSm2rwKRfnV2M+Ty9GWDFI64KEe+kysA==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.22.0.tgz", + "integrity": "sha512-/lwVV0UYgkj7wPSw0o8URy6YI64QmcOdwHuGuxWIYznO6d45ER0wXUbksr9pYdViAofpUCNJx/tAzNukgvaaiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.22.0", + "@typescript-eslint/visitor-keys": "8.22.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.22.0.tgz", + "integrity": "sha512-0S4M4baNzp612zwpD4YOieP3VowOARgK2EkN/GBn95hpyF8E2fbMT55sRHWBq+Huaqk3b3XK+rxxlM8sPgGM6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.22.0.tgz", + "integrity": "sha512-SJX99NAS2ugGOzpyhMza/tX+zDwjvwAtQFLsBo3GQxiGcvaKlqGBkmZ+Y1IdiSi9h4Q0Lr5ey+Cp9CGWNY/F/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.22.0", + "@typescript-eslint/visitor-keys": "8.22.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/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, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/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, + "license": "MIT" + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.22.0.tgz", + "integrity": "sha512-T8oc1MbF8L+Bk2msAvCUzjxVB2Z2f+vXYfcucE2wOmYs7ZUwco5Ep0fYZw8quNwOiw9K8GYVL+Kgc2pETNTLOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.22.0", + "@typescript-eslint/types": "8.22.0", + "@typescript-eslint/typescript-estree": "8.22.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.22.0.tgz", + "integrity": "sha512-AWpYAXnUgvLNabGTy3uBylkgZoosva/miNd1I8Bz3SjotmQPbVqhO4Cczo8AsZ44XVErEBPr/CRSgaj8sG7g0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.22.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/bintrees": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", + "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001696", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001696.tgz", + "integrity": "sha512-pDCPkvzfa39ehJtJ+OwGT/2yvT2SbjfHhiIW2LWOAcMQ7BzwxT/XuyUp4OTOd0XFWA6BKw0JalnBHgSi5DGJBQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "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/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "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, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.90", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.90.tgz", + "integrity": "sha512-C3PN4aydfW91Natdyd449Kw+BzhLmof6tzy5W1pFC5SpQxVXT+oyiyOG9AgYYSN9OdA/ik3YkCrpwqI8ug5Tug==", + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint": { + "version": "9.19.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.19.0.tgz", + "integrity": "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.0", + "@eslint/core": "^0.10.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "9.19.0", + "@eslint/plugin-kit": "^0.2.5", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jest": { + "version": "28.11.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.11.0.tgz", + "integrity": "sha512-QAfipLcNCWLVocVbZW8GimKn5p5iiMcgGbRzz8z/P5q7xw+cNEpYqyzFMtIF/ZgF2HLOyy+dYBut+DoYolvqig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "engines": { + "node": "^16.10.0 || ^18.12.0 || >=20.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0", + "jest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/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, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/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, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/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, + "license": "MIT" + }, + "node_modules/eslint/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, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "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, + "license": "BSD-2-Clause", + "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, + "license": "BSD-2-Clause", + "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, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/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, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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==", + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", + "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "dev": true, + "license": "ISC" + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.2.tgz", + "integrity": "sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==", + "license": "MIT", + "dependencies": { + "dezalgo": "^1.0.4", + "hexoid": "^2.0.0", + "once": "^1.4.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "function-bind": "^1.1.2", + "get-proto": "^1.0.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.14.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz", + "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hexoid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-2.0.0.tgz", + "integrity": "sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/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, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "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==", + "license": "MIT", + "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==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "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==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "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==", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/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==", + "license": "MIT" + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/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==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-inspect": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "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==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prom-client": { + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz", + "integrity": "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.4.0", + "tdigest": "^0.1.1" + }, + "engines": { + "node": "^16 || ^18 || >=20" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "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" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "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" + } + ], + "license": "MIT", + "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==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/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==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/superagent": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", + "integrity": "sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==", + "license": "MIT", + "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": "^3.5.1", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/superagent/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==", + "license": "MIT" + }, + "node_modules/supertest": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.0.0.tgz", + "integrity": "sha512-qlsr7fIC0lSddmA3tzojvzubYxvlGtzumcdHgPwbFWMISQwL22MhM2Y3LNt+6w9Yyx7559VW5ab70dgphm8qQA==", + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^9.0.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tdigest": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", + "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", + "license": "MIT", + "dependencies": { + "bintrees": "1.0.2" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "license": "BSD-3-Clause" + }, + "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==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-api-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", + "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "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, + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", + "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "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, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/app_nodejs/package.json b/app_nodejs/package.json new file mode 100644 index 0000000000..1a867f2690 --- /dev/null +++ b/app_nodejs/package.json @@ -0,0 +1,30 @@ +{ + "name": "app_nodejs", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "jest", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "prettier --write ." + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "express": "^4.21.2", + "jest": "^29.7.0", + "moment": "^2.30.1", + "prettier": "^3.4.2", + "prom-client": "^15.1.3", + "supertest": "^7.0.0" + }, + "devDependencies": { + "@eslint/js": "^9.19.0", + "@types/jest": "^29.5.14", + "eslint": "^9.19.0", + "eslint-plugin-jest": "^28.11.0", + "globals": "^15.14.0" + } +} diff --git a/app_nodejs/public/index.html b/app_nodejs/public/index.html new file mode 100644 index 0000000000..53aba31222 --- /dev/null +++ b/app_nodejs/public/index.html @@ -0,0 +1,52 @@ + + + + + + Abu Dabi Time + + + +
+

Current Time in Abu Dabi

+

Loading...

+
+ + + + diff --git a/app_nodejs/tests/app.test.js b/app_nodejs/tests/app.test.js new file mode 100644 index 0000000000..e476c62db1 --- /dev/null +++ b/app_nodejs/tests/app.test.js @@ -0,0 +1,31 @@ +const request = require("supertest"); +const app = require("../app"); + +jest.mock("moment", () => { + return jest.fn(() => ({ + utcOffset: jest.fn().mockReturnValue({ + format: jest.fn().mockReturnValue("12:34:56"), + }), + })); +}); + +describe("API Tests - /current-time", () => { + test("Should return a valid time string", async () => { + const response = await request(app).get("/current-time"); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("time"); + expect(response.body.time).toMatch(/^\d{2}:\d{2}:\d{2}$/); + }); + + test("Should return correct Abu Dhabi time (UTC+4)", async () => { + const response = await request(app).get("/current-time"); + expect(response.body.time).toBe("12:34:56"); + }); + + test("Should return 404 for an invalid route", async () => { + const response = await request(app).get("/invalid-route"); + expect(response.status).toBe(404); + }); +}); + diff --git a/app_python/.dockerignore b/app_python/.dockerignore new file mode 100644 index 0000000000..6fda638b79 --- /dev/null +++ b/app_python/.dockerignore @@ -0,0 +1,9 @@ +.git +.gitignore +**.md +__pycache__ +.venv +**.png +tests/ +.coverage +.pytest_cache diff --git a/app_python/.gitignore b/app_python/.gitignore new file mode 100644 index 0000000000..e39170a670 --- /dev/null +++ b/app_python/.gitignore @@ -0,0 +1,7 @@ +Thumbs.db +__pycache__ +.venv +.DS_Store +tests/__pycache__ +.coverage +.pytest_cache diff --git a/app_python/CI.md b/app_python/CI.md new file mode 100644 index 0000000000..6b90555fac --- /dev/null +++ b/app_python/CI.md @@ -0,0 +1,31 @@ +# CI Pipeline + +## Best Practices + +1. Branch and Path-Based Triggers + + The workflow runs only when files in app_python/ or the workflow itself change, preventing unnecessary runs. + +2. Separate Stages for Better Modularity + + The CI pipeline is split into dependencies, lint, run, test, and docker stages, ensuring clear separation of concerns. + +3. Job Run Reduction + + Each job only runs when its dependencies are met, avoiding redundant work and improving efficiency. + +4. Fail Fast on Linting Errors + + The pipeline stops immediately if linting fails, ensuring that no further steps run with improperly formatted code. + +5. Detached Mode + + Using nohup pipeline runs containers in deatached mode for stability. + +6. Virtual Environment Caching + + Python virtual environment (venv) is cached and restored across jobs. This prevents redundant dependency installation, reducing execution time. + +7. Efficient Docker Caching + + The pipeline uses cache-from and cache-to in Docker builds to speed up image creation. diff --git a/app_python/DOCKER.md b/app_python/DOCKER.md new file mode 100644 index 0000000000..8950d66731 --- /dev/null +++ b/app_python/DOCKER.md @@ -0,0 +1,33 @@ +# Docker Best Practices + +## Best Practices Employed + +1. **Non-Root User**: + - The Dockerfile creates a non-root user (`nonrootuser`) to avoid running containers as root, improving security. + +2. **Efficient Layer Caching**: + - Copied `requirements.txt` first to leverage Docker's caching mechanism, avoiding reinstallation of dependencies when source code changes. + +3. **Clean Installations**: + - Used `--no-cache-dir` with `pip install` to avoid unnecessary temporary files in the image. + +4. **File Ownership**: + - Used `--chown` to assign ownership of files to the non-root user. + +5. **.dockerignore Usage**: + - Added a `.dockerignore` file to exclude unnecessary files and reduce the build context. + +6. **Ephemeral Container**: + - Designed the container to be stateless and modular, enabling easy replacement and scaling. + +7. **Multi-Stage Builds**: + - Named build stage to enhances clarity and maintainability. It ensures instructions remain functional even if the Dockerfile's stages are re-ordered. + +## Distroless Image + +The Distroless image minimizes the attack surface and reduces overall image size by excluding unnecessary OS components. However, in my case, I used Python Slim for the Distroless image, resulting in a size of 87.9MB—about 25MB larger than the previous image, which was based on Python Alpine and has size 63.1MB. + +To experiment, I modified the previous Dockerfile by switching from Alpine to Slim, which increased the image size to 161MB. This demonstrates that the Distroless image is still smaller, as it includes only the minimal runtime environment needed to run the application. This enhances security, performance, and deployment efficiency. + +![Docker Image Size Comparison](./images_size.png) + diff --git a/app_python/Dockerfile b/app_python/Dockerfile new file mode 100644 index 0000000000..f75b201b51 --- /dev/null +++ b/app_python/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.11-alpine3.18 AS build + +RUN adduser -D app_python_user + +USER app_python_user + +WORKDIR /app_python + +COPY --chown=app_python_user:app_python_user app.py requirements.txt ./ + +RUN pip install --no-cache-dir -r requirements.txt + +COPY --chown=app_python_user:app_python_user templates/ ./templates/ + +EXPOSE 5000 + +CMD ["python3", "app.py"] + diff --git a/app_python/PYTHON.md b/app_python/PYTHON.md new file mode 100644 index 0000000000..8200d63b54 --- /dev/null +++ b/app_python/PYTHON.md @@ -0,0 +1,45 @@ +# Moscow Time Application + +## Framework Choice + +I chose Flask because: + +- It is lightweight and easy to set up. +- It has extensive documentation and a lot of tutorials. + +Additionally, I used the `pytz` library to handle timezone conversion. + +## Best Practices + +- Used Flask's routing and template rendering to dynamically show Moscow time. +- Followed a clear project structure of logical code sections. +- Avoided redundancy and prioritized simplicity. + +## Coding Standards + +- Followed PEP 8 for clean and readable code. +- Used **Lower Camel Case** for variable and function names to maintain consistent naming conventions. + +## Testing + +- Tested the application to verify that the current Moscow time updates correctly upon refreshing. +- Manually checked the responsiveness and usability of the web interface. + +## Unit Tests + +### Unit Tests Overview + +The tests verify: + +- The homepage loads successfully (200 OK). +- The displayed time is correctly formatted and matches a mocked value. +- The template renders valid time output. +- A non-existent route returns a 404 Not Found error. + +### Best Practices Applied + +- Used pytest.fixture to create a reusable test client. +- Ensured deterministic tests by mocking the current time using monkeypatch. +- Extracted and verified the time format in responses instead of relying on hardcoded text. +- Included a test for incorrect routes to verify proper error handling. + diff --git a/app_python/README.md b/app_python/README.md new file mode 100644 index 0000000000..e93b4ced50 --- /dev/null +++ b/app_python/README.md @@ -0,0 +1,124 @@ +# Python Web Application + +![Python CI](https://github.com/IlsiyaNasibullina/S25-core-course-labs/actions/workflows/app_python.yml/badge.svg) + +## Overview + +This web application displays the current time in Moscow. The app is written in Python and built using Flask . + +## Installation + +### Prerequisites + +- Python 3.x is installed. +- All requirements from requirements.txt are installed. + +### Steps + +1. Clone the repository and go to `app_python` folder. +2. Install the dependencies from requirements.txt file: + + ```bash + pip install -r requirements.txt + ``` + +3. Run the application: + + ```bash + python app.py + ``` + +4. Open the app on link: +`http://127.0.0.1:5000/` + +## Docker + +### Build + +To build the Docker image: + +```bash + docker build --no-cache -t app_python:latest . +``` + +### Pull + +To pull the Docker image: + +```bash + docker pull ilsiia/app_python:latest +``` + +### Run + +To run the Docker image: + +```bash + docker run -p 5000:5000 ilsiia/app_python:latest +``` + +Or if port 5000 on your machine is in use, replace the port 5000 with the free port like that: + +```bash + docker run -p :5000 ilsiia/app_python:latest +``` + +## Distroless Image Version + +### Pull distroless image + +To pull the docker image: + +```bash + docker pull ilsiia/python-app:distroless +``` + +### Run distroless image + +To run the docker image: + +```bash + docker run --rm -p 5000:5000 python-app:distroless +``` + +Or if you have somathing runnong in port 5000 on your machine change port with the free one: + +```bash + docker run --rm -p :5000 python-app:distroless +``` + +### Build distroless image + +To build the image: + +```bash + docker build -t python-app:distroless -f distroless.Dockerfile . +``` + +## Unit Tests + +The tests validate: + +- Correct time is displayed based on the Moscow timezone. +- Homepage and error pages return expected status codes. +- Time format is properly rendered in the template. + +To run the tests, use this command in `app_python/` folder: + +```bash + pytest tests/ +``` + +## CI Pipeline + +This project includes a GitHub Actions CI pipeline to automate testing and deployment. The workflow follows these stages: + +- Dependencies - Installs required dependencies. +- Lint - Checks code for style and syntax issues. +- Snyk - Checks for security vulnerabilities. +- Run - Starts the application. +- Test - Runs unit tests to verify functionality. +- Docker - Builds and pushes the Docker image to Docker Hub. + +The CI pipeline is triggered on pushes and pull requests for the app_python/ directory. + diff --git a/app_python/app.py b/app_python/app.py new file mode 100644 index 0000000000..9cfd35db15 --- /dev/null +++ b/app_python/app.py @@ -0,0 +1,32 @@ +from datetime import datetime +import pytz +from flask import Flask, render_template +from prometheus_client import generate_latest, CONTENT_TYPE_LATEST, Counter, Gauge + +app = Flask(__name__) + +# Define Prometheus metrics +REQUEST_COUNT = Counter("app_requests_total", "Total number of requests") +CURRENT_TIME = Gauge("app_current_time", "Current Moscow time in seconds") + + +@app.route("/") +def getCurrentTime(): + REQUEST_COUNT.inc() # Increment request count + + moscowTimeZone = pytz.timezone("Europe/Moscow") + currentTime = datetime.now(moscowTimeZone).strftime("%H:%M:%S") + + # Store time in seconds since epoch for Prometheus + CURRENT_TIME.set(datetime.now(moscowTimeZone).timestamp()) + + return render_template("index.html", time=currentTime) + + +@app.route("/metrics") +def metrics(): + return generate_latest(), 200, {"Content-Type": CONTENT_TYPE_LATEST} + + +if __name__ == "__main__": + app.run(debug=True, host="0.0.0.0", port=5000) diff --git a/app_python/distroless.Dockerfile b/app_python/distroless.Dockerfile new file mode 100644 index 0000000000..e56e8f37d0 --- /dev/null +++ b/app_python/distroless.Dockerfile @@ -0,0 +1,27 @@ +FROM python:3.11-slim AS build + +WORKDIR /app_python + +COPY app.py . + +COPY requirements.txt . + +COPY templates/ templates/ + +RUN pip install --no-cache-dir -r requirements.txt + +FROM gcr.io/distroless/python3:nonroot AS final + +COPY --from=build /app_python /app_python + +COPY --from=build /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages + +WORKDIR /app_python + +EXPOSE 5000 + +ENV PYTHONPATH=/usr/local/lib/python3.11/site-packages + +CMD ["app.py"] + + diff --git a/app_python/images_size.png b/app_python/images_size.png new file mode 100644 index 0000000000..cb1e9207db Binary files /dev/null and b/app_python/images_size.png differ diff --git a/app_python/requirements.txt b/app_python/requirements.txt new file mode 100644 index 0000000000..878281642a --- /dev/null +++ b/app_python/requirements.txt @@ -0,0 +1,7 @@ +Flask +pytz +flake8 +pytest +prometheus_client +werkzeug + diff --git a/app_python/templates/index.html b/app_python/templates/index.html new file mode 100644 index 0000000000..ef8bebbb27 --- /dev/null +++ b/app_python/templates/index.html @@ -0,0 +1,49 @@ + + + + + + Moscow Time + + + +
+

Current Time in Moscow

+

{{ time }}

+
Updated on page refresh
+
+ + + diff --git a/app_python/tests/__init__.py b/app_python/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app_python/tests/test_app.py b/app_python/tests/test_app.py new file mode 100644 index 0000000000..17e32e86cd --- /dev/null +++ b/app_python/tests/test_app.py @@ -0,0 +1,55 @@ +from datetime import datetime + +import pytest + +from app import app + + +@pytest.fixture +def client(): + """Create a test client for the Flask app.""" + with app.test_client() as client: + yield client + + +def test_homepage_status_code(client): + """Checks if the homepage loads successfully.""" + response = client.get("/") + assert response.status_code == 200 + + +def test_homepage_content(client, monkeypatch): + """Test if the homepage returns the correct time and renders correctly.""" + + class MockDatetime: + @classmethod + def now(cls, tz=None): + return datetime(2024, 1, 1, 12, 34, 56) + + monkeypatch.setattr("app.datetime", MockDatetime) + + response = client.get("/") + assert response.status_code == 200 + assert "12:34:56" in response.data.decode("utf-8") # Ensures time matches mock + + +def test_template_rendering(client): + """Ensure that the response contains a valid time format.""" + response = client.get("/") + assert response.status_code == 200 + + import re + + match = re.search(r"\d{2}:\d{2}:\d{2}", response.data.decode("utf-8")) + assert match is not None + + +def test_404_error(client): + """Ensure that a non-existent route returns a 404 status.""" + response = client.get("/nonexistent") + assert response.status_code == 404 + + +def test_debug_mode(): + app.debug = True + assert app.debug is True diff --git a/k8s/11.md b/k8s/11.md new file mode 100644 index 0000000000..9c286a5586 --- /dev/null +++ b/k8s/11.md @@ -0,0 +1,325 @@ +# Secrets Management + +## Task 1 + +Creation of a secret using `kubectl`: + +```bash +k8s % kubectl create secret generic my-secret \ + --from-literal=username=admin \ + --from-literal=password='SuperSecret123' + +secret/my-secret created +``` + +Checking secret using `kubectl get secrets` command and in list I have my recently created secret `my-secret`: + +```bash +k8s % kubectl get secrets +NAME TYPE DATA AGE +my-secret Opaque 2 8s +sh.helm.release.v1.app-nodejs-library.v1 helm.sh/release.v1 1 5d21h +sh.helm.release.v1.app-nodejs-library.v2 helm.sh/release.v1 1 5d20h +sh.helm.release.v1.app-nodejs.v1 helm.sh/release.v1 1 5d21h +sh.helm.release.v1.app-nodejs.v2 helm.sh/release.v1 1 5d21h +sh.helm.release.v1.app-python-library.v1 helm.sh/release.v1 1 5d21h +sh.helm.release.v1.app-python-library.v2 helm.sh/release.v1 1 5d21h +sh.helm.release.v1.app-python.v1 helm.sh/release.v1 1 5d23h +sh.helm.release.v1.helm-hooks.v1 helm.sh/release.v1 1 5d22h +sh.helm.release.v1.helm-hooks.v2 helm.sh/release.v1 1 5d22h +sh.helm.release.v1.helm-hooks.v3 helm.sh/release.v1 1 5d22h +sh.helm.release.v1.helm-hooks.v4 helm.sh/release.v1 1 5d21h +sh.helm.release.v1.helm-hooks.v5 helm.sh/release.v1 1 5d21h +``` + +Verification of secret presence: + +```bash +k8s % kubectl get secret my-secret -o yaml + +apiVersion: v1 +data: + password: U3VwZXJTZWNyZXQxMjM= + username: YWRtaW4= +kind: Secret +metadata: + creationTimestamp: "2025-03-04T07:21:22Z" + name: my-secret + namespace: default + resourceVersion: "28948" + uid: 986b2283-54ed-42a6-83d6-36eddae26b84 +type: Opaque +``` + +Decryption of secret: + +![Decrypt secret](./pics/kubectl_secret.png) + +The creation of gpg key: + +```bash +% gpg --list-keys +gpg: проверка таблицы доверия +gpg: marginals needed: 3 completes needed: 1 trust model: pgp +gpg: глубина: 0 достоверных: 1 подписанных: 0 доверие: 0-, 0q, 0n, 0m, 0f, 1u +gpg: срок следующей проверки таблицы доверия 2028-03-03 +[keyboxd] +--------- +pub ed25519 2025-03-04 [SC] [ годен до: 2028-03-03] + C182F57B183FB6DC69DB7BC9ADB764F4E121FE81 +uid [ абсолютно ] Ilsiia +sub cv25519 2025-03-04 [E] [ годен до: 2028-03-03] +``` + +After following the tutorial video I installed `app-python` with secrets.yaml: + +```bash +app-python % helm secrets install app-python . -f secrets.yaml +NAME: app-python +LAST DEPLOYED: Tue Mar 4 15:15:28 2025 +NAMESPACE: default +STATUS: deployed +REVISION: 1 +NOTES: +1. Get the application URL by running these commands: + export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=app-python,app.kubernetes.io/instance=app-python" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace default $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace default port-forward $POD_NAME 8080:$CONTAINER_PORT +secrets.yaml.dec +``` + +Verification of creating secrets with `sops`: + +![Sops](./pics/helm_view.png) + +The pods name is highlighted in the screenshot below: + +![Kubectl pod](./pics/kubectl_get_po.png) + +The result of command kubectl exec app-python -- printenv | grep MY_PASS`: + +![Pass](./pics/app_python_pass.png) + +## Task 2 + +Installing Vault: + +![Vault](./pics/kubectl_vault.png) + +Setting the secret in vault: + +```bash +app-python % kubectl exec -it vault-0 -- /bin/sh +/ $ vault secrets enable -path=internal kv-v2 +Success! Enabled the kv-v2 secrets engine at: internal/ +/ $ vault kv put internal/database/config username="db-readonly-username" password="db-secret-password" +======== Secret Path ======== +internal/data/database/config + +======= Metadata ======= +Key Value +--- ----- +created_time 2025-03-04T12:36:14.027193549Z +custom_metadata +deletion_time n/a +destroyed false +version 1 +/ $ vault kv get internal/database/config +======== Secret Path ======== +internal/data/database/config + +======= Metadata ======= +Key Value +--- ----- +created_time 2025-03-04T12:36:14.027193549Z +custom_metadata +deletion_time n/a +destroyed false +version 1 + +====== Data ====== +Key Value +--- ----- +password db-secret-password +username db-readonly-username +/ $ exit +``` + +Configuring Kubernetes Authentication: + +```bash +app-python % kubectl exec -it vault-0 -- /bin/sh +/ $ vault auth enable kubernetes +Success! Enabled kubernetes auth method at: kubernetes/ +/ $ vault write auth/kubernetes/config \ +> kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443" +Success! Data written to: auth/kubernetes/config +/ $ +/ $ vault policy write app-python - < path "internal/data/database/config" { +> capabilities = ["read"] +> } +> EOF +Success! Uploaded policy: app-python +/ $ vault write auth/kubernetes/role/app-python \ +> bound_service_account_names=app-python \ +> bound_service_account_namespaces=default \ +> policies=app-python \ +> ttl=24h +Success! Data written to: auth/kubernetes/role/app-python +/ $ exit +``` + +The result of injecting credentials: + +```bash +app-python % kubectl exec -it app-python-55d9655f8b-7mqrz -- /bin/sh +Defaulted container "app-python" out of: app-python, vault-agent, vault-agent-init (init) +/app_python $ df -h +Filesystem Size Used Available Use% Mounted on +overlay 58.4G 43.4G 12.0G 78% / +tmpfs 64.0M 0 64.0M 0% /dev +shm 64.0M 16.0K 64.0M 0% /dev/shm +tmpfs 3.8G 4.0K 3.8G 0% /vault/secrets +/dev/vda1 58.4G 43.4G 12.0G 78% /dev/termination-log +/dev/vda1 58.4G 43.4G 12.0G 78% /etc/resolv.conf +/dev/vda1 58.4G 43.4G 12.0G 78% /etc/hostname +/dev/vda1 58.4G 43.4G 12.0G 78% /etc/hosts +tmpfs 3.8G 12.0K 3.8G 0% /run/secrets/kubernetes.io/serviceaccount +tmpfs 64.0M 0 64.0M 0% /proc/kcore +tmpfs 64.0M 0 64.0M 0% /proc/keys +tmpfs 64.0M 0 64.0M 0% /proc/timer_list +tmpfs 1.9G 0 1.9G 0% /sys/firmware +/app_python $ cat /vault/secrets/database-config.txt +postgresql://db-readonly-username:db-secret-password@postgres:5432/wizard +``` + +## Bonus + +### Limiting resources + +For resource limiting I added these lines in `values.yaml` in `app-python` and `app-nodejs` charts: + +```yaml +resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi +``` + +The result in `app-python`: + +```bash +app-python % kubectl describe deployments.apps app-python +Name: app-python +Namespace: default +CreationTimestamp: Tue, 04 Mar 2025 17:41:43 +0300 +Labels: app.kubernetes.io/instance=app-python + app.kubernetes.io/managed-by=Helm + app.kubernetes.io/name=app-python + app.kubernetes.io/version=1.16.0 + helm.sh/chart=app-python-0.1.0 +Annotations: deployment.kubernetes.io/revision: 6 + meta.helm.sh/release-name: app-python + meta.helm.sh/release-namespace: default +Selector: app.kubernetes.io/instance=app-python,app.kubernetes.io/name=app-python +Replicas: 1 desired | 1 updated | 1 total | 1 available | 0 unavailable +StrategyType: RollingUpdate +MinReadySeconds: 0 +RollingUpdateStrategy: 25% max unavailable, 25% max surge +Pod Template: + Labels: app=app-python + app.kubernetes.io/instance=app-python + app.kubernetes.io/managed-by=Helm + app.kubernetes.io/name=app-python + app.kubernetes.io/version=1.16.0 + helm.sh/chart=app-python-0.1.0 + Annotations: kubectl.kubernetes.io/restartedAt: 2025-03-04T18:31:24+03:00 + vault.hashicorp.com/agent-inject: true + vault.hashicorp.com/agent-inject-secret-database-config.txt: internal/data/database/config + vault.hashicorp.com/agent-inject-status: update + vault.hashicorp.com/agent-inject-template-database-config.txt: + {{- with secret "internal/data/database/config" -}} + postgresql://{{ .Data.data.username }}:{{ .Data.data.password }}@postgres:5432/wizard + {{- end -}} + vault.hashicorp.com/role: app-python + Service Account: app-python + Containers: + app-python: + Image: ilsiia/app-python:latest + Port: 5000/TCP + Host Port: 0/TCP + Limits: + cpu: 100m + memory: 128Mi + Requests: + cpu: 100m + memory: 128Mi + Liveness: http-get http://:http/ delay=0s timeout=1s period=10s #success=1 #failure=3 + Readiness: http-get http://:http/ delay=0s timeout=1s period=10s #success=1 #failure=3 +``` + +The result in `app-nodejs`: + +```bash +app-nodejs % kubectl describe deployments.apps app-nodejs +Name: app-nodejs +Namespace: default +CreationTimestamp: Wed, 26 Feb 2025 12:45:05 +0300 +Labels: app.kubernetes.io/instance=app-nodejs + app.kubernetes.io/managed-by=Helm + app.kubernetes.io/name=app-nodejs + app.kubernetes.io/version=1.16.0 + helm.sh/chart=app-nodejs-0.1.0 +Annotations: deployment.kubernetes.io/revision: 2 + meta.helm.sh/release-name: app-nodejs + meta.helm.sh/release-namespace: default +Selector: app.kubernetes.io/instance=app-nodejs,app.kubernetes.io/name=app-nodejs +Replicas: 1 desired | 1 updated | 1 total | 1 available | 0 unavailable +StrategyType: RollingUpdate +MinReadySeconds: 0 +RollingUpdateStrategy: 25% max unavailable, 25% max surge +Pod Template: + Labels: app.kubernetes.io/instance=app-nodejs + app.kubernetes.io/managed-by=Helm + app.kubernetes.io/name=app-nodejs + app.kubernetes.io/version=1.16.0 + helm.sh/chart=app-nodejs-0.1.0 + Service Account: app-nodejs + Containers: + app-nodejs: + Image: ilsiia/app-nodejs:latest + Port: 3001/TCP + Host Port: 0/TCP + Limits: + cpu: 100m + memory: 128Mi + Requests: + cpu: 100m + memory: 128Mi + Liveness: http-get http://:http/ delay=0s timeout=1s period=10s #success=1 #failure=3 + Readiness: http-get http://:http/ delay=0s timeout=1s period=10s #success=1 #failure=3 +``` + +### Environment Variables + +The result of adding environment variables in `app-nodejs`: + +```bash +app-nodejs % kubectl exec app-nodejs-5d6b4fbcb-scgzt -- env | grep MY +MY_ENV=hello +``` + +The result of adding environment variables in `app-python`: + +```bash +app-python % kubectl exec app-python-97d4c854f-q26mq -- env | grep M +Defaulted container "app-python" out of: app-python, vault-agent, vault-agent-init (init) +HOSTNAME=app-python-97d4c854f-q26mq +MY_PASS=SuperSecret1234! +MY_VAR=ilsiia +``` diff --git a/k8s/HELM.md b/k8s/HELM.md new file mode 100644 index 0000000000..fcc7d8ab9b --- /dev/null +++ b/k8s/HELM.md @@ -0,0 +1,503 @@ +# Helm + +## Task 1 + +Firt of all, I installed helm: + +![install helm](./pics/helm_install.png) + +After that I created app-python chart and installed it: + +![install app-python chart](./pics/python_install.png) + +Minikube dashboard: + +![Python dash](./pics/helm_dash_1.png) +![Python dash](./pics/helm_dash_2.png) +![Python dash](./pics/helm_dash_3.png) + +Chart was created: + +```bash +k8s % helm list + +NAME NAMESPACE REVISION UPDATED STATUS CHART +APP VERSION +app-python default 1 2025-02-26 10:58:55.100689 +0300 MSK deployed app-python-0.1.0 +1.16.0 + + + +k8s % kubectl get pods,svc +NAME READY STATUS RESTARTS AGE +pod/app-nodejs-deployment-5cc557f5d6-4vsk7 1/1 Running 2 (47m ago) 18h +pod/app-nodejs-deployment-5cc557f5d6-9rw2q 1/1 Running 2 (47m ago) 18h +pod/app-nodejs-deployment-5cc557f5d6-qlxjs 1/1 Running 2 (47m ago) 18h +pod/app-python-58c4758cb-zkmjn 1/1 Running 0 12m +pod/app-python-deployment-79d8d46c8d-9zqkp 1/1 Running 2 (47m ago) 18h +pod/app-python-deployment-79d8d46c8d-tfrl2 1/1 Running 2 (47m ago) 18h +pod/app-python-deployment-79d8d46c8d-v6hs8 1/1 Running 2 (47m ago) 18h + +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +service/app-nodejs-service ClusterIP 10.110.72.250 80/TCP 18h +service/app-python ClusterIP 10.110.73.32 5000/TCP 12m +service/app-python-service ClusterIP 10.105.82.35 80/TCP 18h +service/kubernetes ClusterIP 10.96.0.1 443/TCP 22h +``` + +And screenshot with `minikude service app-python`: + +![minikube](./pics/python-app.png) + +## Task 2 + +The output of `helm lint app-python`: + +![Lint](./pics/python_lint.png) + +The output of `helm install --dry-run helm-hooks app-python`: + +```bash +k8s % helm install --dry-run helm-hooks app-python +NAME: helm-hooks +LAST DEPLOYED: Wed Feb 26 11:47:12 2025 +NAMESPACE: default +STATUS: pending-install +REVISION: 1 +HOOKS: +--- +# Source: app-python/templates/post-install-hook.yaml +apiVersion: v1 +kind: Pod +metadata: + name: postinstall-hook + annotations: + "helm.sh/hook": "post-install" +spec: + containers: + - name: post-install-container + image: busybox + imagePullPolicy: Always + command: ['sh', '-c', 'echo Post-install hook is running... && sleep 15'] + restartPolicy: Never + terminationGracePeriodSeconds: 0 +--- +# Source: app-python/templates/pre-install-hook.yaml +apiVersion: v1 +kind: Pod +metadata: + name: preinstall-hook + annotations: + "helm.sh/hook": "pre-install" +spec: + containers: + - name: pre-install-container + image: busybox + imagePullPolicy: IfNotPresent + command: ['sh', '-c', 'echo Pre-install hook is running... && sleep 20'] + restartPolicy: Never + terminationGracePeriodSeconds: 0 +--- +# Source: app-python/templates/tests/test-connection.yaml +apiVersion: v1 +kind: Pod +metadata: + name: "helm-hooks-app-python-test-connection" + labels: + helm.sh/chart: app-python-0.1.0 + app.kubernetes.io/name: app-python + app.kubernetes.io/instance: helm-hooks + app.kubernetes.io/version: "1.16.0" + app.kubernetes.io/managed-by: Helm + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['helm-hooks-app-python:5000'] + restartPolicy: Never +MANIFEST: +--- +# Source: app-python/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: helm-hooks-app-python + labels: + helm.sh/chart: app-python-0.1.0 + app.kubernetes.io/name: app-python + app.kubernetes.io/instance: helm-hooks + app.kubernetes.io/version: "1.16.0" + app.kubernetes.io/managed-by: Helm +automountServiceAccountToken: true +--- +# Source: app-python/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: helm-hooks-app-python + labels: + helm.sh/chart: app-python-0.1.0 + app.kubernetes.io/name: app-python + app.kubernetes.io/instance: helm-hooks + app.kubernetes.io/version: "1.16.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + ports: + - port: 5000 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: app-python + app.kubernetes.io/instance: helm-hooks +--- +# Source: app-python/templates/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: helm-hooks-app-python + labels: + helm.sh/chart: app-python-0.1.0 + app.kubernetes.io/name: app-python + app.kubernetes.io/instance: helm-hooks + app.kubernetes.io/version: "1.16.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: app-python + app.kubernetes.io/instance: helm-hooks + template: + metadata: + labels: + helm.sh/chart: app-python-0.1.0 + app.kubernetes.io/name: app-python + app.kubernetes.io/instance: helm-hooks + app.kubernetes.io/version: "1.16.0" + app.kubernetes.io/managed-by: Helm + spec: + serviceAccountName: helm-hooks-app-python + containers: + - name: app-python + image: "ilsiia/app-python:latest" + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 5000 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + +NOTES: +1. Get the application URL by running these commands: + export POD_NAME=$(kubectl get pods --namespace default -l +"app.kubernetes.io/name=app-python,app.kubernetes.io/instance=helm-hooks" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace default $POD_NAME -o +jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace default port-forward $POD_NAME 8080:$CONTAINER_PORT +``` + +The output of `kubectl get po`: + +![kubectl](./pics/python_po.png) + +The output of `kubectl describe po postinstall-hook` and `kubectl describe po preinstall-hook`: + +```bash +k8s % kubectl describe po postinstall-hook +Name: postinstall-hook +Namespace: default +Priority: 0 +Service Account: default +Node: minikube/192.168.49.2 +Start Time: Wed, 26 Feb 2025 12:10:35 +0300 +Labels: +Annotations: helm.sh/hook: post-install +Status: Succeeded +IP: 10.244.0.86 +IPs: + IP: 10.244.0.86 +Containers: + post-install-container: + Container ID: docker://fba4ec4a46077faff8283d298134eb49dbfe30713b581c2c834686f03e5aaba6 + Image: busybox + Image ID: docker-pullable://busybox@sha256:498a000f370d8c37927118ed80afe8adc38d1edcbfc071627d17b25c88efcab0 + Port: + Host Port: + Command: + sh + -c + echo Post-install hook is running... && sleep 15 + State: Terminated + Reason: Completed + Exit Code: 0 + Started: Wed, 26 Feb 2025 12:10:43 +0300 + Finished: Wed, 26 Feb 2025 12:10:59 +0300 + Ready: False + Restart Count: 0 + Environment: + Mounts: + /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-w26rk (ro) +Conditions: + Type Status + PodReadyToStartContainers False + Initialized True + Ready False + ContainersReady False + PodScheduled True +Volumes: + kube-api-access-w26rk: + Type: Projected (a volume that contains injected data from multiple sources) + TokenExpirationSeconds: 3607 + ConfigMapName: kube-root-ca.crt + ConfigMapOptional: + DownwardAPI: true +QoS Class: BestEffort +Node-Selectors: +Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 300s + node.kubernetes.io/unreachable:NoExecute op=Exists for 300s +Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Normal Scheduled 2m19s default-scheduler Successfully assigned default/postinstall-hook to minikube + Normal Pulling 2m19s kubelet Pulling image "busybox" + Normal Pulled 2m11s kubelet Successfully pulled image "busybox" in 8.047s (8.047s including waiting). +Image size: 4042190 bytes. + Normal Created 2m11s kubelet Created container: post-install-container + Normal Started 2m10s kubelet Started container post-install-container +ilsianasibullina@MacBook-Air-Ilsia k8s % kubectl describe po preinstall-hook +Name: preinstall-hook +Namespace: default +Priority: 0 +Service Account: default +Node: minikube/192.168.49.2 +Start Time: Wed, 26 Feb 2025 12:08:58 +0300 +Labels: +Annotations: helm.sh/hook: pre-install +Status: Succeeded +IP: 10.244.0.85 +IPs: + IP: 10.244.0.85 +Containers: + pre-install-container: + Container ID: docker://0f3f91666083c091c2af74eb25ddc6d1974d9accc32069fd031b1a3760e83e96 + Image: busybox + Image ID: docker-pullable://busybox@sha256:498a000f370d8c37927118ed80afe8adc38d1edcbfc071627d17b25c88efcab0 + Port: + Host Port: + Command: + sh + -c + echo Pre-install hook is running... && sleep 20 + State: Terminated + Reason: Completed + Exit Code: 0 + Started: Wed, 26 Feb 2025 12:08:59 +0300 + Finished: Wed, 26 Feb 2025 12:09:19 +0300 + Ready: False + Restart Count: 0 + Environment: + Mounts: + /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-t8992 (ro) +Conditions: + Type Status + PodReadyToStartContainers False + Initialized True + Ready False + ContainersReady False + PodScheduled True +Volumes: + kube-api-access-t8992: + Type: Projected (a volume that contains injected data from multiple sources) + TokenExpirationSeconds: 3607 + ConfigMapName: kube-root-ca.crt + ConfigMapOptional: + DownwardAPI: true +QoS Class: BestEffort +Node-Selectors: +Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 300s + node.kubernetes.io/unreachable:NoExecute op=Exists for 300s +Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Normal Scheduled 4m37s default-scheduler Successfully assigned default/preinstall-hook to minikube + Normal Pulled 4m36s kubelet Container image "busybox" already present on machine + Normal Created 4m36s kubelet Created container: pre-install-container + Normal Started 4m36s kubelet Started container pre-install-container + +``` + +After intriducing Delete Policy: + +![delete policy](./pics/python_kubectl.png) + +```bash +k8s % helm status helm-hooks + +NAME: helm-hooks +LAST DEPLOYED: Wed Feb 26 12:17:30 2025 +NAMESPACE: default +STATUS: deployed +REVISION: 2 +NOTES: +1. Get the application URL by running these commands: + export POD_NAME=$(kubectl get pods --namespace default -l +"app.kubernetes.io/name=app-python,app.kubernetes.io/instance=helm-hooks" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace default $POD_NAME -o +jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace default port-forward $POD_NAME 8080:$CONTAINER_PORT + +``` + +## Bonus task + +The screenshot with `minikude service app-nodejs` after creating app-nodejs chart: + +![Minikube](./pics/nodejs-app.png) + +The output of `kubectl get pods,svc` after installing chart for second app: + +```bash +ilsianasibullina@MacBook-Air-Ilsia k8s % kubectl get pods,svc +NAME READY STATUS RESTARTS AGE +pod/app-nodejs-869869d8fd-shdts 1/1 Running 0 52s +pod/app-nodejs-deployment-5cc557f5d6-4vsk7 1/1 Running 2 (142m ago) 20h +pod/app-nodejs-deployment-5cc557f5d6-9rw2q 1/1 Running 2 (142m ago) 20h +pod/app-nodejs-deployment-5cc557f5d6-qlxjs 1/1 Running 2 (142m ago) 20h +pod/app-python-deployment-79d8d46c8d-9thvn 1/1 Running 0 43m +pod/app-python-deployment-79d8d46c8d-d69dx 1/1 Running 0 43m +pod/app-python-deployment-79d8d46c8d-lqdkh 1/1 Running 0 43m +pod/helm-hooks-app-python-645fd5ddd5-mvrx5 1/1 Running 0 24m + +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +service/app-nodejs ClusterIP 10.108.29.112 3001/TCP 52s +service/app-nodejs-service ClusterIP 10.110.72.250 80/TCP 20h +service/app-python ClusterIP 10.110.73.32 5000/TCP 107m +service/app-python-service ClusterIP 10.107.188.222 80/TCP 43m +service/helm-hooks-app-python ClusterIP 10.111.90.107 5000/TCP 23m +service/kubernetes ClusterIP 10.96.0.1 443/TCP 24h + +``` + +The output of linting: + +![Lint](./pics/nodejs_lint.png) + +The output of `kubectl get po` after introducing preinstall-hook and postinstall-hook for second app: + +```bash +k8s % kubectl get po +NAME READY STATUS RESTARTS AGE +app-nodejs-869869d8fd-shdts 1/1 Running 0 10m +app-nodejs-deployment-5cc557f5d6-4vsk7 1/1 Running 2 (151m ago) 20h +app-nodejs-deployment-5cc557f5d6-9rw2q 1/1 Running 2 (151m ago) 20h +app-nodejs-deployment-5cc557f5d6-qlxjs 1/1 Running 2 (151m ago) 20h +app-python-deployment-79d8d46c8d-9thvn 1/1 Running 0 52m +app-python-deployment-79d8d46c8d-d69dx 1/1 Running 0 52m +app-python-deployment-79d8d46c8d-lqdkh 1/1 Running 0 52m +helm-hooks-app-python-645fd5ddd5-mvrx5 1/1 Running 0 33m +postinstall-hook 0/1 Completed 0 19s +preinstall-hook 0/1 Completed 0 24s + +``` + +The output agter introducing delete policy: + +```bash +k8s % helm upgrade --install helm-hooks app-nodejs + +Release "helm-hooks" has been upgraded. Happy Helming! +NAME: helm-hooks +LAST DEPLOYED: Wed Feb 26 12:59:24 2025 +NAMESPACE: default +STATUS: deployed +REVISION: 5 +NOTES: +1. Get the application URL by running these commands: + export POD_NAME=$(kubectl get pods --namespace default -l +"app.kubernetes.io/name=app-nodejs,app.kubernetes.io/instance=helm-hooks" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace default $POD_NAME -o +jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace default port-forward $POD_NAME 8080:$CONTAINER_PORT + + +k8s % kubectl get po +NAME READY STATUS RESTARTS AGE +app-nodejs-869869d8fd-shdts 1/1 Running 0 14m +app-nodejs-deployment-5cc557f5d6-4vsk7 1/1 Running 2 (155m ago) 20h +app-nodejs-deployment-5cc557f5d6-9rw2q 1/1 Running 2 (155m ago) 20h +app-nodejs-deployment-5cc557f5d6-qlxjs 1/1 Running 2 (155m ago) 20h +app-python-deployment-79d8d46c8d-9thvn 1/1 Running 0 56m +app-python-deployment-79d8d46c8d-d69dx 1/1 Running 0 56m +app-python-deployment-79d8d46c8d-lqdkh 1/1 Running 0 56m +helm-hooks-app-nodejs-66d5b45754-z54dj 1/1 Running 0 14s +``` + +### Library Chart + +Output of upgrading charts after adding library chart as dependency: + +```bash +k8s % helm dependency update app-python +Hang tight while we grab the latest from your chart repositories... +...Successfully got an update from the "stable" chart repository +Update Complete. ⎈Happy Helming!⎈ +Saving 1 charts +Deleting outdated charts +ilsianasibullina@MacBook-Air-Ilsia k8s % helm dependency update app-nodejs +Hang tight while we grab the latest from your chart repositories... +...Successfully got an update from the "stable" chart repository +Update Complete. ⎈Happy Helming!⎈ +Saving 1 charts +Deleting outdated charts +``` + +The output of installing: + +```bash +k8s % helm upgrade --install app-python-library app-python +Release "app-python-library" has been upgraded. Happy Helming! +NAME: app-python-library +LAST DEPLOYED: Wed Feb 26 13:21:27 2025 +NAMESPACE: default +STATUS: deployed +REVISION: 2 +NOTES: +1. Get the application URL by running these commands: + export POD_NAME=$(kubectl get pods --namespace default -l +"app.kubernetes.io/name=app-python,app.kubernetes.io/instance=app-python-library" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace default $POD_NAME -o +jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace default port-forward $POD_NAME 8080:$CONTAINER_PORT +k8s % helm upgrade --install app-nodejs-library app-nodejs +Release "app-nodejs-library" has been upgraded. Happy Helming! +NAME: app-nodejs-library +LAST DEPLOYED: Wed Feb 26 13:21:40 2025 +NAMESPACE: default +STATUS: deployed +REVISION: 2 +NOTES: +1. Get the application URL by running these commands: + export POD_NAME=$(kubectl get pods --namespace default -l +"app.kubernetes.io/name=app-nodejs,app.kubernetes.io/instance=app-nodejs-library" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace default $POD_NAME -o +jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace default port-forward $POD_NAME 8080:$CONTAINER_PORT + +``` diff --git a/k8s/README.md b/k8s/README.md new file mode 100644 index 0000000000..19d0b927db --- /dev/null +++ b/k8s/README.md @@ -0,0 +1,92 @@ +# Kubernetes and Minikubes + +## Task 1 + +The result of running deployment and service: + +```bash +S25-core-course-labs % kubectl get pods,svc +NAME READY STATUS RESTARTS AGE +pod/app-python-7596dddfb6-gtgxv 1/1 Running 0 5m + +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +service/app-python NodePort 10.96.32.180 5000:30663/TCP 2m42s +service/kubernetes ClusterIP 10.96.0.1 443/TCP 23m + +``` + +## Task 2 + +Manifests development.yml and service.yml were created: + +```bash +k8s % kubectl apply -f app-python + +deployment.apps/app-python-deployment created +service/app-python-service created +``` + +The output of `kubectl get pods,svc`: + +```bash +k8s % kubectl get pods,svc +NAME READY STATUS RESTARTS AGE +pod/app-python-deployment-79d8d46c8d-5nwg2 1/1 Running 0 6m27s +pod/app-python-deployment-79d8d46c8d-m9vtg 1/1 Running 0 6m19s +pod/app-python-deployment-79d8d46c8d-pf9z9 1/1 Running 0 6m37s + +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +service/app-python-service NodePort 10.100.44.227 5000:30001/TCP 11s +service/kubernetes ClusterIP 10.96.0.1 443/TCP 113m + +``` + +The output of `minikube service --all`: + +```bash +k8s % minikube service --all +|-----------|--------------------|-------------|---------------------------| +| NAMESPACE | NAME | TARGET PORT | URL | +|-----------|--------------------|-------------|---------------------------| +| default | app-python-service | 5000 | http://192.168.49.2:30001 | +|-----------|--------------------|-------------|---------------------------| +|-----------|------------|-------------|--------------| +| NAMESPACE | NAME | TARGET PORT | URL | +|-----------|------------|-------------|--------------| +| default | kubernetes | | No node port | +|-----------|------------|-------------|--------------| +😿 service default/kubernetes has no node port +❗ Services [default/kubernetes] have type "ClusterIP" not meant to be exposed, however for local development minikube allows you to access this ! +🏃 Starting tunnel for service app-python-service. +🏃 Starting tunnel for service kubernetes. +|-----------|--------------------|-------------|------------------------| +| NAMESPACE | NAME | TARGET PORT | URL | +|-----------|--------------------|-------------|------------------------| +| default | app-python-service | | http://127.0.0.1:60190 | +| default | kubernetes | | http://127.0.0.1:60191 | +|-----------|--------------------|-------------|------------------------| +🎉 Opening service default/app-python-service in default browser... +🎉 Opening service default/kubernetes in default browser... +❗ Because you are using a Docker driver on darwin, the terminal needs to be open to run it. +``` + +![Screenshot of app-python](./task2.png) + +## Bonust task + +Manifests for an extra app were created: + +![Extra app](./pics/extra_app.png) + +![Screenshot of apps running](./pics/two_apps.png) + +Ingress was added: + +![Adding ingress](./pics/ingress_add.png) + +After creating manifest, curl tool was used to check: + +![Curl python](./pics/curl_python.png) + +![Curl nodejs](./pics/curl_nodejs.png) + diff --git a/k8s/app-nodejs/.helmignore b/k8s/app-nodejs/.helmignore new file mode 100644 index 0000000000..0e8a0eb36f --- /dev/null +++ b/k8s/app-nodejs/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/k8s/app-nodejs/Chart.lock b/k8s/app-nodejs/Chart.lock new file mode 100644 index 0000000000..9a4a8ea1c8 --- /dev/null +++ b/k8s/app-nodejs/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: my-library-chart + repository: file://../my-library-chart + version: 0.1.0 +digest: sha256:ed10b8f0480b8dc19b5b7becf59e12beebf19b15630d9b3db439625b70873d05 +generated: "2025-02-26T13:20:32.251637+03:00" diff --git a/k8s/app-nodejs/Chart.yaml b/k8s/app-nodejs/Chart.yaml new file mode 100644 index 0000000000..77929e7088 --- /dev/null +++ b/k8s/app-nodejs/Chart.yaml @@ -0,0 +1,29 @@ +apiVersion: v2 +name: app-nodejs +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" + +dependencies: + - name: my-library-chart + version: 0.1.0 + repository: file://../my-library-chart diff --git a/k8s/app-nodejs/charts/my-library-chart-0.1.0.tgz b/k8s/app-nodejs/charts/my-library-chart-0.1.0.tgz new file mode 100644 index 0000000000..c99cae9ed6 Binary files /dev/null and b/k8s/app-nodejs/charts/my-library-chart-0.1.0.tgz differ diff --git a/k8s/app-nodejs/deployment.yml b/k8s/app-nodejs/deployment.yml new file mode 100644 index 0000000000..73911f6ee2 --- /dev/null +++ b/k8s/app-nodejs/deployment.yml @@ -0,0 +1,19 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: app-nodejs-deployment +spec: + replicas: 3 + selector: + matchLabels: + app: app-nodejs + template: + metadata: + labels: + app: app-nodejs + spec: + containers: + - name: app-nodejs + image: ilsiia/app-nodejs:latest + ports: + - containerPort: 3001 diff --git a/k8s/app-nodejs/service.yml b/k8s/app-nodejs/service.yml new file mode 100644 index 0000000000..d50da75cf6 --- /dev/null +++ b/k8s/app-nodejs/service.yml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: app-nodejs-service +spec: + selector: + app: app-nodejs + ports: + - protocol: TCP + port: 80 + targetPort: 3001 + type: ClusterIP + + diff --git a/k8s/app-nodejs/templates/NOTES.txt b/k8s/app-nodejs/templates/NOTES.txt new file mode 100644 index 0000000000..3b70b10a3a --- /dev/null +++ b/k8s/app-nodejs/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "app-nodejs.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "app-nodejs.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "app-nodejs.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "app-nodejs.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/k8s/app-nodejs/templates/_helpers.tpl b/k8s/app-nodejs/templates/_helpers.tpl new file mode 100644 index 0000000000..8acf50809b --- /dev/null +++ b/k8s/app-nodejs/templates/_helpers.tpl @@ -0,0 +1,68 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "app-nodejs.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "app-nodejs.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "app-nodejs.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "app-nodejs.labels" -}} +helm.sh/chart: {{ include "app-nodejs.chart" . }} +{{ include "app-nodejs.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "app-nodejs.selectorLabels" -}} +app.kubernetes.io/name: {{ include "app-nodejs.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "app-nodejs.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "app-nodejs.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + + +{{- define "common.env" }} +- name: MY_ENV + value: {{ .Values.env.MY_ENV | quote }} +{{ end }} diff --git a/k8s/app-nodejs/templates/deployment.yaml b/k8s/app-nodejs/templates/deployment.yaml new file mode 100644 index 0000000000..5561d8dfdf --- /dev/null +++ b/k8s/app-nodejs/templates/deployment.yaml @@ -0,0 +1,80 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "app-nodejs.fullname" . }} + labels: + {{- include "app-nodejs.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "app-nodejs.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "app-nodejs.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "app-nodejs.serviceAccountName" . }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + {{ include "common.env" . | nindent 12 }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/k8s/app-nodejs/templates/hpa.yaml b/k8s/app-nodejs/templates/hpa.yaml new file mode 100644 index 0000000000..a254470e59 --- /dev/null +++ b/k8s/app-nodejs/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "app-nodejs.fullname" . }} + labels: + {{- include "app-nodejs.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "app-nodejs.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/k8s/app-nodejs/templates/ingress.yaml b/k8s/app-nodejs/templates/ingress.yaml new file mode 100644 index 0000000000..7d47f2d64d --- /dev/null +++ b/k8s/app-nodejs/templates/ingress.yaml @@ -0,0 +1,43 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "app-nodejs.fullname" . }} + labels: + {{- include "app-nodejs.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- with .pathType }} + pathType: {{ . }} + {{- end }} + backend: + service: + name: {{ include "app-nodejs.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/k8s/app-nodejs/templates/post-install-hook.yaml b/k8s/app-nodejs/templates/post-install-hook.yaml new file mode 100644 index 0000000000..471bc1992d --- /dev/null +++ b/k8s/app-nodejs/templates/post-install-hook.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: postinstall-hook + annotations: + "helm.sh/hook": "post-install" + "helm.sh/hook-delete-policy": "hook-succeeded" +spec: + containers: + - name: post-install-container + image: busybox + imagePullPolicy: Always + command: ['sh', '-c', 'echo Post-install hook is running... && sleep 15'] + restartPolicy: Never + terminationGracePeriodSeconds: 0 diff --git a/k8s/app-nodejs/templates/pre-install-hook.yaml b/k8s/app-nodejs/templates/pre-install-hook.yaml new file mode 100644 index 0000000000..266f7c7b6c --- /dev/null +++ b/k8s/app-nodejs/templates/pre-install-hook.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: preinstall-hook + annotations: + "helm.sh/hook": "pre-install" + "helm.sh/hook-delete-policy": "hook-succeeded" +spec: + containers: + - name: pre-install-container + image: busybox + imagePullPolicy: IfNotPresent + command: ['sh', '-c', 'echo Pre-install hook is running... && sleep 20'] + restartPolicy: Never + terminationGracePeriodSeconds: 0 diff --git a/k8s/app-nodejs/templates/service.yaml b/k8s/app-nodejs/templates/service.yaml new file mode 100644 index 0000000000..eb8374ecfa --- /dev/null +++ b/k8s/app-nodejs/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "app-nodejs.fullname" . }} + labels: + {{- include "app-nodejs.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "app-nodejs.selectorLabels" . | nindent 4 }} diff --git a/k8s/app-nodejs/templates/serviceaccount.yaml b/k8s/app-nodejs/templates/serviceaccount.yaml new file mode 100644 index 0000000000..705271fdd7 --- /dev/null +++ b/k8s/app-nodejs/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "app-nodejs.serviceAccountName" . }} + labels: + {{- include "app-nodejs.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/k8s/app-nodejs/templates/tests/test-connection.yaml b/k8s/app-nodejs/templates/tests/test-connection.yaml new file mode 100644 index 0000000000..3bcdeef4ac --- /dev/null +++ b/k8s/app-nodejs/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "app-nodejs.fullname" . }}-test-connection" + labels: + {{- include "app-nodejs.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "app-nodejs.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/k8s/app-nodejs/values.yaml b/k8s/app-nodejs/values.yaml new file mode 100644 index 0000000000..d126b17e35 --- /dev/null +++ b/k8s/app-nodejs/values.yaml @@ -0,0 +1,133 @@ +# Default values for app-nodejs. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ +replicaCount: 1 + +# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ +image: + repository: ilsiia/app-nodejs + # This sets the pull policy for images. + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "latest" + +# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ +imagePullSecrets: [] +# This is to override the chart name. +nameOverride: "" +fullnameOverride: "" + +# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +# This is for setting Kubernetes Annotations to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +podAnnotations: {} +# This is for setting Kubernetes Labels to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ +service: + # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + type: ClusterIP + # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports + port: 3001 + +# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ +livenessProbe: + httpGet: + path: / + port: http +readinessProbe: + httpGet: + path: / + port: http + +# This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/ +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +env: + MY_ENV: hello diff --git a/k8s/app-python/.helmignore b/k8s/app-python/.helmignore new file mode 100644 index 0000000000..0e8a0eb36f --- /dev/null +++ b/k8s/app-python/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/k8s/app-python/Chart.lock b/k8s/app-python/Chart.lock new file mode 100644 index 0000000000..d328c99922 --- /dev/null +++ b/k8s/app-python/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: my-library-chart + repository: file://../my-library-chart + version: 0.1.0 +digest: sha256:ed10b8f0480b8dc19b5b7becf59e12beebf19b15630d9b3db439625b70873d05 +generated: "2025-02-26T13:20:07.586451+03:00" diff --git a/k8s/app-python/Chart.yaml b/k8s/app-python/Chart.yaml new file mode 100644 index 0000000000..bc149697f0 --- /dev/null +++ b/k8s/app-python/Chart.yaml @@ -0,0 +1,28 @@ +apiVersion: v2 +name: app-python +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" +dependencies: + - name: my-library-chart + version: 0.1.0 + repository: file://../my-library-chart diff --git a/k8s/app-python/charts/my-library-chart-0.1.0.tgz b/k8s/app-python/charts/my-library-chart-0.1.0.tgz new file mode 100644 index 0000000000..46532d2666 Binary files /dev/null and b/k8s/app-python/charts/my-library-chart-0.1.0.tgz differ diff --git a/k8s/app-python/deployment.yml b/k8s/app-python/deployment.yml new file mode 100644 index 0000000000..afafb548c9 --- /dev/null +++ b/k8s/app-python/deployment.yml @@ -0,0 +1,21 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: app-python-deployment +spec: + replicas: 3 + selector: + matchLabels: + app: app-python + template: + metadata: + labels: + app: app-python + spec: + serviceAccountName: app-python + containers: + - name: app-python + image: ilsiia/app-python:latest + ports: + - containerPort: 5000 + diff --git a/k8s/app-python/patch-inject-secrets.yaml b/k8s/app-python/patch-inject-secrets.yaml new file mode 100644 index 0000000000..73510a2fa3 --- /dev/null +++ b/k8s/app-python/patch-inject-secrets.yaml @@ -0,0 +1,12 @@ +spec: + template: + metadata: + annotations: + vault.hashicorp.com/agent-inject: 'true' + vault.hashicorp.com/agent-inject-status: 'update' + vault.hashicorp.com/role: 'app-python' + vault.hashicorp.com/agent-inject-secret-database-config.txt: 'internal/data/database/config' + vault.hashicorp.com/agent-inject-template-database-config.txt: | + {{- with secret "internal/data/database/config" -}} + postgresql://{{ .Data.data.username }}:{{ .Data.data.password }}@postgres:5432/wizard + {{- end -}} diff --git a/k8s/app-python/secrets.yaml b/k8s/app-python/secrets.yaml new file mode 100644 index 0000000000..5fe3228436 --- /dev/null +++ b/k8s/app-python/secrets.yaml @@ -0,0 +1,24 @@ +password: ENC[AES256_GCM,data:QfywSekmpIaooBNm0sVkDA==,iv:v6TXwVeQGtxba8Jtg/bqLUQKHJywdigk1zTmHAlopAE=,tag:PuvgNglUUlNlpjwFYDGFrQ==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: [] + lastmodified: "2025-03-04T11:36:23Z" + mac: ENC[AES256_GCM,data:snO7plnTq1iaL6DpvefcrtubGE2Smn6zNZIa2za5L6xBUCi+VAXckfUL9xFzlQl3RPVQJAlvgWGa6u0S5ILlaH7mRtp3EDcWlHxVWYJEmh8V/LpQkdOg/nPs1COYoyljBFc3GOi0Bwc/BAyQSRlriMgplI3TtjMI7m2H3xgZhF4=,iv:b3aYXq3PAQNkC4WPXjGdamnnVcjdgAAOk2NcHDqY2Lo=,tag:6TdQi34XsXpoBPBQc6jLMg==,type:str] + pgp: + - created_at: "2025-03-04T11:35:29Z" + enc: |- + -----BEGIN PGP MESSAGE----- + + hF4DhED6YYg+gBMSAQdA7/jOmIAytZpl/7wKvCihu9HLe+HEDqREKZ31u+5itm4w + kagZ8q1IWMa5DfkTSmVA5Qe53Dd6q4+QnYwqZj5ivP4/bd1MKuoElEZRNQtIUXhs + 1GgBCQIQYRAyxVvRT9o92O++t/koyDxmCXUKi13zlfg2ZwQy+x81+trCBG0etiHM + HTqeWZKlrPUw34YApC5HE5ootLJpSlfrV8YMxAYn0ewODkjaj6S4ACHYi1SL2m/I + DNoykMumUrz7oQ== + =WR4F + -----END PGP MESSAGE----- + fp: B9A88DF905B313E9E10BA1FE20D34538E6666292 + unencrypted_suffix: _unencrypted + version: 3.9.4 diff --git a/k8s/app-python/service.yml b/k8s/app-python/service.yml new file mode 100644 index 0000000000..a2ba34b567 --- /dev/null +++ b/k8s/app-python/service.yml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: app-python-service +spec: + selector: + app: app-python + ports: + - protocol: TCP + port: 80 + targetPort: 5000 + type: ClusterIP + diff --git a/k8s/app-python/templates/NOTES.txt b/k8s/app-python/templates/NOTES.txt new file mode 100644 index 0000000000..5ca1c62622 --- /dev/null +++ b/k8s/app-python/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "app-python.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "app-python.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "app-python.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "app-python.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/k8s/app-python/templates/_helpers.tpl b/k8s/app-python/templates/_helpers.tpl new file mode 100644 index 0000000000..3a66d9959a --- /dev/null +++ b/k8s/app-python/templates/_helpers.tpl @@ -0,0 +1,67 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "app-python.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "app-python.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "app-python.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "app-python.labels" -}} +helm.sh/chart: {{ include "app-python.chart" . }} +{{ include "app-python.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "app-python.selectorLabels" -}} +app.kubernetes.io/name: {{ include "app-python.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "app-python.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "app-python.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{- define "common.env" }} +- name: MY_VAR + value: {{ .Values.env.MY_VAR | quote }} +{{ end }} diff --git a/k8s/app-python/templates/deployment.yaml b/k8s/app-python/templates/deployment.yaml new file mode 100644 index 0000000000..a2c1996547 --- /dev/null +++ b/k8s/app-python/templates/deployment.yaml @@ -0,0 +1,85 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "app-python.fullname" . }} + labels: + {{- include "app-python.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "app-python.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "app-python.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "app-python.serviceAccountName" . }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: MY_PASS + valueFrom: + secretKeyRef: + name: mysecret + key: password + {{ include "common.env" . | nindent 12 }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/k8s/app-python/templates/hpa.yaml b/k8s/app-python/templates/hpa.yaml new file mode 100644 index 0000000000..897a1223f5 --- /dev/null +++ b/k8s/app-python/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "app-python.fullname" . }} + labels: + {{- include "app-python.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "app-python.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/k8s/app-python/templates/ingress.yaml b/k8s/app-python/templates/ingress.yaml new file mode 100644 index 0000000000..02fb27f94a --- /dev/null +++ b/k8s/app-python/templates/ingress.yaml @@ -0,0 +1,43 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "app-python.fullname" . }} + labels: + {{- include "app-python.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- with .pathType }} + pathType: {{ . }} + {{- end }} + backend: + service: + name: {{ include "app-python.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/k8s/app-python/templates/post-install-hook.yaml b/k8s/app-python/templates/post-install-hook.yaml new file mode 100644 index 0000000000..392c575cdd --- /dev/null +++ b/k8s/app-python/templates/post-install-hook.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Pod +metadata: + name: postinstall-hook + annotations: + "helm.sh/hook": "post-install" + "helm.sh/hook-delete-policy": "hook-succeeded" +spec: + containers: + - name: post-install-container + image: busybox + imagePullPolicy: Always + command: ['sh', '-c', 'echo Post-install hook is running... && sleep 15'] + restartPolicy: Never + terminationGracePeriodSeconds: 0 + diff --git a/k8s/app-python/templates/pre-install-hook.yaml b/k8s/app-python/templates/pre-install-hook.yaml new file mode 100644 index 0000000000..1653c1d0ea --- /dev/null +++ b/k8s/app-python/templates/pre-install-hook.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Pod +metadata: + name: preinstall-hook + annotations: + "helm.sh/hook": "pre-install" + "helm.sh/hook-delete-policy": "hook-succeeded" +spec: + containers: + - name: pre-install-container + image: busybox + imagePullPolicy: IfNotPresent + command: ['sh', '-c', 'echo Pre-install hook is running... && sleep 20'] + restartPolicy: Never + terminationGracePeriodSeconds: 0 + diff --git a/k8s/app-python/templates/secrets.yaml b/k8s/app-python/templates/secrets.yaml new file mode 100644 index 0000000000..64d8bb72a7 --- /dev/null +++ b/k8s/app-python/templates/secrets.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: mysecret + namespace: default +type: Opaque +data: + password: {{ .Values.password | b64enc | quote }} + diff --git a/k8s/app-python/templates/service.yaml b/k8s/app-python/templates/service.yaml new file mode 100644 index 0000000000..c09bd9852d --- /dev/null +++ b/k8s/app-python/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "app-python.fullname" . }} + labels: + {{- include "app-python.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "app-python.selectorLabels" . | nindent 4 }} diff --git a/k8s/app-python/templates/serviceaccount.yaml b/k8s/app-python/templates/serviceaccount.yaml new file mode 100644 index 0000000000..cc89ba9533 --- /dev/null +++ b/k8s/app-python/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "app-python.serviceAccountName" . }} + labels: + {{- include "app-python.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/k8s/app-python/templates/tests/test-connection.yaml b/k8s/app-python/templates/tests/test-connection.yaml new file mode 100644 index 0000000000..66d581c341 --- /dev/null +++ b/k8s/app-python/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "app-python.fullname" . }}-test-connection" + labels: + {{- include "app-python.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "app-python.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/k8s/app-python/values.yaml b/k8s/app-python/values.yaml new file mode 100644 index 0000000000..093b5f5a46 --- /dev/null +++ b/k8s/app-python/values.yaml @@ -0,0 +1,138 @@ +# Default values for app-python. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ +replicaCount: 1 + +# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ +image: + repository: ilsiia/app-python + # This sets the pull policy for images. + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "latest" + +# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ +imagePullSecrets: [] +# This is to override the chart name. +nameOverride: "" +fullnameOverride: "" + +# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +# This is for setting Kubernetes Annotations to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +podAnnotations: { + vault.hashicorp.com/agent-inject: 'true', + vault.hashicorp.com/role: 'app-python', + vault.hashicorp.com/agent-inject-secret-database-config.txt: 'internal/data/database/config' +} + +# This is for setting Kubernetes Labels to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ +service: + # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + type: ClusterIP + # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports + port: 5000 + +# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ +livenessProbe: + httpGet: + path: / + port: http +readinessProbe: + httpGet: + path: / + port: http + +# This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/ +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +env: + MY_VAR: ilsiia diff --git a/k8s/ingress.yml b/k8s/ingress.yml new file mode 100644 index 0000000000..fea3eb6e09 --- /dev/null +++ b/k8s/ingress.yml @@ -0,0 +1,28 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: multi-app-ingress +spec: + ingressClassName: nginx + rules: + - host: app-python.local + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: app-python-service + port: + number: 80 + - host: app-nodejs.local + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: app-nodejs-service + port: + number: 80 + diff --git a/k8s/my-library-chart/.helmignore b/k8s/my-library-chart/.helmignore new file mode 100644 index 0000000000..0e8a0eb36f --- /dev/null +++ b/k8s/my-library-chart/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/k8s/my-library-chart/Chart.yaml b/k8s/my-library-chart/Chart.yaml new file mode 100644 index 0000000000..320c4b4916 --- /dev/null +++ b/k8s/my-library-chart/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: my-library-chart +description: A Helm chart for reusable templates (labels template) +type: library +version: 0.1.0 + diff --git a/k8s/my-library-chart/templates/_helpers.tpl b/k8s/my-library-chart/templates/_helpers.tpl new file mode 100644 index 0000000000..263bb3626d --- /dev/null +++ b/k8s/my-library-chart/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "my-library-chart.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "my-library-chart.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "my-library-chart.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "my-library-chart.labels" -}} +helm.sh/chart: {{ include "my-library-chart.chart" . }} +{{ include "my-library-chart.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "my-library-chart.selectorLabels" -}} +app.kubernetes.io/name: {{ include "my-library-chart.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "my-library-chart.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "my-library-chart.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/k8s/my-library-chart/templates/_labels.tpl b/k8s/my-library-chart/templates/_labels.tpl new file mode 100644 index 0000000000..7b5739fb6f --- /dev/null +++ b/k8s/my-library-chart/templates/_labels.tpl @@ -0,0 +1,7 @@ +{{- define "my-library-chart.labels" -}} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/k8s/pics/app_python_pass.png b/k8s/pics/app_python_pass.png new file mode 100644 index 0000000000..f4adcab986 Binary files /dev/null and b/k8s/pics/app_python_pass.png differ diff --git a/k8s/pics/curl_nodejs.png b/k8s/pics/curl_nodejs.png new file mode 100644 index 0000000000..1f9e4e9866 Binary files /dev/null and b/k8s/pics/curl_nodejs.png differ diff --git a/k8s/pics/curl_python.png b/k8s/pics/curl_python.png new file mode 100644 index 0000000000..ffd7cf99ef Binary files /dev/null and b/k8s/pics/curl_python.png differ diff --git a/k8s/pics/extra_app.png b/k8s/pics/extra_app.png new file mode 100644 index 0000000000..67726b261c Binary files /dev/null and b/k8s/pics/extra_app.png differ diff --git a/k8s/pics/helm_dash_1.png b/k8s/pics/helm_dash_1.png new file mode 100644 index 0000000000..7a5e59bcef Binary files /dev/null and b/k8s/pics/helm_dash_1.png differ diff --git a/k8s/pics/helm_dash_2.png b/k8s/pics/helm_dash_2.png new file mode 100644 index 0000000000..40ea0d42b4 Binary files /dev/null and b/k8s/pics/helm_dash_2.png differ diff --git a/k8s/pics/helm_dash_3.png b/k8s/pics/helm_dash_3.png new file mode 100644 index 0000000000..29364f389e Binary files /dev/null and b/k8s/pics/helm_dash_3.png differ diff --git a/k8s/pics/helm_install.png b/k8s/pics/helm_install.png new file mode 100644 index 0000000000..fd7192e8f4 Binary files /dev/null and b/k8s/pics/helm_install.png differ diff --git a/k8s/pics/helm_view.png b/k8s/pics/helm_view.png new file mode 100644 index 0000000000..ecf7b5ad26 Binary files /dev/null and b/k8s/pics/helm_view.png differ diff --git a/k8s/pics/ingress_add.png b/k8s/pics/ingress_add.png new file mode 100644 index 0000000000..9ca516eea2 Binary files /dev/null and b/k8s/pics/ingress_add.png differ diff --git a/k8s/pics/kubectl_get_po.png b/k8s/pics/kubectl_get_po.png new file mode 100644 index 0000000000..67ad4d492c Binary files /dev/null and b/k8s/pics/kubectl_get_po.png differ diff --git a/k8s/pics/kubectl_secret.png b/k8s/pics/kubectl_secret.png new file mode 100644 index 0000000000..785c259c99 Binary files /dev/null and b/k8s/pics/kubectl_secret.png differ diff --git a/k8s/pics/kubectl_vault.png b/k8s/pics/kubectl_vault.png new file mode 100644 index 0000000000..88d901ebb2 Binary files /dev/null and b/k8s/pics/kubectl_vault.png differ diff --git a/k8s/pics/nodejs-app.png b/k8s/pics/nodejs-app.png new file mode 100644 index 0000000000..7a25532dbd Binary files /dev/null and b/k8s/pics/nodejs-app.png differ diff --git a/k8s/pics/nodejs_lint.png b/k8s/pics/nodejs_lint.png new file mode 100644 index 0000000000..a932b5c167 Binary files /dev/null and b/k8s/pics/nodejs_lint.png differ diff --git a/k8s/pics/python-app.png b/k8s/pics/python-app.png new file mode 100644 index 0000000000..0b5d058070 Binary files /dev/null and b/k8s/pics/python-app.png differ diff --git a/k8s/pics/python_install.png b/k8s/pics/python_install.png new file mode 100644 index 0000000000..8c1d4d68d2 Binary files /dev/null and b/k8s/pics/python_install.png differ diff --git a/k8s/pics/python_kubectl.png b/k8s/pics/python_kubectl.png new file mode 100644 index 0000000000..7ec415f3c7 Binary files /dev/null and b/k8s/pics/python_kubectl.png differ diff --git a/k8s/pics/python_lint.png b/k8s/pics/python_lint.png new file mode 100644 index 0000000000..d17bf0eb18 Binary files /dev/null and b/k8s/pics/python_lint.png differ diff --git a/k8s/pics/python_po.png b/k8s/pics/python_po.png new file mode 100644 index 0000000000..f217d63fb7 Binary files /dev/null and b/k8s/pics/python_po.png differ diff --git a/k8s/pics/task1_get.png b/k8s/pics/task1_get.png new file mode 100644 index 0000000000..5dd1e3265b Binary files /dev/null and b/k8s/pics/task1_get.png differ diff --git a/k8s/pics/task2.png b/k8s/pics/task2.png new file mode 100644 index 0000000000..ad43d7ad59 Binary files /dev/null and b/k8s/pics/task2.png differ diff --git a/k8s/pics/two_apps.png b/k8s/pics/two_apps.png new file mode 100644 index 0000000000..232ebfc144 Binary files /dev/null and b/k8s/pics/two_apps.png differ diff --git a/monitoring/.gitignore b/monitoring/.gitignore new file mode 100644 index 0000000000..a965c80654 --- /dev/null +++ b/monitoring/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +grafana-provisioning/ diff --git a/monitoring/Docker containers.png b/monitoring/Docker containers.png new file mode 100644 index 0000000000..8ac6213e30 Binary files /dev/null and b/monitoring/Docker containers.png differ diff --git a/monitoring/Docker logs 2.png b/monitoring/Docker logs 2.png new file mode 100644 index 0000000000..de18c51840 Binary files /dev/null and b/monitoring/Docker logs 2.png differ diff --git a/monitoring/Doker logs.png b/monitoring/Doker logs.png new file mode 100644 index 0000000000..26b3d8ea99 Binary files /dev/null and b/monitoring/Doker logs.png differ diff --git a/monitoring/Grafana.png b/monitoring/Grafana.png new file mode 100644 index 0000000000..de2137cda1 Binary files /dev/null and b/monitoring/Grafana.png differ diff --git a/monitoring/LOGGING.md b/monitoring/LOGGING.md new file mode 100644 index 0000000000..58974285bd --- /dev/null +++ b/monitoring/LOGGING.md @@ -0,0 +1,53 @@ +# Overview + +Overview of the logging stack implemented within Lab7 + Bonus + +## Loki + +- Loki is a horizontally scalable, highly available log aggregation system. +- It collects logs from Promtail and stores them efficiently. +- Exposes logs via an API that Grafana can query for visualization. + +## Promtail + +- Promtail collects logs from Docker containers and sends them to Loki. +- Uses promtail-config.yml to define scraping jobs. + +## Grafana + +- Grafana provides a UI for querying logs stored in Loki. +- Configured with a Loki data source using provisioning/datasources/ds.yaml. +- Allows filtering logs based on labels. + +## Screenshots + +![Logs of Job Docker](./Doker%20logs.png) + +![Logs of Job Docker](./Docker%20logs%202.png) + +## Bonus screenshots + +Docker containers of all services + +![IDs of Docker containers](./Docker containers.png) + +Logs of Grafana: + +![Logs of Grafana](./Grafana.png) + +Logs of Loki: + +![Loki logs](./Loki.png) + +Logs of Promtail: + +![Promtail Logs](./Promtail.png) + +Logs of python app: + +![Python app logs](./app_python.png) + +Logs of second app: + +![Second app logs](./app_nodejs.png) + diff --git a/monitoring/Loki.png b/monitoring/Loki.png new file mode 100644 index 0000000000..5ed38c44c7 Binary files /dev/null and b/monitoring/Loki.png differ diff --git a/monitoring/METRICS.md b/monitoring/METRICS.md new file mode 100644 index 0000000000..3c3da4ad2d --- /dev/null +++ b/monitoring/METRICS.md @@ -0,0 +1,63 @@ +# Overview + +## Task 1 + +The set up of collecting metrics from Loki and Prometheus + +![Metrics](./metrics_1.png) + +## Task 2 + +### Dashboards + +Configured Dashboards for Loki and Prometheus: + +![dashboard](./dashboard_loki.png) + +![dashboard](./dashboard_prom.png) + +### Log Mechanism and Memory Limits + +I used YAML Anchors & Aliases to specify: + +- x-logging: &default-logging → Defines a log rotation mechanism (max-size 200KB, max 10 files). + +- x-resources: &default-resources → Defines memory limits for all services (512MB limit, 256MB reserved). + +```yml +x-logging: &default-logging + driver: "json-file" + options: + max-size: "200k" + max-file: "10" + +x-resources: &default-resources + limits: + memory: "512m" + reservations: + memory: "256m" +``` + +### All services + +All services from `docker-compose.yml` were configured to gather metrics: + +![metrics](./metrics_2.png) + +## Bonus + +Configured apps to export metrics: + +![Python metrics](./metrics_py.png) + +![NodeJS metrics](./metrics_js.png) + +Healthcheking was also introduced like this example: + +```yml +healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:9080/ready || exit 1"] + interval: 30s + timeout: 10s + retries: 5 +``` diff --git a/monitoring/Promtail.png b/monitoring/Promtail.png new file mode 100644 index 0000000000..2f12eabe11 Binary files /dev/null and b/monitoring/Promtail.png differ diff --git a/monitoring/app_nodejs.png b/monitoring/app_nodejs.png new file mode 100644 index 0000000000..a9b158e98f Binary files /dev/null and b/monitoring/app_nodejs.png differ diff --git a/monitoring/app_python.png b/monitoring/app_python.png new file mode 100644 index 0000000000..e068bb24e4 Binary files /dev/null and b/monitoring/app_python.png differ diff --git a/monitoring/dashboard_loki.png b/monitoring/dashboard_loki.png new file mode 100644 index 0000000000..71e8fcc12c Binary files /dev/null and b/monitoring/dashboard_loki.png differ diff --git a/monitoring/dashboard_prom.png b/monitoring/dashboard_prom.png new file mode 100644 index 0000000000..a1352a03c0 Binary files /dev/null and b/monitoring/dashboard_prom.png differ diff --git a/monitoring/docker-compose.yml b/monitoring/docker-compose.yml new file mode 100644 index 0000000000..545d259055 --- /dev/null +++ b/monitoring/docker-compose.yml @@ -0,0 +1,153 @@ +version: "3.8" + +networks: + monitoring: + +volumes: + loki_vol: + +x-logging: &default-logging + driver: "json-file" + options: + max-size: "200k" + max-file: "10" + +x-resources: &default-resources + limits: + memory: "512m" + reservations: + memory: "256m" + +services: + loki: + image: grafana/loki:latest + container_name: loki + ports: + - "3100:3100" + command: -config.file=/etc/loki/local-config.yaml + volumes: + - loki_vol:/loki + networks: + - monitoring + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:3100/ready"] + interval: 10s + timeout: 5s + retries: 5 + logging: *default-logging + deploy: + resources: *default-resources + + promtail: + image: grafana/promtail:latest + container_name: promtail + volumes: + - /var/lib/docker/containers:/var/lib/docker/containers + - /var/log:/var/log + - ./promtail-config.yml:/etc/promtail/config.yml + command: -config.file=/etc/promtail/config.yml + networks: + - monitoring + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:9080/ready || exit 1"] + interval: 30s + timeout: 10s + retries: 5 + logging: *default-logging + deploy: + resources: *default-resources + + grafana: + image: grafana/grafana:latest + container_name: grafana + ports: + - "3000:3000" + environment: + - GF_PATHS_PROVISIONING=/etc/grafana/provisioning + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + volumes: + - ./grafana-provisioning:/etc/grafana/provisioning + entrypoint: + - sh + - -euc + - | + mkdir -p /etc/grafana/provisioning/datasources + cat < /etc/grafana/provisioning/datasources/ds.yaml + apiVersion: 0 + datasources: + - name: Loki + type: loki + access: proxy + orgId: 0 + url: http://loki:3100 + basicAuth: false + isDefault: true + version: 0 + editable: false + EOF + /run.sh + networks: + - monitoring + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/health"] + interval: 10s + timeout: 5s + retries: 5 + logging: *default-logging + deploy: + resources: *default-resources + + app: + image: ilsiia/app_python:latest + container_name: app_python_1 + ports: + - "5001:5000" + networks: + - monitoring + restart: always + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5000"] + interval: 10s + timeout: 5s + retries: 5 + logging: *default-logging + deploy: + resources: *default-resources + + app2: + image: ilsiia/app_nodejs:latest + container_name: app_nodejs_2 + ports: + - "3001:3001" + networks: + - monitoring + restart: always + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3001"] + interval: 10s + timeout: 5s + retries: 5 + logging: *default-logging + deploy: + resources: *default-resources + + prometheus: + image: prom/prometheus:latest + container_name: prometheus + ports: + - "9090:9090" + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml + command: --config.file=/etc/prometheus/prometheus.yml + networks: + - monitoring + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:9090/-/ready"] + interval: 10s + timeout: 5s + retries: 5 + logging: *default-logging + deploy: + resources: *default-resources + diff --git a/monitoring/metrics_1.png b/monitoring/metrics_1.png new file mode 100644 index 0000000000..bceb0d775c Binary files /dev/null and b/monitoring/metrics_1.png differ diff --git a/monitoring/metrics_2.png b/monitoring/metrics_2.png new file mode 100644 index 0000000000..5bd121931f Binary files /dev/null and b/monitoring/metrics_2.png differ diff --git a/monitoring/metrics_js.png b/monitoring/metrics_js.png new file mode 100644 index 0000000000..132c57d120 Binary files /dev/null and b/monitoring/metrics_js.png differ diff --git a/monitoring/metrics_py.png b/monitoring/metrics_py.png new file mode 100644 index 0000000000..ede2394476 Binary files /dev/null and b/monitoring/metrics_py.png differ diff --git a/monitoring/prometheus.yml b/monitoring/prometheus.yml new file mode 100644 index 0000000000..d834ada8a6 --- /dev/null +++ b/monitoring/prometheus.yml @@ -0,0 +1,25 @@ +global: + scrape_interval: 5s + +scrape_configs: + - job_name: 'prometheus' + static_configs: + - targets: ['prometheus:9090'] + + - job_name: 'loki' + static_configs: + - targets: ['loki:3100'] + + - job_name: 'grafana' + metrics_path: "/metrics" + static_configs: + - targets: ['grafana:3000'] + + - job_name: 'app_python' + static_configs: + - targets: ['app:5000'] + + - job_name: 'app_nodejs' + static_configs: + - targets: ['app2:3001'] + diff --git a/monitoring/promtail-config.yml b/monitoring/promtail-config.yml new file mode 100644 index 0000000000..f572282f7e --- /dev/null +++ b/monitoring/promtail-config.yml @@ -0,0 +1,19 @@ +server: + http_listen_port: 9080 + grpc_listen_port: 0 + +positions: + filename: /tmp/positions.yaml + +client: + url: http://loki:3100/api/prom/push + +scrape_configs: + - job_name: docker-logs + static_configs: + - targets: + - localhost + labels: + job: containers + __path__: /var/lib/docker/containers/*/*log + diff --git a/terraform/.gitignore b/terraform/.gitignore new file mode 100644 index 0000000000..ea12d0fefa --- /dev/null +++ b/terraform/.gitignore @@ -0,0 +1,7 @@ +**/.terraform/ +**/.terraform.lock.hcl +**/terraform.tfstate +**/terraform.tfstate.backup +yandex/key.json +**/terraform.tfstate.*.backup + diff --git a/terraform/TF.md b/terraform/TF.md new file mode 100644 index 0000000000..0cb6982506 --- /dev/null +++ b/terraform/TF.md @@ -0,0 +1,1102 @@ +# Overview + +## Docker + +In the task requirement was not specified which app to deploy with terraform, so I deployed both (`app_python` and `app_java`) + +1. Execution of `terraform state list` + + ```bash + docker_container.node_container + docker_container.python_container + docker_image.node_app + docker_image.python_app + ``` + +2. Execution of `terraform state show docker_container.python_container` + + ```bash + # docker_container.python_container: + resource "docker_container" "python_container" { + attach = false + bridge = null + command = [ + "python3", + "app.py", + ] + container_read_refresh_timeout_milliseconds = 15000 + cpu_set = null + cpu_shares = 0 + domainname = null + entrypoint = [] + env = [] + hostname = "34deebe40130" + id = "34deebe4013039295ef83c20634c914fb1b70fef5b340888f987c5e66e16c9bf" + image = "sha256:2ad58fe0e6345bcee281b0f67a00718de166e00b7d77574bbea2103237bda012" + init = false + ipc_mode = "private" + log_driver = "json-file" + logs = false + max_retry_count = 0 + memory = 0 + memory_swap = 0 + must_run = true + name = "app_python" + network_data = [ + { + gateway = "172.17.0.1" + global_ipv6_address = null + global_ipv6_prefix_length = 0 + ip_address = "172.17.0.2" + ip_prefix_length = 16 + ipv6_gateway = null + mac_address = "02:42:ac:11:00:02" + network_name = "bridge" + }, + ] + network_mode = "default" + pid_mode = null + 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 = null + stop_timeout = 0 + tty = false + user = "app_python_user" + userns_mode = null + wait = false + wait_timeout = 60 + working_dir = "/app_python" + + ports { + external = 5001 + internal = 5000 + ip = "0.0.0.0" + protocol = "tcp" + } + } + ``` + +3. Execution `terraform state show docker_container.node_container` + + ```bash + # docker_container.node_container: + resource "docker_container" "node_container" { + attach = false + bridge = null + command = [ + "node", + "app.js", + ] + container_read_refresh_timeout_milliseconds = 15000 + cpu_set = null + cpu_shares = 0 + domainname = null + entrypoint = [ + "docker-entrypoint.sh", + ] + env = [] + hostname = "8d3eee95310f" + id = "8d3eee95310fd465f05af143cc539996ec39d192f37eacf57d9ea8d75b3c58b4" + image = "sha256:b8b6db326d2e9fd630fea846c11587cf3b3dbaa2e1c83bb456922f6decee02f2" + init = false + ipc_mode = "private" + log_driver = "json-file" + logs = false + max_retry_count = 0 + memory = 0 + memory_swap = 0 + must_run = true + name = "app_nodejs" + network_data = [ + { + gateway = "172.17.0.1" + global_ipv6_address = null + global_ipv6_prefix_length = 0 + ip_address = "172.17.0.3" + ip_prefix_length = 16 + ipv6_gateway = null + mac_address = "02:42:ac:11:00:03" + network_name = "bridge" + }, + ] + network_mode = "default" + pid_mode = null + 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 = null + stop_timeout = 0 + tty = false + user = "nonrootuser" + userns_mode = null + wait = false + wait_timeout = 60 + working_dir = "/app_nodejs" + + ports { + external = 3000 + internal = 3000 + ip = "0.0.0.0" + protocol = "tcp" + } + } + ``` + +4. Execution of `terraform apply` + + ```bash + docker_image.python_app: Refreshing state... [id=sha256:b3cd6feced8137fbd6a18840bbf0f1e1f45516546f0fb5d6488128a1a7338362python:3.11] + docker_image.node_app: Refreshing state... [id=sha256:8b665fc4f032e5b6f6f3386c26df3c4844ff72667b502863ce9a67d385f4d489node:18] + + 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.node_container will be created + + resource "docker_container" "node_container" { + + 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 = "ilsiia/app_nodejs:latest" + + init = (known after apply) + + ipc_mode = (known after apply) + + log_driver = (known after apply) + + logs = false + + must_run = true + + name = "app_nodejs" + + 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 + + + healthcheck (known after apply) + + + labels (known after apply) + + + ports { + + external = 3000 + + internal = 3000 + + ip = "0.0.0.0" + + protocol = "tcp" + } + } + + # docker_container.python_container will be created + + resource "docker_container" "python_container" { + + 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 = "ilsiia/app_python:latest" + + init = (known after apply) + + ipc_mode = (known after apply) + + log_driver = (known after apply) + + logs = false + + must_run = true + + name = "app_python" + + 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 + + + healthcheck (known after apply) + + + labels (known after apply) + + + ports { + + external = 5001 + + internal = 5000 + + ip = "0.0.0.0" + + protocol = "tcp" + } + } + + Plan: 2 to add, 0 to change, 0 to destroy. + + 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.node_container: Creating... + docker_container.python_container: Creating... + docker_container.node_container: Creation complete after 1s [id=8d3eee95310fd465f05af143cc539996ec39d192f37eacf57d9ea8d75b3c58b4] + docker_container.python_container: Creation complete after 1s [id=34deebe4013039295ef83c20634c914fb1b70fef5b340888f987c5e66e16c9bf] + + Apply complete! Resources: 2 added, 0 changed, 0 destroyed. + ``` + +5. Execution of `terraform output` + + ```bash + Outputs: + + node_container_id = "376ec66f9402c45ee45f5662986541014e29d7172b9291be61aab70893f0fc86" + node_container_image = "ilsiia/app_nodejs:latest" + node_container_ip = "172.17.0.2" + node_container_name = "app_nodejs" + node_container_port = 3000 + python_container_id = "3362b897ba28a7950501994f85886caf353306100d0d2003eead44be1dda4120" + python_container_image = "ilsiia/app_python:latest" + python_container_ip = "172.17.0.3" + python_container_name = "app_python" + python_container_port = 5001 + ``` + +## Yandex + +I followed the guide `https://yandex.cloud/en-ru/docs/tutorials/infrastructure-management/terraform-quickstart#linux_1` and also created payment accoung to receive grant. During this task I came up with several difficulties. I messed up with a lot of ids and after several attempts to run `terraform apply` free attempts for creating networks, subnets and vms were out. So I had to deal with it and clean everything I had on my account. + +1. Execution of `terraform plan` + + ```bash + 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) + + hardware_generation = (known after apply) + + hostname = (known after apply) + + id = (known after apply) + + maintenance_grace_period = (known after apply) + + maintenance_policy = (known after apply) + + metadata = { + + "ssh-keys" = <<-EOT + ubuntu:ssh-ed25519 AAAAC3NzaC.....Y0eoh ilsiyanasibullina@gmail.com + EOT + } + + name = "terraform-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 = "fd800c7s2p483i648ifv" + + name = (known after apply) + + size = 20 + + snapshot_id = (known after apply) + + type = "network-hdd" + } + } + + + metadata_options (known after apply) + + + 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) + } + + + placement_policy (known after apply) + + + resources { + + core_fraction = 100 + + cores = 2 + + memory = 2 + } + + + scheduling_policy (known after apply) + } + + # 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 = "default-1" + + 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 = (known after apply) + + network_id = (known after apply) + + v4_cidr_blocks = [ + + "10.131.0.0/24", + ] + + v6_cidr_blocks = (known after apply) + + zone = "ru-central1-b" + } + + Plan: 3 to add, 0 to change, 0 to destroy. + ``` + +2. Execution of `terraform apply` + + ```bash + 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) + + hardware_generation = (known after apply) + + hostname = (known after apply) + + id = (known after apply) + + maintenance_grace_period = (known after apply) + + maintenance_policy = (known after apply) + + metadata = { + + "ssh-keys" = <<-EOT + ubuntu:ssh-ed25519 AAAAC3...tMY0eoh ilsiyanasibullina@gmail.com + EOT + } + + name = "terraform-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 = "fd800c7s2p483i648ifv" + + name = (known after apply) + + size = 20 + + snapshot_id = (known after apply) + + type = "network-hdd" + } + } + + + metadata_options (known after apply) + + + 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) + } + + + placement_policy (known after apply) + + + resources { + + core_fraction = 100 + + cores = 2 + + memory = 2 + } + + + scheduling_policy (known after apply) + } + + # 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 = "default-1" + + 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 = (known after apply) + + network_id = (known after apply) + + v4_cidr_blocks = [ + + "10.131.0.0/24", + ] + + v6_cidr_blocks = (known after apply) + + zone = "ru-central1-b" + } + + Plan: 3 to add, 0 to change, 0 to destroy. + yandex_vpc_network.network-1: Creating... + yandex_vpc_network.network-1: Creation complete after 4s [id=enpv8co8mmap3b4082ug] + yandex_vpc_subnet.subnet-1: Creating... + yandex_vpc_subnet.subnet-1: Creation complete after 1s [id=e2l64uv00rgch8lfhp9h] + yandex_compute_instance.vm-1: Creating... + yandex_compute_instance.vm-1: Still creating... [10s elapsed] + yandex_compute_instance.vm-1: Still creating... [20s elapsed] + yandex_compute_instance.vm-1: Still creating... [30s elapsed] + yandex_compute_instance.vm-1: Still creating... [40s elapsed] + yandex_compute_instance.vm-1: Still creating... [50s elapsed] + yandex_compute_instance.vm-1: Creation complete after 52s [id=epde3epsabo34tur1b8p] + + Apply complete! Resources: 3 added, 0 changed, 0 destroyed. + ``` + +3. Exection of `terraform state list` + + ```bash + yandex_compute_instance.vm-1 + yandex_vpc_network.network-1 + yandex_vpc_subnet.subnet-1 + ``` + +4. Execution of `terraform state show yandex_vpc_network.network-1` + + ```bash + # yandex_vpc_network.network-1: + resource "yandex_vpc_network" "network-1" { + created_at = "2025-02-05T07:57:10Z" + default_security_group_id = "enp93a2sd6gmqio445qk" + description = null + folder_id = "b1g5rr7vs8qnd5ikd5ee" + id = "enpv8co8mmap3b4082ug" + labels = {} + name = "default-1" + subnet_ids = [] + } + ``` + +5. Execution of `terraform state show yandex_vpc_subnet.subnet-1` + + ```bash + # yandex_vpc_subnet.subnet-1: + resource "yandex_vpc_subnet" "subnet-1" { + created_at = "2025-02-05T07:57:13Z" + description = null + folder_id = "b1g5rr7vs8qnd5ikd5ee" + id = "e2l64uv00rgch8lfhp9h" + labels = {} + name = null + network_id = "enpv8co8mmap3b4082ug" + route_table_id = null + v4_cidr_blocks = [ + "10.131.0.0/24", + ] + v6_cidr_blocks = [] + zone = "ru-central1-b" + } + ``` + +6. Execution of `terraform state show yandex_compute_instance.vm-1` + + ```bash + # yandex_compute_instance.vm-1: + resource "yandex_compute_instance" "vm-1" { + created_at = "2025-02-05T07:57:14Z" + description = null + folder_id = "b1g5rr7vs8qnd5ikd5ee" + fqdn = "epde3epsabo34tur1b8p.auto.internal" + gpu_cluster_id = null + hardware_generation = [ + { + generation2_features = [] + legacy_features = [ + { + pci_topology = "PCI_TOPOLOGY_V1" + }, + ] + }, + ] + hostname = null + id = "epde3epsabo34tur1b8p" + maintenance_grace_period = null + metadata = { + "ssh-keys" = <<-EOT + ubuntu:ssh-ed25519 AAAA....0eoh ilsiyanasibullina@gmail.com + EOT + } + name = "terraform-vm-1" + network_acceleration_type = "standard" + platform_id = "standard-v1" + service_account_id = null + status = "running" + zone = "ru-central1-b" + + boot_disk { + auto_delete = true + device_name = "epdg669p39kd4a20vic3" + disk_id = "epdg669p39kd4a20vic3" + mode = "READ_WRITE" + + initialize_params { + block_size = 4096 + description = null + image_id = "fd800c7s2p483i648ifv" + kms_key_id = null + name = null + size = 20 + snapshot_id = null + 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 = "10.131.0.32" + ipv4 = true + ipv6 = false + ipv6_address = null + mac_address = "d0:0d:e1:bb:3c:52" + nat = true + nat_ip_address = "158.160.64.42" + nat_ip_version = "IPV4" + security_group_ids = [] + subnet_id = "e2l64uv00rgch8lfhp9h" + } + + placement_policy { + host_affinity_rules = [] + placement_group_id = null + placement_group_partition = 0 + } + + resources { + core_fraction = 100 + cores = 2 + gpus = 0 + memory = 2 + } + + scheduling_policy { + preemptible = false + } + } + ``` + +## Github + +1. Execution of `terraform plan -out deploy.tfplan` + + ```bash + var.token + GitHub personal access token + + Enter a value: + + github_repository.repo: Refreshing state... [id=my-terraform-repo] + + 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: + + # github_branch_default.default will be created + + resource "github_branch_default" "default" { + + branch = "main" + + id = (known after apply) + + repository = "my-terraform-repo" + } + + # github_branch_protection.default will be created + + resource "github_branch_protection" "default" { + + allows_deletions = false + + allows_force_pushes = false + + blocks_creations = false + + enforce_admins = true + + id = (known after apply) + + pattern = "main" + + repository_id = "my-terraform-repo" + + require_conversation_resolution = true + + require_signed_commits = false + + required_linear_history = false + + + required_pull_request_reviews { + + required_approving_review_count = 1 + } + } + + Plan: 2 to add, 0 to change, 0 to destroy. + ``` + +2. Execution of `terraform apply "deploy.tfplan"` + + ```bash + github_branch_default.default: Creating... + github_branch_default.default: Creation complete after 2s [id=my-terraform-repo] + github_branch_protection.default: Creating... + github_branch_protection.default: Creation complete after 4s [id=BPR_kwDON0ssZ84Dibcb] + + Apply complete! Resources: 2 added, 0 changed, 0 destroyed. + ``` + +3. Execution of `terraform state list` + + ```bash + github_branch_default.default + github_branch_protection.default + github_repository.repo + ``` + +4. Execution of `terraform state show github_branch_default.default` + + ```bash + # github_branch_default.default: + resource "github_branch_default" "default" { + branch = "main" + id = "my-terraform-repo" + repository = "my-terraform-repo" + } + ``` + +5. Execution of `terraform state show github_branch_protection.default` + + ```bash + # github_branch_protection.default: + resource "github_branch_protection" "default" { + allows_deletions = false + allows_force_pushes = false + blocks_creations = false + enforce_admins = true + id = "BPR_kwDON0ssZ84Dibcb" + pattern = "main" + repository_id = "my-terraform-repo" + require_conversation_resolution = true + require_signed_commits = false + required_linear_history = false + + required_pull_request_reviews { + dismiss_stale_reviews = false + require_code_owner_reviews = false + required_approving_review_count = 1 + restrict_dismissals = false + } + } + ``` + +6. Execution of `terraform state show github_repository.repo` + + ```bash + # github_repository.repo: + resource "github_repository" "repo" { + allow_auto_merge = false + allow_merge_commit = true + allow_rebase_merge = true + allow_squash_merge = true + archived = false + auto_init = true + branches = [ + { + name = "main" + protected = false + }, + ] + default_branch = "main" + delete_branch_on_merge = false + description = "Managed with Terraform" + etag = "W/\"beafd9847cdfff641d3bbb3c69e85c2d4c4beb45a34a8bc2b86f52a0d96d3108\"" + full_name = "IlsiyaNasibullina/my-terraform-repo" + git_clone_url = "git://github.com/IlsiyaNasibullina/my-terraform-repo.git" + has_downloads = false + has_issues = true + has_projects = false + has_wiki = false + homepage_url = null + html_url = "https://github.com/IlsiyaNasibullina/my-terraform-repo" + http_clone_url = "https://github.com/IlsiyaNasibullina/my-terraform-repo.git" + id = "my-terraform-repo" + is_template = false + merge_commit_message = "PR_TITLE" + merge_commit_title = "MERGE_MESSAGE" + name = "my-terraform-repo" + node_id = "R_kgDON0ssZw" + private = false + repo_id = 927673447 + squash_merge_commit_message = "COMMIT_MESSAGES" + squash_merge_commit_title = "COMMIT_OR_PR_TITLE" + ssh_clone_url = "git@github.com:IlsiyaNasibullina/my-terraform-repo.git" + svn_url = "https://github.com/IlsiyaNasibullina/my-terraform-repo" + visibility = "public" + vulnerability_alerts = false + } + ``` + +7. Execution of importing existing repository in terraform with command `terraform import "github_repository.existing_repo" "S25-core-course-labs"` + + ```bash + var.token + GitHub personal access token + + Enter a value: + + github_repository.existing_repo: Importing from ID "S25-core-course-labs"... + github_repository.existing_repo: Import prepared! + Prepared github_repository for import + github_repository.existing_repo: Refreshing state... [id=S25-core-course-labs] + + Import successful! + + The resources that were imported are shown above. These resources are now in + your Terraform state and will henceforth be managed by Terraform. + ``` + +8. Execution of `terraform apply` for changes + + ```bash + var.token + GitHub personal access token + + Enter a value: + + github_repository.existing_repo: Refreshing state... [id=S25-core-course-labs] + github_repository.repo: Refreshing state... [id=my-terraform-repo] + github_branch_default.default: Refreshing state... [id=my-terraform-repo] + github_branch_protection.default: Refreshing state... [id=BPR_kwDON0ssZ84Dibcb] + + Terraform used the selected providers to generate the following execution plan. Resource actions are + indicated with the + following symbols: + ~ update in-place + + Terraform will perform the following actions: + + # github_repository.existing_repo will be updated in-place + ~ resource "github_repository" "existing_repo" { + ~ auto_init = false -> true + + description = "Managed with Terraform" + - has_downloads = true -> null + - has_projects = true -> null + - has_wiki = true -> null + id = "S25-core-course-labs" + name = "S25-core-course-labs" + # (28 unchanged attributes hidden) + } + + Plan: 0 to add, 1 to change, 0 to destroy. + + 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 + + github_repository.existing_repo: Modifying... [id=S25-core-course-labs] + github_repository.existing_repo: Modifications complete after 4s [id=S25-core-course-labs] + + Apply complete! Resources: 0 added, 1 changed, 0 destroyed. + ``` + +## Best Practices Applied + +1. Use of Input Variables (`variables.tf`) + + Defined input variables for container names, images, and other configurable values for increasing flexibility and avoiding hardcoding values. + +2. Explicit Resource Dependencies + + Ensured Terraform resources are created in the correct order by defining dependencies implicitly via references to prevents race conditions. + +3. Use of Resource Names Instead of Direct IDs + + Used Terraform resource names instead of manually specifying IDs when referencing resources to improve readability and maintainability. + +4. Consistent Naming Conventions + + Used meaningful and consistent naming conventions for Terraform resources. + +5. Used `terraform fmt` and `terraform validate` + + Applied Terraform formatting to ensure clean and standardized code. Used Terraform's built-in validation to check for errors in .tf files. + +## Bonus task - Github Teams + +1. Exection of `terraform state list` + + ```bash + github_branch_default.default + github_repository.repo + github_team.admins + github_team.dev_team + github_team.qa_team + github_team.secret_team + github_team_repository.admins_access + github_team_repository.developers_access + github_team_repository.qa_access + github_team_repository.secret_access + ``` + +2. Execution of `terraform apply` + + ```bash + var.token + GitHub personal access token + + Enter a value: + + + 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: + + # github_branch_default.default will be created + + resource "github_branch_default" "default" { + + branch = "main" + + id = (known after apply) + + repository = "terraform-teams" + } + + # github_repository.repo will be created + + resource "github_repository" "repo" { + + allow_auto_merge = false + + allow_merge_commit = true + + allow_rebase_merge = true + + allow_squash_merge = true + + archived = false + + auto_init = true + + branches = (known after apply) + + default_branch = (known after apply) + + delete_branch_on_merge = false + + description = "Managed with Terraform" + + etag = (known after apply) + + full_name = (known after apply) + + git_clone_url = (known after apply) + + html_url = (known after apply) + + http_clone_url = (known after apply) + + id = (known after apply) + + merge_commit_message = "PR_TITLE" + + merge_commit_title = "MERGE_MESSAGE" + + name = "terraform-teams" + + node_id = (known after apply) + + private = (known after apply) + + repo_id = (known after apply) + + squash_merge_commit_message = "COMMIT_MESSAGES" + + squash_merge_commit_title = "COMMIT_OR_PR_TITLE" + + ssh_clone_url = (known after apply) + + svn_url = (known after apply) + + visibility = "public" + } + + # github_team.admins will be created + + resource "github_team" "admins" { + + create_default_maintainer = false + + description = "Administrators with full access" + + etag = (known after apply) + + id = (known after apply) + + members_count = (known after apply) + + name = "Admins" + + node_id = (known after apply) + + privacy = "closed" + + slug = (known after apply) + } + + # github_team.dev_team will be created + + resource "github_team" "dev_team" { + + create_default_maintainer = false + + description = "Team for developers" + + etag = (known after apply) + + id = (known after apply) + + members_count = (known after apply) + + name = "Developers" + + node_id = (known after apply) + + privacy = "closed" + + slug = (known after apply) + } + + # github_team.qa_team will be created + + resource "github_team" "qa_team" { + + create_default_maintainer = false + + description = "Team for QA engineers" + + etag = (known after apply) + + id = (known after apply) + + members_count = (known after apply) + + name = "QA" + + node_id = (known after apply) + + privacy = "closed" + + slug = (known after apply) + } + + # github_team.secret_team will be created + + resource "github_team" "secret_team" { + + create_default_maintainer = false + + description = "A private team with restricted visibility" + + etag = (known after apply) + + id = (known after apply) + + members_count = (known after apply) + + name = "SecretTeam" + + node_id = (known after apply) + + privacy = "secret" + + slug = (known after apply) + } + + # github_team_repository.admins_access will be created + + resource "github_team_repository" "admins_access" { + + etag = (known after apply) + + id = (known after apply) + + permission = "admin" + + repository = "terraform-teams" + + team_id = (known after apply) + } + + # github_team_repository.developers_access will be created + + resource "github_team_repository" "developers_access" { + + etag = (known after apply) + + id = (known after apply) + + permission = "push" + + repository = "terraform-teams" + + team_id = (known after apply) + } + + # github_team_repository.qa_access will be created + + resource "github_team_repository" "qa_access" { + + etag = (known after apply) + + id = (known after apply) + + permission = "pull" + + repository = "terraform-teams" + + team_id = (known after apply) + } + + # github_team_repository.secret_access will be created + + resource "github_team_repository" "secret_access" { + + etag = (known after apply) + + id = (known after apply) + + permission = "maintain" + + repository = "terraform-teams" + + team_id = (known after apply) + } + + Plan: 10 to add, 0 to change, 0 to destroy. + + 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 + + github_team.secret_team: Creating... + github_team.dev_team: Creating... + github_team.admins: Creating... + github_team.qa_team: Creating... + github_repository.repo: Creating... + github_team.dev_team: Still creating... [10s elapsed] + github_team.qa_team: Still creating... [10s elapsed] + github_repository.repo: Still creating... [10s elapsed] + github_team.secret_team: Still creating... [10s elapsed] + github_team.admins: Still creating... [10s elapsed] + github_team.qa_team: Creation complete after 19s [id=12119612] + github_team_repository.qa_access: Creating... + github_team.dev_team: Still creating... [20s elapsed] + github_team.secret_team: Still creating... [20s elapsed] + github_team.admins: Still creating... [20s elapsed] + github_repository.repo: Still creating... [20s elapsed] + github_team.admins: Creation complete after 23s [id=12119611] + github_team_repository.admins_access: Creating... + github_team.dev_team: Creation complete after 24s [id=12119613] + github_team_repository.developers_access: Creating... + github_team.secret_team: Creation complete after 24s [id=12119615] + github_team_repository.secret_access: Creating... + github_team_repository.qa_access: Creation complete after 9s [id=12119612:terraform-teams] + github_repository.repo: Still creating... [30s elapsed] + github_repository.repo: Creation complete after 32s [id=terraform-teams] + github_branch_default.default: Creating... + github_team_repository.admins_access: Creation complete after 9s [id=12119611:terraform-teams] + github_team_repository.developers_access: Creation complete after 8s [id=12119613:terraform-teams] + github_team_repository.secret_access: Creation complete after 9s [id=12119615:terraform-teams] + github_branch_default.default: Creation complete after 2s [id=terraform-teams] + + Apply complete! Resources: 10 added, 0 changed, 0 destroyed. + ``` +As a result, the teams were created: + +![Created teams](./lab4-teams.png) diff --git a/terraform/docker/main.tf b/terraform/docker/main.tf new file mode 100644 index 0000000000..bc007dee94 --- /dev/null +++ b/terraform/docker/main.tf @@ -0,0 +1,37 @@ +terraform { + required_providers { + docker = { + source = "kreuzwerker/docker" + version = "~> 3.0.1" + } + } +} + +provider "docker" {} + +resource "docker_image" "python_app" { + name = "python:3.11" +} + +resource "docker_image" "node_app" { + name = "node:18" +} + +resource "docker_container" "python_container" { + name = var.python_container_name + image = var.python_image_name + ports { + internal = 5000 + external = 5001 + } +} + +resource "docker_container" "node_container" { + name = var.node_container_name + image = var.node_image_name + ports { + internal = 3000 + external = 3000 + } +} + diff --git a/terraform/docker/outputs.tf b/terraform/docker/outputs.tf new file mode 100644 index 0000000000..5f16f02536 --- /dev/null +++ b/terraform/docker/outputs.tf @@ -0,0 +1,50 @@ +output "python_container_id" { + description = "The ID of the Python container" + value = docker_container.python_container.id +} + +output "node_container_id" { + description = "The ID of the Node.js container" + value = docker_container.node_container.id +} + +output "python_container_name" { + description = "The name of the Python container" + value = docker_container.python_container.name +} + +output "node_container_name" { + description = "The name of the Node.js container" + value = docker_container.node_container.name +} + +output "python_container_ip" { + description = "The IP address of the Python container" + value = docker_container.python_container.network_data[0].ip_address +} + +output "node_container_ip" { + description = "The IP address of the Node.js container" + value = docker_container.node_container.network_data[0].ip_address +} + +output "python_container_port" { + description = "The exposed port of the Python container" + value = docker_container.python_container.ports[0].external +} + +output "node_container_port" { + description = "The exposed port of the Node.js container" + value = docker_container.node_container.ports[0].external +} + +output "python_container_image" { + description = "The image used for the Python container" + value = docker_container.python_container.image +} + +output "node_container_image" { + description = "The image used for the Node.js container" + value = docker_container.node_container.image +} + diff --git a/terraform/docker/variables.tf b/terraform/docker/variables.tf new file mode 100644 index 0000000000..78bc9ca19b --- /dev/null +++ b/terraform/docker/variables.tf @@ -0,0 +1,20 @@ +variable "python_container_name" { + type = string + default = "app_python" +} + +variable "python_image_name" { + type = string + default = "ilsiia/app_python:latest" +} + +variable "node_container_name" { + type = string + default = "app_nodejs" +} + +variable "node_image_name" { + type = string + default = "ilsiia/app_nodejs:latest" +} + diff --git a/terraform/github/deploy.tfplan b/terraform/github/deploy.tfplan new file mode 100644 index 0000000000..8b8d8a2e0a Binary files /dev/null and b/terraform/github/deploy.tfplan differ diff --git a/terraform/github/main.tf b/terraform/github/main.tf new file mode 100644 index 0000000000..5c3daccb8d --- /dev/null +++ b/terraform/github/main.tf @@ -0,0 +1,37 @@ +terraform { + required_providers { + github = { + source = "integrations/github" + version = "~> 4.0" + } + } +} + +provider "github" { + token = var.token +} + +resource "github_repository" "repo" { + name = var.repo_name + description = var.repo_description + visibility = var.repo_visibility + auto_init = true + has_issues = true +} + +resource "github_branch_default" "default" { + repository = github_repository.repo.name + branch = var.default_branch +} + +resource "github_branch_protection" "default" { + repository_id = github_repository.repo.id + pattern = github_branch_default.default.branch + require_conversation_resolution = true + enforce_admins = true + + required_pull_request_reviews { + required_approving_review_count = 1 + } +} + diff --git a/terraform/github/variables.tf b/terraform/github/variables.tf new file mode 100644 index 0000000000..1b5bd7acc8 --- /dev/null +++ b/terraform/github/variables.tf @@ -0,0 +1,29 @@ +variable "token" { + description = "GitHub personal access token" + type = string + sensitive = true +} + +variable "repo_name" { + description = "GitHub repository name" + type = string + default = "my-terraform-repo" +} + +variable "repo_description" { + description = "Repository description" + type = string + default = "Managed with Terraform" +} + +variable "repo_visibility" { + description = "Repository visibility (public or private)" + type = string + default = "public" +} + +variable "default_branch" { + description = "The default branch for the repository" + default = "main" +} + diff --git a/terraform/github_teams/main.tf b/terraform/github_teams/main.tf new file mode 100644 index 0000000000..cde4e45470 --- /dev/null +++ b/terraform/github_teams/main.tf @@ -0,0 +1,73 @@ +terraform { + required_providers { + github = { + source = "integrations/github" + version = "~> 4.0" + } + } +} + +provider "github" { + token = var.token + owner = var.organization +} + +resource "github_repository" "repo" { + name = var.repo_name + description = var.repo_description + visibility = var.repo_visibility + auto_init = true +} + +resource "github_branch_default" "default" { + repository = github_repository.repo.name + branch = var.default_branch +} + +resource "github_team" "dev_team" { + name = "Developers" + description = "Team for developers" + privacy = "closed" +} + +resource "github_team" "qa_team" { + name = "QA" + description = "Team for QA engineers" + privacy = "closed" +} + +resource "github_team" "admins" { + name = "Admins" + description = "Administrators with full access" + privacy = "closed" +} + +resource "github_team" "secret_team" { + name = "SecretTeam" + description = "A private team with restricted visibility" + privacy = "secret" +} + +resource "github_team_repository" "developers_access" { + team_id = github_team.dev_team.id + repository = var.repo_name + permission = "push" +} + +resource "github_team_repository" "admins_access" { + team_id = github_team.admins.id + repository = var.repo_name + permission = "admin" +} + +resource "github_team_repository" "qa_access" { + team_id = github_team.qa_team.id + repository = var.repo_name + permission = "pull" +} + +resource "github_team_repository" "secret_access" { + team_id = github_team.secret_team.id + repository = var.repo_name + permission = "maintain" +} diff --git a/terraform/github_teams/variables.tf b/terraform/github_teams/variables.tf new file mode 100644 index 0000000000..7e2c2e8fef --- /dev/null +++ b/terraform/github_teams/variables.tf @@ -0,0 +1,35 @@ +variable "token" { + description = "GitHub personal access token" + type = string + sensitive = true +} + +variable "repo_name" { + description = "GitHub repository name" + type = string + default = "terraform-teams" +} + +variable "repo_description" { + description = "Repository description" + type = string + default = "Managed with Terraform" +} + +variable "repo_visibility" { + description = "Repository visibility (public or private)" + type = string + default = "public" +} + +variable "default_branch" { + description = "The default branch for the repository" + default = "main" +} + +variable "organization" { + description = "The name of github organization" + type = string + default = "S25-devops-terraform" +} + diff --git a/terraform/lab4-teams.png b/terraform/lab4-teams.png new file mode 100644 index 0000000000..8f95a95fd2 Binary files /dev/null and b/terraform/lab4-teams.png differ diff --git a/terraform/yandex/main.tf b/terraform/yandex/main.tf new file mode 100644 index 0000000000..92bd8845b5 --- /dev/null +++ b/terraform/yandex/main.tf @@ -0,0 +1,53 @@ +terraform { + required_providers { + yandex = { + source = "yandex-cloud/yandex" + } + } + required_version = ">=0.13" +} + +provider "yandex" { + token = var.yc_token + cloud_id = var.cloud_id + folder_id = var.folder_id + zone = var.zone +} + +resource "yandex_vpc_network" "network-1" { + name = "default-1" +} + + +resource "yandex_vpc_subnet" "subnet-1" { + zone = var.zone + network_id = yandex_vpc_network.network-1.id + v4_cidr_blocks = ["10.131.0.0/24"] +} + + +resource "yandex_compute_instance" "vm-1" { + name = "terraform-vm-1" + + resources { + cores = var.cores + memory = var.memory + } + + boot_disk { + initialize_params { + image_id = var.image_id + size = var.boot_size + } + } + + network_interface { + subnet_id = yandex_vpc_subnet.subnet-1.id + nat = true + } + + metadata = { + ssh-keys = "ubuntu:${file(var.ssh_key)}" + } +} + diff --git a/terraform/yandex/variables.tf b/terraform/yandex/variables.tf new file mode 100644 index 0000000000..0c2db3120e --- /dev/null +++ b/terraform/yandex/variables.tf @@ -0,0 +1,46 @@ +variable "yc_token" { + type = string + default = "t1.9euelZrPn....Your Token" +} + +variable "folder_id" { + type = string + default = "b1g5rr7vs8qnd5ikd5ee" +} + +variable "cloud_id" { + type = string + default = "b1gblh32diardc9mii5u" +} + +variable "zone" { + type = string + default = "ru-central1-b" +} + +variable "cores" { + type = number + default = 2 +} + +variable "memory" { + type = number + default = 2 +} + +variable "image_id" { + type = string + default = "fd800c7s2p483i648ifv" +} + +variable "boot_size" { + type = number + default = 20 +} + +variable "ssh_key" { + type = string + default = "~/.ssh/id_ed25519.pub" +} + +