diff --git a/.github/workflows/build-and-deploy-test-stack.yml b/.github/workflows/build-and-deploy-test-stack.yml new file mode 100644 index 000000000..8b3a5a90d --- /dev/null +++ b/.github/workflows/build-and-deploy-test-stack.yml @@ -0,0 +1,49 @@ +name: Build and deploy GovTool test stack +run-name: Deploy by @${{ github.actor }} + +on: + push: + branches: + - test + +env: + ENVIRONMENT: "test" + CARDANO_NETWORK: "sanchonet" + +jobs: + deploy: + name: Deploy app + runs-on: ubuntu-latest + env: + GRAFANA_ADMIN_PASSWORD: ${{ secrets.GRAFANA_ADMIN_PASSWORD }} + GRAFANA_SLACK_RECIPIENT: ${{ secrets.GRAFANA_SLACK_RECIPIENT }} + GRAFANA_SLACK_OAUTH_TOKEN: ${{ secrets.GRAFANA_SLACK_OAUTH_TOKEN }} + SENTRY_DSN_BACKEND: ${{ secrets.SENTRY_DSN_BACKEND }} + GTM_ID: ${{ secrets.GTM_ID }} + SENTRY_DSN: ${{ secrets.SENTRY_DSN_FRONTEND }} + PIPELINE_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + USERSNAP_SPACE_API_KEY: ${{ secrets.USERSNAP_SPACE_API_KEY }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup SSH agent + uses: webfactory/ssh-agent@v0.8.0 + with: + ssh-private-key: ${{ secrets.TEST_STACK_SSH_KEY }} + + - name: Run Ansible playbook + uses: dawidd6/action-ansible-playbook@v2 + with: + playbook: playbook.yml + directory: ./tests/test-infrastructure + key: ${{ secrets.TEST_STACK_SSH_KEY }} + inventory: | + [test_server] + ${{ secrets.TEST_STACK_SERVER_IP }} ansible_user=ec2-user + options: | + --verbose + env: + GOVTOOL_TAG: ${{ github.sha }} \ No newline at end of file diff --git a/.github/workflows/lighthouse.yml b/.github/workflows/lighthouse.yml index 2bf75ada6..213d2fdf8 100644 --- a/.github/workflows/lighthouse.yml +++ b/.github/workflows/lighthouse.yml @@ -1,47 +1,31 @@ name: Lighthouse on: - push: - paths: - - govtool/frontend/** - - .github/workflows/lighthouse.yml + workflow_run: + workflows: + - Build and deploy GovTool test stack + types: + - completed jobs: lighthouse: runs-on: ubuntu-latest - env: - NODE_OPTIONS: --max_old_space_size=4096 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: node-version: 16 - - name: Install dependencies - run: npm install - working-directory: ./govtool/frontend - - - name: Cache npm dependencies - id: npm-cache - uses: actions/cache@v3 - with: - path: | - ~/.npm - key: ${{ runner.os }}-npm-${{ hashFiles('govtool/frontend/package-lock.json', 'tests/govtool-frontend/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-npm- - - run: npm install -g @lhci/cli@0.12.x - - name: Run build and lighthouse task + - name: Run lighthouse task working-directory: ./govtool/frontend run: | - npm install - VITE_BASE_URL=https://staging.govtool.byron.network/ npm run build lhci collect - name: Evaluate reports if: github.repository_owner != 'IntersectMBO' + working-directory: ./govtool/frontend run: | lhci assert --preset "lighthouse:recommended" @@ -50,9 +34,4 @@ jobs: if: github.repository_owner == 'IntersectMBO' run: | lhci assert --preset lighthouse:recommended || echo "LightHouse Assertion error ignored ..." - lhci upload --githubAppToken="${{ secrets.LHCI_GITHUB_APP_TOKEN }}" --token="${{ secrets.LHCI_SERVER_TOKEN }}" --serverBaseUrl=https://lighthouse.cardanoapi.io --ignoreDuplicateBuildFailure - curl -X POST https://ligththouse.cardanoapi.io/api/metrics/build-reports \ - -d "@./lighthouseci/$(ls ./.lighthouseci |grep 'lhr.*\.json' | head -n 1)" \ - -H "commit-hash: $(git rev-parse HEAD)" \ - -H "secret-token: ${{ secrets.METRICS_SERVER_SECRET_TOKEN }}" \ - -H 'Content-Type: application/json' || echo "Metric Upload error ignored ..." + lhci upload --githubAppToken="${{ secrets.LHCI_GITHUB_APP_TOKEN }}" --token="${{ secrets.LHCI_SERVER_TOKEN }}" --serverBaseUrl=https://lighthouse-govtool.cardanoapi.io --ignoreDuplicateBuildFailure diff --git a/.github/workflows/test_integration_playwright.yml b/.github/workflows/test_integration_playwright.yml index f9e10d27b..80f0cb971 100644 --- a/.github/workflows/test_integration_playwright.yml +++ b/.github/workflows/test_integration_playwright.yml @@ -5,7 +5,7 @@ on: paths: - .github/workflows/test_integration_playwright.yml workflow_run: - workflows: ["Build and deploy GovTool to TEST server"] + workflows: ["Build and deploy GovTool test stack"] types: [completed] jobs: diff --git a/gov-action-loader/backend/.env.example b/gov-action-loader/backend/.env.example index 654ec757e..8997b5adf 100644 --- a/gov-action-loader/backend/.env.example +++ b/gov-action-loader/backend/.env.example @@ -1,6 +1,2 @@ KUBER_API_URL=https://sanchonet.kuber.cardanoapi.io KUBER_API_KEY=xxxxxxxxxxxxx - -## Not required anymore -BLOCKFROST_API_URL= -BLOCKFROST_PROJECT_ID= diff --git a/gov-action-loader/backend/app/settings.py b/gov-action-loader/backend/app/settings.py index ce543cf33..02095e720 100644 --- a/gov-action-loader/backend/app/settings.py +++ b/gov-action-loader/backend/app/settings.py @@ -3,10 +3,6 @@ class Settings(BaseSettings): kuber_api_url: str - kuber_api_key: str - - blockfrost_api_url: str - blockfrost_project_id: str - + kuber_api_key: str = '' settings = Settings() diff --git a/govtool/backend/Dockerfile b/govtool/backend/Dockerfile index b8882627d..7da291284 100644 --- a/govtool/backend/Dockerfile +++ b/govtool/backend/Dockerfile @@ -1,5 +1,6 @@ -ARG BASE_IMAGE_TAG -FROM 733019650473.dkr.ecr.eu-west-1.amazonaws.com/backend-base:$BASE_IMAGE_TAG +ARG BASE_IMAGE_TAG=latest +ARG BASE_IMAGE_REPO=733019650473.dkr.ecr.eu-west-1.amazonaws.com/backend-base +FROM $BASE_IMAGE_REPO:$BASE_IMAGE_TAG WORKDIR /src COPY . . RUN cabal build diff --git a/govtool/frontend/.lighthouserc.yml b/govtool/frontend/.lighthouserc.yml index 5e963f7ae..fe06450bb 100644 --- a/govtool/frontend/.lighthouserc.yml +++ b/govtool/frontend/.lighthouserc.yml @@ -1,5 +1,6 @@ ci: collect: - staticDistDir: "./dist" url: - - "http://localhost" + - https://govtool.cardanoapi.io + - https://govtool.cardanoapi.io/drep_directory + - https://govtool.cardanoapi.io/governance_actions \ No newline at end of file diff --git a/tests/test-infrastructure/.env.example b/tests/test-infrastructure/.env.example index fd9687eeb..81ff08a2b 100644 --- a/tests/test-infrastructure/.env.example +++ b/tests/test-infrastructure/.env.example @@ -1,4 +1,4 @@ -STACK_NAME=govtool -BASE_DOMAIN=cardanoapi.io -BLOCKFROST_API_URL="" -BLOCKFROST_PROJECT_ID="" +PROJECT_NAME=govtool +CARDANO_NETWORK=sanchonet +BASE_DOMAIN=govtool.cardanoapi.io +GOVTOOL_TAG=test \ No newline at end of file diff --git a/tests/test-infrastructure/.gitignore b/tests/test-infrastructure/.gitignore index e433f6cb7..990529fba 100644 --- a/tests/test-infrastructure/.gitignore +++ b/tests/test-infrastructure/.gitignore @@ -1,5 +1,4 @@ secrets/ configs/ -docker-compose-rendered.yml -docker-compose-swarm-rendered.yml -docker-compose-services-rendered.yml +/*-rendered.yml + diff --git a/tests/test-infrastructure/README.md b/tests/test-infrastructure/README.md index d91eeaa44..054cda6f5 100644 --- a/tests/test-infrastructure/README.md +++ b/tests/test-infrastructure/README.md @@ -1,134 +1,41 @@ GovTool Test Infrastructure ==================== -Services required for testing GovTool +Compose files and scripts to deploy and test environment of govtool. +Additionally, it deploys services required to perform integration test on the environment -## 1. Setting up the services +## Compose files and services +1. [basic-services](./docker-compose-basic-services.yml) : postgres and gateway +2. [cardano](./docker-compose-cardano.yml) : node, dbsync and kuber +3. [govtool](./docker-compose-govtool.yml) : govtool-frontend and govtool-backend +4. [govaction-loader](./docker-compose-govaction-loader.yml) : govaction-loader frontend and badkcne +5. [test](./docker-compose-test.yml) : lighthouse-server and metadata-api +## Setting up the services -#### a. Deploy with docker on swarm mode. + +#### a. Update .env file and DNS records - Create `.env` file by copying `.env.example` and update it. - Make sure that DNS is pointed to the right server. Following are the domains used. - - lighthouse.BASE_DOMAIN - - metabase.BASE_DOMAIN - - sonarqube.BASE_DOMAIN - - metrics.BASE_DOMAIN - - kuber.BASE_DOMAIN - - -`docker stack deploy` command doesn't support `.env` file secret/config files. -There's a helper script `deploy-swarm.sh` to load the environment variables from `.env` file and generate rendered docker compose file. -```bash -cd ./test/test-infrastructire # cd into the test-infrastructure folder -docker swarm init # if swarm mode is not enabled yet. -docker compose build # build the images -docker node update xxxx --label-add govtool-test-stack=true ## set the node to be used for deploying the services -./gen-configs.sh # generate configs and secrets. -./deploy-swarm.sh prepare # start postgres and nginx -sleep 30 # wait for 30 secs for postgres to be healthy -./deploy-swarm.sh finalize # deploy all the required services. -``` - -#### b. Setup -When the stack is ready, further configuration is required it the services and github repo secrets and workflow files. - -# 2. Services List - -## SonarQube Server -#### Requires -- postgres database - -#### Used by -- Github Action to submit sonar-sacanner result - -`sonar-scanner` is used for static analysis of code. -The analysis generated by sonar-scanner is saved to SonarQube server for better visibility and to see progress over time. - - -**Docker Image:** [mc1arke/sonarqube-with-community-branch-plugin:9.9-community](https://hub.docker.com/layers/mc1arke/sonarqube-with-community-branch-plugin/9.9-community/images/sha256-b91ac551bea0fc3b394eaf7f82ea79115e03db9ab47d26610b9e1566723a07a5?context=explore) - -**See :** [sonar-scanner](https://docs.sonarsource.com/sonarqube/latest/analyzing-source-code/scanners/sonarscanner/), [actions/sonar-scanner](https://github.com/marketplace/actions/sonar-scanner) - -### Initial configuration. - -- Login and change the initial password. -``` -username: admin -password: admin -``` -- Create new project and set the projectKey in file [govtool/frontend/sonar-project.properties](../../govtool/frontend/sonar-project.properties) -- Update the github action secrets - - SONAR_HOST_URL - - SONAR_TOKEN - - -## Metabase Server -#### Requires -- postgres database - -Metabase provides UI to show graphs and visualization from different datasource. -It is used for visualizing the test metrics and the api response times over time. - -**Docker Image:** [metabase/metabase:v0.46.6.4](https://hub.docker.com/layers/metabase/metabase/v0.46.6.4/images/sha256-95c60db0c87c5da9cb81f6aefd0cd548fe2c14ff8c8dcba2ea58a338865cdbd9?context=explore) - -### Initial Configuration - - Setup initial account for ligin via the webapp. - - Under database section in admin settings, add the `govtool_lithghouse` and `govtool_metrics` databases - - Select the database and add visualizations, queries for the data. - -## LightHouse Report Server -#### Requires -- postgres database - -#### Used by -- Github Action to submit lighthouse report. - -Lighthouse has audits for performance, accessibility, progressive web apps, SEO, and more. -Lighthouse-Server is used to host and display the audits generated by lighthouse. - -**Docker Image:** [patrickhulce/lhci-server:0.12.0](https://hub.docker.com/r/patrickhulce/lhci-server) - -### Initial Configuration -- install lhci locally and run `lhci wizard` to setup project -- update `--serverBaseUrl={{...}}` parameter in [.github/workflows/lighthouse.yml](../../.github/workflows/lighthouse.yml) -- update `LHCI_SERVER_TOKEN` in github secrets. -- install lighthouse github app on the repo -- obtain app token from lighthouse app and update `LHCI_GITHUB_APP_TOKEN` secret - -See: **[lighthouse-server-docs](https://googlechrome.github.io/lighthouse-ci/docs/server.html)** - - -## Metrics API Server -#### Requires -- postgres database -- metabase *(for result visualization) - - -#### Used by -- Github Action - backend test to submit test metrics. - -Metrics API Server receives metrics collected during backend test and saves them to database. -The results are visualized in metabase. - -### Initial Configuration -- update `RECORD_METRICS_API` variable in file [.github/workflows/test_backend.yml](../../.github/workflows/test_backend.yml) - - -**Source Code:** [tests/test-metrics-api](../test-metrics-api) - -## Kuber Server -#### Requires -- cardano-node's socket connection - -#### Used by -- Cypress integration test -- Governance Data Loader - -Opensource API server for transaction building and querying the ledger . -Kuber makes it easy to construct and submit transaction from the frontend. - -**Docker Image:** [dquadrant/kuber:70be9b0166177eab5cf33e603fd3dc579e14cf31](https://hub.docker.com/layers/dquadrant/kuber/70be9b0166177eab5cf33e603fd3dc579e14cf31/images/sha256-d3b3f7c2304da8c4777155b26220238b682c81a3ff2b14753a5dc41c4f151364?context=explore) + - lighthouse-{BASE_DOMAIN} + - kuber-{BASE_DOMAIN} + - metadata-{BASE_DOMAIN} + - governance-{BASE_DOMAIN} + +### b. Prepare the machine. + - Buy a virtual server + - Install `docker` and enable `docker compose` plugin. + - execute `docker swarm init` command. + +### c. One time setup on the machine. + - Generate secrets and configurations required by the services + `./gen-configs.sh` + - Mark the nodes with labels to specify where the services should be run. In case of single node + docker swarm, all labels can be set to single node. + `./deploy.sh prepare` + +### d. Build images and deploy the stacks. + - `./build-images.sh` + - `./deploy.sh stack all` -### Initial Configuration -- update `CYPRESS_kuberApiUrl` variable in [.github/workflows/test_integration_cypress.yml](../../.github/workflows/test_integration_cypress.yml) diff --git a/tests/test-infrastructure/build-and-deploy.sh b/tests/test-infrastructure/build-and-deploy.sh new file mode 100755 index 000000000..919f64a15 --- /dev/null +++ b/tests/test-infrastructure/build-and-deploy.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +export BASE_IMAGE_NAME=govtool +export PROJECT_NAME=govtool +export CARDANO_NETWORK=sanchonet +export BASE_DOMAIN=govtool.cardanoapi.io + +if [ -z "$GOVTOOL_TAG" ]; then + GOVTOOL_TAG="$(git rev-parse HEAD)" +fi +export GOVTOOL_TAG + +. ./scripts/deploy-stack.sh + +check_env + +# Build images +./build-images.sh +function update-service(){ + docker service update --image "$2" "$1" +} + +if [[ "$1" == "update-images" ]] +then + update-service govtool_backend "$BASE_IMAGE_NAME"/backend:${GOVTOOL_TAG} + update-service govtool_frontend "$BASE_IMAGE_NAME"/frontend:${GOVTOOL_TAG} + update-service govtool_metadata-validation "$BASE_IMAGE_NAME"/metadata-validation:${GOVTOOL_TAG} + + update-service govaction-loader_backend "$BASE_IMAGE_NAME"/gov-action-loader-backend:${GOVTOOL_TAG} + update-service govaction-loader_frontend "$BASE_IMAGE_NAME"/gov-action-loader-frontend:${GOVTOOL_TAG} + + # test metadata API + update-service test_metadata-api "$BASE_IMAGE_NAME"/metadata-api:${GOVTOOL_TAG} + +elif [[ $1 == "full" ]] +then + ./deploy.sh stack all +fi diff --git a/tests/test-infrastructure/build-images.sh b/tests/test-infrastructure/build-images.sh new file mode 100755 index 000000000..e86103e7a --- /dev/null +++ b/tests/test-infrastructure/build-images.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -e +export BASE_IMAGE_NAME="govtool" +BASE_IMAGE_EXISTS=$(docker images -q "$BASE_IMAGE_NAME"/backend-base) + +if [ -z "$BASE_IMAGE_EXISTS" ]; then + echo "Building the base image..." + docker build -t "$BASE_IMAGE_NAME"/backend-base -f ../../govtool/backend/Dockerfile.base ../../govtool/backend +else + echo "Base image already exists. Skipping build." +fi + +docker compose -f ./docker-compose-govtool.yml build +docker compose -f ./docker-compose-govaction-loader.yml build +docker compose -f ./docker-compose-test.yml build \ No newline at end of file diff --git a/tests/test-infrastructure/configs_template/backend_config.json b/tests/test-infrastructure/configs_template/backend_config.json new file mode 100644 index 000000000..43cf7f251 --- /dev/null +++ b/tests/test-infrastructure/configs_template/backend_config.json @@ -0,0 +1,15 @@ +{ + "dbsyncconfig" : { + "host" : "postgres", + "dbname" : "${DBSYNC_DATABASE}", + "user" : "postgres", + "password" : "${POSTGRES_PASSWORD}", + "port" : 5432 + }, + "port" : 8080, + "host" : "0.0.0.0", + "cachedurationseconds": 20, + "sentrydsn": "https://username:password@senty.host/id", + "metadatavalidationhost": "http://metadata-validation", + "metadatavalidationport": 3000 +} \ No newline at end of file diff --git a/tests/test-infrastructure/configs_template/postgres_db_setup.sql b/tests/test-infrastructure/configs_template/postgres_db_setup.sql index 2934a2840..1c87ab3f1 100644 --- a/tests/test-infrastructure/configs_template/postgres_db_setup.sql +++ b/tests/test-infrastructure/configs_template/postgres_db_setup.sql @@ -1,4 +1,4 @@ -CREATE database ${STACK_NAME}_metabase; -CREATE database ${STACK_NAME}_lighthouse; -CREATE database ${STACK_NAME}_metrics; -CREATE database ${STACK_NAME}_sonarqube; +CREATE database ${PROJECT_NAME}_lighthouse; +CREATE database ${PROJECT_NAME}_metrics; +CREATE database ${PROJECT_NAME}_sonarqube; +CREATE database ${PROJECßT_NAME}_dbsync; \ No newline at end of file diff --git a/tests/test-infrastructure/deploy-swarm.sh b/tests/test-infrastructure/deploy-swarm.sh deleted file mode 100755 index 90c7fc269..000000000 --- a/tests/test-infrastructure/deploy-swarm.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/bin/bash -## Load environment variables and deploy to the docker swarm. -## -## Usages: -## ./deploy-swarm prepare -## -set -eo pipefail -set -a -. ./.env -set +a - -if [ "$1" == "destroy" ] -then - echo "This will remove everything in your stack including volumes" - echo "Are you Sure? (Y/N)" - read user_input - if ! ( [ "$user_input" = "y" ] || [ "$user_input" = "Y" ]) - then - exit 1 - fi - echo "Proceeding..." # Delete the Docker stack if "destroy" argument is provided - docker stack rm "${STACK_NAME}-services" || echo "${STACK_NAME}-services doesn't exist" - docker stack rm ${STACK_NAME} || echo "${STACK_NAME} doesn't exist" - ./gen-configs.sh clean - - for VOLUME in $(docker volume ls --filter "label=com.docker.stack.namespace=${STACK_NAME}" -q) "${STACK_NAME}-services_postgres" - do - echo -n "Removing Volume : " - docker volume rm "$VOLUME" - done - -elif [ "$1" == "prepare" ] -then - ## apply the enviroment to services compose file - ## and deploy the stack - envsubst < ./docker-compose-services.yml > ./docker-compose-services-rendered.yml - docker stack deploy -c './docker-compose-services-rendered.yml' ${STACK_NAME}-services - -elif [ "$1" == "finalize" ] -then - ## apply the environment to compose file - ## deploy the govtool test infrastructure stack - envsubst < ./docker-compose.yml > ./docker-compose-rendered.yml - docker stack deploy -c './docker-compose-rendered.yml' ${STACK_NAME} -else - echo "Something is wrong with the command" - echo - echo " Usage:" - echo " $0 (prepare | destroy | finalize)" - echo '' - echo " Options:" - echo " prepare -> deploys the services required by the test stack. i.e 'postgres' and 'reverse-proxy'" - echo " finalize -> deploys the test infrastructure services" - echo " destroy -> teardown everything except the volumes" -fi diff --git a/tests/test-infrastructure/deploy.sh b/tests/test-infrastructure/deploy.sh new file mode 100755 index 000000000..c14d575b9 --- /dev/null +++ b/tests/test-infrastructure/deploy.sh @@ -0,0 +1,112 @@ +#!/bin/bash +## Load environment variables and deploy to the docker swarm. +## +## Usages: +## ./deploy-swarm prepare +## +set -eo pipefail +. ./scripts/deploy-stack.sh +load_env + +DOCKER_STACKS=("basic-services" "cardano" "govaction-loader" "govtool" "test") + +if [ "$1" == "destroy" ] +then + echo "This will remove everything in your stack except volumes, configs and secrets" + echo "Are you Sure? (Y/N)" + read user_input + if ! ( [ "$user_input" = "y" ] || [ "$user_input" = "Y" ]) + then + exit 1 + fi + echo "Proceeding..." # Delete the Docker stack if "destroy" argument is provided + + REVERSE_STACKS=() + for ((i=${#STACKS[@]}-1; i>=0; i--)); do + REVERSE_STACKS+=("${STACKS[i]}") + done + + for CUR_STACK in "${REVERSE_STACKS[@]}"; do + docker stack rm "$CUR_STACK" + sleep 6 # wait 6 seconds for each stack cleanup. + done + +# ./gen-configs.sh clean + +# for VOLUME in $(docker volume ls --filter "label=com.docker.stack.namespace=${STACK_NAME}" -q) "${STACK_NAME}-services_postgres" +# do +# echo -n "Removing Volume : " +# docker volume rm "$VOLUME" +# done +elif [ "$1" == 'prepare' ] +then + + # Get the number of nodes in the swarm + NODES=$(docker node ls --format "{{.ID}}" | wc -l) + + # If there is only one node, set the labels + if [ "$NODES" -eq 1 ]; then + NODE_ID=$(docker node ls --format "{{.ID}}") + + docker node update --label-add govtool-test-stack=true \ + --label-add blockchain=true \ + --label-add gateway=true \ + --label-add govtool=true \ + --label-add gov-action-loader=true \ + "$NODE_ID" + + echo "Labels set on node: $NODE_ID" + else + echo "There are multiple nodes in the docker swarm." + echo "Please set the following labels to correct nodes manually." + echo " - govtool-test-stack " + echo " - blockchain" + echo " - gateway" + echo " - govtool" + echo " - gov-action-loader" + echo "" + echo " e.g. $ docker node update xxxx --label-add gateway=true" + + exit 1 + fi + +elif [ "$1" == 'stack' ] +then + if [ "$#" -ne 2 ] + then + echo "stack requires the stack name". + echo "Usage :" + echo " > $0 stack [stack-name]". + echo "" + echo " stack-name : One of the following"ß + echo " $DOCKER_STACKS" + else + case "$2" in + all) + + for DEPLOY_STACK in "${DOCKER_STACKS[@]}"; do + deploy-stack "$DEPLOY_STACK" "docker-compose-$DEPLOY_STACK.yml" + done + + ;; + *) + if [[ ! -f ./"docker-compose-$2.yml" ]] + then + echo "Invalid stack name. $2" + else + deploy-stack $2 "docker-compose-$2.yml" + fi + ;; + esac + fi +else + echo "Something is wrong with the command" + echo + echo " Usage:" + echo " $0 (prepare | destroy | deploy)" + echo '' + echo " Options:" + echo " prepare -> set required labels to docker swarm node." + echo " destroy -> teardown everything except the volumes" + echo " deploy [stack_name] -> Deploy the stack." +fi diff --git a/tests/test-infrastructure/docker-compose-services.yml b/tests/test-infrastructure/docker-compose-basic-services.yml similarity index 75% rename from tests/test-infrastructure/docker-compose-services.yml rename to tests/test-infrastructure/docker-compose-basic-services.yml index d7563a2ad..e0b417494 100644 --- a/tests/test-infrastructure/docker-compose-services.yml +++ b/tests/test-infrastructure/docker-compose-basic-services.yml @@ -2,24 +2,15 @@ version: "3.9" secrets: postgres_user: external: true - name: ${STACK_NAME}_postgres_user + name: ${PROJECT_NAME}_postgres_user postgres_password: external: true - name: ${STACK_NAME}_postgres_password + name: ${PROJECT_NAME}_postgres_password configs: postgres_db_setup.sql: external: true - name: ${STACK_NAME}_postgres_db_setup.sql + name: ${PROJECT_NAME}_postgres_db_setup.sql -### secrets and configs in docker compose -# secrets: -# postgres_user: -# file: "./secrets/${STACK_NAME}_postgres_user" -# postgres_password: -# file: "./secrets/${STACK_NAME}_postgres_password" -# configs: -# postgres_db_setup.sql: -# file: "./configs/${STACK_NAME}_postgres_db_setup.sql" volumes: postgres: nginx_dhparam: @@ -54,7 +45,7 @@ services: deploy: placement: constraints: - - node.labels.govtool-test-stack == true + - node.labels.gateway == true restart_policy: delay: "10s" postgres: @@ -73,6 +64,8 @@ services: - postgres volumes: - postgres:/var/lib/postgresql/data + ports: + - 5432:5432 restart: always healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] @@ -83,6 +76,6 @@ services: deploy: placement: constraints: - - node.labels.govtool-test-stack == true + - node.labels.blockchain == true restart_policy: delay: "30s" diff --git a/tests/test-infrastructure/docker-compose-cardano.yml b/tests/test-infrastructure/docker-compose-cardano.yml new file mode 100644 index 000000000..514324cfc --- /dev/null +++ b/tests/test-infrastructure/docker-compose-cardano.yml @@ -0,0 +1,103 @@ +version: "3.9" +secrets: + postgres_user: + external: true + name: ${PROJECT_NAME}_postgres_user + postgres_password: + external: true + name: ${PROJECT_NAME}_postgres_password + dbsync_database: + external: true + name: ${PROJECT_NAME}_dbsync_database + +volumes: + node_data: + node_ipc: + dbsync_data: + +networks: + postgres: + external: true + frontend: + external: true + cardano: + attachable: true + name: cardano + +services: + node: + image: ghcr.io/intersectmbo/cardano-node:8.11.0-sancho + environment: + NETWORK: ${CARDANO_NETWORK} + volumes: + - node_data:/data + - node_ipc:/ipc + stop_grace_period: 1m + logging: + driver: "json-file" + options: + max-size: "10M" + max-file: "10" + ports: + - target: 3001 + published: 3001 + protocol: tcp + mode: host + deploy: + placement: + constraints: + - node.labels.blockchain==true + restart_policy: + condition: on-failure + delay: 15s + dbsync: + image: ghcr.io/intersectmbo/cardano-db-sync:sancho-4-2-1 + networks: + - postgres + environment: + NETWORK: ${CARDANO_NETWORK} + POSTGRES_HOST: postgres + POSTGRES_PORT: 5432 + DISABLE_CACHE: "" + DISABLE_LEDGER: "" + DISABLE_EPOCH: "" + secrets: + - postgres_user + - source: dbsync_database + target: postgres_db + - postgres_password + volumes: + - dbsync_data:/var/lib/cexplorer + - node_ipc:/node-ipc + logging: + driver: "json-file" + options: + max-size: "10M" + max-file: "10" + deploy: + labels: + "co_elastic_logs/enable": "false" + placement: + constraints: + - node.labels.blockchain== true + restart_policy: + condition: on-failure + delay: 15s + kuber: + image: dquadrant/kuber:4c3c5230db9a9b8ac84487fbc11ccd28b0cd5917-amd64 + environment: + CARDANO_NODE_SOCKET_PATH: /ipc/node.socket + VIRTUAL_HOST: https://kuber-${BASE_DOMAIN} + NETWORK: 4 + START_ERA: CONWAY + volumes: + - node_ipc:/ipc/ + networks: + - cardano + - frontend + deploy: + placement: + constraints: + - node.labels.blockchain== true + restart_policy: + delay: "30s" diff --git a/tests/test-infrastructure/docker-compose-govaction-loader.yml b/tests/test-infrastructure/docker-compose-govaction-loader.yml new file mode 100644 index 000000000..269bc4202 --- /dev/null +++ b/tests/test-infrastructure/docker-compose-govaction-loader.yml @@ -0,0 +1,55 @@ +version: "3.9" + +networks: + frontend: + external: true + cardano: + external: true + +services: + + frontend: + image: govtool/gov-action-loader-frontend:${GOVTOOL_TAG} + build: + context: ../../gov-action-loader/frontend + dockerfile: Dockerfile + environment: + VIRTUAL_HOST: https://governance-${BASE_DOMAIN} + networks: + - frontend + deploy: + placement: + constraints: + - node.labels.gov-action-loader == true + restart_policy: + delay: "30s" + resources: + limits: + memory: 500M + reservations: + memory: 100M + + backend: + image: govtool/gov-action-loader-backend:${GOVTOOL_TAG} + build: + context: ../../gov-action-loader/backend + dockerfile: Dockerfile + environment: + KUBER_API_URL: "http://kuber:8081" + KUBER_API_KEY: "" + VIRTUAL_HOST: https://governance-${BASE_DOMAIN}/api/ -> /api/ + networks: + - default + - frontend + - cardano + deploy: + placement: + constraints: + - node.labels.gov-action-loader == true + restart_policy: + delay: "30s" + resources: + limits: + memory: 1G + reservations: + memory: 500M \ No newline at end of file diff --git a/tests/test-infrastructure/docker-compose-govtool.yml b/tests/test-infrastructure/docker-compose-govtool.yml new file mode 100644 index 000000000..b31f5b9ae --- /dev/null +++ b/tests/test-infrastructure/docker-compose-govtool.yml @@ -0,0 +1,67 @@ +version: "3.9" +networks: + frontend: + external: true + postgres: + external: true +configs: + config.json: + name: govtool_backend_config.json + external: true +services: + backend: + image: govtool/backend:${GOVTOOL_TAG} + build: + context: ../../govtool/backend + args: + BASE_IMAGE_REPO: govtool/backend-base + entrypoint: + - sh + - -c + - vva-be -c /config.json start-app + environment: + VIRTUAL_HOST: https://${BASE_DOMAIN}/api/ -> :8080/ + VIRTUAL_HOST_2: https://${BASE_DOMAIN}/swagger -> :8080/swagger + + networks: + - frontend + - postgres + configs: + - config.json + deploy: + restart_policy: + delay: "30s" + placement: + constraints: + - node.labels.govtool==true + frontend: + image: govtool/frontend:${GOVTOOL_TAG} + build: + context: ../../govtool/frontend + args: + VITE_BASE_URL: "/api" + environment: + VIRTUAL_HOST: https://${BASE_DOMAIN} + networks: + - frontend + deploy: + restart_policy: + delay: "30s" + placement: + constraints: + - node.labels.govtool==true + metadata-validation: + image: govtool/metadata-validation:${GOVTOOL_TAG} + build: + context: ../../govtool/metadata-validation + environment: + VIRTUAL_HOST: https://${BASE_DOMAIN}/metadata-validation/ -> :3000 + PORT: '3000' + networks: + - frontend + deploy: + restart_policy: + delay: "30s" + placement: + constraints: + - node.labels.govtool==true diff --git a/tests/test-infrastructure/docker-compose-test.yml b/tests/test-infrastructure/docker-compose-test.yml new file mode 100644 index 000000000..6b03e358d --- /dev/null +++ b/tests/test-infrastructure/docker-compose-test.yml @@ -0,0 +1,56 @@ +version: "3.9" +secrets: + lighthouserc.json: + external: true + name: ${PROJECT_NAME}_lighthouserc.json + +volumes: + lhci_data: + metadata_data: +networks: + postgres: + external: true + frontend: + external: true + +services: + lhci-server: + image: patrickhulce/lhci-server:0.12.0 + environment: + VIRTUAL_HOST: https://lighthouse-${BASE_DOMAIN} -> :9001 + volumes: + - lhci_data:/data + secrets: + - source: lighthouserc.json + target: /usr/src/lhci/lighthouserc.json + networks: + - postgres + - frontend + deploy: + placement: + constraints: + - node.labels.govtool-test-stack == true + restart_policy: + delay: "30s" + resources: + limits: + memory: 1G + reservations: + memory: 300M + + metadata-api: + image: govtool/metadata-api:${GOVTOOL_TAG} + build: + context: ../test-metadata-api + environment: + VIRTUAL_HOST: https://metadata-${BASE_DOMAIN} -> :3000 + networks: + - frontend + volumes: + - metadata_data:/data + deploy: + restart_policy: + delay: "30s" + placement: + constraints: + - node.labels.govtool-test-stack==true \ No newline at end of file diff --git a/tests/test-infrastructure/docker-compose.yml b/tests/test-infrastructure/docker-compose.yml deleted file mode 100644 index 9e8f77da5..000000000 --- a/tests/test-infrastructure/docker-compose.yml +++ /dev/null @@ -1,246 +0,0 @@ -version: "3.9" -secrets: - postgres_user: - external: true - name: ${STACK_NAME}_postgres_user - postgres_password: - external: true - name: ${STACK_NAME}_postgres_password - lighthouserc.json: - external: true - name: ${STACK_NAME}_lighthouserc.json - metrics_api_secret_token: - external: true - name: ${STACK_NAME}_metrics_api_secret - -## secrets syntax for docker compose stack -# secrets: -# postgres_user: -# file: "./secrets/${STACK_NAME}_postgres_user" -# postgres_password: -# file: "./secrets/${STACK_NAME}_postgres_password" -# postgres_db: -# file: "./secrets/${STACK_NAME}_postgres_user" -# lighthouserc.json: -# file: "./secrets/${STACK_NAME}_lighthouserc.json" -# metrics_api_secret_token: -# file: "./secrets/${STACK_NAME}_metrics_api_secret" -volumes: - lhci_data: - sonar_data: - sonar_logs: - node_data: - node_ipc: - -networks: - postgres: - external: true - frontend: - external: true - -services: - metabase: - image: metabase/metabase:v0.46.6.2 - hostname: metabase - volumes: - - /dev/urandom:/dev/random:ro - environment: - VIRTUAL_HOST: https://metabase.${BASE_DOMAIN} - MB_DB_TYPE: postgres - MB_DB_DBNAME: ${STACK_NAME}_metabase - MB_DB_PORT: 5432 - MB_DB_USER_FILE: /run/secrets/postgres_user - MB_DB_PASS_FILE: /run/secrets/postgres_password - MB_DB_HOST: postgres - networks: - - postgres - - frontend - secrets: - - postgres_password - - postgres_user - deploy: - placement: - constraints: - - node.labels.govtool-test-stack == true - restart_policy: - delay: "30s" - resources: - limits: - memory: 3G - reservations: - memory: 1.8G - - healthcheck: - test: curl --fail -I http://localhost:3000/api/health || exit 1 - interval: 15s - timeout: 5s - retries: 5 - - metrics_api: - image: voltaire-era/govtool-metrics-api - build: - context: ../test-metrics-api - - environment: - VIRTUAL_HOST: https://metrics.${BASE_DOMAIN}/ -> :3000/ - PGHOST: postgres - PGDATABASE: ${STACK_NAME}_metrics - secrets: - - source: postgres_password - target: /run/secrets/pgpassword - - source: postgres_user - target: /run/secrets/pguser - - source: metrics_api_secret_token - target: /run/secrets/api_secret_token - networks: - - postgres - - frontend - deploy: - placement: - constraints: - - node.labels.govtool-test-stack == true - restart_policy: - delay: "30s" - resources: - limits: - memory: 600M - reservations: - memory: 100M - - lhci-server: - image: patrickhulce/lhci-server:0.12.0 - environment: - VIRTUAL_HOST: https://lighthouse.${BASE_DOMAIN} -> :9001 - volumes: - - lhci_data:/data - secrets: - - source: lighthouserc.json - target: /usr/src/lhci/lighthouserc.json - networks: - - postgres - - frontend - deploy: - placement: - constraints: - - node.labels.govtool-test-stack == true - restart_policy: - delay: "30s" - resources: - limits: - memory: 1G - reservations: - memory: 300M - - governance-action-loader-ui: - image: voltaire-era/govtool-governance-action-loader - build: - context: ../../src/gov-action-loader-fe - dockerfile: Dockerfile - environment: - VIRTUAL_HOST: https://govtool-governance.${BASE_DOMAIN} - networks: - - frontend - deploy: - placement: - constraints: - - node.labels.govtool-test-stack == true - restart_policy: - delay: "30s" - resources: - limits: - memory: 500M - reservations: - memory: 100M - - governance-action-loader-api: - image: voltaire-era/govtool-kuber-proposal-loader-proxy - build: - context: ../../src/gov-action-loader-be - dockerfile: Dockerfile - environment: - KUBER_API_URL: "http://kuber:8081" - KUBER_API_KEY: "" - BLOCKFROST_API_URL: "${BLOCKFROST_API_URL}" - BLOCKFROST_PROJECT_ID: "${BLOCKFROST_PROJECT_ID}" - VIRTUAL_HOST: https://govtool-governance.${BASE_DOMAIN}/api/ -> /api/ - networks: - - default - - frontend - deploy: - placement: - constraints: - - node.labels.govtool-test-stack == true - restart_policy: - delay: "30s" - resources: - limits: - memory: 1G - reservations: - memory: 500M - - sonarqube_server: - image: mc1arke/sonarqube-with-community-branch-plugin:9.9-community - networks: - - frontend - - postgres - environment: - SONAR_JDBC_URL: jdbc:postgresql://postgres:5432/${STACK_NAME}_sonarqube - VIRTUAL_HOST: https+wss://sonarqube.${BASE_DOMAIN} -> :9000 - SONAR_JDBC_USERNAME: postgres - volumes: - - sonar_data:/opt/sonarqube/data - - sonar_logs:/opt/sonarqube/logs - entrypoint: "sh -c 'SONAR_JDBC_PASSWORD=\"$$( cat /run/secrets/postgres_password )\" /opt/sonarqube/docker/entrypoint.sh'" - secrets: - - postgres_password - deploy: - placement: - constraints: - - node.labels.govtool-test-stack == true - restart_policy: - delay: 15s - resources: - limits: - memory: 3.5G - reservations: - memory: 2.2G - cardano-node: - image: ghcr.io/intersectmbo/cardano-node:8.7.1-pre - environment: - NETWORK: sanchonet - volumes: - - node_data:/data - - node_ipc:/ipc - stop_grace_period: 1m - logging: - driver: "json-file" - options: - max-size: "200k" - max-file: "10" - ports: - - target: 3001 - published: 30004 - protocol: tcp - mode: host - deploy: - placement: - constraints: - - node.labels.govtool-test-stack == true - restart_policy: - condition: on-failure - delay: 15s - kuber: - image: dquadrant/kuber - environment: - CARDANO_NODE_SOCKET_PATH: /ipc/node.socket - VIRTUAL_HOST: https://kuber.${BASE_DOMAIN} - NETWORK: 4 - START_ERA: CONWAY - volumes: - - node_ipc:/ipc/ - deploy: - placement: - constraints: - - node.labels.govtool-test-stack == true - restart_policy: - delay: "30s" diff --git a/tests/test-infrastructure/gen-configs.sh b/tests/test-infrastructure/gen-configs.sh index 19acbcc75..7d8d83e65 100755 --- a/tests/test-infrastructure/gen-configs.sh +++ b/tests/test-infrastructure/gen-configs.sh @@ -2,55 +2,53 @@ ####### Script for generating docker secret files and configs. ####### If the docker is in swarm mode, it will also generate the docker swarm secrets. ####### +set -e if ! [ -f ./.env ] then echo ".env file is missing" exit 1 fi + set -a . ./.env set +a + # Function to generate a random secret in base64 format without padding and '+' function generate_secret() { - openssl rand -base64 16 | tr -d '=+/' + local filename=$2 + local var_name=$1 + if [ -s "$filename" ]; then + export "$var_name"=$(<"$filename") + else + local secret=$(openssl rand -base64 16 | tr -d '=+/') + echo -n "$secret" > "$filename" + export "$var_name"="$secret" + fi } -# Generate random secrets -export POSTGRES_USER=postgres -export POSTGRES_PASSWORD=$(generate_secret) -metrics_api_secret=$(generate_secret) - - if [ "$1" == "clean" ]; then - set -x - rm -rf ./configs; - rm -rf ./secrets; - - set +x - docker info | grep 'Swarm: active' > /dev/null 2>/dev/null || exit 0 - for CONFIG_FILE in $(ls ./configs_template) + # Create secrets from files + for SECRET_FILE in $(ls ./secrets) do - echo -n "Removing Config : " - docker config rm "${STACK_NAME}_${CONFIG_FILE}" || true + SECRET_NAME="$(basename $SECRET_FILE)" + echo -n "Removing secret: ${PROJECT_NAME}_${SECRET_NAME}" + docker secret rm "${PROJECT_NAME}_${SECRET_NAME}" || true done - for SECRET_FILE in "$(ls ./secrets_template)" "postgres_user" "postgres_password" "metrics_api_secret" + # Create configs from files + for CONFIG_FILE in $(ls ./configs) do - echo -n "Removing Secret : " - docker secret rm "${STACK_NAME}_${SECRET_FILE}" ||true + CONFIG_NAME=$(basename $CONFIG_FILE) + echo -n "Removing config: ${PROJECT_NAME}_${CONFIG_NAME}" + docker config rm "${PROJECT_NAME}_${CONFIG_NAME}" || true done - exit 0 -fi -## Check if one fo the secrets already exists -if [[ -f ./secrets/govtool_postgres_user ]] -then - echo "File ./secrets/govtool_postgres_user already exists." - echo "Assuming that the secrets were already generated" - echo " Use:" - echo " > ./gen-configs.sh clean" - echo " To clean up the configs and secrets" + set -x + rm -rf ./configs; + rm -rf ./secrets; + + set +x; exit 0 fi @@ -59,53 +57,48 @@ mkdir -p ./configs; mkdir -p ./secrets; -## save secrets to secrets folder -echo -n $POSTGRES_USER > ./secrets/govtool_postgres_user -echo -n $POSTGRES_PASSWORD > ./secrets/govtool_postgres_password -echo -n $metrics_api_secret > ./secrets/govtool_metrics_api_secret +# Generate random secrets +export POSTGRES_USER=postgres +export DBSYNC_DATABASE="${PROJECT_NAME}_dbsync" +# Save secrets to files +echo -n $POSTGRES_USER > ./secrets/postgres_user +echo -n "$DBSYNC_DATABASE" > ./secrets/dbsync_database -## loop over templates and updaete them. +# generate or load the secret +generate_secret "POSTGRES_PASSWORD" "./secrets/postgres_password" +## loop over templates and update them. for CONFIG_FILE in $(ls ./configs_template) do - echo -n "Config ${STACK_NAME}_${CONFIG_FILE}: " - envsubst < "./configs_template/$CONFIG_FILE" > "./configs/${STACK_NAME}_${CONFIG_FILE}" + echo -n "Config ${PROJECT_NAME}_${CONFIG_FILE}: " + envsubst < "./configs_template/$CONFIG_FILE" > "./configs/${CONFIG_FILE}" done for SECRET_FILE in $(ls ./secrets_template) do - echo -n "Secret ${STACK_NAME}_${SECRET_FILE}: " - envsubst < "./secrets_template/$SECRET_FILE" > "./secrets/${STACK_NAME}_${SECRET_FILE}" + echo -n "Secret ${PROJECT_NAME}_${SECRET_FILE}: " + envsubst < "./secrets_template/$SECRET_FILE" > "./secrets/${SECRET_FILE}" done - - ################################################################################ ################ Create secret/config for swarm ############################### ################################################################################ docker info | grep 'Swarm: active' > /dev/null 2>/dev/null || exit 0 -echo "Creating Secret: ${STACK_NAME}_postgres_user" -echo "$POSTGRES_USER" | (docker secret create "${STACK_NAME}_postgres_user" - ) || true - -echo "Generating Secret: ${STACK_NAME}_postgres_password" -echo "$POSTGRES_PASSWORD" | (docker secret create "${STACK_NAME}_postgres_password" - ) || true - -echo "Generating Secret: ${STACK_NAME}_metrics_api_secret" -echo "$metrics_api_secret" | (docker secret create "${STACK_NAME}_metrics_api_secret" - )|| true - - - -for CONFIG_FILE in $(ls ./configs_template) -do - echo -n "Creating Config: ${STACK_NAME}_${CONFIG_FILE} " - cat "./configs/${STACK_NAME}_${CONFIG_FILE}" | docker config create "${STACK_NAME}_${CONFIG_FILE}" - || true +# Create secrets from files +ls ./secrets | while IFS= read -r SECRET_FILE; do + SECRET_NAME=$(basename "$SECRET_FILE") + echo -n "Secret: ${PROJECT_NAME}_${SECRET_NAME}: " + cat "./secrets/$SECRET_NAME" | (docker secret create "${PROJECT_NAME}_${SECRET_NAME}" -) || true done -for SECRET_FILE in $(ls ./secrets_template) + +# Create configs from files +for CONFIG_FILE in $(ls ./configs) do - echo -n "Creating Secret: ${STACK_NAME}_${SECRET_FILE} " - cat "./secrets/${STACK_NAME}_${SECRET_FILE}" | docker secret create "${STACK_NAME}_${SECRET_FILE}" - ||true -done + CONFIG_NAME=$(basename $CONFIG_FILE) + echo -n "Config: ${PROJECT_NAME}_${CONFIG_NAME}: " + cat "./configs/$CONFIG_NAME" | (docker config create "${PROJECT_NAME}_${CONFIG_NAME}" -) || true +done \ No newline at end of file diff --git a/tests/test-infrastructure/playbook.yml b/tests/test-infrastructure/playbook.yml new file mode 100644 index 000000000..37656787a --- /dev/null +++ b/tests/test-infrastructure/playbook.yml @@ -0,0 +1,22 @@ +--- +- name: Update deployed images + hosts: test_server + gather_facts: no + tasks: + - name: Checkout to GOVTOOL_TAG commit + ansible.builtin.git: + repo: https://github.com/intersectmbo/govtool + dest: /opt/govtool + version: "{{ lookup('env', 'GOVTOOL_TAG') }}" + force: yes + update: yes + clone: yes + become: yes + + - name: Execute build-and-deploy.sh + ansible.builtin.shell: "/opt/govtool/tests/test-infrastructure/build-and-deploy.sh update-images" + args: + chdir: "/opt/govtool/tests/test-infrastructure" + environment: + GOVTOOL_TAG: "{{ lookup('env', 'GOVTOOL_TAG') }}" + become: yes \ No newline at end of file diff --git a/tests/test-infrastructure/scripts/deploy-stack.sh b/tests/test-infrastructure/scripts/deploy-stack.sh new file mode 100755 index 000000000..38920f07e --- /dev/null +++ b/tests/test-infrastructure/scripts/deploy-stack.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +## Docker swarm doesn't read .env file. +## This script reads env file and variables +## and apply them to compose file and +## then execute `docker stack deploy` + +set -eo pipefail + +function load_env(){ + if [[ -f ./.env ]] + then + set -a + . ./.env + set +a + fi + check_env +} + + +function check_env(){ + + # Path to the .env.example file + EXAMPLE_FILE=".env.example" + + unset_keys=() + + # Read each line of the .env.example file + while IFS= read -r line || [ -n "$line" ]; do + # Skip empty lines + if [ -z "$line" ]; then + continue + fi + + line=$(echo "$line" | sed -e 's/^[[:space:]]*//') + + # Extract the key from each line + key=$(echo "$line" | cut -d'=' -f1) + + if [ -z "${!key}" ]; then + unset_keys+=("$key") + fi + done < "$EXAMPLE_FILE" + + # Print error message for unset keys + if [ ${#unset_keys[@]} -gt 0 ]; then + echo "The following keys are not set in the environment:" + for key in "${unset_keys[@]}"; do + echo "- $key" + done + echo " Exiting due to missing env variables" + exit 2 + fi +} +function deploy-stack(){ + echo "++ deploy-stack" "$@" + ## apply the environment to compose file + ## deploy the govtool test infrastructure stack + ## first argument is stack name and 2nd argument is the file name + STACK_NAME=$1 + COMPOSE_FILE=$2 + FILENAME=$(basename -- "$COMPOSE_FILE") + EXTENSION="${FILENAME##*.}" + FILENAME_WITHOUT_EXT="${FILENAME%.*}" + RENDERED_FILENAME="${FILENAME_WITHOUT_EXT}-rendered.${EXTENSION}" + envsubst < "$COMPOSE_FILE" > "$RENDERED_FILENAME" + docker stack deploy -c "$RENDERED_FILENAME" ${STACK_NAME} +} \ No newline at end of file diff --git a/tests/test-infrastructure/secrets_template/lighthouserc.json b/tests/test-infrastructure/secrets_template/lighthouserc.json index 65930f8fa..ee7be38d2 100644 --- a/tests/test-infrastructure/secrets_template/lighthouserc.json +++ b/tests/test-infrastructure/secrets_template/lighthouserc.json @@ -3,7 +3,7 @@ "server": { "storage": { "sqlDialect": "postgres", - "sqlConnectionUrl": "postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres/${STACK_NAME}_lighthouse?application_name=lighthouse-ci-server" + "sqlConnectionUrl": "postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres/${PROJECT_NAME}_lighthouse?application_name=lighthouse-ci-server" } } } diff --git a/tests/test-metadata-api/.dockerignore b/tests/test-metadata-api/.dockerignore new file mode 100644 index 000000000..7501e1983 --- /dev/null +++ b/tests/test-metadata-api/.dockerignore @@ -0,0 +1,3 @@ +json_files +Dockerfile +README.md \ No newline at end of file diff --git a/tests/test-metadata-api/.gitignore b/tests/test-metadata-api/.gitignore new file mode 100644 index 000000000..995da051a --- /dev/null +++ b/tests/test-metadata-api/.gitignore @@ -0,0 +1,2 @@ +json_files +node_modules \ No newline at end of file diff --git a/tests/test-metadata-api/Dockerfile b/tests/test-metadata-api/Dockerfile new file mode 100644 index 000000000..b30a1fd6b --- /dev/null +++ b/tests/test-metadata-api/Dockerfile @@ -0,0 +1,9 @@ +FROM node:18-alpine +WORKDIR /src +COPY package.json yarn.lock ./ +RUN yarn install +COPY . . +VOLUME /data +ENV DATA_DIR=/data +EXPOSE 3000 +CMD [ "yarn", "start"] \ No newline at end of file diff --git a/tests/test-metadata-api/README.md b/tests/test-metadata-api/README.md new file mode 100644 index 000000000..3a98a748b --- /dev/null +++ b/tests/test-metadata-api/README.md @@ -0,0 +1,47 @@ +Test metadata API +================= + +Simple service to host json metadata during testing. + +## Installation + +``` +git clone https://github.com/your/repository.git +yarn install +yarn start +``` +#### Swagger UI + +``` +http://localhost:3000/docs +``` + +## Metadata Endpoints + +### 1. Save File + +- **Endpoint:** `PUT /data/{filename}` +- **Description:** Saves data to a file with the specified filename. + +### 2. Get File + +- **Endpoint:** `GET /data/{filename}` +- **Description:** Retrieves the content of the file with the specified filename. + +### 3. Delete File + +- **Endpoint:** `DELETE /data/{filename}` +- **Description:** Deletes the file with the specified filename. + +## Locks Endpoint +### 1. Acquire Lock +- **Endpoint:** `POST /lock/{key}?expiry={expiry_secs}` +- **Description:** Acquire a lock for the specified key for given time. By default the lock is set for 180 secs. +- **Responses:** + - `200 OK`: Lock acquired successfully. + - `423 Locked`: Lock not available. + +### 2. Release Lock + +- **Endpoint:** `POST/unlock/{key}` +- **Description:** Release a lock for the specified key. diff --git a/tests/test-metadata-api/index.js b/tests/test-metadata-api/index.js new file mode 100644 index 000000000..b689ac0f1 --- /dev/null +++ b/tests/test-metadata-api/index.js @@ -0,0 +1,151 @@ +const express = require('express'); +const fs = require('fs'); +const path = require('path'); +const lock_api = require('./locks_api') + +const swaggerUi = require('swagger-ui-express'); +const swaggerJsdoc = require('swagger-jsdoc'); +const app = express(); + + +const dataDir = process.env.DATA_DIR || path.join(__dirname, 'json_files'); + +if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir, { recursive: true }); +} +// Middleware to parse text request bodies +app.use(express.text()); + +// Swagger configuration +const swaggerOptions = { + definition: { + openapi: '3.0.0', + info: { + title: 'File API', + version: '1.0.0', + description: 'API for saving and deleting files', + }, + }, + apis: ['index.js','locks_api.js'], // Update the path to reflect the compiled JavaScript file +}; + +const swaggerSpec = swaggerJsdoc(swaggerOptions); + +// Serve Swagger UI +app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); + +// PUT endpoint to save a file +/** + * @swagger + * /data/{filename}: + * put: + * summary: Save data to a file + * tags: [Metadata File] + * parameters: + * - in: path + * name: filename + * schema: + * type: string + * required: true + * description: The name of the file to save + * requestBody: + * required: true + * content: + * text/plain: + * schema: + * type: string + * responses: + * '201': + * description: File saved successfully + */ +app.put('/data/:filename', (req, res) => { + const filename = req.params.filename; + const filePath = path.join(dataDir, filename); + + fs.writeFile(filePath, req.body, (err) => { + if (err) { + console.error(err); + return res.status(500).send('Failed to save file'); + } + res.status(201).send({'success': true}); + }); +}); + + +// GET endpoint to retrieve a file +/** + * @swagger + * /data/{filename}: + * get: + * summary: Get a file + * tags: [Metadata File] + * parameters: + * - in: path + * name: filename + * schema: + * type: string + * required: true + * description: The name of the file to retrieve + * responses: + * '200': + * description: File retrieved successfully + * content: + * text/plain: + * schema: + * type: string + */ +app.get('/data/:filename', (req, res) => { + const filename = req.params.filename; + const filePath = path.join(dataDir, filename); + + fs.readFile(filePath, 'utf8', (err, data) => { + if (err) { + console.error(err); + return res.status(404).send({'message': 'File not found'}); + } + res.status(200).send(data); + }); +}); + + + +// DELETE endpoint to delete a file +/** + * @swagger + * /data/{filename}: + * delete: + * summary: Delete a file + * tags: [Metadata File] + * parameters: + * - in: path + * name: filename + * schema: + * type: string + * required: true + * description: The name of the file to delete + * responses: + * '200': + * description: File deleted successfully + */ +app.delete('/data/:filename', (req, res) => { + const filename = req.params.filename; + const filePath = path.join(dataDir, filename); + + fs.unlink(filePath, (err) => { + if (err) { + console.error(err); + return res.status(500).send({'message':'Failed to delete file'}); + } + res.send('File deleted successfully'); + }); +}); + +app.get('/', (req, res) => { + res.redirect('/docs'); +}); +lock_api.setup(app) +// Start the server +const PORT = process.env.PORT || 3000; +app.listen(PORT, () => { + console.log(`Server is running on port ${PORT}`); +}); diff --git a/tests/test-metadata-api/locks_api.js b/tests/test-metadata-api/locks_api.js new file mode 100644 index 000000000..5196a70ea --- /dev/null +++ b/tests/test-metadata-api/locks_api.js @@ -0,0 +1,109 @@ +const { v4: uuidv4 } = require('uuid'); +const lock = {}; + +function acquireLock(key, expiry_secs = 180) { + const now = Date.now(); + if (!lock[key] || lock[key].expiry < now) { + const uuid = uuidv4(); + lock[key] = { + locked: true, + expiry: now + expiry_secs * 1000, + uuid: uuid, + }; + return uuid + } +} +function releaseLock(key,uuid) { + if(uuid){ + _lock=lock[key] + if(_lock && (_lock.uuid != uuid)){ + // if the uuid doesn't match, the lock should + // have expired and obtained by process. + return; + } + } + delete lock[key]; +} + + +function setup(app) { + /** + * @swagger + * /lock/{key}: + * post: + * summary: Acquire lock + * tags: [Locks] + * parameters: + * - in: path + * name: key + * schema: + * type: string + * required: true + * description: The key of the lock to acquire + * - in: query + * name: expiry_secs + * schema: + * type: integer + * minimum: 1 + * default: 180 + * description: The expiration time of the lock in seconds (default is 180s) + * responses: + * '200': + * description: Lock acquired successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * uuid: + * type: string + * description: The UUID of the acquired lock + * '423': + * description: Lock not available + */ + app.post('/lock/:key', (req, res) => { + const key = req.params.key; + const expiry_secs = req.query.expiry_secs ? parseInt(req.query.expiry_secs) : 180; + const lock_uuid=acquireLock(key, expiry_secs) + if(lock_uuid){ + res.json({ uuid: lock_uuid }) + }else{ + res.status(423).json({ status: 423, message: 'Lock not available' }); + + } + }); + + /** + * @swagger + * /unlock/{key}: + * post: + * summary: Release lock + * tags: [Locks] + * parameters: + * - in: path + * name: key + * schema: + * type: string + * required: true + * description: The key of the lock to release + * - in: query + * name: uuid + * schema: + * type: string + * required: false + * description: The UUID of the lock to release + * responses: + * '200': + * description: Lock released successfully + */ + app.post('/unlock/:key', (req, res) => { + const key = req.params.key; + const uuid = req.query.uuid; + + releaseLock(key, uuid); + res.send('Lock released.'); + + }); +} + +module.exports.setup = setup; diff --git a/tests/test-metadata-api/package.json b/tests/test-metadata-api/package.json new file mode 100644 index 000000000..520a75019 --- /dev/null +++ b/tests/test-metadata-api/package.json @@ -0,0 +1,15 @@ +{ + "name": "test-metadata-api", + "version": "0.0.1", + "main": "index.js", + "license": "MIT", + "scripts": { + "start": "node index.js" + }, + "dependencies": { + "express": "^4.19.2", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.0", + "uuid": "^9.0.1" + } +} diff --git a/tests/test-metadata-api/yarn.lock b/tests/test-metadata-api/yarn.lock new file mode 100644 index 000000000..cf7a4ed22 --- /dev/null +++ b/tests/test-metadata-api/yarn.lock @@ -0,0 +1,683 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@apidevtools/json-schema-ref-parser@^9.0.6": + version "9.1.2" + resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz#8ff5386b365d4c9faa7c8b566ff16a46a577d9b8" + integrity sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg== + dependencies: + "@jsdevtools/ono" "^7.1.3" + "@types/json-schema" "^7.0.6" + call-me-maybe "^1.0.1" + js-yaml "^4.1.0" + +"@apidevtools/openapi-schemas@^2.0.4": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz#9fa08017fb59d80538812f03fc7cac5992caaa17" + integrity sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ== + +"@apidevtools/swagger-methods@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz#b789a362e055b0340d04712eafe7027ddc1ac267" + integrity sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg== + +"@apidevtools/swagger-parser@10.0.3": + version "10.0.3" + resolved "https://registry.yarnpkg.com/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz#32057ae99487872c4dd96b314a1ab4b95d89eaf5" + integrity sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g== + dependencies: + "@apidevtools/json-schema-ref-parser" "^9.0.6" + "@apidevtools/openapi-schemas" "^2.0.4" + "@apidevtools/swagger-methods" "^3.0.2" + "@jsdevtools/ono" "^7.1.3" + call-me-maybe "^1.0.1" + z-schema "^5.0.1" + +"@jsdevtools/ono@^7.1.3": + version "7.1.3" + resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" + integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== + +"@types/json-schema@^7.0.6": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +body-parser@1.20.2: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== + 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.11.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +call-me-maybe@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.2.tgz#03f964f19522ba643b1b0693acb9152fe2074baa" + integrity sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ== + +commander@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.0.tgz#b990bfb8ac030aedc6d11bc04d1488ffef56db75" + integrity sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q== + +commander@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +doctrine@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +express@^4.19.2: + version "4.19.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" + integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.2" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.6.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +glob@7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== + +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== + +lodash.mergewith@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" + integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +minimatch@^3.0.4: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +object-inspect@^1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" + integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +safe-buffer@5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + 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" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +side-channel@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +swagger-jsdoc@^6.2.8: + version "6.2.8" + resolved "https://registry.yarnpkg.com/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz#6d33d9fb07ff4a7c1564379c52c08989ec7d0256" + integrity sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ== + dependencies: + commander "6.2.0" + doctrine "3.0.0" + glob "7.1.6" + lodash.mergewith "^4.6.2" + swagger-parser "^10.0.3" + yaml "2.0.0-1" + +swagger-parser@^10.0.3: + version "10.0.3" + resolved "https://registry.yarnpkg.com/swagger-parser/-/swagger-parser-10.0.3.tgz#04cb01c18c3ac192b41161c77f81e79309135d03" + integrity sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg== + dependencies: + "@apidevtools/swagger-parser" "10.0.3" + +swagger-ui-dist@>=5.0.0: + version "5.17.2" + resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-5.17.2.tgz#de31813b18ff34e9a428cd6b9ede521164621996" + integrity sha512-V/NqUw6QoTrjSpctp2oLQvxrl3vW29UsUtZyq7B1CF0v870KOFbYGDQw8rpKaKm0JxTwHpWnW1SN9YuKZdiCyw== + +swagger-ui-express@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/swagger-ui-express/-/swagger-ui-express-5.0.0.tgz#7a00a18dd909574cb0d628574a299b9ba53d4d49" + integrity sha512-tsU9tODVvhyfkNSvf03E6FAk+z+5cU3lXAzMy6Pv4av2Gt2xA0++fogwC4qo19XuFf6hdxevPuVCSKFuMHJhFA== + dependencies: + swagger-ui-dist ">=5.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== + +validator@^13.7.0: + version "13.11.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.11.0.tgz#23ab3fd59290c61248364eabf4067f04955fbb1b" + integrity sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ== + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +yaml@2.0.0-1: + version "2.0.0-1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.0.0-1.tgz#8c3029b3ee2028306d5bcf396980623115ff8d18" + integrity sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ== + +z-schema@^5.0.1: + version "5.0.6" + resolved "https://registry.yarnpkg.com/z-schema/-/z-schema-5.0.6.tgz#46d6a687b15e4a4369e18d6cb1c7b8618fc256c5" + integrity sha512-+XR1GhnWklYdfr8YaZv/iu+vY+ux7V5DS5zH1DQf6bO5ufrt/5cgNhVO5qyhsjFXvsqQb/f08DWE9b6uPscyAg== + dependencies: + lodash.get "^4.4.2" + lodash.isequal "^4.5.0" + validator "^13.7.0" + optionalDependencies: + commander "^10.0.0"