diff --git a/.github/TEMPLATES/secret-mapping-opencrvs-deps.yml b/.github/TEMPLATES/secret-mapping-opencrvs-deps.yml index 28c33dba..16abd61c 100644 --- a/.github/TEMPLATES/secret-mapping-opencrvs-deps.yml +++ b/.github/TEMPLATES/secret-mapping-opencrvs-deps.yml @@ -1,11 +1,3 @@ - -backup-encryption-secret: - type: Opaque - data: - # This is the password that is used to encrypt all the backups that OpenCRVS creates from - # a production server and that are stored on the backup server. Use this passphrase to decrypt the backups. - - BACKUP_ENCRYPTION_PASSPHRASE:backup_encryption_key - elasticsearch-admin-user: type: Opaque data: @@ -47,3 +39,27 @@ traefik-cert: data: - TRAEFIK_CERT: cert - TRAEFIK_KEY: key + +# If backup is configured then workflow will use GitHub secrets for current environment +# If restore is configured then workflow will fetch secrets from source environment (usually production) +backup-server-ssh-credentials: + type: Opaque + data: + - BACKUP_SERVER_USER: user + - BACKUP_HOST_PRIVATE_KEY: ssh_key + - BACKUP_HOST: host + +backup-encryption-secret: + type: Opaque + data: + # This is the password that is used to encrypt all the backups that OpenCRVS creates from + # a production server and that are stored on the backup server. Use this passphrase to decrypt the backups. + - BACKUP_ENCRYPTION_PASSPHRASE: backup_encryption_key + + +# RESTORE_ENCRYPTION_PASSPHRASE is fetched from source environment (usually production) +# Check workflow: github-to-k8s-sync-env.yml +restore-encryption-secret: + type: Opaque + data: + - RESTORE_ENCRYPTION_PASSPHRASE: backup_encryption_key diff --git a/.github/workflows/deploy-dependencies.yml b/.github/workflows/deploy-dependencies.yml index 534383bc..9a2fa426 100644 --- a/.github/workflows/deploy-dependencies.yml +++ b/.github/workflows/deploy-dependencies.yml @@ -9,7 +9,9 @@ on: default: "dev" type: choice options: - - "" + - demo1 + - production + - staging jobs: approve: environment: ${{ inputs.environment }} diff --git a/.github/workflows/deploy-opencrvs.yml b/.github/workflows/deploy-opencrvs.yml index 9b502212..05a1b09a 100644 --- a/.github/workflows/deploy-opencrvs.yml +++ b/.github/workflows/deploy-opencrvs.yml @@ -25,7 +25,9 @@ on: default: "dev" type: choice options: - - "" + - demo1 + - production + - staging jobs: approve: diff --git a/.github/workflows/get-secret-from-environment.yml b/.github/workflows/get-secret-from-environment.yml new file mode 100644 index 00000000..7700e98e --- /dev/null +++ b/.github/workflows/get-secret-from-environment.yml @@ -0,0 +1,102 @@ +name: Reusable Fetch Secret Workflow +# TODO: Check if this workflow can be simplified +on: + workflow_call: + inputs: + secret_name: + required: true + type: string + env_name: + required: true + type: string + outputs: + secret_value: + description: 'Secret value, encrypted with the encryption key' + value: ${{ jobs.fetch-credentials.outputs.secret_value }} + environment_exists: + description: 'Whether the environment exists or not' + value: ${{ jobs.check-environment.outputs.environment_exists }} + secrets: + gh_token: + required: true + encryption_key: + required: true + # All secrets that are we want to allow access to need + # to be defined in this list + BACKUP_ENCRYPTION_PASSPHRASE: + required: false + BACKUP_HOST_PRIVATE_KEY: + required: false + BACKUP_HOST: + required: false + BACKUP_SERVER_USER: + required: false + +jobs: + check-environment: + name: Check if Environment Exists + runs-on: ubuntu-24.04 + outputs: + environment_exists: ${{ steps.check-env.outputs.exists }} + steps: + - name: Check if GITHUB_TOKEN is set + id: check-token + run: | + if [ -z "${{ secrets.gh_token }}" ]; then + echo "Environment secret GITHUB_TOKEN is not set. Make sure you add a correct Github API token before running this pipeline." + exit 1 + fi + + - name: Verify GitHub token validity + id: verify-token + run: | + RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer ${{ secrets.gh_token }}" \ + "https://api.github.com/user") + if [ "$RESPONSE" -ne 200 ]; then + echo "Invalid or expired GitHub token." + exit 1 + fi + echo "GitHub token is valid." + + - name: Check if environment exists + id: check-env + run: | + ENV_NAME="${{ inputs.env_name }}" + RESPONSE=$(curl -s -H "Authorization: Bearer ${{ secrets.gh_token }}" \ + "https://api.github.com/repos/${{ github.repository }}/environments/$ENV_NAME") + if echo "$RESPONSE" | grep -q '"name": "'$ENV_NAME'"'; then + echo "Environment $ENV_NAME exists." + echo "exists=true" >> $GITHUB_OUTPUT + else + echo "Environment $ENV_NAME does not exist." + echo "exists=false" >> $GITHUB_OUTPUT + fi + + fetch-credentials: + name: Fetch Secret + runs-on: ubuntu-24.04 + environment: ${{ inputs.env_name }} + needs: check-environment + # Without this Github actions will create the environment when it doesnt exist + if: needs.check-environment.outputs.environment_exists == 'true' + outputs: + secret_value: ${{ steps.fetch-credentials.outputs.secret_value }} + steps: + - name: Fetch the secret + id: fetch-credentials + env: + SECRET_NAME: ${{ inputs.secret_name }} + run: | + SECRET_VALUE="${{ secrets[env.SECRET_NAME] }}" + if [ -z "$SECRET_VALUE" ]; then + echo "Secret ${{ inputs.secret_name }} is empty. Usually this means you have not explicitly stated the secrets" + echo "in both the workflow file get-secrets-from-environment and in the file you are using the reusable workflow from." + echo "Please make sure you have added the secret to the workflow files and retry." + exit 1 + fi + echo -n "$SECRET_VALUE" | openssl enc -aes-256-cbc -pbkdf2 -salt -k "${{ secrets.encryption_key }}" -out encrypted_key.bin + ENCODED_ENCRYPTED_SECRET=$(base64 < encrypted_key.bin) + EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) + echo "secret_value<<$EOF" >> $GITHUB_OUTPUT + echo "$ENCODED_ENCRYPTED_SECRET" >> $GITHUB_OUTPUT + echo "$EOF" >> $GITHUB_OUTPUT diff --git a/.github/workflows/github-to-k8s-sync-env.yml b/.github/workflows/github-to-k8s-sync-env.yml index f829aecb..774d1164 100644 --- a/.github/workflows/github-to-k8s-sync-env.yml +++ b/.github/workflows/github-to-k8s-sync-env.yml @@ -7,10 +7,12 @@ on: environment: description: "Target environment" required: true - default: "development" + default: "staging" type: choice options: - - "" + - demo1 + - production + - staging namespace_template: description: "Secrets mapping template" default: "opencrvs" @@ -25,10 +27,84 @@ on: default: dev namespace_template: type: string - default: opencrvs + default: opencrvs jobs: + get-restore-env-name: + name: Get restore environment name + outputs: + restore_env_name: ${{ steps.set-restore-env.outputs.restore-env-name }} + runs-on: ubuntu-24.04 + environment: ${{ inputs.environment }} + steps: + - name: Set restore environment name + id: set-restore-env + run: | + if [ "${{ vars.RESTORE_ENVIRONMENT_NAME }}" != "false" ]; then + echo "restore-env-name=${{ vars.RESTORE_ENVIRONMENT_NAME }}" + echo "restore-env-name=${{ vars.RESTORE_ENVIRONMENT_NAME }}" >> $GITHUB_OUTPUT + fi + - name: "Env name: ${{ steps.set-restore-env.outputs.restore-env-name }}" + run: | + echo "Determined restore environment name: ${{ steps.set-restore-env.outputs.restore-env-name }}" + + get-restore-encryption-key: + needs: get-restore-env-name + if: needs.get-restore-env-name.outputs.restore_env_name + name: Get backup encryption key + uses: ./.github/workflows/get-secret-from-environment.yml + with: + secret_name: 'BACKUP_ENCRYPTION_PASSPHRASE' + env_name: ${{ needs.get-restore-env-name.outputs.restore_env_name }} + secrets: + gh_token: ${{ secrets.GH_TOKEN }} + encryption_key: ${{ secrets.GH_ENCRYPTION_PASSWORD }} + BACKUP_ENCRYPTION_PASSPHRASE: ${{ secrets.BACKUP_ENCRYPTION_PASSPHRASE }} + get-restore-ssh-key: + needs: get-restore-env-name + if: needs.get-restore-env-name.outputs.restore_env_name + name: Get backup ssh key + uses: ./.github/workflows/get-secret-from-environment.yml + with: + secret_name: 'BACKUP_HOST_PRIVATE_KEY' + env_name: ${{ needs.get-restore-env-name.outputs.restore_env_name }} + secrets: + gh_token: ${{ secrets.GH_TOKEN }} + encryption_key: ${{ secrets.GH_ENCRYPTION_PASSWORD }} + BACKUP_HOST_PRIVATE_KEY: ${{ secrets.BACKUP_HOST_PRIVATE_KEY }} + get-restore-host: + needs: get-restore-env-name + if: needs.get-restore-env-name.outputs.restore_env_name + name: Get backup host + uses: ./.github/workflows/get-secret-from-environment.yml + with: + secret_name: 'BACKUP_HOST' + env_name: ${{ needs.get-restore-env-name.outputs.restore_env_name }} + secrets: + gh_token: ${{ secrets.GH_TOKEN }} + encryption_key: ${{ secrets.GH_ENCRYPTION_PASSWORD }} + BACKUP_HOST: ${{ secrets.BACKUP_HOST }} + get-restore-ssh-user: + needs: get-restore-env-name + if: needs.get-restore-env-name.outputs.restore_env_name + name: Get backup ssh user + uses: ./.github/workflows/get-secret-from-environment.yml + with: + secret_name: 'BACKUP_SERVER_USER' + env_name: ${{ needs.get-restore-env-name.outputs.restore_env_name }} + secrets: + gh_token: ${{ secrets.GH_TOKEN }} + encryption_key: ${{ secrets.GH_ENCRYPTION_PASSWORD }} + BACKUP_SERVER_USER: ${{ secrets.BACKUP_SERVER_USER }} sync-env: + if: always() + needs: + - get-restore-env-name + - get-restore-ssh-key + - get-restore-host + - get-restore-ssh-user + - get-restore-encryption-key + name: Sync GitHub env to Kubernetes environment: ${{ inputs.environment }} runs-on: - self-hosted @@ -40,7 +116,7 @@ jobs: # to store GitHub Secrets and Variables (base64 encoded) # Making those values base64 encoded allows us to avoid further complex escaping issues # with special characters and multiline values when generating Kubernetes Secret manifests - - name: Export all secrets and environment variables + - name: Export all secrets and environment variables from GitHub run: | ENV_FILE=$(mktemp) jq -n -r ' @@ -52,7 +128,31 @@ jobs: ' > $ENV_FILE echo env_file=$ENV_FILE >> $GITHUB_ENV - cat $ENV_FILE + cat $ENV_FILE | cut -d\= -f1 + - name: Save restore (backup source) environment secrets + if: needs.get-restore-env-name.outputs.restore_env_name + run: | + RESTORE_ENCRYPTION_PASSPHRASE=$(echo "${{ needs.get-restore-encryption-key.outputs.secret_value }}" | base64 --decode | \ + openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.GH_ENCRYPTION_PASSWORD }}" | base64) + echo "::add-mask::$RESTORE_ENCRYPTION_PASSPHRASE" + + BACKUP_HOST=$(echo "${{ needs.get-restore-host.outputs.secret_value }}" | base64 --decode | \ + openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.GH_ENCRYPTION_PASSWORD }}" | base64) + echo "::add-mask::$BACKUP_HOST" + + BACKUP_HOST_PRIVATE_KEY=$(echo "${{ needs.get-restore-ssh-key.outputs.secret_value }}" | base64 --decode | \ + openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.GH_ENCRYPTION_PASSWORD }}" | base64 | tr -d ' \n') + echo "::add-mask::$BACKUP_HOST_PRIVATE_KEY" + + BACKUP_SERVER_USER=$(echo "${{ needs.get-restore-ssh-user.outputs.secret_value }}" | base64 --decode | \ + openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.GH_ENCRYPTION_PASSWORD }}" | base64) + echo "::add-mask::$BACKUP_SERVER_USER" + + echo RESTORE_ENCRYPTION_PASSPHRASE=$RESTORE_ENCRYPTION_PASSPHRASE >> $env_file + echo BACKUP_HOST=$BACKUP_HOST >> $env_file + echo BACKUP_HOST_PRIVATE_KEY=$BACKUP_HOST_PRIVATE_KEY >> $env_file + echo BACKUP_SERVER_USER=$BACKUP_SERVER_USER >> $env_file + grep BACKUP_HOST_PRIVATE_KEY $env_file || echo "No restore encryption passphrase to add" - name: Preprocess mapping into Secret YAMLs run: | diff --git a/.github/workflows/k8s-reindex.yml b/.github/workflows/k8s-reindex.yml index cf4dfb9e..fb8c3f71 100644 --- a/.github/workflows/k8s-reindex.yml +++ b/.github/workflows/k8s-reindex.yml @@ -9,7 +9,9 @@ on: default: "dev" type: choice options: - - "" + - demo1 + - production + - staging workflow_call: inputs: environment: diff --git a/.github/workflows/k8s-reset-data.yml b/.github/workflows/k8s-reset-data.yml index bcb1bdd6..f1ed702c 100644 --- a/.github/workflows/k8s-reset-data.yml +++ b/.github/workflows/k8s-reset-data.yml @@ -9,7 +9,9 @@ on: default: "dev" type: choice options: - - "" + - demo1 + - production + - staging workflow_call: inputs: environment: diff --git a/.github/workflows/k8s-seed-data.yml b/.github/workflows/k8s-seed-data.yml index 7e8fe4d7..4e7ec716 100644 --- a/.github/workflows/k8s-seed-data.yml +++ b/.github/workflows/k8s-seed-data.yml @@ -9,7 +9,9 @@ on: default: "dev" type: choice options: - - "" + - demo1 + - production + - staging workflow_call: inputs: environment: diff --git a/.github/workflows/provision.yml b/.github/workflows/provision.yml index 4082b763..832a6136 100644 --- a/.github/workflows/provision.yml +++ b/.github/workflows/provision.yml @@ -9,7 +9,9 @@ on: default: 'dev' type: choice options: - - "" + - staging + - production + - demo1 tags: description: 'Tags to apply to the provisioned resources' required: true @@ -77,6 +79,8 @@ jobs: smtp_from: "team@opencrvs.org" smtp_password: ${{ secrets.SMTP_PASSWORD }} alert_email: ${{ secrets.ALERT_EMAIL }} + backup_host_public_key: ${{ secrets.BACKUP_HOST_PUBLIC_KEY }} + backup_server_user: ${{ secrets.BACKUP_SERVER_USER }} - name: checkout repository uses: actions/checkout@v6 - name: Run Ansible Playbook diff --git a/.github/workflows/publish-charts.yml b/.github/workflows/publish-charts.yml index 75c28ed7..c7dc543d 100644 --- a/.github/workflows/publish-charts.yml +++ b/.github/workflows/publish-charts.yml @@ -30,7 +30,7 @@ jobs: env: GITHUB_TOKEN_REGISTRY: ${{ secrets.PACKAGE_GITHUB_TOKEN }} run: | - echo $GITHUB_TOKEN_REGISTRY | helm registry login ghcr.io --username adskyiproger --password-stdin + echo $GITHUB_TOKEN_REGISTRY | helm registry login ghcr.io --username ocrvs-bot --password-stdin for package in $(ls -1 packages) do helm push packages/$package oci://ghcr.io/opencrvs diff --git a/.github/workflows/reset-2fa.yml b/.github/workflows/reset-2fa.yml index a67f0737..586a9ab8 100644 --- a/.github/workflows/reset-2fa.yml +++ b/.github/workflows/reset-2fa.yml @@ -13,7 +13,9 @@ on: default: required: true options: - - "" + - staging + - production + - demo1 jobs: approve: diff --git a/charts/opencrvs-services/Chart.yaml b/charts/opencrvs-services/Chart.yaml index 7fda9017..8296132a 100644 --- a/charts/opencrvs-services/Chart.yaml +++ b/charts/opencrvs-services/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: opencrvs-services description: OpenCRVS Services type: application -version: 0.1.27 +version: 0.1.26 appVersion: 1.9.5 diff --git a/charts/opencrvs-services/templates/postgres-on-update-analytics-job.yaml b/charts/opencrvs-services/templates/postgres-on-update-analytics-job.yaml index 01c86f53..551b4b22 100644 --- a/charts/opencrvs-services/templates/postgres-on-update-analytics-job.yaml +++ b/charts/opencrvs-services/templates/postgres-on-update-analytics-job.yaml @@ -28,9 +28,8 @@ spec: - sh - -c - > - [[ -d /assets/postgres/ ]] && cp -R /assets/postgres/* /data-assets/ || - cp -LR /scripts/* /data-assets/; - chmod +x /data-assets/*.sh || true + [[ -d /assets/postgres/ ]] && cp -Rv /assets/postgres/* /data-assets/ || + cp -Rv /scripts/* /data-assets/; volumeMounts: - name: assets mountPath: /data-assets @@ -39,7 +38,8 @@ spec: name: postgres-on-update-script containers: - name: postgres-on-update-analytics - command: ["bash", "-c", "/data-assets/setup-analytics.sh"] + # command: ["bash", "-c", "/data-assets/setup-analytics.sh"] + command: ["sleep", "3600"] image: postgres:17 env: - name: POSTGRES_HOST diff --git a/environments/demo1/dependencies/values.yaml b/environments/demo1/dependencies/values.yaml new file mode 100644 index 00000000..cffcaa7f --- /dev/null +++ b/environments/demo1/dependencies/values.yaml @@ -0,0 +1,25 @@ +storage_type: host_path + +environment_type: non-production + +ingress: + tls_resolver: letsencrypt + +minio: + use_default_credentials: false + +elasticsearch: + use_default_credentials: false + +mongodb: + use_default_credentials: false + +postgres: + use_default_credentials: false + +monitoring: + enabled: true + +elastalert: + env: + HTTP_POST2_ALERT_URL: http://countryconfig.opencrvs-demo2.svc.cluster.local:3040 diff --git a/environments/demo1/mosip-api/values.yaml b/environments/demo1/mosip-api/values.yaml new file mode 100644 index 00000000..442be8ee --- /dev/null +++ b/environments/demo1/mosip-api/values.yaml @@ -0,0 +1,2 @@ +ingress: + ssl_enabled: true \ No newline at end of file diff --git a/environments/demo1/opencrvs-services/values.yaml b/environments/demo1/opencrvs-services/values.yaml new file mode 100644 index 00000000..0ded4e9b --- /dev/null +++ b/environments/demo1/opencrvs-services/values.yaml @@ -0,0 +1,56 @@ +######################################################################################## +# Initial configuration file for OpenCRVS installation +######################################################################################## +# Some properties are not defined in this file and should be provided as key/value at +# installation time: +# - hostname: valid DNS name for opencrvs +# - countryconfig.image.name: Countryconfig image repository +# - countryconfig.image.tag: Countryconfig image tag +ingress: + tls_resolver: letsencrypt + +environment_type: non-production + +hpa: + enabled: false + +env: + APN_SERVICE_URL: "http://apm-server.opencrvs-deps-demo1.svc.cluster.local:8200" + +influxdb: + host: influxdb-0.influxdb.opencrvs-deps-demo1.svc.cluster.local +elasticsearch: + auth_mode: auto + host: elasticsearch.opencrvs-deps-demo1.svc.cluster.local + + +minio: + auth_mode: use_secret + host: minio-0.minio.opencrvs-deps-demo1.svc.cluster.local + external_hostname: minio.test-k8s.opencrvs.dev + +mongodb: + auth_mode: auto + host: mongodb-0.mongodb.opencrvs-deps-demo1.svc.cluster.local + +redis: + auth_mode: acl + host: redis-0.redis.opencrvs-deps-demo1.svc.cluster.local + +postgres: + auth_mode: auto + host: postgres-0.postgres.opencrvs-deps-demo1.svc.cluster.local + +imagePullSecrets: + # Default value for credentials created while yarn environment:init + - name: dockerhub-credentials + +countryconfig: + smtp-config: + - ALERT_EMAIL + - SENDER_EMAIL_ADDRESS + - SMTP_HOST + - SMTP_PASSWORD + - SMTP_PORT + - SMTP_SECURE + - SMTP_USERNAME diff --git a/environments/demo1/traefik/values.yaml b/environments/demo1/traefik/values.yaml new file mode 100644 index 00000000..38f45b4e --- /dev/null +++ b/environments/demo1/traefik/values.yaml @@ -0,0 +1,72 @@ +# Overwriting https://github.com/traefik/traefik-helm-chart/blob/master/traefik/values.yaml +namespaceOverride: "traefik" +logs: + general: + # "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL", "PANIC" + level: "INFO" + # format: "common" # For local environment + format: "json" # For server environment + access: + # -- To enable access logs + enabled: true + format: "json" + +ingressRoute: + dashboard: + enabled: false + +# Be explicit that we only use CRDs, not ingress/gw support +providers: + kubernetesCRD: + enabled: true + kubernetesIngress: + enabled: false + kubernetesGateway: + enabled: false + +service: + enabled: true + single: false + type: NodePort + +ports: + web: + port: 8000 + hostPort: 80 + protocol: TCP + nodePort: 30080 + websecure: + port: 8443 + nodePort: 30443 + hostPort: 443 + protocol: TCP + +certificatesResolvers: + letsencrypt: + acme: + tlsChallenge: false + httpChallenge: + entryPoint: web + email: vadym@opencrvs.org + # Storage for certificates: + storage: /certificates/acme.json + # NOTE: Sometimes Let's Encrypt hit production SSL certificate issuing limits + # If you are having issues, switch to staging + # Staging server + # caServer: https://acme-staging-v02.api.letsencrypt.org/directory + # Production server + caServer: https://acme-v02.api.letsencrypt.org/directory + +deployment: + hostNetwork: true + additionalVolumes: + - name: acme + hostPath: + path: /data/traefik + +additionalVolumeMounts: + - name: acme + mountPath: /certificates + +nodeSelector: + traefik-role: ingress diff --git a/environments/production/dependencies/values.yaml b/environments/production/dependencies/values.yaml new file mode 100644 index 00000000..8a5ccc4d --- /dev/null +++ b/environments/production/dependencies/values.yaml @@ -0,0 +1,31 @@ +storage_type: host_path + +environment_type: production + +minio: + use_default_credentials: false + +elasticsearch: + use_default_credentials: false + +mongodb: + use_default_credentials: false + +postgres: + use_default_credentials: false + +redis: + auth_mode: acl + +monitoring: + enabled: true + +elastalert: + env: + HTTP_POST2_ALERT_URL: http://countryconfig.opencrvs-production.svc.cluster.local:3040 + +# Backup configuration +backup: + enabled: true + schedule: "0 1 * * *" + backup_server_dir: /home/backup/production diff --git a/environments/production/mosip-api/values.yaml b/environments/production/mosip-api/values.yaml new file mode 100644 index 00000000..442be8ee --- /dev/null +++ b/environments/production/mosip-api/values.yaml @@ -0,0 +1,2 @@ +ingress: + ssl_enabled: true \ No newline at end of file diff --git a/environments/production/opencrvs-services/values.yaml b/environments/production/opencrvs-services/values.yaml new file mode 100644 index 00000000..e3d4959a --- /dev/null +++ b/environments/production/opencrvs-services/values.yaml @@ -0,0 +1,53 @@ +######################################################################################## +# Initial configuration file for OpenCRVS installation +######################################################################################## +# Some properties are not defined in this file and should be provided as key/value at +# installation time: +# - hostname: valid DNS name for opencrvs +# - countryconfig.image.name: Countryconfig image repository +# - countryconfig.image.tag: Countryconfig image tag +environment_type: production + +hpa: + enabled: false + +env: + APN_SERVICE_URL: "http://apm-server.opencrvs-deps-production.svc.cluster.local:8200" + QA_ENV: true +influxdb: + host: influxdb-0.influxdb.opencrvs-deps-production.svc.cluster.local +elasticsearch: + auth_mode: auto + host: elasticsearch.opencrvs-deps-production.svc.cluster.local + + +minio: + auth_mode: use_secret + host: minio-0.minio.opencrvs-deps-production.svc.cluster.local + +mongodb: + auth_mode: auto + host: mongodb-0.mongodb.opencrvs-deps-production.svc.cluster.local + +redis: + auth_mode: use_secret + host: redis-0.redis.opencrvs-deps-production.svc.cluster.local + +postgres: + auth_mode: auto + host: postgres-0.postgres.opencrvs-deps-production.svc.cluster.local + +imagePullSecrets: + # Default value for credentials created while yarn environment:init + - name: dockerhub-credentials + +countryconfig: + secrets: + smtp-config: + - ALERT_EMAIL + - SENDER_EMAIL_ADDRESS + - SMTP_HOST + - SMTP_PASSWORD + - SMTP_PORT + - SMTP_SECURE + - SMTP_USERNAME diff --git a/environments/production/traefik/values.yaml b/environments/production/traefik/values.yaml new file mode 100644 index 00000000..e076f9b3 --- /dev/null +++ b/environments/production/traefik/values.yaml @@ -0,0 +1,82 @@ +# Overwriting https://github.com/traefik/traefik-helm-chart/blob/master/traefik/values.yaml +namespaceOverride: "traefik" +logs: + general: + # "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL", "PANIC" + level: "INFO" + # format: "common" # For local environment + format: "json" # For server environment + access: + # -- To enable access logs + enabled: true + format: "json" + +ingressRoute: + dashboard: + enabled: false + +# Be explicit that we only use CRDs, not ingress/gw support +providers: + kubernetesCRD: + enabled: true + kubernetesIngress: + enabled: false + kubernetesGateway: + enabled: false + +service: + enabled: true + single: false + type: NodePort + +ports: + web: + port: 8000 + hostPort: 80 + protocol: TCP + nodePort: 30080 + redirections: + entryPoint: + to: websecure + scheme: https + permanent: true + + websecure: + port: 8443 + hostPort: 443 + protocol: TCP + nodePort: 30443 + # 👇 Adjust this section at websecure entrypoint + tls: + enabled: true + certResolver: letsencrypt + +# 👇 Adjust this section if needed +certificatesResolvers: + letsencrypt: + acme: + tlsChallenge: true + # 👇 Provide admin email address + email: vadym@opencrvs.org + # Storage for certificates: + storage: /certificates/acme.json + # NOTE: Sometimes Let's Encrypt hit production SSL certificate issuing limits + # If you are having issues, switch to staging + # Staging server + caServer: https://acme-staging-v02.api.letsencrypt.org/directory + # Production server + # caServer: https://acme-v02.api.letsencrypt.org/directory + +deployment: + hostNetwork: true + additionalVolumes: + - name: acme + hostPath: + path: /data/traefik + +additionalVolumeMounts: + - name: acme + mountPath: /certificates + +nodeSelector: + traefik-role: ingress diff --git a/environments/staging/dependencies/values.yaml b/environments/staging/dependencies/values.yaml new file mode 100644 index 00000000..77905417 --- /dev/null +++ b/environments/staging/dependencies/values.yaml @@ -0,0 +1,41 @@ +storage_type: host_path + +environment_type: undefined + +minio: + use_default_credentials: false + +elasticsearch: + use_default_credentials: false + +mongodb: + use_default_credentials: false + +postgres: + use_default_credentials: false + +redis: + auth_mode: acl + +monitoring: + enabled: true + +elastalert: + env: + HTTP_POST2_ALERT_URL: http://countryconfig.opencrvs-staging.svc.cluster.local:3040/email + +# Backup configuration +backup: + enabled: false + schedule: "0 1 * * *" + backup_server_secret: backup-server-ssh-credentials + backup_server_dir: /home/backup/staging + + +# Restore configuration +restore: + enabled: true + schedule: "0 0 * * *" + backup_server_secret: backup-server-ssh-credentials + backup_server_dir: /home/backup/production + backup_encryption_secret: restore-encryption-secret \ No newline at end of file diff --git a/environments/staging/mosip-api/values.yaml b/environments/staging/mosip-api/values.yaml new file mode 100644 index 00000000..442be8ee --- /dev/null +++ b/environments/staging/mosip-api/values.yaml @@ -0,0 +1,2 @@ +ingress: + ssl_enabled: true \ No newline at end of file diff --git a/environments/staging/opencrvs-services/values.yaml b/environments/staging/opencrvs-services/values.yaml new file mode 100644 index 00000000..b2737f12 --- /dev/null +++ b/environments/staging/opencrvs-services/values.yaml @@ -0,0 +1,56 @@ +######################################################################################## +# Initial configuration file for OpenCRVS installation +######################################################################################## +# Some properties are not defined in this file and should be provided as key/value at +# installation time: +# - hostname: valid DNS name for opencrvs +# - countryconfig.image.name: Countryconfig image repository +# - countryconfig.image.tag: Countryconfig image tag +environment_type: production + +hpa: + enabled: false + +env: + APN_SERVICE_URL: "http://apm-server.opencrvs-deps-staging.svc.cluster.local:8200" + QA_ENV: true +influxdb: + host: influxdb-0.influxdb.opencrvs-deps-staging.svc.cluster.local +elasticsearch: + auth_mode: auto + host: elasticsearch.opencrvs-deps-staging.svc.cluster.local + cronjob: + enabled: true + schedule: "0 3 * * *" + + +minio: + auth_mode: use_secret + host: minio-0.minio.opencrvs-deps-staging.svc.cluster.local + +mongodb: + auth_mode: auto + host: mongodb-0.mongodb.opencrvs-deps-staging.svc.cluster.local + +redis: + auth_mode: use_secret + host: redis-0.redis.opencrvs-deps-staging.svc.cluster.local + +postgres: + auth_mode: auto + host: postgres-0.postgres.opencrvs-deps-staging.svc.cluster.local + +imagePullSecrets: + # Default value for credentials created while yarn environment:init + - name: dockerhub-credentials + +countryconfig: + secrets: + smtp-config: + - ALERT_EMAIL + - SENDER_EMAIL_ADDRESS + - SMTP_HOST + - SMTP_PASSWORD + - SMTP_PORT + - SMTP_SECURE + - SMTP_USERNAME diff --git a/environments/staging/traefik/values.yaml b/environments/staging/traefik/values.yaml new file mode 100644 index 00000000..38f45b4e --- /dev/null +++ b/environments/staging/traefik/values.yaml @@ -0,0 +1,72 @@ +# Overwriting https://github.com/traefik/traefik-helm-chart/blob/master/traefik/values.yaml +namespaceOverride: "traefik" +logs: + general: + # "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL", "PANIC" + level: "INFO" + # format: "common" # For local environment + format: "json" # For server environment + access: + # -- To enable access logs + enabled: true + format: "json" + +ingressRoute: + dashboard: + enabled: false + +# Be explicit that we only use CRDs, not ingress/gw support +providers: + kubernetesCRD: + enabled: true + kubernetesIngress: + enabled: false + kubernetesGateway: + enabled: false + +service: + enabled: true + single: false + type: NodePort + +ports: + web: + port: 8000 + hostPort: 80 + protocol: TCP + nodePort: 30080 + websecure: + port: 8443 + nodePort: 30443 + hostPort: 443 + protocol: TCP + +certificatesResolvers: + letsencrypt: + acme: + tlsChallenge: false + httpChallenge: + entryPoint: web + email: vadym@opencrvs.org + # Storage for certificates: + storage: /certificates/acme.json + # NOTE: Sometimes Let's Encrypt hit production SSL certificate issuing limits + # If you are having issues, switch to staging + # Staging server + # caServer: https://acme-staging-v02.api.letsencrypt.org/directory + # Production server + caServer: https://acme-v02.api.letsencrypt.org/directory + +deployment: + hostNetwork: true + additionalVolumes: + - name: acme + hostPath: + path: /data/traefik + +additionalVolumeMounts: + - name: acme + mountPath: /certificates + +nodeSelector: + traefik-role: ingress diff --git a/infrastructure/environments/derived-variables.ts b/infrastructure/environments/derived-variables.ts new file mode 100644 index 00000000..881d2c8f --- /dev/null +++ b/infrastructure/environments/derived-variables.ts @@ -0,0 +1,156 @@ +export const derivedVariables = [ + { + name: 'ACTIVATE_USERS', + valueLabel: 'ACTIVATE_USERS', + valueType: 'VARIABLE', + type: 'disabled', + scope: 'ENVIRONMENT' + }, + { + name: 'AUTH_HOST', + valueLabel: 'AUTH_HOST', + valueType: 'VARIABLE', + type: 'disabled', + scope: 'ENVIRONMENT' + }, + { + name: 'COUNTRY_CONFIG_HOST', + valueLabel: 'COUNTRY_CONFIG_HOST', + valueType: 'VARIABLE', + type: 'disabled', + scope: 'ENVIRONMENT' + }, + { + name: 'GATEWAY_HOST', + valueLabel: 'GATEWAY_HOST', + valueType: 'VARIABLE', + type: 'disabled', + scope: 'ENVIRONMENT' + }, + { + name: 'CONTENT_SECURITY_POLICY_WILDCARD', + valueLabel: 'CONTENT_SECURITY_POLICY_WILDCARD', + valueType: 'VARIABLE', + type: 'disabled', + scope: 'ENVIRONMENT' + }, + { + name: 'CLIENT_APP_URL', + valueLabel: 'CLIENT_APP_URL', + valueType: 'VARIABLE', + type: 'disabled', + scope: 'ENVIRONMENT' + }, + { + name: 'LOGIN_URL', + valueLabel: 'LOGIN_URL', + valueType: 'VARIABLE', + type: 'disabled', + scope: 'ENVIRONMENT' + }, + { + name: 'ELASTICSEARCH_SUPERUSER_PASSWORD', + valueLabel: 'ELASTICSEARCH_SUPERUSER_PASSWORD', + valueType: 'SECRET', + type: 'disabled', + scope: 'ENVIRONMENT' + }, + { + name: 'KIBANA_SYSTEM_PASSWORD', + valueLabel: 'KIBANA_SYSTEM_PASSWORD', + valueType: 'SECRET', + type: 'disabled', + scope: 'ENVIRONMENT' + }, + { + name: 'MINIO_ROOT_USER', + valueLabel: 'MINIO_ROOT_USER', + valueType: 'SECRET', + type: 'disabled', + scope: 'ENVIRONMENT' + }, + { + name: 'MINIO_ROOT_PASSWORD', + valueLabel: 'MINIO_ROOT_PASSWORD', + valueType: 'SECRET', + type: 'disabled', + scope: 'ENVIRONMENT' + }, + { + name: 'MONGODB_ADMIN_USER', + valueLabel: 'MONGODB_ADMIN_USER', + valueType: 'SECRET', + type: 'disabled', + scope: 'ENVIRONMENT' + }, + { + name: 'MONGODB_ADMIN_PASSWORD', + valueLabel: 'MONGODB_ADMIN_PASSWORD', + valueType: 'SECRET', + type: 'disabled', + scope: 'ENVIRONMENT' + }, + { + name: 'POSTGRES_USER', + valueLabel: 'POSTGRES_USER', + valueType: 'SECRET', + type: 'disabled', + scope: 'ENVIRONMENT' + }, + { + name: 'POSTGRES_PASSWORD', + valueLabel: 'POSTGRES_PASSWORD', + valueType: 'SECRET', + type: 'disabled', + scope: 'ENVIRONMENT' + }, + { + name: 'SUPER_USER_PASSWORD', + valueLabel: 'SUPER_USER_PASSWORD', + valueType: 'SECRET', + type: 'disabled', + scope: 'ENVIRONMENT' + }, + { + name: 'ENCRYPTION_KEY', + valueLabel: 'ENCRYPTION_KEY', + valueType: 'SECRET', + type: 'disabled', + scope: 'ENVIRONMENT' + }, + { + name: 'GH_ENCRYPTION_PASSWORD', + valueLabel: 'GH_ENCRYPTION_PASSWORD', + valueType: 'SECRET', + type: 'disabled', + scope: 'REPOSITORY' + }, + { + name: 'BACKUP_ENCRYPTION_PASSPHRASE', + valueLabel: 'BACKUP_ENCRYPTION_PASSPHRASE', + valueType: 'SECRET', + type: 'disabled', + scope: 'ENVIRONMENT' + }, + { + name: 'BACKUP_HOST_PRIVATE_KEY', + valueLabel: 'BACKUP_HOST_PRIVATE_KEY', + valueType: 'SECRET', + type: 'disabled', + scope: 'ENVIRONMENT' + }, + { + name: 'BACKUP_HOST_PUBLIC_KEY', + valueLabel: 'BACKUP_HOST_PUBLIC_KEY', + valueType: 'SECRET', + type: 'disabled', + scope: 'ENVIRONMENT' + }, + { + name: 'RESTORE_ENVIRONMENT_NAME', + valueLabel: 'RESTORE_ENVIRONMENT_NAME', + valueType: 'VARIABLE', + type: 'disabled', + scope: 'ENVIRONMENT' + }, +] as const; \ No newline at end of file diff --git a/infrastructure/environments/github.ts b/infrastructure/environments/github.ts index 2b27921d..ce51f0e4 100644 --- a/infrastructure/environments/github.ts +++ b/infrastructure/environments/github.ts @@ -235,6 +235,24 @@ export async function createRepositorySecret( ) } +export async function getRepositoryEnvironments( + octokit: Octokit, + githubOrganisation: string, + repository: string +): Promise { + + const response = await octokit.request('GET /repos/{githubOrganisation}/{repository}/environments', { + githubOrganisation, + repository, + }); + + // Safe access with fallback to empty array if undefined + const environments = (response.data.environments ?? []).map( + (env: { name: string }) => env.name + ); + return environments; +} + export async function createEnvironment( octokit: Octokit, environment: string, diff --git a/infrastructure/environments/questions.ts b/infrastructure/environments/questions.ts new file mode 100644 index 00000000..15b2cd4b --- /dev/null +++ b/infrastructure/environments/questions.ts @@ -0,0 +1,417 @@ +import kleur from 'kleur' +import { generateLongPassword } from './utils' + + +const notEmpty = (value: string | number) => + value.toString().trim().length > 0 ? true : 'Please enter a value' + +export const environmentQuestions = [ + { + name: 'environment_type', + type: 'select' as const, + scope: 'ENVIRONMENT' as const, + message: 'Purpose for the environment?', + choices: [ + { title: 'Development/Quality assurance/Testing (no PII data)', value: 'non-production' }, + { title: 'Staging/Production (hosts PII data, requires frequent backups)', value: 'production' }, + ] + }, + { + name: 'environment', + type: 'text' as const, + message: 'What is the name of your environment?', + validate: notEmpty, + initial: process.env.ENV, + scope: 'REPOSITORY' as const + } +] +export const dockerhubQuestions = [ + { + name: 'dockerhubOrganisation', + type: 'text' as const, + message: 'What is the name of your Docker Hub organisation?', + valueType: 'SECRET' as const, + valueLabel: 'DOCKERHUB_ACCOUNT', + validate: notEmpty, + initial: process.env.DOCKER_ORGANISATION, + scope: 'REPOSITORY' as const + }, + { + name: 'dockerhubRepository', + type: 'text' as const, + message: 'What is the name of your private Docker Hub repository?', + valueType: 'SECRET' as const, + valueLabel: 'DOCKERHUB_REPO', + validate: notEmpty, + initial: process.env.DOCKER_REPO, + scope: 'REPOSITORY' as const + }, + { + name: 'dockerhubUsername', + type: 'text' as const, + message: + 'What is the Docker Hub username the the target server should be using?', + valueType: 'SECRET' as const, + valueLabel: 'DOCKER_USERNAME', + validate: notEmpty, + initial: process.env.DOCKER_USERNAME, + scope: 'REPOSITORY' as const + }, + { + name: 'dockerhubToken', + type: 'text' as const, + message: 'What is the token of this Docker Hub account?', + valueType: 'SECRET' as const, + valueLabel: 'DOCKER_TOKEN', + validate: notEmpty, + initial: process.env.DOCKER_TOKEN, + scope: 'REPOSITORY' as const + } +] + +export const githubQuestions = [ + { + name: 'githubOrganisation', + type: 'text' as const, + message: 'What is the name of your Github organisation?', + validate: notEmpty, + initial: process.env.GITHUB_ORGANISATION, + scope: 'REPOSITORY' as const + }, + { + name: 'githubRepository', + type: 'text' as const, + message: 'What is your Github infrastructure repository?', + validate: notEmpty, + initial: process.env.GITHUB_REPOSITORY, + scope: 'REPOSITORY' as const + }, +] +export const githubOtherQuestions = [ + { + name: 'githubApprovers', + type: 'text' as const, + message: 'Please provide/update list of production approvers?', + initial: process.env.GH_APPROVERS, + valueType: 'VARIABLE' as const, + valueLabel: 'GH_APPROVERS', + scope: 'REPOSITORY' as const + }, + { + name: 'approvalRequired', + type: 'select' as const, + message: 'Would you like to enable approvals process for GitHub action workflows?', + choices: [ + { + title: 'True', + value: 'true' + }, + { + title: 'False', + value: 'false' + } + ], + valueType: 'VARIABLE' as const, + validate: notEmpty, + valueLabel: 'APPROVAL_REQUIRED', + initial: process.env.APPROVAL_REQUIRED, + scope: 'ENVIRONMENT' as const + }, +] +export const githubTokenQuestion = [ + { + name: 'githubToken', + type: 'text' as const, + message: 'What is your Github token?', + validate: notEmpty, + initial: process.env.GITHUB_TOKEN, + valueType: 'SECRET' as const, + scope: 'REPOSITORY' as const, + valueLabel: 'GH_TOKEN' + } +] + +export const countryQuestions = [ + { + name: 'country', + type: 'text' as const, + message: + 'What is the ISO 3166-1 alpha-3 country-code? (e.g. "NZL" for New Zealand) Reference: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3', + valueType: 'VARIABLE' as const, + valueLabel: 'COUNTRY', + initial: process.env.COUNTRY, + scope: 'REPOSITORY' as const + } +] + +export const infrastructureQuestions = [ + { + name: 'domain', + type: 'text' as const, + message: 'What is the web domain applied after all subdomains in URLs?', + valueType: 'VARIABLE' as const, + validate: notEmpty, + valueLabel: 'DOMAIN', + initial: process.env.DOMAIN, + scope: 'ENVIRONMENT' as const + }, + { + name: 'kubeAPIHost', + type: 'text' as const, + message: + `Please enter host/IP to expose Kubernetes API endpoint, (default: Master first IP address):`, + valueType: 'VARIABLE' as const, + // validate: notEmpty, + valueLabel: 'KUBE_API_HOST', + initial: process.env.KUBE_API_HOST || '', + scope: 'ENVIRONMENT' as const + }, + { + name: 'workerNodes', + type: 'text' as const, + message: + `Please enter Kubernetes workers hosts/IP addresses (comma-separated), (default: no workers):`, + valueType: 'VARIABLE' as const, + // validate: notEmpty, + valueLabel: 'WORKER_NODES', + initial: process.env.WORKER_NODES || '', + scope: 'ENVIRONMENT' as const, + }, +] + +export const backupQuestions = [ + { + name: 'backupHost', + type: 'text' as const, + message: + `Please enter backup server host/IP address, (default: no backup):`, + valueType: 'SECRET' as const, + // validate: notEmpty, + valueLabel: 'BACKUP_HOST', + initial: process.env.BACKUP_HOST || '', + scope: 'ENVIRONMENT' as const, + }, + { + name: 'backupUser', + type: 'text' as const, + message: + `Please enter backup server user name:`, + valueType: 'SECRET' as const, + validate: notEmpty, + valueLabel: 'BACKUP_SERVER_USER', + initial: process.env.BACKUP_SERVER_USER || 'backup', + scope: 'ENVIRONMENT' as const, + }, +] + +export const diskQuestions = [ + { + name: 'diskSpace', + type: 'text' as const, + message: `What is the amount of diskspace that should be dedicated to OpenCRVS data and will become the size of an encrypted cryptfs data directory. + \n${kleur.red('DO NOT USE ALL DISKSPACE FOR OPENCRVS!')} + \nLeave at least 50g available for OS use.`, + valueType: 'VARIABLE' as const, + validate: notEmpty, + valueLabel: 'DISK_SPACE', + initial: process.env.DISK_SPACE || '200g', + scope: 'ENVIRONMENT' as const, + }, +] + +export const databaseAndMonitoringQuestions = [ + { + name: 'kibanaUsername', + type: 'text' as const, + message: 'Input the username for logging in to Kibana', + valueType: 'SECRET' as const, + validate: notEmpty, + valueLabel: 'KIBANA_USERNAME', + initial: process.env.KIBANA_USERNAME || 'opencrvs-admin', + scope: 'ENVIRONMENT' as const + }, + { + name: 'kibanaPassword', + type: 'text' as const, + message: 'Input the password for logging in to Kibana', + valueType: 'SECRET' as const, + validate: notEmpty, + valueLabel: 'KIBANA_PASSWORD', + initial: process.env.KIBANA_PASSWORD || generateLongPassword(), + scope: 'ENVIRONMENT' as const + } +] + +export const notificationTransportQuestions = [ + { + name: 'notificationTransport', + type: 'select' as const, + message: 'Notification transport for 2FA, informant and user messaging', + choices: [ + { + title: 'Email (with SMTP details)', + value: 'email' + }, + { + title: 'SMS (Infobip)', + value: 'sms' + } + ], + valueLabel: 'NOTIFICATION_TRANSPORT', + valueType: 'VARIABLE' as const, + scope: 'ENVIRONMENT' as const, + initial: process.env.NOTIFICATION_TRANSPORT + } +] + +export const smsQuestions = [ + { + name: 'infobipApiKey', + type: 'text' as const, + message: 'What is your Infobip API key?', + valueType: 'SECRET' as const, + validate: notEmpty, + valueLabel: 'INFOBIP_API_KEY', + initial: process.env.INFOBIP_API_KEY, + scope: 'ENVIRONMENT' as const + }, + { + name: 'infobipGatewayEndpoint', + type: 'text' as const, + message: 'What is your Infobip gateway endpoint?', + valueType: 'SECRET' as const, + validate: notEmpty, + valueLabel: 'INFOBIP_GATEWAY_ENDPOINT', + initial: process.env.INFOBIP_GATEWAY_ENDPOINT, + scope: 'ENVIRONMENT' as const + }, + { + name: 'infobipSenderId', + type: 'text' as const, + message: 'What is your Infobip sender ID?', + valueType: 'SECRET' as const, + validate: notEmpty, + valueLabel: 'INFOBIP_SENDER_ID', + initial: process.env.INFOBIP_SENDER_ID, + scope: 'ENVIRONMENT' as const + } +] + +export const emailQuestions = [ + { + name: 'smtpHost', + type: 'text' as const, + message: 'What is your SMTP host?', + valueType: 'SECRET' as const, + validate: notEmpty, + valueLabel: 'SMTP_HOST', + initial: process.env.SMTP_HOST, + scope: 'ENVIRONMENT' as const + }, + { + name: 'smtpUsername', + type: 'text' as const, + message: 'What is your SMTP username?', + valueType: 'SECRET' as const, + validate: notEmpty, + valueLabel: 'SMTP_USERNAME', + initial: process.env.SMTP_USERNAME, + scope: 'ENVIRONMENT' as const + }, + { + name: 'smtpPassword', + type: 'text' as const, + message: 'What is your SMTP password?', + valueType: 'SECRET' as const, + validate: notEmpty, + valueLabel: 'SMTP_PASSWORD', + initial: process.env.SMTP_PASSWORD, + scope: 'ENVIRONMENT' as const + }, + { + name: 'smtpPort', + type: 'text' as const, + message: 'What is your SMTP port?', + valueType: 'SECRET' as const, + validate: notEmpty, + valueLabel: 'SMTP_PORT', + initial: process.env.SMTP_PORT, + scope: 'ENVIRONMENT' as const + }, + { + name: 'smtpSecure', + type: 'select' as const, + message: 'Is the SMTP connection made securely using TLS?', + choices: [ + { + title: 'True', + value: 'true' + }, + { + title: 'False', + value: 'false' + } + ], + valueType: 'SECRET' as const, + validate: notEmpty, + valueLabel: 'SMTP_SECURE', + initial: process.env.SMTP_SECURE, + scope: 'ENVIRONMENT' as const + }, + { + name: 'senderEmailAddress', + type: 'text' as const, + message: 'What is your sender email address?', + valueType: 'SECRET' as const, + validate: notEmpty, + valueLabel: 'SENDER_EMAIL_ADDRESS', + initial: process.env.SENDER_EMAIL_ADDRESS, + scope: 'ENVIRONMENT' as const + }, + { + name: 'alertEmail', + type: 'text' as const, + message: + 'What is the email address to receive alert emails or a Slack channel email link?', + valueType: 'SECRET' as const, + validate: notEmpty, + valueLabel: 'ALERT_EMAIL', + initial: process.env.ALERT_EMAIL, + scope: 'ENVIRONMENT' as const + } +] + +export const sentryQuestions = [ + { + name: 'sentryDsn', + type: 'text' as const, + message: 'What is your Sentry DSN?', + valueType: 'SECRET' as const, + validate: notEmpty, + valueLabel: 'SENTRY_DSN', + initial: process.env.SENTRY_DSN, + scope: 'ENVIRONMENT' as const + } +] + +export const metabaseAdminQuestions = [ + { + valueType: 'SECRET' as const, + name: 'OPENCRVS_METABASE_ADMIN_EMAIL', + type: 'text' as const, + message: + 'Email for Metabase super admin. Used as a username when logging in to the dashboard', + valueLabel: 'OPENCRVS_METABASE_ADMIN_EMAIL', + scope: 'ENVIRONMENT' as const, + initial: 'user@opencrvs.org' + }, + { + valueType: 'SECRET' as const, + name: 'OPENCRVS_METABASE_ADMIN_PASSWORD', + type: 'text' as const, + message: 'Password for Metabase super admin.', + valueLabel: 'OPENCRVS_METABASE_ADMIN_PASSWORD', + scope: 'ENVIRONMENT' as const, + initial: generateLongPassword() + } +] diff --git a/infrastructure/environments/setup-environment.ts b/infrastructure/environments/setup-environment.ts index 35eedf50..585cee9c 100644 --- a/infrastructure/environments/setup-environment.ts +++ b/infrastructure/environments/setup-environment.ts @@ -16,16 +16,36 @@ import { listRepositorySecrets, listRepositoryVariables, updateEnvironmentVariable, - updateRepositoryVariable + updateRepositoryVariable, + getRepositoryEnvironments } from './github' +import { derivedVariables } from './derived-variables' +import { generateLongPassword } from './utils' + import { readFileSync, writeFileSync } from 'fs' import { error, info, log, success, warn } from './logger' import { generateInventory, copyChartsValues } from './templates' import { updateWorkflowEnvironments } from './update-workflows'; +import { generateSSHKeyPair } from "./ssh-keygen"; +import { + environmentQuestions, + dockerhubQuestions, + githubQuestions, + githubTokenQuestion, + githubOtherQuestions, + infrastructureQuestions, + countryQuestions, + databaseAndMonitoringQuestions, + diskQuestions, + sentryQuestions, + notificationTransportQuestions, + smsQuestions, + emailQuestions, + metabaseAdminQuestions, + backupQuestions, +} from './questions'; -const notEmpty = (value: string | number) => - value.toString().trim().length > 0 ? true : 'Please enter a value' type Question = PromptObject & { name: T @@ -79,6 +99,64 @@ function questionToPrompt({ const ALL_QUESTIONS: Array> = [] const ALL_ANSWERS: Array> = [] +function getAnswers(existingValues: (Secret | Variable)[]): Answers { + return ALL_ANSWERS.flatMap((answerObject) => { + const questionsThatAreSecretsOrVariables = Object.entries( + answerObject + ).filter(([key, value]) => { + if (value === '') { + return false + } + const existingQuestion = ALL_QUESTIONS.find( + (question) => question.name === key + ) + const valueType = existingQuestion?.valueType + return valueType === 'VARIABLE' || valueType === 'SECRET' + }) + + return questionsThatAreSecretsOrVariables.map(([key, value]) => { + const existingQuestion = ALL_QUESTIONS.find( + (question) => question.name === key + ) + const valueType = existingQuestion!.valueType! + + if (valueType === 'SECRET') { + const existingSecret = findExistingValue( + existingQuestion!.valueLabel!, + 'SECRET', + existingQuestion?.scope!, + existingValues + ) + return { + type: valueType, + name: existingQuestion?.valueLabel!, + value: value.toString(), + didExist: existingSecret, + scope: existingQuestion!.scope! + } + } + const existingVariable = findExistingValue( + existingQuestion?.valueLabel!, + valueType, + existingQuestion?.scope!, + existingValues + ) + return { + type: valueType, + name: existingQuestion?.valueLabel!, + didExist: findExistingValue( + existingQuestion?.valueLabel!, + valueType, + existingQuestion?.scope!, + existingValues + ), + value: value.toString() || existingVariable?.value || '', + scope: existingQuestion!.scope! + } + }) + }) +} + function findExistingValue( name: string, type: T, @@ -199,14 +277,6 @@ async function promptAndStoreAnswer( return { ...Object.fromEntries(existingValuesForQuestions), ...result } } -function generateLongPassword() { - const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' - let result = '' - for (let i = 16; i > 0; --i) - result += chars[Math.floor(Math.random() * chars.length)] - return result -} - function storeSecrets(environment: string, answers: Answers) { let envConfig: Record = {} try { @@ -229,520 +299,8 @@ function storeSecrets(environment: string, answers: Answers) { writeFileSync(`.env.${environment}`, allLines.join('\n')) } -const githubQuestions = [ - { - name: 'githubOrganisation', - type: 'text' as const, - message: 'What is the name of your Github organisation?', - validate: notEmpty, - initial: process.env.GITHUB_ORGANISATION, - scope: 'REPOSITORY' as const - }, - { - name: 'githubRepository', - type: 'text' as const, - message: 'What is your Github infrastructure repository?', - validate: notEmpty, - initial: process.env.GITHUB_REPOSITORY, - scope: 'REPOSITORY' as const - }, -] -const githubOtherQuestions = [ - { - name: 'githubApprovers', - type: 'text' as const, - message: 'Please provide/update list of production approvers?', - initial: process.env.GH_APPROVERS, - valueType: 'VARIABLE' as const, - valueLabel: 'GH_APPROVERS', - scope: 'REPOSITORY' as const - }, - { - name: 'approvalRequired', - type: 'select' as const, - message: 'Would you like to enable approvals process for GitHub action workflows?', - choices: [ - { - title: 'True', - value: 'true' - }, - { - title: 'False', - value: 'false' - } - ], - valueType: 'VARIABLE' as const, - validate: notEmpty, - valueLabel: 'APPROVAL_REQUIRED', - initial: process.env.APPROVAL_REQUIRED, - scope: 'ENVIRONMENT' as const - }, -] -const githubTokenQuestion = [ - { - name: 'githubToken', - type: 'text' as const, - message: 'What is your Github token?', - validate: notEmpty, - initial: process.env.GITHUB_TOKEN, - valueType: 'SECRET' as const, - scope: 'REPOSITORY' as const, - valueLabel: 'GH_TOKEN' - } -] - -const dockerhubQuestions = [ - { - name: 'dockerhubOrganisation', - type: 'text' as const, - message: 'What is the name of your Docker Hub organisation?', - valueType: 'SECRET' as const, - valueLabel: 'DOCKERHUB_ACCOUNT', - validate: notEmpty, - initial: process.env.DOCKER_ORGANISATION, - scope: 'REPOSITORY' as const - }, - { - name: 'dockerhubRepository', - type: 'text' as const, - message: 'What is the name of your private Docker Hub repository?', - valueType: 'SECRET' as const, - valueLabel: 'DOCKERHUB_REPO', - validate: notEmpty, - initial: process.env.DOCKER_REPO, - scope: 'REPOSITORY' as const - }, - { - name: 'dockerhubUsername', - type: 'text' as const, - message: - 'What is the Docker Hub username the the target server should be using?', - valueType: 'SECRET' as const, - valueLabel: 'DOCKER_USERNAME', - validate: notEmpty, - initial: process.env.DOCKER_USERNAME, - scope: 'REPOSITORY' as const - }, - { - name: 'dockerhubToken', - type: 'text' as const, - message: 'What is the token of this Docker Hub account?', - valueType: 'SECRET' as const, - valueLabel: 'DOCKER_TOKEN', - validate: notEmpty, - initial: process.env.DOCKER_TOKEN, - scope: 'REPOSITORY' as const - } -] - -const countryQuestions = [ - { - name: 'country', - type: 'text' as const, - message: - 'What is the ISO 3166-1 alpha-3 country-code? (e.g. "NZL" for New Zealand) Reference: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3', - valueType: 'VARIABLE' as const, - valueLabel: 'COUNTRY', - initial: process.env.COUNTRY, - scope: 'REPOSITORY' as const - } -] - -const infrastructureQuestions = [ - { - name: 'domain', - type: 'text' as const, - message: 'What is the web domain applied after all subdomains in URLs?', - valueType: 'VARIABLE' as const, - validate: notEmpty, - valueLabel: 'DOMAIN', - initial: process.env.DOMAIN, - scope: 'ENVIRONMENT' as const - }, - { - name: 'kubeAPIHost', - type: 'text' as const, - message: - `Please enter host/IP to expose Kubernetes API endpoint, (default: Master first IP address):`, - valueType: 'VARIABLE' as const, - // validate: notEmpty, - valueLabel: 'KUBE_API_HOST', - initial: process.env.KUBE_API_HOST || '', - scope: 'ENVIRONMENT' as const - }, - { - name: 'workerNodes', - type: 'text' as const, - message: - `Please enter Kubernetes workers hosts/IP addresses (comma-separated), (default: no workers):`, - valueType: 'VARIABLE' as const, - // validate: notEmpty, - valueLabel: 'WORKER_NODES', - initial: process.env.WORKER_NODES || '', - scope: 'ENVIRONMENT' as const, - }, - { - name: 'backupHost', - type: 'text' as const, - message: - `Please enter backup server host/IP address, (default: no backup):`, - valueType: 'VARIABLE' as const, - // validate: , - valueLabel: 'BACKUP_HOST', - initial: process.env.BACKUP_HOST || '', - scope: 'ENVIRONMENT' as const, - }, -] - -const diskQuestions = [ - { - name: 'diskSpace', - type: 'text' as const, - message: `What is the amount of diskspace that should be dedicated to OpenCRVS data and will become the size of an encrypted cryptfs data directory. - \n${kleur.red('DO NOT USE ALL DISKSPACE FOR OPENCRVS!')} - \nLeave at least 50g available for OS use.`, - valueType: 'VARIABLE' as const, - validate: notEmpty, - valueLabel: 'DISK_SPACE', - initial: process.env.DISK_SPACE || '200g', - scope: 'ENVIRONMENT' as const, - }, -] - -const databaseAndMonitoringQuestions = [ - { - name: 'kibanaUsername', - type: 'text' as const, - message: 'Input the username for logging in to Kibana', - valueType: 'SECRET' as const, - validate: notEmpty, - valueLabel: 'KIBANA_USERNAME', - initial: process.env.KIBANA_USERNAME || 'opencrvs-admin', - scope: 'ENVIRONMENT' as const - }, - { - name: 'kibanaPassword', - type: 'text' as const, - message: 'Input the password for logging in to Kibana', - valueType: 'SECRET' as const, - validate: notEmpty, - valueLabel: 'KIBANA_PASSWORD', - initial: process.env.KIBANA_PASSWORD || generateLongPassword(), - scope: 'ENVIRONMENT' as const - } -] - -const notificationTransportQuestions = [ - { - name: 'notificationTransport', - type: 'select' as const, - message: 'Notification transport for 2FA, informant and user messaging', - choices: [ - { - title: 'Email (with SMTP details)', - value: 'email' - }, - { - title: 'SMS (Infobip)', - value: 'sms' - } - ], - valueLabel: 'NOTIFICATION_TRANSPORT', - valueType: 'VARIABLE' as const, - scope: 'ENVIRONMENT' as const, - initial: process.env.NOTIFICATION_TRANSPORT - } -] - -const smsQuestions = [ - { - name: 'infobipApiKey', - type: 'text' as const, - message: 'What is your Infobip API key?', - valueType: 'SECRET' as const, - validate: notEmpty, - valueLabel: 'INFOBIP_API_KEY', - initial: process.env.INFOBIP_API_KEY, - scope: 'ENVIRONMENT' as const - }, - { - name: 'infobipGatewayEndpoint', - type: 'text' as const, - message: 'What is your Infobip gateway endpoint?', - valueType: 'SECRET' as const, - validate: notEmpty, - valueLabel: 'INFOBIP_GATEWAY_ENDPOINT', - initial: process.env.INFOBIP_GATEWAY_ENDPOINT, - scope: 'ENVIRONMENT' as const - }, - { - name: 'infobipSenderId', - type: 'text' as const, - message: 'What is your Infobip sender ID?', - valueType: 'SECRET' as const, - validate: notEmpty, - valueLabel: 'INFOBIP_SENDER_ID', - initial: process.env.INFOBIP_SENDER_ID, - scope: 'ENVIRONMENT' as const - } -] - -const emailQuestions = [ - { - name: 'smtpHost', - type: 'text' as const, - message: 'What is your SMTP host?', - valueType: 'SECRET' as const, - validate: notEmpty, - valueLabel: 'SMTP_HOST', - initial: process.env.SMTP_HOST, - scope: 'ENVIRONMENT' as const - }, - { - name: 'smtpUsername', - type: 'text' as const, - message: 'What is your SMTP username?', - valueType: 'SECRET' as const, - validate: notEmpty, - valueLabel: 'SMTP_USERNAME', - initial: process.env.SMTP_USERNAME, - scope: 'ENVIRONMENT' as const - }, - { - name: 'smtpPassword', - type: 'text' as const, - message: 'What is your SMTP password?', - valueType: 'SECRET' as const, - validate: notEmpty, - valueLabel: 'SMTP_PASSWORD', - initial: process.env.SMTP_PASSWORD, - scope: 'ENVIRONMENT' as const - }, - { - name: 'smtpPort', - type: 'text' as const, - message: 'What is your SMTP port?', - valueType: 'SECRET' as const, - validate: notEmpty, - valueLabel: 'SMTP_PORT', - initial: process.env.SMTP_PORT, - scope: 'ENVIRONMENT' as const - }, - { - name: 'smtpSecure', - type: 'select' as const, - message: 'Is the SMTP connection made securely using TLS?', - choices: [ - { - title: 'True', - value: 'true' - }, - { - title: 'False', - value: 'false' - } - ], - valueType: 'SECRET' as const, - validate: notEmpty, - valueLabel: 'SMTP_SECURE', - initial: process.env.SMTP_SECURE, - scope: 'ENVIRONMENT' as const - }, - { - name: 'senderEmailAddress', - type: 'text' as const, - message: 'What is your sender email address?', - valueType: 'SECRET' as const, - validate: notEmpty, - valueLabel: 'SENDER_EMAIL_ADDRESS', - initial: process.env.SENDER_EMAIL_ADDRESS, - scope: 'ENVIRONMENT' as const - }, - { - name: 'alertEmail', - type: 'text' as const, - message: - 'What is the email address to receive alert emails or a Slack channel email link?', - valueType: 'SECRET' as const, - validate: notEmpty, - valueLabel: 'ALERT_EMAIL', - initial: process.env.ALERT_EMAIL, - scope: 'ENVIRONMENT' as const - } -] - -const sentryQuestions = [ - { - name: 'sentryDsn', - type: 'text' as const, - message: 'What is your Sentry DSN?', - valueType: 'SECRET' as const, - validate: notEmpty, - valueLabel: 'SENTRY_DSN', - initial: process.env.SENTRY_DSN, - scope: 'ENVIRONMENT' as const - } -] - -const derivedVariables = [ - { - name: 'ACTIVATE_USERS', - valueLabel: 'ACTIVATE_USERS', - valueType: 'VARIABLE', - type: 'disabled', - scope: 'ENVIRONMENT' - }, - { - name: 'AUTH_HOST', - valueLabel: 'AUTH_HOST', - valueType: 'VARIABLE', - type: 'disabled', - scope: 'ENVIRONMENT' - }, - { - name: 'COUNTRY_CONFIG_HOST', - valueLabel: 'COUNTRY_CONFIG_HOST', - valueType: 'VARIABLE', - type: 'disabled', - scope: 'ENVIRONMENT' - }, - { - name: 'GATEWAY_HOST', - valueLabel: 'GATEWAY_HOST', - valueType: 'VARIABLE', - type: 'disabled', - scope: 'ENVIRONMENT' - }, - { - name: 'CONTENT_SECURITY_POLICY_WILDCARD', - valueLabel: 'CONTENT_SECURITY_POLICY_WILDCARD', - valueType: 'VARIABLE', - type: 'disabled', - scope: 'ENVIRONMENT' - }, - { - name: 'CLIENT_APP_URL', - valueLabel: 'CLIENT_APP_URL', - valueType: 'VARIABLE', - type: 'disabled', - scope: 'ENVIRONMENT' - }, - { - name: 'LOGIN_URL', - valueLabel: 'LOGIN_URL', - valueType: 'VARIABLE', - type: 'disabled', - scope: 'ENVIRONMENT' - }, - { - name: 'ELASTICSEARCH_SUPERUSER_PASSWORD', - valueLabel: 'ELASTICSEARCH_SUPERUSER_PASSWORD', - valueType: 'SECRET', - type: 'disabled', - scope: 'ENVIRONMENT' - }, - { - name: 'KIBANA_SYSTEM_PASSWORD', - valueLabel: 'KIBANA_SYSTEM_PASSWORD', - valueType: 'SECRET', - type: 'disabled', - scope: 'ENVIRONMENT' - }, - { - name: 'MINIO_ROOT_USER', - valueLabel: 'MINIO_ROOT_USER', - valueType: 'SECRET', - type: 'disabled', - scope: 'ENVIRONMENT' - }, - { - name: 'MINIO_ROOT_PASSWORD', - valueLabel: 'MINIO_ROOT_PASSWORD', - valueType: 'SECRET', - type: 'disabled', - scope: 'ENVIRONMENT' - }, - { - name: 'MONGODB_ADMIN_USER', - valueLabel: 'MONGODB_ADMIN_USER', - valueType: 'SECRET', - type: 'disabled', - scope: 'ENVIRONMENT' - }, - { - name: 'MONGODB_ADMIN_PASSWORD', - valueLabel: 'MONGODB_ADMIN_PASSWORD', - valueType: 'SECRET', - type: 'disabled', - scope: 'ENVIRONMENT' - }, - { - name: 'POSTGRES_USER', - valueLabel: 'POSTGRES_USER', - valueType: 'SECRET', - type: 'disabled', - scope: 'ENVIRONMENT' - }, - { - name: 'POSTGRES_PASSWORD', - valueLabel: 'POSTGRES_PASSWORD', - valueType: 'SECRET', - type: 'disabled', - scope: 'ENVIRONMENT' - }, - { - name: 'SUPER_USER_PASSWORD', - valueLabel: 'SUPER_USER_PASSWORD', - valueType: 'SECRET', - type: 'disabled', - scope: 'ENVIRONMENT' - }, - { - name: 'ENCRYPTION_KEY', - valueLabel: 'ENCRYPTION_KEY', - valueType: 'SECRET', - type: 'disabled', - scope: 'ENVIRONMENT' - }, - { - name: 'GH_ENCRYPTION_PASSWORD', - valueLabel: 'GH_ENCRYPTION_PASSWORD', - valueType: 'SECRET', - type: 'disabled', - scope: 'REPOSITORY' - }, - { - name: 'BACKUP_ENCRYPTION_PASSPHRASE', - valueLabel: 'BACKUP_ENCRYPTION_PASSPHRASE', - valueType: 'SECRET', - type: 'disabled', - scope: 'ENVIRONMENT' - } -] as const - -const metabaseAdminQuestions = [ - { - valueType: 'SECRET' as const, - name: 'OPENCRVS_METABASE_ADMIN_EMAIL', - type: 'text' as const, - message: - 'Email for Metabase super admin. Used as a username when logging in to the dashboard', - valueLabel: 'OPENCRVS_METABASE_ADMIN_EMAIL', - scope: 'ENVIRONMENT' as const, - initial: 'user@opencrvs.org' - }, - { - valueType: 'SECRET' as const, - name: 'OPENCRVS_METABASE_ADMIN_PASSWORD', - type: 'text' as const, - message: 'Password for Metabase super admin.', - valueLabel: 'OPENCRVS_METABASE_ADMIN_PASSWORD', - scope: 'ENVIRONMENT' as const, - initial: generateLongPassword() - } -] - ALL_QUESTIONS.push( + ...environmentQuestions, ...githubTokenQuestion, ...githubOtherQuestions, ...dockerhubQuestions, @@ -760,38 +318,12 @@ ALL_QUESTIONS.push( ; (async () => { - const { type: environment_type } = await prompts( - [ - { - name: 'type', - type: 'select' as const, - scope: 'ENVIRONMENT' as const, - message: 'Purpose for the environment?', - choices: [ - { title: 'Development/Quality assurance/Testing (no PII data)', value: 'non-production' }, - { title: 'Staging/Production (hosts PII data, requires frequent backups)', value: 'production' }, - ] - } - ].map(questionToPrompt) - ) - const { environment } = await prompts( - [ - { - name: 'environment', - type: 'text' as const, - message: 'What is the name of your environment?', - validate: notEmpty, - initial: process.env.ENV, - scope: 'REPOSITORY' as const - } - ].map(questionToPrompt), - { - onCancel: () => { - process.exit(1) - } + const { environment_type, environment } = await prompts(environmentQuestions.map(questionToPrompt), { + onCancel: () => { + process.exit(1) } - ) + }) // Read users .env file based on the environment name they gave above, e.g. .env.production dotenv.config({ @@ -810,7 +342,7 @@ ALL_QUESTIONS.push( ) const { githubToken } = await promptAndStoreAnswer( - environment, + '', githubTokenQuestion, [] ) @@ -819,6 +351,8 @@ ALL_QUESTIONS.push( auth: githubToken }) + let existingEnvironments = await getRepositoryEnvironments(octokit, githubOrganisation, githubRepository); + await createEnvironment( octokit, environment, @@ -869,8 +403,8 @@ ALL_QUESTIONS.push( log( '\nEnvironment with the name', environment, - 'already exists in Github.\n', - 'Found', + 'already exists in Github.', + '\nFound', existingEnvironmentVariables.length, 'existing variables and', existingEnvironmentSecrets.length, @@ -900,32 +434,101 @@ ALL_QUESTIONS.push( const workerNodes = infrastructure.workerNodes ? infrastructure.workerNodes.split(',').map((ip: string) => ip.trim()) : [] - const backupHost = infrastructure.backupHost || '' - log('\n', kleur.bold().underline('Running configuration files updates')) - generateInventory( - environment, - { - worker_nodes: workerNodes, - backup_host: backupHost, - kube_api_host: infrastructure.kubeAPIHost || '' - } + + log('\n', kleur.bold().underline('Backup configuration')) + const backupHost = findExistingValue( + 'BACKUP_HOST', + 'VARIABLE', + 'ENVIRONMENT', + existingValues ) - copyChartsValues( + let configureBackup = backupHost ? true : false + if (!backupHost) { + configureBackup = (await prompts( + [ + { + name: 'configureBackup', + type: 'confirm' as const, + message: 'Do you want to configure backup?', + scope: 'ENVIRONMENT' as const, + initial: Boolean(process.env.CONFIGURE_BACKUP) + } + ].map(questionToPrompt) + )).configureBackup + } + let backupHostPrivateKeyExists = findExistingValue( + 'BACKUP_HOST_PRIVATE_KEY', + 'SECRET', + 'ENVIRONMENT', + existingValues + ) + let backupHostPrivateKey = '' + let backupHostPublicKey = '' + + if (configureBackup) { + const { backupHost } = await promptAndStoreAnswer( environment, - { - env: environment, - environment_type: environment_type, - // FIXME: In general that should be environment_type, - // Hardcode like this blocks us from being generic: - // https://github.com/opencrvs/opencrvs-core/issues/11171 - is_qa_env: environment !== 'production' ? "true" : "false" - } + backupQuestions, + existingValues ) - await updateWorkflowEnvironments(); + + if (backupHost && !backupHostPrivateKeyExists) { + const { publicKey, privateKey } = generateSSHKeyPair(); + backupHostPublicKey = publicKey; + backupHostPrivateKey = privateKey; + log(kleur.bold().green('✔'), kleur.bold().yellow(`Generated SSH key pair for backup host: ${backupHost}`)) + } + } + + log('\n', kleur.bold().underline('Restore configuration')) + let restoreEnvironmentName = findExistingValue( + 'RESTORE_ENVIRONMENT_NAME', + 'VARIABLE', + 'ENVIRONMENT', + existingValues + )?.value + + if (!restoreEnvironmentName) { + let configureRestore = (await prompts( + [ + { + name: 'configureRestore', + type: 'confirm' as const, + message: 'Do you want to configure restore?', + scope: 'ENVIRONMENT' as const, + initial: Boolean(process.env.CONFIGURE_RESTORE) + } + ].map(questionToPrompt) + )).configureRestore + if (configureRestore) { + const env_list_filtered = existingEnvironments.filter(env => env !== environment); + restoreEnvironmentName = (await promptAndStoreAnswer( + environment, + [ + { + name: 'restoreEnvironmentName', + type: 'autocomplete' as const, + message: 'What is the name of your environment to restore?', + scope: 'ENVIRONMENT' as const, + choices: env_list_filtered.map(env => ({ + title: env, + value: env + })), + initial: '', + validate: (input: string) => + env_list_filtered.includes(input) ? true : 'Please select a valid environment.' + } + ], + existingValues + )).restoreEnvironmentName + } + } else { + log('\n', kleur.bold().green('✔'), kleur.bold().yellow('Restore environment is already set to'), kleur.bold().blue(restoreEnvironmentName)) + } log('\n', kleur.bold().underline('Databases & monitoring')) - var enableEncryption = true + let enableEncryption = true const encryption_key_defined = findExistingValue( 'ENCRYPTION_KEY', 'SECRET', @@ -1333,9 +936,58 @@ ALL_QUESTIONS.push( existingValues ), scope: 'ENVIRONMENT' as const + }, + { + type: 'VARIABLE' as const, + name: 'LOGIN_URL', + value: allAnswers.restoreEnvironmentName, + didExist: findExistingValue( + 'LOGIN_URL', + 'VARIABLE', + 'ENVIRONMENT', + existingValues + ), + scope: 'ENVIRONMENT' as const + }, + { + type: 'VARIABLE' as const, + name: 'RESTORE_ENVIRONMENT_NAME', + value: answerOrExisting(restoreEnvironmentName, findExistingValue( + 'RESTORE_ENVIRONMENT_NAME', + 'VARIABLE', + 'ENVIRONMENT', + existingValues + ), (val) => val || ''), + didExist: findExistingValue( + 'RESTORE_ENVIRONMENT_NAME', + 'VARIABLE', + 'ENVIRONMENT', + existingValues + ), + scope: 'ENVIRONMENT' as const } ] + // if (restoreEnvironmentName) { + // applicationServerUpdates.push({ + // type: 'VARIABLE' as const, + // name: 'RESTORE_ENVIRONMENT_NAME', + // value: answerOrExisting(restoreEnvironmentName, findExistingValue( + // 'RESTORE_ENVIRONMENT_NAME', + // 'VARIABLE', + // 'ENVIRONMENT', + // existingValues + // ), (val) => val || ''), + // didExist: findExistingValue( + // 'RESTORE_ENVIRONMENT_NAME', + // 'VARIABLE', + // 'ENVIRONMENT', + // existingValues + // ), + // scope: 'ENVIRONMENT' as const + + // }) + // } if (enableEncryption) { applicationServerUpdates.push({ name: 'ENCRYPTION_KEY', @@ -1356,6 +1008,23 @@ ALL_QUESTIONS.push( }) } + if (configureBackup) { + applicationServerUpdates.push({ + name: 'BACKUP_HOST_PRIVATE_KEY', + type: 'SECRET' as const, + didExist: undefined, + value: backupHostPrivateKey, + scope: 'ENVIRONMENT' as const + }) + applicationServerUpdates.push({ + name: 'BACKUP_HOST_PUBLIC_KEY', + type: 'SECRET' as const, + didExist: undefined, + value: backupHostPublicKey, + scope: 'ENVIRONMENT' as const + }) + } + derivedUpdates.push(...applicationServerUpdates) @@ -1623,6 +1292,31 @@ ALL_QUESTIONS.push( ) ) + log('\n', kleur.bold().underline('Running configuration files updates')) + generateInventory( + environment, + { + worker_nodes: workerNodes, + backup_host: backupHost, + kube_api_host: infrastructure.kubeAPIHost || '' + } + ) + copyChartsValues( + environment, + { + env: environment, + environment_type: environment_type, + // FIXME: In general that should be environment_type, + // Hardcode like this blocks us from being generic: + // https://github.com/opencrvs/opencrvs-core/issues/11171 + is_qa_env: environment !== 'production' ? "true" : "false", + backup_enabled: configureBackup ? "true" : "false", + restore_enabled: restoreEnvironmentName ? "true" : "false", + restore_environment_name: restoreEnvironmentName || "" + } + ) + await updateWorkflowEnvironments(); + const worker_message = workerNodes.length > 0 ? ` ----------------------- @@ -1633,7 +1327,7 @@ ALL_QUESTIONS.push( curl -sfL https://raw.githubusercontent.com/opencrvs/infrastructure/refs/heads/develop/scripts/bootstrap/opencrvs-bootstrap.sh -o opencrvs-bootstrap.sh && \\ bash opencrvs-bootstrap.sh --ssh-public-key ${kleur.bold('[PUT PROVISION USER PUBLIC KEY FROM MASTER NODE]')}` : '' - const backup_message = backupHost && backupHost !== "" ? + const backup_message = backupHost && backupHost.name !== "" ? ` ----------------------- ➡️ ${kleur.bold().yellow('COPY the SSH public key from the master VM to your clipboard')} @@ -1665,60 +1359,3 @@ ${kleur.yellow('━━━━━━━━━━━━━━━━━━━━━ log(kleur.bold().yellow('DO NOT COMMIT THIS FILE TO GIT!')) })() -function getAnswers(existingValues: (Secret | Variable)[]): Answers { - return ALL_ANSWERS.flatMap((answerObject) => { - const questionsThatAreSecretsOrVariables = Object.entries( - answerObject - ).filter(([key, value]) => { - if (value === '') { - return false - } - const existingQuestion = ALL_QUESTIONS.find( - (question) => question.name === key - ) - const valueType = existingQuestion?.valueType - return valueType === 'VARIABLE' || valueType === 'SECRET' - }) - - return questionsThatAreSecretsOrVariables.map(([key, value]) => { - const existingQuestion = ALL_QUESTIONS.find( - (question) => question.name === key - ) - const valueType = existingQuestion!.valueType! - - if (valueType === 'SECRET') { - const existingSecret = findExistingValue( - existingQuestion!.valueLabel!, - 'SECRET', - existingQuestion?.scope!, - existingValues - ) - return { - type: valueType, - name: existingQuestion?.valueLabel!, - value: value.toString(), - didExist: existingSecret, - scope: existingQuestion!.scope! - } - } - const existingVariable = findExistingValue( - existingQuestion?.valueLabel!, - valueType, - existingQuestion?.scope!, - existingValues - ) - return { - type: valueType, - name: existingQuestion?.valueLabel!, - didExist: findExistingValue( - existingQuestion?.valueLabel!, - valueType, - existingQuestion?.scope!, - existingValues - ), - value: value.toString() || existingVariable?.value || '', - scope: existingQuestion!.scope! - } - }) - }) -} diff --git a/infrastructure/environments/ssh-keygen.ts b/infrastructure/environments/ssh-keygen.ts new file mode 100644 index 00000000..3f131604 --- /dev/null +++ b/infrastructure/environments/ssh-keygen.ts @@ -0,0 +1,6 @@ +import { utils } from "ssh2"; + +export function generateSSHKeyPair(): { publicKey: string; privateKey: string } { + let keys = utils.generateKeyPairSync('ed25519'); + return { publicKey: keys.public.toString(), privateKey: keys.private.toString() }; +} diff --git a/infrastructure/environments/templates/charts-values/dependencies/values.yaml b/infrastructure/environments/templates/charts-values/dependencies/values.yaml index 468c1738..0d04e11e 100644 --- a/infrastructure/environments/templates/charts-values/dependencies/values.yaml +++ b/infrastructure/environments/templates/charts-values/dependencies/values.yaml @@ -23,3 +23,19 @@ monitoring: elastalert: env: HTTP_POST2_ALERT_URL: http://countryconfig.opencrvs-{{env}}.svc.cluster.local:3040/email + +# Backup configuration +backup: + enabled: {{backup_enabled}} + schedule: "0 1 * * *" + backup_server_secret: backup-server-ssh-credentials + backup_server_dir: /home/backup/{{env}} + + +# Restore configuration +restore: + enabled: {{restore_enabled}} + schedule: "0 0 * * *" + backup_server_secret: backup-server-ssh-credentials + backup_server_dir: /home/backup/{{restore_environment_name}} + backup_encryption_secret: restore-encryption-secret \ No newline at end of file diff --git a/infrastructure/environments/utils.ts b/infrastructure/environments/utils.ts new file mode 100644 index 00000000..9a1eedb0 --- /dev/null +++ b/infrastructure/environments/utils.ts @@ -0,0 +1,8 @@ + +export function generateLongPassword() { + const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + let result = '' + for (let i = 16; i > 0; --i) + result += chars[Math.floor(Math.random() * chars.length)] + return result +} \ No newline at end of file diff --git a/infrastructure/server-setup/group_vars/all.yml b/infrastructure/server-setup/group_vars/all.yml index 6ceaf752..173ca7ac 100644 --- a/infrastructure/server-setup/group_vars/all.yml +++ b/infrastructure/server-setup/group_vars/all.yml @@ -11,8 +11,6 @@ ansible_python_interpreter: /usr/bin/python3 crontab_user: root provisioning_user: provision -# Kubernetes secret in opencrvs-deps- namespace -backup_server_ssh_credentials_secret: "backup-server-ssh-credentials" # Path on backup server where to private key is stored backup_ssh_key_path: /home/backup/.ssh/id_ed25519 # User for ssh connection to backup server diff --git a/infrastructure/server-setup/inventory/demo1.yml b/infrastructure/server-setup/inventory/demo1.yml new file mode 100644 index 00000000..1b388a31 --- /dev/null +++ b/infrastructure/server-setup/inventory/demo1.yml @@ -0,0 +1,47 @@ +all: + vars: + # Domain/IP address for remote access to your cluster API (see ~/.kube/config) + # - If you are behind VPN, use private IP address + # - If your server is exposed (not recommeded), use public IP address + # - If you would like to run kubectl commands from the remote server, leave this field empty + # kube_api_host: '' + kube_api_host: + # Default ansible provision user, keep as is + ansible_user: provision + + # single_node: + # For development/qa/testing/staging keep true + # For production keep false + # Defaults production configuration: + # - master node + # - 2 worker nodes + single_node: true + + # users: Add as many users as you wish + # Configuration example + # - name: + # ssh_keys: + # - + # - + # state: present + # role: admin + # Allowed roles: + # - operator, read only access to OS, full access to kubernetes cluster + # - admin, full access + # Allowed states: + # - present, user is allowed to login + # - absent, account is disabled + users: [] + + children: + master: + hosts: + # Replace master with value returned by command: hostname + master: + # Keep values (ansible_host, ansible_connection) as is + # Ansible is executed on master node + ansible_host: localhost + ansible_connection: local + labels: + # traefik-role label is used to identify where to deploy traefik + traefik-role: ingress diff --git a/infrastructure/server-setup/inventory/production.yml b/infrastructure/server-setup/inventory/production.yml new file mode 100644 index 00000000..5c24ed5a --- /dev/null +++ b/infrastructure/server-setup/inventory/production.yml @@ -0,0 +1,143 @@ +all: + vars: + # single_node: + # For development/qa/testing/staging keep true + # For production keep false + # Defaults production configuration: + # - master node + # - 2 worker nodes + single_node: false + + # Domain/IP address for remote access to your cluster API + # Domain/IP address will be added as main endpoint to your ~/.kube/config + # - If you are behind VPN, use private IP address + # - If your server is exposed (not recommeded), use public IP address + # - If you would like to run kubectl commands from the remote server, leave this field empty + # kube_api_endpoint: '' + + # IMPORTANT: If master VM has multiple ethernet interfaces, put private IP address at kube_api_address + # kube_api_host: 10.10.10.10 + kube_api_host: + + # Default ansible provision user, keep as is + ansible_user: provision + + # users: Add as many users as you wish + # Configuration example + # - name: + # ssh_keys: + # - + # - + # state: present + # role: admin + # Allowed roles: + # - operator, read only access to OS, full access to kubernetes cluster + # - admin, full access + # Allowed states: + # - present, user is allowed to login + # - absent, account is disabled + users: + - name: pyry + ssh_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJBcrSLLdrkLrhqNQi7Uo/ZIWXb1y4kc0vGb16e2s0Jq pyry@opencrvs.org + state: present + role: admin + - name: tameem + ssh_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGUprcQyUFYwRto0aRpgriR95C1pgNxrQ0lEWEe1D8he haidertameem@gmail.com + state: present + role: admin + - name: riku + ssh_keys: + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDWIF63S4f3z9wQMvWibmvl7MPuJ6EVrkP0HuvgNhcs/4DZYMcR/GRBvV4ldOSYMlBevIXycgGzNDxKJgENUuwIWanjBu7uVAHyD6+cIRD1h63qq7Cjv/2HYTfBDKOrKzPOhA6zWvKO0ZGWsjRXk5LWMCbKOkvKJCxOpj/NVBxeE4FTK5YADYPV3OSsmBtqTHrVLm2sMmShU/2hMYYswWkobidjX65+nK/X+3C+yJbHwiydVvn+QCrFlFfCLPWKe8rUpOxyxofPqWVQh6CHhHfT8okaOc9sOE8Qeip9ljo84DftJh3Xm3ynOdWK1hH2BvRvxNadWqcE1qECbkg4tx2x riku.rouvila@gmail.com + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDGfWxxQHJv6Md/vBVoDH2UNm/uYgIBlFpP1mfh2Yj6jRNiQ/TQrfwpTawq0Sg+UW4LfYk5yxttsZ0h6L/v6PLiawgbMtf2ZqSviRTYSZTSihkK2zLmeJA2ByBCh57w4tR6IGqJK4w0kjYQSaaU6V5skQ4u+gnLQoKtkVQ4K34EFXAiIur96tLwjwDd/xCm+9T91+cAxGLv8Pe0PjirjwnvktUtzpgOhedkYK7KX0l8SKxQXUK6Ul2/QbpGO3rmguzEdtrl3Dw1TAEfu2njXbNGVQ+JWV9htH+ymsMIGoeumJRaaAZ4AXLlQPBCxTXcdQDuAjfFDPuppms/h7qB1S4Aioz7zqyd7pL7Z6Z8mJBZZlP3PsfGvADM2CdShpbL4HAa+n9miNNSYcJ7cHvC/zCitNjfaEYLVYkB5G+ggeK8Ss/MDcnsh3YFB8WnT582zt/TTJda5n+5Q7tquc1m+61t2gEKKTfBoDft9UYW2/4ViHj3ROL2Oyj7udrh/oAqV8M= riku@MBP16inch2231 + state: present + role: admin + - name: euan + ssh_keys: + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDECqHO65UpyrrO8uueD06RxGaVVq22f152Rf8qVQQAAIGAMu6gCs7ztlZ8a3yQgSEIjM/Jl1/RqIVs6CziTEef74nLFTZ5Ufz3CLRVgdebBeSBEmhTfTUV0HLkSyNzwKFpuzJxucGd72ulPvEp6eHvyJAPJz37YcU8cjaL1v05T6s2ee99li35GlDDtCzfjVV4ZPAg5JdfWuTj41RAVC0LQhk2/NB4qEu37UxGGjhRFSjBEsS5LxI9QfvgrsHpl/VOn+soH7ZkK7kS6qRgNP/uYsXRWXhHaamcl5OX68gJWTbrW6c7PCqlbCWGnsHJswCmqPIthwXXMfC7ULDNLSKG6mslAt5Dyc8/MCr3vTW7pDyr2d0FvvY86SMQUggxv3qF7TZewqfX1bhK0fMLarIxVMQ1RFo//wN9QGA+2we8rxd2Y1Kr1DBuJyuwXPfv+Exo8yNYQ+x/AYH5k6UVcSYuaB8eYmplG2KQCxt8RBFtoChrwOKNRWLqXdKyfpdp5XmnnWxPvR95gf3h3yLocVYkF0i0uvKKJ0vt8J0Ezfkdfow0B1kUg5bPXKJROX7PwbaCPdYcxyDaO6wwOigRnSmoFvkH1pLb4j1RQAXcX531CHgfN6Izi/h0mpMS4bnyIUcv2GQr+h4z4TxcCtj7qpH2y6yw7XG12jVh7TfeesXG2Q== euanmillar77@gmail.com + state: present + role: admin + - name: tahmid + ssh_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINUml9O5ySwPtEMD1yGEYHlf9Z3jro97NWAnM9+ew9gn tahmidrahman.dsi@gmail.com + state: present + role: admin + - name: tareq + ssh_keys: + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCWQihdKkwxTItN+rwYAX1vBg+8sv59sFsjYoVaO2mzS01rARfh+M+UVqpEv3zFT/3v6Dr5Z5VhzYvvbH8akiGQxURqie9quEi1iBCqcq+LApkMZxNm7yyvexlFsbkKMHsSZyVCzjE2Wt+6fwR1NqkMQgJjZS+b4CB+CUTNP2i6ytUTmck9K5iAOp1Gpm+Xgyvz6ZEJPkAJ16gV7gzNJUt/DSCkCyV8G2BqYLWeR2QxAbKyuf3LzO5i4XZdiZi9o60QAt3A6KGGLazd0UuYdehQDqVwXzwimLeeuZbaPNmwoAy7DeatOdurrWbnL7ytaiPvAbwai6Grt3PhhM41qO+uojnqTdnFdSOEPVIYMR7+mYu9tuwHZcMJIbbvMPD6EvKumD5Ndn5OxiLY/zQF5PuG89pBdTkTzzREvbV1Dkh2hwAIvgavlZl3P64On+4+FAgjrAx5U55khoRAe2FbEvB+EUGwro0bRffiM2NmxkUBraEuT2Xt5K01ZoBU6F4feO0= tareq.aziz@dsinnovators.com + state: present + role: admin + - name: jamil + ssh_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINMSNTIIsM0C3uJg3V/Fqh2gi4lvl2y6nenrb2Ft1JlX jamil31415926@gmail.com + state: present + role: admin + - name: ashikul + ssh_keys: + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDFr/v3hUGEbc2wQsDLCmqLrwiz964yVrnLZ6kafemjmX8aRGLp1CNFvrZ674SLnXidZGMkx9d5xVvv8IdFR3R50MqSqfolF43MV34/JVHjQHh9Vk4MJT/3GIaeNmr2GQ/38qAmt2BQn1ecnb7FjNO2bFvHokLhm2wCXt+A4avuTgJe0p4e6uu01IHeIzDb5sPzZ3ID0h6jJnjEDcET+Lf5NGpCjn7YKhLhBWSSl9cXQdOGLzNzg3aBk32kgJ1beP1funSeVd0jniJPZeZRC1G/kRdqBUOHKiENtwgquzZxXzdHkZV9+4mF7YGlx6LpQdNuDpW7JADtYNldtdbexdyfrgNoRzKwyMmaKNDbeHd1FsIHSDJmGm9hCoLTM2dEtsGzgghfe0tat8sOWmsj5v2en0V8rKV+w8OQEmHtaQkgMjqmZaAnd8uWiB2xIbrUuax5Pq8zkj37xnfbRxUPOEkMlOUbhh1wzGbqeUEB7nbv/vXZxwC0b7ryMk5egBP+0ZRONsdib9RkSTr3B9uSb7iTOQftdhy+CTqqOq+6s+TyC2qnu12B1WZb9sx9jQl0mBHd9gx/FgYDs8jfIr2vF4jRkejW/moaVqvCd/FLyS91eCMXQjIXdGKWKPUUL7GEBqdZRLnYSJOqgPp9sk1+NEvMabTXlWmoUjaShq8z+o7JsQ== nileeeem36@gmail.com + state: present + role: admin + - name: markus + ssh_keys: + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDGvvjFxXqcdKn9kk8VHzm38R3nLWvvwP8W0e3uXxOgby/7LJZx2bosXCZ28FyPTYwVRezHE9lguKiaCo2kxqzNwwx64MzUFRH60sE5cYeH1IqjCBTY3Ht8hkZlYaVoRmsHiqiqogW+bJPo8PBO+ydCh53KUdJFEOXAYvKZ/RfDsWh7/SjeQrQzpRFNeb9keefX+uNNBbKRm9/AEWIHFCGJpDvpJcz3i8hKbRPtXi5OTcEx1Kr4iOMikGXvGzsC1u84qgiy5moeBzpWeROwyJOHRLqPqQ/IHvUkE4F1BXen02G69nHpFdmjTOcjBbT1RzGTeWZs+ehc/kJaS3dUMHd5rSPsimjiCKZ5+wCAyxc5gJlQof71IpHVN4ZDoetH4Lo2bnLdA1YX6DaVU1Fd/6rPWw02DA1OEIhrjJ3Gak87/HUYGNhpZVyIxyNYGXBMPkmHCHCjzjN7sPdMRvkbl5tahD2PoS4172tsO7YYMfAZ/UYYZw745CDxQYIjjfrFRn8= markuslaurila@MacBook-Pro.local + state: present + role: admin + - name: vmudryi + ssh_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINgMcsSBwTE0EbMDRSF1T4vJDcN/5HAjKGbi2DqV7g/Q vmudryi@opencrvs.org + state: present + role: admin + - name: cihan + ssh_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEEtz5M5hYKcUehDiCm84BplV+3t1ex8DPjIsMtQEWGv cihan.m.bebek@gmail.com + state: present + role: admin + - name: shoumik + ssh_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINRSRTqm3vOfjyTutISEtbVp7ZddoWa9NZDZLeWZGzLy dsi@dsi-Inspiron-15-3511 + state: present + role: admin + - name: pankaj + ssh_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL23KrPmFT6tZxR5d7dsTybtDf5j9DWzJcoR/Y7iJfL6 github + state: present + role: admin + + children: + master: + hosts: + # Replace master with value returned by command: hostname + master: + # Keep values (ansible_host, ansible_connection) as is + # Ansible is executed on master node + ansible_host: localhost + ansible_connection: local + labels: + # traefik-role label is used to identify where to deploy traefik + traefik-role: ingress + + # Workers section is optional, for single node cluster feel free to remove this section + # section can be added later + # more workers can be added later as well + + workers: + hosts: + worker0: + ansible_host: 10.2.0.5 + labels: + # By default all datastores are deployed to worker node with role data1 + role: data1 + + worker1: + ansible_host: 10.2.0.6 + + + + # backup section is optional, feel free to remove if backups are not enabled + # section can be added later + + backup: + hosts: + backup1: + ansible_host: 10.2.0.3 diff --git a/infrastructure/server-setup/inventory/staging.yml b/infrastructure/server-setup/inventory/staging.yml new file mode 100644 index 00000000..8643f2f1 --- /dev/null +++ b/infrastructure/server-setup/inventory/staging.yml @@ -0,0 +1,118 @@ +all: + vars: + # Domain/IP address for remote access to your cluster API (see ~/.kube/config) + # - If you are behind VPN, use private IP address + # - If your server is exposed (not recommeded), use public IP address + # - If you would like to run kubectl commands from the remote server, leave this field empty + # kube_api_host: '' + kube_api_host: tmp-stg.opencrvs.dev + # Default ansible provision user, keep as is + ansible_user: provision + + # single_node: + # For development/qa/testing/staging keep true + # For production keep false + # Defaults production configuration: + # - master node + # - 2 worker nodes + single_node: true + + # users: Add as many users as you wish + # Configuration example + # - name: + # ssh_keys: + # - + # - + # state: present + # role: admin + # Allowed roles: + # - operator, read only access to OS, full access to kubernetes cluster + # - admin, full access + # Allowed states: + # - present, user is allowed to login + # - absent, account is disabled + users: + - name: pyry + ssh_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJBcrSLLdrkLrhqNQi7Uo/ZIWXb1y4kc0vGb16e2s0Jq pyry@opencrvs.org + state: present + role: admin + - name: tameem + ssh_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGUprcQyUFYwRto0aRpgriR95C1pgNxrQ0lEWEe1D8he haidertameem@gmail.com + state: present + role: admin + - name: riku + ssh_keys: + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDWIF63S4f3z9wQMvWibmvl7MPuJ6EVrkP0HuvgNhcs/4DZYMcR/GRBvV4ldOSYMlBevIXycgGzNDxKJgENUuwIWanjBu7uVAHyD6+cIRD1h63qq7Cjv/2HYTfBDKOrKzPOhA6zWvKO0ZGWsjRXk5LWMCbKOkvKJCxOpj/NVBxeE4FTK5YADYPV3OSsmBtqTHrVLm2sMmShU/2hMYYswWkobidjX65+nK/X+3C+yJbHwiydVvn+QCrFlFfCLPWKe8rUpOxyxofPqWVQh6CHhHfT8okaOc9sOE8Qeip9ljo84DftJh3Xm3ynOdWK1hH2BvRvxNadWqcE1qECbkg4tx2x riku.rouvila@gmail.com + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDGfWxxQHJv6Md/vBVoDH2UNm/uYgIBlFpP1mfh2Yj6jRNiQ/TQrfwpTawq0Sg+UW4LfYk5yxttsZ0h6L/v6PLiawgbMtf2ZqSviRTYSZTSihkK2zLmeJA2ByBCh57w4tR6IGqJK4w0kjYQSaaU6V5skQ4u+gnLQoKtkVQ4K34EFXAiIur96tLwjwDd/xCm+9T91+cAxGLv8Pe0PjirjwnvktUtzpgOhedkYK7KX0l8SKxQXUK6Ul2/QbpGO3rmguzEdtrl3Dw1TAEfu2njXbNGVQ+JWV9htH+ymsMIGoeumJRaaAZ4AXLlQPBCxTXcdQDuAjfFDPuppms/h7qB1S4Aioz7zqyd7pL7Z6Z8mJBZZlP3PsfGvADM2CdShpbL4HAa+n9miNNSYcJ7cHvC/zCitNjfaEYLVYkB5G+ggeK8Ss/MDcnsh3YFB8WnT582zt/TTJda5n+5Q7tquc1m+61t2gEKKTfBoDft9UYW2/4ViHj3ROL2Oyj7udrh/oAqV8M= riku@MBP16inch2231 + state: present + role: admin + - name: euan + ssh_keys: + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDECqHO65UpyrrO8uueD06RxGaVVq22f152Rf8qVQQAAIGAMu6gCs7ztlZ8a3yQgSEIjM/Jl1/RqIVs6CziTEef74nLFTZ5Ufz3CLRVgdebBeSBEmhTfTUV0HLkSyNzwKFpuzJxucGd72ulPvEp6eHvyJAPJz37YcU8cjaL1v05T6s2ee99li35GlDDtCzfjVV4ZPAg5JdfWuTj41RAVC0LQhk2/NB4qEu37UxGGjhRFSjBEsS5LxI9QfvgrsHpl/VOn+soH7ZkK7kS6qRgNP/uYsXRWXhHaamcl5OX68gJWTbrW6c7PCqlbCWGnsHJswCmqPIthwXXMfC7ULDNLSKG6mslAt5Dyc8/MCr3vTW7pDyr2d0FvvY86SMQUggxv3qF7TZewqfX1bhK0fMLarIxVMQ1RFo//wN9QGA+2we8rxd2Y1Kr1DBuJyuwXPfv+Exo8yNYQ+x/AYH5k6UVcSYuaB8eYmplG2KQCxt8RBFtoChrwOKNRWLqXdKyfpdp5XmnnWxPvR95gf3h3yLocVYkF0i0uvKKJ0vt8J0Ezfkdfow0B1kUg5bPXKJROX7PwbaCPdYcxyDaO6wwOigRnSmoFvkH1pLb4j1RQAXcX531CHgfN6Izi/h0mpMS4bnyIUcv2GQr+h4z4TxcCtj7qpH2y6yw7XG12jVh7TfeesXG2Q== euanmillar77@gmail.com + state: present + role: admin + - name: tahmid + ssh_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINUml9O5ySwPtEMD1yGEYHlf9Z3jro97NWAnM9+ew9gn tahmidrahman.dsi@gmail.com + state: present + role: admin + - name: tareq + ssh_keys: + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCWQihdKkwxTItN+rwYAX1vBg+8sv59sFsjYoVaO2mzS01rARfh+M+UVqpEv3zFT/3v6Dr5Z5VhzYvvbH8akiGQxURqie9quEi1iBCqcq+LApkMZxNm7yyvexlFsbkKMHsSZyVCzjE2Wt+6fwR1NqkMQgJjZS+b4CB+CUTNP2i6ytUTmck9K5iAOp1Gpm+Xgyvz6ZEJPkAJ16gV7gzNJUt/DSCkCyV8G2BqYLWeR2QxAbKyuf3LzO5i4XZdiZi9o60QAt3A6KGGLazd0UuYdehQDqVwXzwimLeeuZbaPNmwoAy7DeatOdurrWbnL7ytaiPvAbwai6Grt3PhhM41qO+uojnqTdnFdSOEPVIYMR7+mYu9tuwHZcMJIbbvMPD6EvKumD5Ndn5OxiLY/zQF5PuG89pBdTkTzzREvbV1Dkh2hwAIvgavlZl3P64On+4+FAgjrAx5U55khoRAe2FbEvB+EUGwro0bRffiM2NmxkUBraEuT2Xt5K01ZoBU6F4feO0= tareq.aziz@dsinnovators.com + state: present + role: admin + - name: jamil + ssh_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINMSNTIIsM0C3uJg3V/Fqh2gi4lvl2y6nenrb2Ft1JlX jamil31415926@gmail.com + state: present + role: admin + - name: ashikul + ssh_keys: + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDFr/v3hUGEbc2wQsDLCmqLrwiz964yVrnLZ6kafemjmX8aRGLp1CNFvrZ674SLnXidZGMkx9d5xVvv8IdFR3R50MqSqfolF43MV34/JVHjQHh9Vk4MJT/3GIaeNmr2GQ/38qAmt2BQn1ecnb7FjNO2bFvHokLhm2wCXt+A4avuTgJe0p4e6uu01IHeIzDb5sPzZ3ID0h6jJnjEDcET+Lf5NGpCjn7YKhLhBWSSl9cXQdOGLzNzg3aBk32kgJ1beP1funSeVd0jniJPZeZRC1G/kRdqBUOHKiENtwgquzZxXzdHkZV9+4mF7YGlx6LpQdNuDpW7JADtYNldtdbexdyfrgNoRzKwyMmaKNDbeHd1FsIHSDJmGm9hCoLTM2dEtsGzgghfe0tat8sOWmsj5v2en0V8rKV+w8OQEmHtaQkgMjqmZaAnd8uWiB2xIbrUuax5Pq8zkj37xnfbRxUPOEkMlOUbhh1wzGbqeUEB7nbv/vXZxwC0b7ryMk5egBP+0ZRONsdib9RkSTr3B9uSb7iTOQftdhy+CTqqOq+6s+TyC2qnu12B1WZb9sx9jQl0mBHd9gx/FgYDs8jfIr2vF4jRkejW/moaVqvCd/FLyS91eCMXQjIXdGKWKPUUL7GEBqdZRLnYSJOqgPp9sk1+NEvMabTXlWmoUjaShq8z+o7JsQ== nileeeem36@gmail.com + state: present + role: admin + - name: markus + ssh_keys: + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDGvvjFxXqcdKn9kk8VHzm38R3nLWvvwP8W0e3uXxOgby/7LJZx2bosXCZ28FyPTYwVRezHE9lguKiaCo2kxqzNwwx64MzUFRH60sE5cYeH1IqjCBTY3Ht8hkZlYaVoRmsHiqiqogW+bJPo8PBO+ydCh53KUdJFEOXAYvKZ/RfDsWh7/SjeQrQzpRFNeb9keefX+uNNBbKRm9/AEWIHFCGJpDvpJcz3i8hKbRPtXi5OTcEx1Kr4iOMikGXvGzsC1u84qgiy5moeBzpWeROwyJOHRLqPqQ/IHvUkE4F1BXen02G69nHpFdmjTOcjBbT1RzGTeWZs+ehc/kJaS3dUMHd5rSPsimjiCKZ5+wCAyxc5gJlQof71IpHVN4ZDoetH4Lo2bnLdA1YX6DaVU1Fd/6rPWw02DA1OEIhrjJ3Gak87/HUYGNhpZVyIxyNYGXBMPkmHCHCjzjN7sPdMRvkbl5tahD2PoS4172tsO7YYMfAZ/UYYZw745CDxQYIjjfrFRn8= markuslaurila@MacBook-Pro.local + state: present + role: admin + - name: vmudryi + ssh_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINgMcsSBwTE0EbMDRSF1T4vJDcN/5HAjKGbi2DqV7g/Q vmudryi@opencrvs.org + state: present + role: admin + - name: cihan + ssh_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEEtz5M5hYKcUehDiCm84BplV+3t1ex8DPjIsMtQEWGv cihan.m.bebek@gmail.com + state: present + role: admin + - name: shoumik + ssh_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINRSRTqm3vOfjyTutISEtbVp7ZddoWa9NZDZLeWZGzLy dsi@dsi-Inspiron-15-3511 + state: present + role: admin + - name: pankaj + ssh_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL23KrPmFT6tZxR5d7dsTybtDf5j9DWzJcoR/Y7iJfL6 github + state: present + role: admin + - name: alise + ssh_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINRSRTqm3vOfjyTutISEtbVp7ZddoWa9NZDZLeWZGzLy/Y7iJfL6 github + state: present + role: operator + + children: + master: + hosts: + # Replace master with value returned by command: hostname + master: + # Keep values (ansible_host, ansible_connection) as is + # Ansible is executed on master node + ansible_host: localhost + ansible_connection: local + labels: + # traefik-role label is used to identify where to deploy traefik + traefik-role: ingress diff --git a/infrastructure/server-setup/k8s.yml b/infrastructure/server-setup/k8s.yml index 125ef615..179512f8 100644 --- a/infrastructure/server-setup/k8s.yml +++ b/infrastructure/server-setup/k8s.yml @@ -61,7 +61,8 @@ - kubernetes-installation tags: - kubernetes-installation -- name: Initialize Kubernetes Master + +- name: Kubernetes Master hosts: master tags: k8s tasks: @@ -72,7 +73,7 @@ - name: Upgrade cluster include_tasks: tasks/k8s/upgrade-k8s-master.yml -- name: Join Worker Nodes +- name: Kubernetes Worker Nodes hosts: workers tags: - join-workers @@ -94,8 +95,6 @@ include_tasks: tasks/k8s/metrics-server.yml - name: Install k8s self-hosted runner include_tasks: tasks/k8s/self-hosted-runner.yml - - name: Create backup server secret - include_tasks: tasks/k8s/backup-secret.yml - name: Additional configuration become: yes diff --git a/infrastructure/server-setup/tasks/all/users.yml b/infrastructure/server-setup/tasks/all/users.yml index 518d4d9c..66af1505 100644 --- a/infrastructure/server-setup/tasks/all/users.yml +++ b/infrastructure/server-setup/tasks/all/users.yml @@ -144,15 +144,15 @@ - name: Check short Diffie-Hellman keys ansible.builtin.shell: | - awk '$5 < 3071' /etc/ssh/moduli | grep -q . + awk '$5 < 3071' /etc/ssh/moduli | grep -q . && echo "found" || echo "none" register: short_dh_keys - ignore_errors: yes + changed_when: false - name: Remove short Diffie-Hellman keys ansible.builtin.shell: | awk '$5 >= 3071' /etc/ssh/moduli > /etc/ssh/moduli.safe mv /etc/ssh/moduli.safe /etc/ssh/moduli - when: short_dh_keys.rc == 0 + when: '"found" in short_dh_keys.stdout' become: yes # Cleanup weak server keys diff --git a/infrastructure/server-setup/tasks/backups/create-backup-server-credentials.yml b/infrastructure/server-setup/tasks/backups/create-backup-server-credentials.yml index 064c38ae..5239ba3c 100644 --- a/infrastructure/server-setup/tasks/backups/create-backup-server-credentials.yml +++ b/infrastructure/server-setup/tasks/backups/create-backup-server-credentials.yml @@ -6,29 +6,12 @@ owner: backup group: backup -- name: Generate an ed25519 SSH keypair for backup user if not present - openssh_keypair: - path: "{{ backup_ssh_key_path }}" - type: ed25519 - owner: backup - group: backup - mode: '0600' - force: false - register: backup_keypair_result - - name: Add public key to authorized_keys ansible.builtin.lineinfile: path: /home/backup/.ssh/authorized_keys - line: "{{ backup_keypair_result.public_key }}" + line: "{{ backup_host_public_key }}" create: yes owner: backup group: backup mode: 0600 when: backup_ssh_key_path is defined - -- name: Fetch private key to control/master node - fetch: - src: /home/backup/.ssh/id_ed25519 - dest: "/tmp/id_ed25519_backup" - flat: yes - mode: '0600' diff --git a/infrastructure/server-setup/tasks/k8s/backup-secret.yml b/infrastructure/server-setup/tasks/k8s/backup-secret.yml deleted file mode 100644 index 0806ba24..00000000 --- a/infrastructure/server-setup/tasks/k8s/backup-secret.yml +++ /dev/null @@ -1,35 +0,0 @@ -- name: Check if /tmp/id_ed25519_backup exists - stat: - path: /tmp/id_ed25519_backup - register: backup_key_file -- name: Read the backup SSH key file - ansible.builtin.slurp: - src: /tmp/id_ed25519_backup - register: ssh_key_file - when: backup_key_file.stat.exists -- name: Ensure namespace opencrvs-deps-{{ k8s_cluster_env }} exists - kubernetes.core.k8s: - api_version: v1 - kind: Namespace - name: opencrvs-deps-{{ k8s_cluster_env }} - state: present - when: backup_key_file.stat.exists -- name: Ensure the Kubernetes secret for backup exists - kubernetes.core.k8s: - kubeconfig: ~/.kube/config - state: present - namespace: "opencrvs-deps-{{ k8s_cluster_env }}" - definition: - apiVersion: v1 - kind: Secret - metadata: - name: "{{ backup_server_ssh_credentials_secret }}" - type: Opaque - data: - ssh_key: "{{ ssh_key_file.content | default('') }}" - user: "{{ backup_server_user | b64encode }}" - host: "{{ hostvars[groups['backup'][0]]['ansible_host'] | b64encode }}" - when: - - "'backup' in groups" - - "groups['backup'] | length > 0" - - backup_key_file.stat.exists \ No newline at end of file diff --git a/package.json b/package.json index 413eb7ba..e33c87d3 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@types/libsodium-wrappers": "^0.7.10", "@types/prompts": "^2.4.9", "@types/js-yaml": "4.0.9", + "@types/ssh2": "^1.15.5", "husky": "9.1.7", "kleur": "^4.1.5", "libsodium-wrappers": "^0.7.13", @@ -32,6 +33,7 @@ "dependencies": { "@types/node": "^24.0.0", "dotenv": "^16.4.5", - "prompts": "^2.4.2" + "prompts": "^2.4.2", + "ssh2": "^1.17.0" } } diff --git a/yarn.lock b/yarn.lock index a9c4f069..7e22ede2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -121,9 +121,9 @@ "@octokit/openapi-types" "^18.0.0" "@tsconfig/node10@^1.0.7": - version "1.0.11" - resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2" - integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw== + version "1.0.12" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.12.tgz#be57ceac1e4692b41be9de6be8c32a106636dba4" + integrity sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ== "@tsconfig/node12@^1.0.7": version "1.0.11" @@ -151,16 +151,23 @@ integrity sha512-5Kv68fXuXK0iDuUir1WPGw2R9fOZUlYlSAa0ztMcL0s0BfIDTqg9GXz8K30VJpPP3sxWhbolnQma2x+/TfkzDQ== "@types/node@*": - version "24.5.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-24.5.1.tgz#dab6917c47113eb4502d27d06e89a407ec0eff95" - integrity sha512-/SQdmUP2xa+1rdx7VwB9yPq8PaKej8TD5cQ+XfKDPWWC+VDJU4rvVVagXqKUzhKjtFoNA8rXDJAkCxQPAe00+Q== + version "25.0.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-25.0.3.tgz#79b9ac8318f373fbfaaf6e2784893efa9701f269" + integrity sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA== + dependencies: + undici-types "~7.16.0" + +"@types/node@^18.11.18": + version "18.19.130" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.130.tgz#da4c6324793a79defb7a62cba3947ec5add00d59" + integrity sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg== dependencies: - undici-types "~7.12.0" + undici-types "~5.26.4" "@types/node@^24.0.0": - version "24.9.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-24.9.1.tgz#b7360b3c789089e57e192695a855aa4f6981a53c" - integrity sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg== + version "24.10.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-24.10.4.tgz#9d27c032a1b2c42a4eab8fb65c5856a8b8e098c4" + integrity sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg== dependencies: undici-types "~7.16.0" @@ -172,6 +179,13 @@ "@types/node" "*" kleur "^3.0.3" +"@types/ssh2@^1.15.5": + version "1.15.5" + resolved "https://registry.yarnpkg.com/@types/ssh2/-/ssh2-1.15.5.tgz#6d8f45db2f39519b8d9377268fa71ed77d969686" + integrity sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ== + dependencies: + "@types/node" "^18.11.18" + acorn-walk@^8.1.1: version "8.3.4" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7" @@ -216,11 +230,30 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +asn1@^0.2.6: + version "0.2.6" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" + integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== + dependencies: + safer-buffer "~2.1.0" + +bcrypt-pbkdf@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== + dependencies: + tweetnacl "^0.14.3" + before-after-hook@^2.2.0: version "2.2.3" resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.3.tgz#c51e809c81a4e354084422b9b26bad88249c517c" integrity sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ== +buildcheck@~0.0.6: + version "0.0.7" + resolved "https://registry.yarnpkg.com/buildcheck/-/buildcheck-0.0.7.tgz#07a5e76c10ead8fa67d9e4c587b68f49e8f29d61" + integrity sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA== + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -233,6 +266,14 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +cpu-features@~0.0.10: + version "0.0.10" + resolved "https://registry.yarnpkg.com/cpu-features/-/cpu-features-0.0.10.tgz#9aae536db2710c7254d7ed67cb3cbc7d29ad79c5" + integrity sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA== + dependencies: + buildcheck "~0.0.6" + nan "^2.19.0" + create-require@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" @@ -354,9 +395,9 @@ libsodium@^0.7.15: integrity sha512-sZwRknt/tUpE2AwzHq3jEyUU5uvIZHtSssktXq7owd++3CSgn8RGrv6UZJJBpP7+iBghBqe7Z06/2M31rI2NKw== lru-cache@^11.0.0: - version "11.2.2" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.2.2.tgz#40fd37edffcfae4b2940379c0722dc6eeaa75f24" - integrity sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg== + version "11.2.4" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.2.4.tgz#ecb523ebb0e6f4d837c807ad1abaea8e0619770d" + integrity sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg== make-error@^1.1.1: version "1.3.6" @@ -364,9 +405,9 @@ make-error@^1.1.1: integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== minimatch@^10.0.3: - version "10.0.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.3.tgz#cf7a0314a16c4d9ab73a7730a0e8e3c3502d47aa" - integrity sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw== + version "10.1.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.1.1.tgz#e6e61b9b0c1dcab116b5a7d1458e8b6ae9e73a55" + integrity sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ== dependencies: "@isaacs/brace-expansion" "^5.0.0" @@ -375,6 +416,11 @@ minipass@^7.1.2: resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== +nan@^2.19.0, nan@^2.23.0: + version "2.24.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.24.0.tgz#a8919b36e692aa5b260831910e4f81419fc0a283" + integrity sha512-Vpf9qnVW1RaDkoNKFUvfxqAbtI8ncb8OJlqZ9wwpXzWPEsvsB1nvdUi6oYrHIkQ1Y/tMDnr1h4nczS0VB9Xykg== + node-fetch@^2.6.7: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" @@ -400,9 +446,9 @@ path-key@^3.1.0: integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== path-scurry@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.0.tgz#9f052289f23ad8bf9397a2a0425e7b8615c58580" - integrity sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg== + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.1.tgz#4b6572376cfd8b811fca9cd1f5c24b3cbac0fe10" + integrity sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA== dependencies: lru-cache "^11.0.0" minipass "^7.1.2" @@ -415,6 +461,11 @@ prompts@^2.4.2: kleur "^3.0.3" sisteransi "^1.0.5" +safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -437,6 +488,17 @@ sisteransi@^1.0.5: resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== +ssh2@^1.17.0: + version "1.17.0" + resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-1.17.0.tgz#dc686e8e3abdbd4ad95d46fa139615903c12258c" + integrity sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ== + dependencies: + asn1 "^0.2.6" + bcrypt-pbkdf "^1.0.2" + optionalDependencies: + cpu-features "~0.0.10" + nan "^2.23.0" + "string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -509,15 +571,20 @@ ts-node@^10.9.1: v8-compile-cache-lib "^3.0.1" yn "3.1.1" +tweetnacl@^0.14.3: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== + typescript@^5.1.6: - version "5.9.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.2.tgz#d93450cddec5154a2d5cabe3b8102b83316fb2a6" - integrity sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A== - -undici-types@~7.12.0: - version "7.12.0" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.12.0.tgz#15c5c7475c2a3ba30659529f5cdb4674b622fafb" - integrity sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ== + version "5.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" + integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== undici-types@~7.16.0: version "7.16.0"