mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-14 20:55:18 +00:00
Compare commits
1 Commits
1.18.4-s.4
...
1.12.2-s.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63e41e3dcd |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
Always localize strings and use the `t` function to convert keys to strings. Add the keys to the en-us.json file. Never edit the other language files, as en-us.json is the single source of truth.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
description:
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
Proxy resources = public resources
|
||||
Private resources = client resources = site resources
|
||||
@@ -28,9 +28,7 @@ LICENSE
|
||||
CONTRIBUTING.md
|
||||
dist
|
||||
.git
|
||||
server/migrations/
|
||||
migrations/
|
||||
config/
|
||||
build.ts
|
||||
tsconfig.json
|
||||
Dockerfile*
|
||||
drizzle.config.ts
|
||||
tsconfig.json
|
||||
@@ -1,3 +1,6 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||
"extends": [
|
||||
"next/core-web-vitals",
|
||||
"next/typescript"
|
||||
]
|
||||
}
|
||||
|
||||
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -1 +0,0 @@
|
||||
* @oschwartz10612 @miloschwartz
|
||||
14
.github/dependabot.yml
vendored
14
.github/dependabot.yml
vendored
@@ -44,9 +44,19 @@ updates:
|
||||
schedule:
|
||||
interval: "daily"
|
||||
groups:
|
||||
patch-updates:
|
||||
dev-patch-updates:
|
||||
dependency-type: "development"
|
||||
update-types:
|
||||
- "patch"
|
||||
minor-updates:
|
||||
dev-minor-updates:
|
||||
dependency-type: "development"
|
||||
update-types:
|
||||
- "minor"
|
||||
prod-patch-updates:
|
||||
dependency-type: "production"
|
||||
update-types:
|
||||
- "patch"
|
||||
prod-minor-updates:
|
||||
dependency-type: "production"
|
||||
update-types:
|
||||
- "minor"
|
||||
482
.github/workflows/cicd.yml
vendored
482
.github/workflows/cicd.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Public CICD Pipeline
|
||||
name: CI/CD Pipeline
|
||||
|
||||
# CI/CD workflow for building, publishing, mirroring, signing container images and building release binaries.
|
||||
# Actions are pinned to specific SHAs to reduce supply-chain risk. This workflow triggers on tag push events.
|
||||
@@ -17,42 +17,16 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- "[0-9]+.[0-9]+.[0-9]+"
|
||||
- "[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+"
|
||||
- "[0-9]+.[0-9]+.[0-9]+.rc.[0-9]+"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
pre-run:
|
||||
runs-on: ubuntu-latest
|
||||
permissions: write-all
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||
role-duration-seconds: 3600
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
|
||||
- name: Verify AWS identity
|
||||
run: aws sts get-caller-identity
|
||||
|
||||
- name: Start EC2 instances
|
||||
run: |
|
||||
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
|
||||
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_AMD_RUNNER }}
|
||||
echo "EC2 instances started"
|
||||
|
||||
|
||||
release-arm:
|
||||
name: Build and Release (ARM64)
|
||||
runs-on: [self-hosted, linux, arm64, us-east-1]
|
||||
needs: [pre-run]
|
||||
if: >-
|
||||
${{
|
||||
needs.pre-run.result == 'success'
|
||||
}}
|
||||
release:
|
||||
name: Build and Release
|
||||
runs-on: [self-hosted, linux, x64]
|
||||
# Job-level timeout to avoid runaway or stuck runs
|
||||
timeout-minutes: 120
|
||||
env:
|
||||
@@ -62,211 +36,29 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Monitor storage space
|
||||
run: |
|
||||
THRESHOLD=75
|
||||
USED_SPACE=$(df / | grep / | awk '{ print $5 }' | sed 's/%//g')
|
||||
echo "Used space: $USED_SPACE%"
|
||||
if [ "$USED_SPACE" -ge "$THRESHOLD" ]; then
|
||||
echo "Used space is below the threshold of 75% free. Running Docker system prune."
|
||||
echo y | docker system prune -a
|
||||
else
|
||||
echo "Storage space is above the threshold. No action needed."
|
||||
fi
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
|
||||
- name: Extract tag name
|
||||
id: get-tag
|
||||
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Update version in package.json
|
||||
run: |
|
||||
TAG=${{ env.TAG }}
|
||||
sed -i "s/export const APP_VERSION = \".*\";/export const APP_VERSION = \"$TAG\";/" server/lib/consts.ts
|
||||
cat server/lib/consts.ts
|
||||
shell: bash
|
||||
|
||||
- name: Check if release candidate
|
||||
id: check-rc
|
||||
run: |
|
||||
TAG=${{ env.TAG }}
|
||||
if [[ "$TAG" == *"-rc."* ]]; then
|
||||
echo "IS_RC=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "IS_RC=false" >> $GITHUB_ENV
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Build and push Docker images (Docker Hub - ARM64)
|
||||
run: |
|
||||
TAG=${{ env.TAG }}
|
||||
if [ "$IS_RC" = "true" ]; then
|
||||
make build-rc-arm tag=$TAG
|
||||
else
|
||||
make build-release-arm tag=$TAG
|
||||
fi
|
||||
echo "Built & pushed ARM64 images to: ${{ env.DOCKERHUB_IMAGE }}:${TAG}"
|
||||
shell: bash
|
||||
|
||||
release-amd:
|
||||
name: Build and Release (AMD64)
|
||||
runs-on: [self-hosted, linux, x64, us-east-1]
|
||||
needs: [pre-run]
|
||||
if: >-
|
||||
${{
|
||||
needs.pre-run.result == 'success'
|
||||
}}
|
||||
# Job-level timeout to avoid runaway or stuck runs
|
||||
timeout-minutes: 120
|
||||
env:
|
||||
# Target images
|
||||
DOCKERHUB_IMAGE: docker.io/fosrl/${{ github.event.repository.name }}
|
||||
GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Monitor storage space
|
||||
run: |
|
||||
THRESHOLD=75
|
||||
USED_SPACE=$(df / | grep / | awk '{ print $5 }' | sed 's/%//g')
|
||||
echo "Used space: $USED_SPACE%"
|
||||
if [ "$USED_SPACE" -ge "$THRESHOLD" ]; then
|
||||
echo "Used space is below the threshold of 75% free. Running Docker system prune."
|
||||
echo y | docker system prune -a
|
||||
else
|
||||
echo "Storage space is above the threshold. No action needed."
|
||||
fi
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
|
||||
- name: Extract tag name
|
||||
id: get-tag
|
||||
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Update version in package.json
|
||||
run: |
|
||||
TAG=${{ env.TAG }}
|
||||
sed -i "s/export const APP_VERSION = \".*\";/export const APP_VERSION = \"$TAG\";/" server/lib/consts.ts
|
||||
cat server/lib/consts.ts
|
||||
shell: bash
|
||||
|
||||
- name: Check if release candidate
|
||||
id: check-rc
|
||||
run: |
|
||||
TAG=${{ env.TAG }}
|
||||
if [[ "$TAG" == *"-rc."* ]]; then
|
||||
echo "IS_RC=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "IS_RC=false" >> $GITHUB_ENV
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Build and push Docker images (Docker Hub - AMD64)
|
||||
run: |
|
||||
TAG=${{ env.TAG }}
|
||||
if [ "$IS_RC" = "true" ]; then
|
||||
make build-rc-amd tag=$TAG
|
||||
else
|
||||
make build-release-amd tag=$TAG
|
||||
fi
|
||||
echo "Built & pushed AMD64 images to: ${{ env.DOCKERHUB_IMAGE }}:${TAG}"
|
||||
shell: bash
|
||||
|
||||
create-manifest:
|
||||
name: Create Multi-Arch Manifests
|
||||
runs-on: [self-hosted, linux, x64, us-east-1]
|
||||
needs: [release-arm, release-amd]
|
||||
if: >-
|
||||
${{
|
||||
needs.release-arm.result == 'success' &&
|
||||
needs.release-amd.result == 'success'
|
||||
}}
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
|
||||
- name: Extract tag name
|
||||
id: get-tag
|
||||
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Check if release candidate
|
||||
id: check-rc
|
||||
run: |
|
||||
TAG=${{ env.TAG }}
|
||||
if [[ "$TAG" == *"-rc."* ]]; then
|
||||
echo "IS_RC=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "IS_RC=false" >> $GITHUB_ENV
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Create multi-arch manifests
|
||||
run: |
|
||||
TAG=${{ env.TAG }}
|
||||
if [ "$IS_RC" = "true" ]; then
|
||||
make create-manifests-rc tag=$TAG
|
||||
else
|
||||
make create-manifests tag=$TAG
|
||||
fi
|
||||
echo "Created multi-arch manifests for tag: ${TAG}"
|
||||
shell: bash
|
||||
|
||||
sign-and-package:
|
||||
name: Sign and Package
|
||||
runs-on: [self-hosted, linux, x64, us-east-1]
|
||||
needs: [release-arm, release-amd, create-manifest]
|
||||
if: >-
|
||||
${{
|
||||
needs.release-arm.result == 'success' &&
|
||||
needs.release-amd.result == 'success' &&
|
||||
needs.create-manifest.result == 'success'
|
||||
}}
|
||||
# Job-level timeout to avoid runaway or stuck runs
|
||||
timeout-minutes: 120
|
||||
env:
|
||||
# Target images
|
||||
DOCKERHUB_IMAGE: docker.io/fosrl/${{ github.event.repository.name }}
|
||||
GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Extract tag name
|
||||
id: get-tag
|
||||
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: 1.25
|
||||
go-version: 1.24
|
||||
|
||||
- name: Update version in package.json
|
||||
run: |
|
||||
@@ -289,21 +81,36 @@ jobs:
|
||||
echo "LATEST_BADGER_TAG=$LATEST_TAG" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Update install/main.go
|
||||
run: |
|
||||
PANGOLIN_VERSION=${{ env.TAG }}
|
||||
GERBIL_VERSION=${{ env.LATEST_GERBIL_TAG }}
|
||||
BADGER_VERSION=${{ env.LATEST_BADGER_TAG }}
|
||||
sed -i "s/config.PangolinVersion = \".*\"/config.PangolinVersion = \"$PANGOLIN_VERSION\"/" install/main.go
|
||||
sed -i "s/config.GerbilVersion = \".*\"/config.GerbilVersion = \"$GERBIL_VERSION\"/" install/main.go
|
||||
sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"$BADGER_VERSION\"/" install/main.go
|
||||
echo "Updated install/main.go with Pangolin version $PANGOLIN_VERSION, Gerbil version $GERBIL_VERSION, and Badger version $BADGER_VERSION"
|
||||
cat install/main.go
|
||||
shell: bash
|
||||
|
||||
- name: Build installer
|
||||
working-directory: install
|
||||
run: |
|
||||
make go-build-release \
|
||||
PANGOLIN_VERSION=${{ env.TAG }} \
|
||||
GERBIL_VERSION=${{ env.LATEST_GERBIL_TAG }} \
|
||||
BADGER_VERSION=${{ env.LATEST_BADGER_TAG }}
|
||||
shell: bash
|
||||
make go-build-release
|
||||
|
||||
- name: Upload artifacts from /install/bin
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: install-bin
|
||||
path: install/bin/
|
||||
|
||||
- name: Build and push Docker images (Docker Hub)
|
||||
run: |
|
||||
TAG=${{ env.TAG }}
|
||||
make build-release tag=$TAG
|
||||
echo "Built & pushed to: ${{ env.DOCKERHUB_IMAGE }}:${TAG}"
|
||||
shell: bash
|
||||
|
||||
- name: Install skopeo + jq
|
||||
# skopeo: copy/inspect images between registries
|
||||
# jq: JSON parsing tool used to extract digest values
|
||||
@@ -314,215 +121,60 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
- name: Login to GHCR
|
||||
env:
|
||||
REGISTRY_AUTH_FILE: ${{ runner.temp }}/containers/auth.json
|
||||
run: |
|
||||
mkdir -p "$(dirname "$REGISTRY_AUTH_FILE")"
|
||||
skopeo login ghcr.io -u "${{ github.actor }}" -p "${{ secrets.GITHUB_TOKEN }}"
|
||||
shell: bash
|
||||
|
||||
- name: Copy tags from Docker Hub to GHCR
|
||||
# Mirror the already-built images (all architectures) to GHCR so we can sign them
|
||||
# Wait a bit for both architectures to be available in Docker Hub manifest
|
||||
env:
|
||||
REGISTRY_AUTH_FILE: ${{ runner.temp }}/containers/auth.json
|
||||
- name: Copy tag from Docker Hub to GHCR
|
||||
# Mirror the already-built image (all architectures) to GHCR so we can sign it
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG=${{ env.TAG }}
|
||||
MAJOR_TAG=$(echo $TAG | cut -d. -f1)
|
||||
MINOR_TAG=$(echo $TAG | cut -d. -f1,2)
|
||||
|
||||
echo "Waiting for multi-arch manifests to be ready..."
|
||||
sleep 30
|
||||
|
||||
# Determine if this is an RC release
|
||||
IS_RC="false"
|
||||
if [[ "$TAG" == *"-rc."* ]]; then
|
||||
IS_RC="true"
|
||||
fi
|
||||
|
||||
if [ "$IS_RC" = "true" ]; then
|
||||
echo "RC release detected - copying version-specific tags only"
|
||||
|
||||
# SQLite OSS
|
||||
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:${TAG} -> ${{ env.GHCR_IMAGE }}:${TAG}"
|
||||
skopeo copy --all --retry-times 3 \
|
||||
docker://$DOCKERHUB_IMAGE:$TAG \
|
||||
docker://$GHCR_IMAGE:$TAG
|
||||
|
||||
# PostgreSQL OSS
|
||||
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:postgresql-${TAG} -> ${{ env.GHCR_IMAGE }}:postgresql-${TAG}"
|
||||
skopeo copy --all --retry-times 3 \
|
||||
docker://$DOCKERHUB_IMAGE:postgresql-$TAG \
|
||||
docker://$GHCR_IMAGE:postgresql-$TAG
|
||||
|
||||
# SQLite Enterprise
|
||||
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:ee-${TAG} -> ${{ env.GHCR_IMAGE }}:ee-${TAG}"
|
||||
skopeo copy --all --retry-times 3 \
|
||||
docker://$DOCKERHUB_IMAGE:ee-$TAG \
|
||||
docker://$GHCR_IMAGE:ee-$TAG
|
||||
|
||||
# PostgreSQL Enterprise
|
||||
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:ee-postgresql-${TAG} -> ${{ env.GHCR_IMAGE }}:ee-postgresql-${TAG}"
|
||||
skopeo copy --all --retry-times 3 \
|
||||
docker://$DOCKERHUB_IMAGE:ee-postgresql-$TAG \
|
||||
docker://$GHCR_IMAGE:ee-postgresql-$TAG
|
||||
else
|
||||
echo "Regular release detected - copying all tags (latest, major, minor, full version)"
|
||||
|
||||
# SQLite OSS - all tags
|
||||
for TAG_SUFFIX in "latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"; do
|
||||
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:${TAG_SUFFIX} -> ${{ env.GHCR_IMAGE }}:${TAG_SUFFIX}"
|
||||
skopeo copy --all --retry-times 3 \
|
||||
docker://$DOCKERHUB_IMAGE:$TAG_SUFFIX \
|
||||
docker://$GHCR_IMAGE:$TAG_SUFFIX
|
||||
done
|
||||
|
||||
# PostgreSQL OSS - all tags
|
||||
for TAG_SUFFIX in "latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"; do
|
||||
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:postgresql-${TAG_SUFFIX} -> ${{ env.GHCR_IMAGE }}:postgresql-${TAG_SUFFIX}"
|
||||
skopeo copy --all --retry-times 3 \
|
||||
docker://$DOCKERHUB_IMAGE:postgresql-$TAG_SUFFIX \
|
||||
docker://$GHCR_IMAGE:postgresql-$TAG_SUFFIX
|
||||
done
|
||||
|
||||
# SQLite Enterprise - all tags
|
||||
for TAG_SUFFIX in "latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"; do
|
||||
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:ee-${TAG_SUFFIX} -> ${{ env.GHCR_IMAGE }}:ee-${TAG_SUFFIX}"
|
||||
skopeo copy --all --retry-times 3 \
|
||||
docker://$DOCKERHUB_IMAGE:ee-$TAG_SUFFIX \
|
||||
docker://$GHCR_IMAGE:ee-$TAG_SUFFIX
|
||||
done
|
||||
|
||||
# PostgreSQL Enterprise - all tags
|
||||
for TAG_SUFFIX in "latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"; do
|
||||
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:ee-postgresql-${TAG_SUFFIX} -> ${{ env.GHCR_IMAGE }}:ee-postgresql-${TAG_SUFFIX}"
|
||||
skopeo copy --all --retry-times 3 \
|
||||
docker://$DOCKERHUB_IMAGE:ee-postgresql-$TAG_SUFFIX \
|
||||
docker://$GHCR_IMAGE:ee-postgresql-$TAG_SUFFIX
|
||||
done
|
||||
fi
|
||||
|
||||
echo "All images copied successfully to GHCR!"
|
||||
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:${TAG} -> ${{ env.GHCR_IMAGE }}:${TAG}"
|
||||
skopeo copy --all --retry-times 3 \
|
||||
docker://$DOCKERHUB_IMAGE:$TAG \
|
||||
docker://$GHCR_IMAGE:$TAG
|
||||
shell: bash
|
||||
|
||||
- name: Login to GitHub Container Registry (for cosign)
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install cosign
|
||||
# cosign is used to sign container images using keyless (OIDC) signing
|
||||
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
||||
# cosign is used to sign and verify container images (key and keyless)
|
||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
||||
|
||||
- name: Sign (GHCR, keyless)
|
||||
# Sign each GHCR image by digest using keyless (OIDC) signing via Sigstore/Rekor.
|
||||
# Signatures are stored in the registry alongside the image.
|
||||
- name: Dual-sign and verify (GHCR & Docker Hub)
|
||||
# Sign each image by digest using keyless (OIDC) and key-based signing,
|
||||
# then verify both the public key signature and the keyless OIDC signature.
|
||||
env:
|
||||
TAG: ${{ env.TAG }}
|
||||
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
|
||||
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
|
||||
COSIGN_PUBLIC_KEY: ${{ secrets.COSIGN_PUBLIC_KEY }}
|
||||
COSIGN_YES: "true"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Determine if this is an RC release
|
||||
IS_RC="false"
|
||||
if [[ "$TAG" == *"-rc."* ]]; then
|
||||
IS_RC="true"
|
||||
fi
|
||||
issuer="https://token.actions.githubusercontent.com"
|
||||
id_regex="^https://github.com/${{ github.repository }}/.+" # accept this repo (all workflows/refs)
|
||||
|
||||
# Define image variants to sign
|
||||
if [ "$IS_RC" = "true" ]; then
|
||||
echo "RC release - signing version-specific tags only"
|
||||
IMAGE_TAGS=(
|
||||
"${TAG}"
|
||||
"postgresql-${TAG}"
|
||||
"ee-${TAG}"
|
||||
"ee-postgresql-${TAG}"
|
||||
)
|
||||
else
|
||||
echo "Regular release - signing all tags"
|
||||
MAJOR_TAG=$(echo $TAG | cut -d. -f1)
|
||||
MINOR_TAG=$(echo $TAG | cut -d. -f1,2)
|
||||
IMAGE_TAGS=(
|
||||
"latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"
|
||||
"postgresql-latest" "postgresql-$MAJOR_TAG" "postgresql-$MINOR_TAG" "postgresql-$TAG"
|
||||
"ee-latest" "ee-$MAJOR_TAG" "ee-$MINOR_TAG" "ee-$TAG"
|
||||
"ee-postgresql-latest" "ee-postgresql-$MAJOR_TAG" "ee-postgresql-$MINOR_TAG" "ee-postgresql-$TAG"
|
||||
)
|
||||
fi
|
||||
for IMAGE in "${GHCR_IMAGE}" "${DOCKERHUB_IMAGE}"; do
|
||||
echo "Processing ${IMAGE}:${TAG}"
|
||||
|
||||
FAILED_TAGS=()
|
||||
SUCCESSFUL_TAGS=()
|
||||
DIGEST="$(skopeo inspect --retry-times 3 docker://${IMAGE}:${TAG} | jq -r '.Digest')"
|
||||
REF="${IMAGE}@${DIGEST}"
|
||||
echo "Resolved digest: ${REF}"
|
||||
|
||||
for IMAGE_TAG in "${IMAGE_TAGS[@]}"; do
|
||||
echo "Processing ${GHCR_IMAGE}:${IMAGE_TAG}"
|
||||
TAG_FAILED=false
|
||||
echo "==> cosign sign (keyless) --recursive ${REF}"
|
||||
cosign sign --recursive "${REF}"
|
||||
|
||||
(
|
||||
set -e
|
||||
DIGEST="$(skopeo inspect --retry-times 3 docker://${GHCR_IMAGE}:${IMAGE_TAG} | jq -r '.Digest')"
|
||||
REF="${GHCR_IMAGE}@${DIGEST}"
|
||||
echo "Resolved digest: ${REF}"
|
||||
echo "==> cosign sign (key) --recursive ${REF}"
|
||||
cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${REF}"
|
||||
|
||||
echo "==> cosign sign (keyless) --recursive ${REF}"
|
||||
cosign sign --recursive "${REF}"
|
||||
) || TAG_FAILED=true
|
||||
echo "==> cosign verify (public key) ${REF}"
|
||||
cosign verify --key env://COSIGN_PUBLIC_KEY "${REF}" -o text
|
||||
|
||||
if [ "$TAG_FAILED" = "true" ]; then
|
||||
echo "⚠️ WARNING: Failed to sign ${GHCR_IMAGE}:${IMAGE_TAG}"
|
||||
FAILED_TAGS+=("${GHCR_IMAGE}:${IMAGE_TAG}")
|
||||
else
|
||||
echo "✓ Successfully signed ${GHCR_IMAGE}:${IMAGE_TAG}"
|
||||
SUCCESSFUL_TAGS+=("${GHCR_IMAGE}:${IMAGE_TAG}")
|
||||
fi
|
||||
echo "==> cosign verify (keyless policy) ${REF}"
|
||||
cosign verify \
|
||||
--certificate-oidc-issuer "${issuer}" \
|
||||
--certificate-identity-regexp "${id_regex}" \
|
||||
"${REF}" -o text
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Sign Summary"
|
||||
echo "=========================================="
|
||||
echo "Successful: ${#SUCCESSFUL_TAGS[@]}"
|
||||
echo "Failed: ${#FAILED_TAGS[@]}"
|
||||
|
||||
if [ ${#FAILED_TAGS[@]} -gt 0 ]; then
|
||||
echo "Failed tags:"
|
||||
for tag in "${FAILED_TAGS[@]}"; do
|
||||
echo " - $tag"
|
||||
done
|
||||
echo "⚠️ WARNING: Some tags failed to sign, but continuing anyway"
|
||||
else
|
||||
echo "✓ All images signed successfully!"
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
post-run:
|
||||
needs: [pre-run, release-arm, release-amd, create-manifest, sign-and-package]
|
||||
if: >-
|
||||
${{
|
||||
always() &&
|
||||
needs.pre-run.result == 'success' &&
|
||||
(needs.release-arm.result == 'success' || needs.release-arm.result == 'skipped' || needs.release-arm.result == 'failure') &&
|
||||
(needs.release-amd.result == 'success' || needs.release-amd.result == 'skipped' || needs.release-amd.result == 'failure') &&
|
||||
(needs.create-manifest.result == 'success' || needs.create-manifest.result == 'skipped' || needs.create-manifest.result == 'failure') &&
|
||||
(needs.sign-and-package.result == 'success' || needs.sign-and-package.result == 'skipped' || needs.sign-and-package.result == 'failure')
|
||||
}}
|
||||
runs-on: ubuntu-latest
|
||||
permissions: write-all
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||
role-duration-seconds: 3600
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
|
||||
- name: Verify AWS identity
|
||||
run: aws sts get-caller-identity
|
||||
|
||||
- name: Stop EC2 instances
|
||||
run: |
|
||||
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
|
||||
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_AMD_RUNNER }}
|
||||
echo "EC2 instances stopped"
|
||||
|
||||
6
.github/workflows/linting.yml
vendored
6
.github/workflows/linting.yml
vendored
@@ -21,12 +21,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
node-version: '24'
|
||||
node-version: '22'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
4
.github/workflows/mirror.yaml
vendored
4
.github/workflows/mirror.yaml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
skopeo --version
|
||||
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
||||
|
||||
- name: Input check
|
||||
run: |
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
skopeo list-tags --retry-times 3 docker://"${SOURCE_IMAGE}" \
|
||||
| jq -r '.Tags[]' | grep -v -e '-arm64' -e '-amd64' | sort -u > src-tags.txt
|
||||
| jq -r '.Tags[]' | sort -u > src-tags.txt
|
||||
echo "Found source tags: $(wc -l < src-tags.txt)"
|
||||
head -n 20 src-tags.txt || true
|
||||
|
||||
|
||||
39
.github/workflows/restart-runners.yml
vendored
39
.github/workflows/restart-runners.yml
vendored
@@ -1,39 +0,0 @@
|
||||
name: Restart Runners
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 */7 * *'
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
ec2-maintenance-prod:
|
||||
runs-on: ubuntu-latest
|
||||
permissions: write-all
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||
role-duration-seconds: 3600
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
|
||||
- name: Verify AWS identity
|
||||
run: aws sts get-caller-identity
|
||||
|
||||
- name: Start EC2 instance
|
||||
run: |
|
||||
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
|
||||
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_AMD_RUNNER }}
|
||||
echo "EC2 instances started"
|
||||
|
||||
- name: Wait
|
||||
run: sleep 600
|
||||
|
||||
- name: Stop EC2 instance
|
||||
run: |
|
||||
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
|
||||
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_AMD_RUNNER }}
|
||||
echo "EC2 instances stopped"
|
||||
160
.github/workflows/saas.yml
vendored
160
.github/workflows/saas.yml
vendored
@@ -1,160 +0,0 @@
|
||||
name: SAAS Pipeline
|
||||
|
||||
# CI/CD workflow for building, publishing, mirroring, signing container images and building release binaries.
|
||||
# Actions are pinned to specific SHAs to reduce supply-chain risk. This workflow triggers on tag push events.
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write # for GHCR push
|
||||
id-token: write # for Cosign Keyless (OIDC) Signing
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "[0-9]+.[0-9]+.[0-9]+-s.[0-9]+"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
pre-run:
|
||||
runs-on: ubuntu-latest
|
||||
permissions: write-all
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||
role-duration-seconds: 3600
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
|
||||
- name: Verify AWS identity
|
||||
run: aws sts get-caller-identity
|
||||
|
||||
- name: Start EC2 instances
|
||||
run: |
|
||||
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
|
||||
echo "EC2 instances started"
|
||||
|
||||
|
||||
release-arm:
|
||||
name: Build and Release (ARM64)
|
||||
runs-on: [self-hosted, linux, arm64, us-east-1]
|
||||
needs: [pre-run]
|
||||
if: >-
|
||||
${{
|
||||
needs.pre-run.result == 'success'
|
||||
}}
|
||||
# Job-level timeout to avoid runaway or stuck runs
|
||||
timeout-minutes: 120
|
||||
env:
|
||||
# Target images
|
||||
AWS_IMAGE: ${{ secrets.aws_account_id }}.dkr.ecr.us-east-1.amazonaws.com/${{ github.event.repository.name }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Download MaxMind GeoLite2 databases
|
||||
env:
|
||||
MAXMIND_LICENSE_KEY: ${{ secrets.MAXMIND_LICENSE_KEY }}
|
||||
run: |
|
||||
echo "Downloading MaxMind GeoLite2 databases..."
|
||||
|
||||
# Download GeoLite2-Country
|
||||
curl -L "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=${MAXMIND_LICENSE_KEY}&suffix=tar.gz" \
|
||||
-o GeoLite2-Country.tar.gz
|
||||
|
||||
# Download GeoLite2-ASN
|
||||
curl -L "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-ASN&license_key=${MAXMIND_LICENSE_KEY}&suffix=tar.gz" \
|
||||
-o GeoLite2-ASN.tar.gz
|
||||
|
||||
# Extract the .mmdb files
|
||||
tar -xzf GeoLite2-Country.tar.gz --strip-components=1 --wildcards '*.mmdb'
|
||||
tar -xzf GeoLite2-ASN.tar.gz --strip-components=1 --wildcards '*.mmdb'
|
||||
|
||||
# Verify files exist
|
||||
if [ ! -f "GeoLite2-Country.mmdb" ]; then
|
||||
echo "ERROR: Failed to download GeoLite2-Country.mmdb"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "GeoLite2-ASN.mmdb" ]; then
|
||||
echo "ERROR: Failed to download GeoLite2-ASN.mmdb"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up tar files
|
||||
rm -f GeoLite2-Country.tar.gz GeoLite2-ASN.tar.gz
|
||||
|
||||
echo "MaxMind databases downloaded successfully"
|
||||
ls -lh GeoLite2-*.mmdb
|
||||
|
||||
- name: Monitor storage space
|
||||
run: |
|
||||
THRESHOLD=75
|
||||
USED_SPACE=$(df / | grep / | awk '{ print $5 }' | sed 's/%//g')
|
||||
echo "Used space: $USED_SPACE%"
|
||||
if [ "$USED_SPACE" -ge "$THRESHOLD" ]; then
|
||||
echo "Used space is below the threshold of 75% free. Running Docker system prune."
|
||||
echo y | docker system prune -a
|
||||
else
|
||||
echo "Storage space is above the threshold. No action needed."
|
||||
fi
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.aws_account_id }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||
role-duration-seconds: 3600
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
|
||||
- name: Login to Amazon ECR
|
||||
id: login-ecr
|
||||
uses: aws-actions/amazon-ecr-login@v2
|
||||
|
||||
- name: Extract tag name
|
||||
id: get-tag
|
||||
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Update version in package.json
|
||||
run: |
|
||||
TAG=${{ env.TAG }}
|
||||
sed -i "s/export const APP_VERSION = \".*\";/export const APP_VERSION = \"$TAG\";/" server/lib/consts.ts
|
||||
cat server/lib/consts.ts
|
||||
shell: bash
|
||||
|
||||
- name: Build and push Docker images (Docker Hub - ARM64)
|
||||
run: |
|
||||
TAG=${{ env.TAG }}
|
||||
make build-saas tag=$TAG
|
||||
echo "Built & pushed ARM64 images to: ${{ env.AWS_IMAGE }}:${TAG}"
|
||||
shell: bash
|
||||
|
||||
post-run:
|
||||
needs: [pre-run, release-arm]
|
||||
if: >-
|
||||
${{
|
||||
always() &&
|
||||
needs.pre-run.result == 'success' &&
|
||||
(needs.release-arm.result == 'success' || needs.release-arm.result == 'skipped' || needs.release-arm.result == 'failure')
|
||||
}}
|
||||
runs-on: ubuntu-latest
|
||||
permissions: write-all
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||
role-duration-seconds: 3600
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
|
||||
- name: Verify AWS identity
|
||||
run: aws sts get-caller-identity
|
||||
|
||||
- name: Stop EC2 instances
|
||||
run: |
|
||||
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
|
||||
echo "EC2 instances stopped"
|
||||
2
.github/workflows/stale-bot.yml
vendored
2
.github/workflows/stale-bot.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||
with:
|
||||
days-before-stale: 14
|
||||
days-before-close: 14
|
||||
|
||||
31
.github/workflows/test.yml
vendored
31
.github/workflows/test.yml
vendored
@@ -12,14 +12,13 @@ on:
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
node-version: '24'
|
||||
node-version: '22'
|
||||
|
||||
- name: Copy config file
|
||||
run: cp config/config.example.yml config/config.yml
|
||||
@@ -34,10 +33,10 @@ jobs:
|
||||
run: npm run set:oss
|
||||
|
||||
- name: Generate database migrations
|
||||
run: npm run db:generate
|
||||
run: npm run db:sqlite:generate
|
||||
|
||||
- name: Apply database migrations
|
||||
run: npm run db:push
|
||||
run: npm run db:sqlite:push
|
||||
|
||||
- name: Test with tsc
|
||||
run: npx tsc --noEmit
|
||||
@@ -58,20 +57,8 @@ jobs:
|
||||
echo "App failed to start"
|
||||
exit 1
|
||||
|
||||
build-sqlite:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Build Docker image sqlite
|
||||
run: make dev-build-sqlite
|
||||
|
||||
build-postgres:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
run: make build-sqlite
|
||||
|
||||
- name: Build Docker image pg
|
||||
run: make dev-build-pg
|
||||
run: make build-pg
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -49,8 +49,4 @@ postgres/
|
||||
dynamic/
|
||||
*.mmdb
|
||||
scratch/
|
||||
tsconfig.json
|
||||
hydrateSaas.ts
|
||||
CLAUDE.md
|
||||
drizzle.config.ts
|
||||
server/setup/migrations.ts
|
||||
tsconfig.json
|
||||
@@ -1,12 +0,0 @@
|
||||
.github/
|
||||
bruno/
|
||||
cli/
|
||||
config/
|
||||
messages/
|
||||
next.config.mjs/
|
||||
public/
|
||||
tailwind.config.js/
|
||||
test/
|
||||
**/*.yml
|
||||
**/*.yaml
|
||||
**/*.md
|
||||
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"recommendations": ["esbenp.prettier-vscode"]
|
||||
}
|
||||
22
.vscode/settings.json
vendored
22
.vscode/settings.json
vendored
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.addMissingImports.ts": "always"
|
||||
},
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
113
Dockerfile
113
Dockerfile
@@ -1,93 +1,66 @@
|
||||
# FROM node:24-slim AS base
|
||||
FROM public.ecr.aws/docker/library/node:24-slim AS base
|
||||
FROM node:22-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
FROM base AS builder-dev
|
||||
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
|
||||
ARG BUILD=oss
|
||||
ARG DATABASE=sqlite
|
||||
|
||||
RUN if [ "$BUILD" = "oss" ]; then rm -rf server/private; fi && \
|
||||
npm run set:$DATABASE && \
|
||||
npm run set:$BUILD && \
|
||||
npm run db:generate && \
|
||||
npm run build && \
|
||||
npm run build:cli && \
|
||||
test -f dist/server.mjs
|
||||
# COPY package.json package-lock.json ./
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Create placeholder files for MaxMind databases to avoid COPY errors
|
||||
# Real files should be present for saas builds, placeholders for oss builds
|
||||
RUN touch /app/GeoLite2-Country.mmdb /app/GeoLite2-ASN.mmdb
|
||||
COPY . .
|
||||
|
||||
FROM base AS builder
|
||||
RUN echo "export * from \"./$DATABASE\";" > server/db/index.ts
|
||||
|
||||
RUN npm ci --omit=dev
|
||||
RUN echo "export const build = \"$BUILD\" as any;" > server/build.ts
|
||||
|
||||
# FROM node:24-slim AS runner
|
||||
FROM public.ecr.aws/docker/library/node:24-slim AS runner
|
||||
# Copy the appropriate TypeScript configuration based on build type
|
||||
RUN if [ "$BUILD" = "oss" ]; then cp tsconfig.oss.json tsconfig.json; \
|
||||
elif [ "$BUILD" = "saas" ]; then cp tsconfig.saas.json tsconfig.json; \
|
||||
elif [ "$BUILD" = "enterprise" ]; then cp tsconfig.enterprise.json tsconfig.json; \
|
||||
fi
|
||||
|
||||
# if the build is oss then remove the server/private directory
|
||||
RUN if [ "$BUILD" = "oss" ]; then rm -rf server/private; fi
|
||||
|
||||
RUN if [ "$DATABASE" = "pg" ]; then npx drizzle-kit generate --dialect postgresql --schema ./server/db/pg/schema --out init; else npx drizzle-kit generate --dialect $DATABASE --schema ./server/db/$DATABASE/schema --out init; fi
|
||||
|
||||
RUN mkdir -p dist
|
||||
RUN npm run next:build
|
||||
RUN node esbuild.mjs -e server/index.ts -o dist/server.mjs -b $BUILD
|
||||
RUN if [ "$DATABASE" = "pg" ]; then \
|
||||
node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs; \
|
||||
else \
|
||||
node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs; \
|
||||
fi
|
||||
|
||||
# test to make sure the build output is there and error if not
|
||||
RUN test -f dist/server.mjs
|
||||
|
||||
RUN npm run build:cli
|
||||
|
||||
FROM node:22-alpine AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y curl tzdata && rm -rf /var/lib/apt/lists/*
|
||||
# Curl used for the health checks
|
||||
RUN apk add --no-cache curl tzdata
|
||||
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
# COPY package.json package-lock.json ./
|
||||
COPY package*.json ./
|
||||
|
||||
COPY --from=builder-dev /app/.next/standalone ./
|
||||
COPY --from=builder-dev /app/.next/static ./.next/static
|
||||
COPY --from=builder-dev /app/dist ./dist
|
||||
COPY --from=builder-dev /app/server/migrations ./dist/init
|
||||
RUN npm ci --omit=dev && npm cache clean --force
|
||||
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
COPY --from=builder /app/.next/static ./.next/static
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/init ./dist/init
|
||||
|
||||
COPY ./cli/wrapper.sh /usr/local/bin/pangctl
|
||||
RUN chmod +x /usr/local/bin/pangctl ./dist/cli.mjs
|
||||
|
||||
COPY server/db/names.json ./dist/names.json
|
||||
COPY server/db/ios_models.json ./dist/ios_models.json
|
||||
COPY server/db/mac_models.json ./dist/mac_models.json
|
||||
COPY public ./public
|
||||
|
||||
# Copy MaxMind databases for SaaS builds
|
||||
ARG BUILD=oss
|
||||
|
||||
RUN mkdir -p ./maxmind
|
||||
|
||||
# Copy MaxMind databases (placeholders exist for oss builds, real files for saas)
|
||||
COPY --from=builder-dev /app/GeoLite2-Country.mmdb ./maxmind/GeoLite2-Country.mmdb
|
||||
COPY --from=builder-dev /app/GeoLite2-ASN.mmdb ./maxmind/GeoLite2-ASN.mmdb
|
||||
|
||||
# Remove MaxMind databases for non-saas builds (keep only for saas)
|
||||
RUN if [ "$BUILD" != "saas" ]; then rm -rf ./maxmind; fi
|
||||
|
||||
# OCI Image Labels - Build Args for dynamic values
|
||||
ARG VERSION="dev"
|
||||
ARG REVISION=""
|
||||
ARG CREATED=""
|
||||
ARG LICENSE="AGPL-3.0"
|
||||
|
||||
# Derive title and description based on BUILD type
|
||||
ARG IMAGE_TITLE="Pangolin"
|
||||
ARG IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere"
|
||||
|
||||
# OCI Image Labels
|
||||
# https://github.com/opencontainers/image-spec/blob/main/annotations.md
|
||||
LABEL org.opencontainers.image.source="https://github.com/fosrl/pangolin" \
|
||||
org.opencontainers.image.url="https://github.com/fosrl/pangolin" \
|
||||
org.opencontainers.image.documentation="https://docs.pangolin.net" \
|
||||
org.opencontainers.image.vendor="Fossorial" \
|
||||
org.opencontainers.image.licenses="${LICENSE}" \
|
||||
org.opencontainers.image.title="${IMAGE_TITLE}" \
|
||||
org.opencontainers.image.description="${IMAGE_DESCRIPTION}" \
|
||||
org.opencontainers.image.version="${VERSION}" \
|
||||
org.opencontainers.image.revision="${REVISION}" \
|
||||
org.opencontainers.image.created="${CREATED}"
|
||||
|
||||
CMD ["npm", "run", "start"]
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
FROM node:24-alpine
|
||||
FROM node:22-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache python3 make g++
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
|
||||
452
Makefile
452
Makefile
@@ -1,32 +1,8 @@
|
||||
.PHONY: build build-pg build-release build-release-arm build-release-amd create-manifests build-arm build-x86 test clean
|
||||
.PHONY: build build-pg build-release build-arm build-x86 test clean
|
||||
|
||||
major_tag := $(shell echo $(tag) | cut -d. -f1)
|
||||
minor_tag := $(shell echo $(tag) | cut -d. -f1,2)
|
||||
|
||||
# OCI label variables
|
||||
CREATED := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
REVISION := $(shell git rev-parse HEAD 2>/dev/null || echo "unknown")
|
||||
|
||||
# Common OCI build args for OSS builds
|
||||
OCI_ARGS_OSS = --build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$(REVISION) \
|
||||
--build-arg CREATED=$(CREATED) \
|
||||
--build-arg IMAGE_TITLE="Pangolin" \
|
||||
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere"
|
||||
|
||||
# Common OCI build args for Enterprise builds
|
||||
OCI_ARGS_EE = --build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$(REVISION) \
|
||||
--build-arg CREATED=$(CREATED) \
|
||||
--build-arg LICENSE="Fossorial Commercial" \
|
||||
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere"
|
||||
|
||||
.PHONY: build-release build-sqlite build-postgresql build-ee-sqlite build-ee-postgresql
|
||||
|
||||
build-release: build-sqlite build-postgresql build-ee-sqlite build-ee-postgresql
|
||||
|
||||
build-sqlite:
|
||||
build-release:
|
||||
@if [ -z "$(tag)" ]; then \
|
||||
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||
exit 1; \
|
||||
@@ -34,55 +10,33 @@ build-sqlite:
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=sqlite \
|
||||
$(OCI_ARGS_OSS) \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:latest \
|
||||
--tag fosrl/pangolin:$(major_tag) \
|
||||
--tag fosrl/pangolin:$(minor_tag) \
|
||||
--tag fosrl/pangolin:$(tag) \
|
||||
--push .
|
||||
|
||||
build-postgresql:
|
||||
@if [ -z "$(tag)" ]; then \
|
||||
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||
exit 1; \
|
||||
fi
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=pg \
|
||||
$(OCI_ARGS_OSS) \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:postgresql-latest \
|
||||
--tag fosrl/pangolin:postgresql-$(major_tag) \
|
||||
--tag fosrl/pangolin:postgresql-$(minor_tag) \
|
||||
--tag fosrl/pangolin:postgresql-$(tag) \
|
||||
--push .
|
||||
|
||||
build-ee-sqlite:
|
||||
@if [ -z "$(tag)" ]; then \
|
||||
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||
exit 1; \
|
||||
fi
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=sqlite \
|
||||
$(OCI_ARGS_EE) \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:ee-latest \
|
||||
--tag fosrl/pangolin:ee-$(major_tag) \
|
||||
--tag fosrl/pangolin:ee-$(minor_tag) \
|
||||
--tag fosrl/pangolin:ee-$(tag) \
|
||||
--push .
|
||||
|
||||
build-ee-postgresql:
|
||||
@if [ -z "$(tag)" ]; then \
|
||||
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||
exit 1; \
|
||||
fi
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=pg \
|
||||
$(OCI_ARGS_EE) \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:ee-postgresql-latest \
|
||||
--tag fosrl/pangolin:ee-postgresql-$(major_tag) \
|
||||
@@ -90,431 +44,47 @@ build-ee-postgresql:
|
||||
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
||||
--push .
|
||||
|
||||
build-saas:
|
||||
@if [ -z "$(tag)" ]; then \
|
||||
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||
exit 1; \
|
||||
fi
|
||||
docker buildx build \
|
||||
--build-arg BUILD=saas \
|
||||
--build-arg DATABASE=pg \
|
||||
--platform linux/arm64 \
|
||||
--tag $(AWS_IMAGE):$(tag) \
|
||||
--push .
|
||||
|
||||
build-release-arm:
|
||||
@if [ -z "$(tag)" ]; then \
|
||||
echo "Error: tag is required. Usage: make build-release-arm tag=<tag>"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@MAJOR_TAG=$$(echo $(tag) | cut -d. -f1); \
|
||||
MINOR_TAG=$$(echo $(tag) | cut -d. -f1,2); \
|
||||
CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg IMAGE_TITLE="Pangolin" \
|
||||
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/arm64 \
|
||||
--tag fosrl/pangolin:latest-arm64 \
|
||||
--tag fosrl/pangolin:$$MAJOR_TAG-arm64 \
|
||||
--tag fosrl/pangolin:$$MINOR_TAG-arm64 \
|
||||
--tag fosrl/pangolin:$(tag)-arm64 \
|
||||
--push . && \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=pg \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg IMAGE_TITLE="Pangolin" \
|
||||
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/arm64 \
|
||||
--tag fosrl/pangolin:postgresql-latest-arm64 \
|
||||
--tag fosrl/pangolin:postgresql-$$MAJOR_TAG-arm64 \
|
||||
--tag fosrl/pangolin:postgresql-$$MINOR_TAG-arm64 \
|
||||
--tag fosrl/pangolin:postgresql-$(tag)-arm64 \
|
||||
--push . && \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg LICENSE="Fossorial Commercial" \
|
||||
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/arm64 \
|
||||
--tag fosrl/pangolin:ee-latest-arm64 \
|
||||
--tag fosrl/pangolin:ee-$$MAJOR_TAG-arm64 \
|
||||
--tag fosrl/pangolin:ee-$$MINOR_TAG-arm64 \
|
||||
--tag fosrl/pangolin:ee-$(tag)-arm64 \
|
||||
--push . && \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=pg \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg LICENSE="Fossorial Commercial" \
|
||||
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/arm64 \
|
||||
--tag fosrl/pangolin:ee-postgresql-latest-arm64 \
|
||||
--tag fosrl/pangolin:ee-postgresql-$$MAJOR_TAG-arm64 \
|
||||
--tag fosrl/pangolin:ee-postgresql-$$MINOR_TAG-arm64 \
|
||||
--tag fosrl/pangolin:ee-postgresql-$(tag)-arm64 \
|
||||
--push .
|
||||
|
||||
build-release-amd:
|
||||
@if [ -z "$(tag)" ]; then \
|
||||
echo "Error: tag is required. Usage: make build-release-amd tag=<tag>"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@MAJOR_TAG=$$(echo $(tag) | cut -d. -f1); \
|
||||
MINOR_TAG=$$(echo $(tag) | cut -d. -f1,2); \
|
||||
CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg IMAGE_TITLE="Pangolin" \
|
||||
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/amd64 \
|
||||
--tag fosrl/pangolin:latest-amd64 \
|
||||
--tag fosrl/pangolin:$$MAJOR_TAG-amd64 \
|
||||
--tag fosrl/pangolin:$$MINOR_TAG-amd64 \
|
||||
--tag fosrl/pangolin:$(tag)-amd64 \
|
||||
--push . && \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=pg \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg IMAGE_TITLE="Pangolin" \
|
||||
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/amd64 \
|
||||
--tag fosrl/pangolin:postgresql-latest-amd64 \
|
||||
--tag fosrl/pangolin:postgresql-$$MAJOR_TAG-amd64 \
|
||||
--tag fosrl/pangolin:postgresql-$$MINOR_TAG-amd64 \
|
||||
--tag fosrl/pangolin:postgresql-$(tag)-amd64 \
|
||||
--push . && \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg LICENSE="Fossorial Commercial" \
|
||||
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/amd64 \
|
||||
--tag fosrl/pangolin:ee-latest-amd64 \
|
||||
--tag fosrl/pangolin:ee-$$MAJOR_TAG-amd64 \
|
||||
--tag fosrl/pangolin:ee-$$MINOR_TAG-amd64 \
|
||||
--tag fosrl/pangolin:ee-$(tag)-amd64 \
|
||||
--push . && \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=pg \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg LICENSE="Fossorial Commercial" \
|
||||
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/amd64 \
|
||||
--tag fosrl/pangolin:ee-postgresql-latest-amd64 \
|
||||
--tag fosrl/pangolin:ee-postgresql-$$MAJOR_TAG-amd64 \
|
||||
--tag fosrl/pangolin:ee-postgresql-$$MINOR_TAG-amd64 \
|
||||
--tag fosrl/pangolin:ee-postgresql-$(tag)-amd64 \
|
||||
--push .
|
||||
|
||||
create-manifests:
|
||||
@if [ -z "$(tag)" ]; then \
|
||||
echo "Error: tag is required. Usage: make create-manifests tag=<tag>"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@MAJOR_TAG=$$(echo $(tag) | cut -d. -f1); \
|
||||
MINOR_TAG=$$(echo $(tag) | cut -d. -f1,2); \
|
||||
echo "Creating multi-arch manifests for sqlite (oss)..." && \
|
||||
docker buildx imagetools create \
|
||||
--tag fosrl/pangolin:latest \
|
||||
--tag fosrl/pangolin:$$MAJOR_TAG \
|
||||
--tag fosrl/pangolin:$$MINOR_TAG \
|
||||
--tag fosrl/pangolin:$(tag) \
|
||||
fosrl/pangolin:latest-arm64 \
|
||||
fosrl/pangolin:latest-amd64 && \
|
||||
echo "Creating multi-arch manifests for postgresql (oss)..." && \
|
||||
docker buildx imagetools create \
|
||||
--tag fosrl/pangolin:postgresql-latest \
|
||||
--tag fosrl/pangolin:postgresql-$$MAJOR_TAG \
|
||||
--tag fosrl/pangolin:postgresql-$$MINOR_TAG \
|
||||
--tag fosrl/pangolin:postgresql-$(tag) \
|
||||
fosrl/pangolin:postgresql-latest-arm64 \
|
||||
fosrl/pangolin:postgresql-latest-amd64 && \
|
||||
echo "Creating multi-arch manifests for sqlite (enterprise)..." && \
|
||||
docker buildx imagetools create \
|
||||
--tag fosrl/pangolin:ee-latest \
|
||||
--tag fosrl/pangolin:ee-$$MAJOR_TAG \
|
||||
--tag fosrl/pangolin:ee-$$MINOR_TAG \
|
||||
--tag fosrl/pangolin:ee-$(tag) \
|
||||
fosrl/pangolin:ee-latest-arm64 \
|
||||
fosrl/pangolin:ee-latest-amd64 && \
|
||||
echo "Creating multi-arch manifests for postgresql (enterprise)..." && \
|
||||
docker buildx imagetools create \
|
||||
--tag fosrl/pangolin:ee-postgresql-latest \
|
||||
--tag fosrl/pangolin:ee-postgresql-$$MAJOR_TAG \
|
||||
--tag fosrl/pangolin:ee-postgresql-$$MINOR_TAG \
|
||||
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
||||
fosrl/pangolin:ee-postgresql-latest-arm64 \
|
||||
fosrl/pangolin:ee-postgresql-latest-amd64 && \
|
||||
echo "All multi-arch manifests created successfully!"
|
||||
|
||||
build-rc:
|
||||
@if [ -z "$(tag)" ]; then \
|
||||
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg IMAGE_TITLE="Pangolin" \
|
||||
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:$(tag) \
|
||||
--push . && \
|
||||
--push .
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=pg \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg IMAGE_TITLE="Pangolin" \
|
||||
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:postgresql-$(tag) \
|
||||
--push . && \
|
||||
--push .
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg LICENSE="Fossorial Commercial" \
|
||||
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:ee-$(tag) \
|
||||
--push . && \
|
||||
--push .
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=pg \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg LICENSE="Fossorial Commercial" \
|
||||
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
||||
--push .
|
||||
|
||||
build-rc-arm:
|
||||
@if [ -z "$(tag)" ]; then \
|
||||
echo "Error: tag is required. Usage: make build-rc-arm tag=<tag>"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg IMAGE_TITLE="Pangolin" \
|
||||
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/arm64 \
|
||||
--tag fosrl/pangolin:$(tag)-arm64 \
|
||||
--push . && \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=pg \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg IMAGE_TITLE="Pangolin" \
|
||||
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/arm64 \
|
||||
--tag fosrl/pangolin:postgresql-$(tag)-arm64 \
|
||||
--push . && \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg LICENSE="Fossorial Commercial" \
|
||||
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/arm64 \
|
||||
--tag fosrl/pangolin:ee-$(tag)-arm64 \
|
||||
--push . && \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=pg \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg LICENSE="Fossorial Commercial" \
|
||||
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/arm64 \
|
||||
--tag fosrl/pangolin:ee-postgresql-$(tag)-arm64 \
|
||||
--push .
|
||||
|
||||
build-rc-amd:
|
||||
@if [ -z "$(tag)" ]; then \
|
||||
echo "Error: tag is required. Usage: make build-rc-amd tag=<tag>"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg IMAGE_TITLE="Pangolin" \
|
||||
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/amd64 \
|
||||
--tag fosrl/pangolin:$(tag)-amd64 \
|
||||
--push . && \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=pg \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg IMAGE_TITLE="Pangolin" \
|
||||
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/amd64 \
|
||||
--tag fosrl/pangolin:postgresql-$(tag)-amd64 \
|
||||
--push . && \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg LICENSE="Fossorial Commercial" \
|
||||
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/amd64 \
|
||||
--tag fosrl/pangolin:ee-$(tag)-amd64 \
|
||||
--push . && \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=pg \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg LICENSE="Fossorial Commercial" \
|
||||
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/amd64 \
|
||||
--tag fosrl/pangolin:ee-postgresql-$(tag)-amd64 \
|
||||
--push .
|
||||
|
||||
create-manifests-rc:
|
||||
@if [ -z "$(tag)" ]; then \
|
||||
echo "Error: tag is required. Usage: make create-manifests-rc tag=<tag>"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "Creating multi-arch manifests for RC sqlite (oss)..." && \
|
||||
docker buildx imagetools create \
|
||||
--tag fosrl/pangolin:$(tag) \
|
||||
fosrl/pangolin:$(tag)-arm64 \
|
||||
fosrl/pangolin:$(tag)-amd64 && \
|
||||
echo "Creating multi-arch manifests for RC postgresql (oss)..." && \
|
||||
docker buildx imagetools create \
|
||||
--tag fosrl/pangolin:postgresql-$(tag) \
|
||||
fosrl/pangolin:postgresql-$(tag)-arm64 \
|
||||
fosrl/pangolin:postgresql-$(tag)-amd64 && \
|
||||
echo "Creating multi-arch manifests for RC sqlite (enterprise)..." && \
|
||||
docker buildx imagetools create \
|
||||
--tag fosrl/pangolin:ee-$(tag) \
|
||||
fosrl/pangolin:ee-$(tag)-arm64 \
|
||||
fosrl/pangolin:ee-$(tag)-amd64 && \
|
||||
echo "Creating multi-arch manifests for RC postgresql (enterprise)..." && \
|
||||
docker buildx imagetools create \
|
||||
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
||||
fosrl/pangolin:ee-postgresql-$(tag)-arm64 \
|
||||
fosrl/pangolin:ee-postgresql-$(tag)-amd64 && \
|
||||
echo "All RC multi-arch manifests created successfully!"
|
||||
|
||||
build-arm:
|
||||
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||
docker buildx build \
|
||||
--build-arg VERSION=dev \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg IMAGE_TITLE="Pangolin" \
|
||||
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/arm64 \
|
||||
-t fosrl/pangolin:latest .
|
||||
docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest .
|
||||
|
||||
build-x86:
|
||||
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||
docker buildx build \
|
||||
--build-arg VERSION=dev \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg IMAGE_TITLE="Pangolin" \
|
||||
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/amd64 \
|
||||
-t fosrl/pangolin:latest .
|
||||
docker buildx build --platform linux/amd64 -t fosrl/pangolin:latest .
|
||||
|
||||
dev-build-sqlite:
|
||||
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||
docker build \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--build-arg VERSION=dev \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg IMAGE_TITLE="Pangolin" \
|
||||
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
-t fosrl/pangolin:latest .
|
||||
build-sqlite:
|
||||
docker build --build-arg DATABASE=sqlite -t fosrl/pangolin:latest .
|
||||
|
||||
dev-build-pg:
|
||||
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||
docker build \
|
||||
--build-arg DATABASE=pg \
|
||||
--build-arg VERSION=dev \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg IMAGE_TITLE="Pangolin" \
|
||||
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
-t fosrl/pangolin:postgresql-latest .
|
||||
build-pg:
|
||||
docker build --build-arg DATABASE=pg -t fosrl/pangolin:postgresql-latest .
|
||||
|
||||
test:
|
||||
docker run -it -p 3000:3000 -p 3001:3001 -p 3002:3002 -v ./config:/app/config fosrl/pangolin:latest
|
||||
|
||||
73
README.md
73
README.md
@@ -31,80 +31,57 @@
|
||||
[](https://pangolin.net/slack)
|
||||
[](https://hub.docker.com/r/fosrl/pangolin)
|
||||

|
||||
[](https://www.youtube.com/@pangolin-net)
|
||||
[](https://www.youtube.com/@fossorial-app)
|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<strong>
|
||||
Get started with Pangolin at <a href="https://app.pangolin.net/auth/signup">app.pangolin.net</a>
|
||||
Start testing Pangolin at <a href="https://app.pangolin.net/auth/signup">app.pangolin.net</a>
|
||||
</strong>
|
||||
</p>
|
||||
|
||||
Pangolin is an open-source, identity-based remote access platform built on WireGuard® that enables secure, seamless connectivity to private and public resources. Pangolin combines reverse proxy and VPN capabilities into one platform, providing browser-based access to web applications and client-based access to any private resources with NAT traversal, all with granular access controls.
|
||||
Pangolin is a self-hosted tunneled reverse proxy server with identity and context aware access control, designed to easily expose and protect applications running anywhere. Pangolin acts as a central hub and connects isolated networks — even those behind restrictive firewalls — through encrypted tunnels, enabling easy access to remote services without opening ports or requiring a VPN.
|
||||
|
||||
## Installation
|
||||
|
||||
- Get started for free with [Pangolin Cloud](https://app.pangolin.net/).
|
||||
- Or, check out the [quick install guide](https://docs.pangolin.net/self-host/quick-install) for how to self-host Pangolin.
|
||||
- Install from the [DigitalOcean marketplace](https://marketplace.digitalocean.com/apps/pangolin-ce-1?refcode=edf0480eeb81) for a one-click pre-configured installer.
|
||||
- Check out the [quick install guide](https://docs.pangolin.net/self-host/quick-install) for how to install and set up Pangolin.
|
||||
- Install from the [DigitalOcean marketplace](https://marketplace.digitalocean.com/apps/pangolin-ce-1?refcode=edf0480eeb81) for a one-click pre-configured installer.
|
||||
|
||||
<img src="public/screenshots/hero.png" alt="Pangolin" width="100%" />
|
||||
<img src="public/screenshots/hero.png" />
|
||||
|
||||
## Deployment Options
|
||||
|
||||
- **Pangolin Cloud** - Fully managed service - no infrastructure required.
|
||||
- **Self-Host: Community Edition** - Free, open source, and licensed under AGPL-3.
|
||||
- **Self-Host: Enterprise Edition** - Licensed under Fossorial Commercial License. Free for personal and hobbyist use, and for businesses making less than \$100K USD gross annual revenue.
|
||||
| <img width=500 /> | Description |
|
||||
|-----------------|--------------|
|
||||
| **Self-Host: Community Edition** | Free, open source, and licensed under AGPL-3. |
|
||||
| **Self-Host: Enterprise Edition** | Licensed under Fossorial Commercial License. Free for personal and hobbyist use, and for businesses earning under \$100K USD annually. |
|
||||
| **Pangolin Cloud** | Fully managed service with instant setup and pay-as-you-go pricing — no infrastructure required. Or, self-host your own [remote node](https://docs.pangolin.net/manage/remote-node/nodes) and connect to our control plane. |
|
||||
|
||||
## Key Features
|
||||
|
||||
### Connect remote networks with sites and NAT traversal
|
||||
Pangolin packages everything you need for seamless application access and exposure into one cohesive platform.
|
||||
|
||||
Pangolin's site connectors provide gateways into networks so you can access any networked resources. Sites use outbound tunnels and intelligent NAT traversal to make networks behind restrictive firewalls available for authorized access without public IPs or open ports. Easily deploy a site as a binary or container on any platform.
|
||||
|
||||
<img src="public/screenshots/sites.png" alt="Sites" width="100%" />
|
||||
|
||||
### Browser-based reverse proxy access
|
||||
|
||||
Expose web applications through identity and context-aware tunneled reverse proxies. Users access applications through any web browser with authentication and granular access control without installing a client. Pangolin handles routing, load balancing, health checking, and automatic SSL certificates without exposing your network directly to the internet.
|
||||
|
||||
<img src="public/clip.gif" alt="Reverse proxy access" width="100%" />
|
||||
|
||||
### Client-based private resource access
|
||||
|
||||
Access private resources like SSH servers, databases, RDP, and entire network ranges through Pangolin clients. Intelligent NAT traversal enables connections even through restrictive firewalls, while DNS aliases provide friendly names and fast connections to resources across all your sites. Add redundancy by routing traffic through multiple connectors in your network.
|
||||
|
||||
<img src="public/screenshots/private-resources.png" alt="Private resources" width="100%" />
|
||||
|
||||
### Give users and roles access to resources
|
||||
|
||||
Use Pangolin's built in users or bring your own identity provider and set up role based access control (RBAC). Grant users access to specific resources, not entire networks. Unlike traditional VPNs that expose full network access, Pangolin's zero-trust model ensures users can only reach the applications, services, and routes you explicitly define.
|
||||
|
||||
<img src="public/screenshots/users.png" alt="Users from identity provider with roles" width="100%" />
|
||||
|
||||
## Download Clients
|
||||
|
||||
Download the Pangolin client for your platform:
|
||||
|
||||
- [Mac](https://pangolin.net/downloads/mac)
|
||||
- [Windows](https://pangolin.net/downloads/windows)
|
||||
- [Linux](https://pangolin.net/downloads/linux)
|
||||
- [iOS](https://pangolin.net/downloads/ios)
|
||||
- [Android](https://pangolin.net/downloads/android)
|
||||
| <img width=500 /> | <img width=500 /> |
|
||||
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
|
||||
| **Manage applications in one place**<br /><br /> Pangolin provides a unified dashboard where you can monitor, configure, and secure all of your services regardless of where they are hosted. | <img src="public/screenshots/hero.png" width=500 /><tr></tr> |
|
||||
| **Reverse proxy across networks anywhere**<br /><br />Route traffic via tunnels to any private network. Pangolin works like a reverse proxy that spans multiple networks and handles routing, load balancing, health checking, and more to the right services on the other end. | <img src="public/screenshots/sites.png" width=500 /><tr></tr> |
|
||||
| **Enforce identity and context aware rules**<br /><br />Protect your applications with identity and context aware rules such as SSO, OIDC, PIN, password, temporary share links, geolocation, IP, and more. | <img src="public/auth-diagram1.png" width=500 /><tr></tr> |
|
||||
| **Quickly connect Pangolin sites**<br /><br />Pangolin's lightweight [Newt](https://github.com/fosrl/newt) client runs in userspace and can run anywhere. Use it as a site connector to route traffic to backends across all of your environments. | <img src="public/clip.gif" width=500 /><tr></tr> |
|
||||
|
||||
## Get Started
|
||||
|
||||
### Sign up now
|
||||
|
||||
Create a free account at [app.pangolin.net](https://app.pangolin.net) to get started with Pangolin Cloud.
|
||||
|
||||
### Check out the docs
|
||||
|
||||
We encourage everyone to read the full documentation first, which is
|
||||
available at [docs.pangolin.net](https://docs.pangolin.net). This README provides only a very brief subset of
|
||||
the docs to illustrate some basic ideas.
|
||||
|
||||
### Sign up and try now
|
||||
|
||||
For Pangolin's managed service, you will first need to create an account at
|
||||
[app.pangolin.net](https://app.pangolin.net). We have a generous free tier to get started.
|
||||
|
||||
## Licensing
|
||||
|
||||
Pangolin is dual licensed under the AGPL-3 and the [Fossorial Commercial License](https://pangolin.net/fcl.html). For inquiries about commercial licensing, please contact us at [contact@pangolin.net](mailto:contact@pangolin.net).
|
||||
@@ -112,3 +89,7 @@ Pangolin is dual licensed under the AGPL-3 and the [Fossorial Commercial License
|
||||
## Contributions
|
||||
|
||||
Please see [CONTRIBUTING](./CONTRIBUTING.md) in the repository for guidelines and best practices.
|
||||
|
||||
---
|
||||
|
||||
WireGuard® is a registered trademark of Jason A. Donenfeld.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
If you discover a security vulnerability, please follow the steps below to responsibly disclose it to us:
|
||||
|
||||
1. **Do not create a public GitHub issue or discussion post.** This could put the security of other users at risk.
|
||||
2. Send a detailed report to [security@pangolin.net](mailto:security@pangolin.net) with the following information:
|
||||
2. Send a detailed report to [security@pangolin.net](mailto:security@pangolin.net) or send a **private** message to a maintainer on [Discord](https://discord.gg/HCJR8Xhme4). Include:
|
||||
|
||||
- Description and location of the vulnerability.
|
||||
- Potential impact of the vulnerability.
|
||||
|
||||
72
blueprint.py
Normal file
72
blueprint.py
Normal file
@@ -0,0 +1,72 @@
|
||||
import requests
|
||||
import yaml
|
||||
import json
|
||||
import base64
|
||||
|
||||
# The file path for the YAML file to be read
|
||||
# You can change this to the path of your YAML file
|
||||
YAML_FILE_PATH = 'blueprint.yaml'
|
||||
|
||||
# The API endpoint and headers from the curl request
|
||||
API_URL = 'http://api.pangolin.net/v1/org/test/blueprint'
|
||||
HEADERS = {
|
||||
'accept': '*/*',
|
||||
'Authorization': 'Bearer <your_token_here>',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
def convert_and_send(file_path, url, headers):
|
||||
"""
|
||||
Reads a YAML file, converts its content to a JSON payload,
|
||||
and sends it via a PUT request to a specified URL.
|
||||
"""
|
||||
try:
|
||||
# Read the YAML file content
|
||||
with open(file_path, 'r') as file:
|
||||
yaml_content = file.read()
|
||||
|
||||
# Parse the YAML string to a Python dictionary
|
||||
# This will be used to ensure the YAML is valid before sending
|
||||
parsed_yaml = yaml.safe_load(yaml_content)
|
||||
|
||||
# convert the parsed YAML to a JSON string
|
||||
json_payload = json.dumps(parsed_yaml)
|
||||
print("Converted JSON payload:")
|
||||
print(json_payload)
|
||||
|
||||
# Encode the JSON string to Base64
|
||||
encoded_json = base64.b64encode(json_payload.encode('utf-8')).decode('utf-8')
|
||||
|
||||
# Create the final payload with the base64 encoded data
|
||||
final_payload = {
|
||||
"blueprint": encoded_json
|
||||
}
|
||||
|
||||
print("Sending the following Base64 encoded JSON payload:")
|
||||
print(final_payload)
|
||||
print("-" * 20)
|
||||
|
||||
# Make the PUT request with the base64 encoded payload
|
||||
response = requests.put(url, headers=headers, json=final_payload)
|
||||
|
||||
# Print the API response for debugging
|
||||
print(f"API Response Status Code: {response.status_code}")
|
||||
print("API Response Content:")
|
||||
print(response.text)
|
||||
|
||||
# Raise an exception for bad status codes (4xx or 5xx)
|
||||
response.raise_for_status()
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"Error: The file '{file_path}' was not found.")
|
||||
except yaml.YAMLError as e:
|
||||
print(f"Error parsing YAML file: {e}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"An error occurred during the API request: {e}")
|
||||
except Exception as e:
|
||||
print(f"An unexpected error occurred: {e}")
|
||||
|
||||
# Run the function
|
||||
if __name__ == "__main__":
|
||||
convert_and_send(YAML_FILE_PATH, API_URL, HEADERS)
|
||||
|
||||
69
blueprint.yaml
Normal file
69
blueprint.yaml
Normal file
@@ -0,0 +1,69 @@
|
||||
client-resources:
|
||||
client-resource-nice-id-uno:
|
||||
name: this is my resource
|
||||
protocol: tcp
|
||||
proxy-port: 3001
|
||||
hostname: localhost
|
||||
internal-port: 3000
|
||||
site: lively-yosemite-toad
|
||||
client-resource-nice-id-duce:
|
||||
name: this is my resource
|
||||
protocol: udp
|
||||
proxy-port: 3000
|
||||
hostname: localhost
|
||||
internal-port: 3000
|
||||
site: lively-yosemite-toad
|
||||
|
||||
proxy-resources:
|
||||
resource-nice-id-uno:
|
||||
name: this is my resource
|
||||
protocol: http
|
||||
full-domain: duce.test.example.com
|
||||
host-header: example.com
|
||||
tls-server-name: example.com
|
||||
# auth:
|
||||
# pincode: 123456
|
||||
# password: sadfasdfadsf
|
||||
# sso-enabled: true
|
||||
# sso-roles:
|
||||
# - Member
|
||||
# sso-users:
|
||||
# - owen@pangolin.net
|
||||
# whitelist-users:
|
||||
# - owen@pangolin.net
|
||||
headers:
|
||||
- name: X-Example-Header
|
||||
value: example-value
|
||||
- name: X-Another-Header
|
||||
value: another-value
|
||||
rules:
|
||||
- action: allow
|
||||
match: ip
|
||||
value: 1.1.1.1
|
||||
- action: deny
|
||||
match: cidr
|
||||
value: 2.2.2.2/32
|
||||
- action: pass
|
||||
match: path
|
||||
value: /admin
|
||||
targets:
|
||||
- site: lively-yosemite-toad
|
||||
path: /path
|
||||
pathMatchType: prefix
|
||||
hostname: localhost
|
||||
method: http
|
||||
port: 8000
|
||||
- site: slim-alpine-chipmunk
|
||||
hostname: localhost
|
||||
path: /yoman
|
||||
pathMatchType: exact
|
||||
method: http
|
||||
port: 8001
|
||||
resource-nice-id-duce:
|
||||
name: this is other resource
|
||||
protocol: tcp
|
||||
proxy-port: 3000
|
||||
targets:
|
||||
- site: lively-yosemite-toad
|
||||
hostname: localhost
|
||||
port: 3000
|
||||
17
bruno/API Keys/Create API Key.bru
Normal file
17
bruno/API Keys/Create API Key.bru
Normal file
@@ -0,0 +1,17 @@
|
||||
meta {
|
||||
name: Create API Key
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
put {
|
||||
url: http://localhost:3000/api/v1/api-key
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"isRoot": true
|
||||
}
|
||||
}
|
||||
11
bruno/API Keys/Delete API Key.bru
Normal file
11
bruno/API Keys/Delete API Key.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: Delete API Key
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
delete {
|
||||
url: http://localhost:3000/api/v1/api-key/dm47aacqxxn3ubj
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
11
bruno/API Keys/List API Key Actions.bru
Normal file
11
bruno/API Keys/List API Key Actions.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: List API Key Actions
|
||||
type: http
|
||||
seq: 6
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1/api-key/ex0izu2c37fjz9x/actions
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
11
bruno/API Keys/List Org API Keys.bru
Normal file
11
bruno/API Keys/List Org API Keys.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: List Org API Keys
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1/org/home-lab/api-keys
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
11
bruno/API Keys/List Root API Keys.bru
Normal file
11
bruno/API Keys/List Root API Keys.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: List Root API Keys
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1/root/api-keys
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
17
bruno/API Keys/Set API Key Actions.bru
Normal file
17
bruno/API Keys/Set API Key Actions.bru
Normal file
@@ -0,0 +1,17 @@
|
||||
meta {
|
||||
name: Set API Key Actions
|
||||
type: http
|
||||
seq: 5
|
||||
}
|
||||
|
||||
post {
|
||||
url: http://localhost:3000/api/v1/api-key/ex0izu2c37fjz9x/actions
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"actionIds": ["listSites"]
|
||||
}
|
||||
}
|
||||
17
bruno/API Keys/Set API Key Orgs.bru
Normal file
17
bruno/API Keys/Set API Key Orgs.bru
Normal file
@@ -0,0 +1,17 @@
|
||||
meta {
|
||||
name: Set API Key Orgs
|
||||
type: http
|
||||
seq: 7
|
||||
}
|
||||
|
||||
post {
|
||||
url: http://localhost:3000/api/v1/api-key/ex0izu2c37fjz9x/orgs
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"orgIds": ["home-lab"]
|
||||
}
|
||||
}
|
||||
3
bruno/API Keys/folder.bru
Normal file
3
bruno/API Keys/folder.bru
Normal file
@@ -0,0 +1,3 @@
|
||||
meta {
|
||||
name: API Keys
|
||||
}
|
||||
18
bruno/Auth/2fa-disable.bru
Normal file
18
bruno/Auth/2fa-disable.bru
Normal file
@@ -0,0 +1,18 @@
|
||||
meta {
|
||||
name: 2fa-disable
|
||||
type: http
|
||||
seq: 6
|
||||
}
|
||||
|
||||
post {
|
||||
url: http://localhost:3000/api/v1/auth/2fa/disable
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"password": "aaaaa-1A",
|
||||
"code": "377289"
|
||||
}
|
||||
}
|
||||
17
bruno/Auth/2fa-enable.bru
Normal file
17
bruno/Auth/2fa-enable.bru
Normal file
@@ -0,0 +1,17 @@
|
||||
meta {
|
||||
name: 2fa-enable
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
post {
|
||||
url: http://localhost:3000/api/v1/auth/2fa/enable
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"code": "374138"
|
||||
}
|
||||
}
|
||||
17
bruno/Auth/2fa-request.bru
Normal file
17
bruno/Auth/2fa-request.bru
Normal file
@@ -0,0 +1,17 @@
|
||||
meta {
|
||||
name: 2fa-request
|
||||
type: http
|
||||
seq: 5
|
||||
}
|
||||
|
||||
post {
|
||||
url: http://localhost:3000/api/v1/auth/2fa/request
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"password": "aaaaa-1A"
|
||||
}
|
||||
}
|
||||
18
bruno/Auth/change-password.bru
Normal file
18
bruno/Auth/change-password.bru
Normal file
@@ -0,0 +1,18 @@
|
||||
meta {
|
||||
name: change-password
|
||||
type: http
|
||||
seq: 9
|
||||
}
|
||||
|
||||
post {
|
||||
url: http://localhost:3000/api/v1/auth/change-password
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"oldPassword": "",
|
||||
"newPassword": ""
|
||||
}
|
||||
}
|
||||
18
bruno/Auth/login.bru
Normal file
18
bruno/Auth/login.bru
Normal file
@@ -0,0 +1,18 @@
|
||||
meta {
|
||||
name: login
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: http://localhost:4000/api/v1/auth/login
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"email": "owen@pangolin.net",
|
||||
"password": "Password123!"
|
||||
}
|
||||
}
|
||||
11
bruno/Auth/logout.bru
Normal file
11
bruno/Auth/logout.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: logout
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
post {
|
||||
url: http://localhost:4000/api/v1/auth/logout
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
17
bruno/Auth/reset-password-request.bru
Normal file
17
bruno/Auth/reset-password-request.bru
Normal file
@@ -0,0 +1,17 @@
|
||||
meta {
|
||||
name: reset-password-request
|
||||
type: http
|
||||
seq: 10
|
||||
}
|
||||
|
||||
post {
|
||||
url: http://localhost:3000/api/v1/auth/reset-password/request
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"email": "milo@pangolin.net"
|
||||
}
|
||||
}
|
||||
19
bruno/Auth/reset-password.bru
Normal file
19
bruno/Auth/reset-password.bru
Normal file
@@ -0,0 +1,19 @@
|
||||
meta {
|
||||
name: reset-password
|
||||
type: http
|
||||
seq: 11
|
||||
}
|
||||
|
||||
post {
|
||||
url: http://localhost:3000/api/v1/auth/reset-password
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"token": "3uhsbom72dwdhboctwrtntyd6jrlg4jtf5oaxy4k",
|
||||
"newPassword": "aaaaa-1A",
|
||||
"code": "6irqCGR3"
|
||||
}
|
||||
}
|
||||
18
bruno/Auth/signup.bru
Normal file
18
bruno/Auth/signup.bru
Normal file
@@ -0,0 +1,18 @@
|
||||
meta {
|
||||
name: signup
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
put {
|
||||
url: http://localhost:3000/api/v1/auth/signup
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"email": "numbat@pangolin.net",
|
||||
"password": "Password123!"
|
||||
}
|
||||
}
|
||||
11
bruno/Auth/verify-email-request.bru
Normal file
11
bruno/Auth/verify-email-request.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: verify-email-request
|
||||
type: http
|
||||
seq: 8
|
||||
}
|
||||
|
||||
post {
|
||||
url: http://localhost:3000/api/v1/auth/verify-email/request
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
17
bruno/Auth/verify-email.bru
Normal file
17
bruno/Auth/verify-email.bru
Normal file
@@ -0,0 +1,17 @@
|
||||
meta {
|
||||
name: verify-email
|
||||
type: http
|
||||
seq: 7
|
||||
}
|
||||
|
||||
post {
|
||||
url: http://localhost:3000/api/v1/auth/verify-email
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"code": "50317187"
|
||||
}
|
||||
}
|
||||
15
bruno/Auth/verify-user.bru
Normal file
15
bruno/Auth/verify-user.bru
Normal file
@@ -0,0 +1,15 @@
|
||||
meta {
|
||||
name: verify-user
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3001/api/v1/badger/verify-user?sessionId=mb52273jkb6t3oys2bx6ur5x7rcrkl26c7warg3e
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
params:query {
|
||||
sessionId: mb52273jkb6t3oys2bx6ur5x7rcrkl26c7warg3e
|
||||
}
|
||||
22
bruno/Clients/createClient.bru
Normal file
22
bruno/Clients/createClient.bru
Normal file
@@ -0,0 +1,22 @@
|
||||
meta {
|
||||
name: createClient
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
put {
|
||||
url: http://localhost:3000/api/v1/site/1/client
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"siteId": 1,
|
||||
"name": "test",
|
||||
"type": "olm",
|
||||
"subnet": "100.90.129.4/30",
|
||||
"olmId": "029yzunhx6nh3y5",
|
||||
"secret": "l0ymp075y3d4rccb25l6sqpgar52k09etunui970qq5gj7x6"
|
||||
}
|
||||
}
|
||||
11
bruno/Clients/pickClientDefaults.bru
Normal file
11
bruno/Clients/pickClientDefaults.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: pickClientDefaults
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1/site/1/pick-client-defaults
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
22
bruno/IDP/Create OIDC Provider.bru
Normal file
22
bruno/IDP/Create OIDC Provider.bru
Normal file
@@ -0,0 +1,22 @@
|
||||
meta {
|
||||
name: Create OIDC Provider
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
put {
|
||||
url: http://localhost:3000/api/v1/org/home-lab/idp/oidc
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"clientId": "JJoSvHCZcxnXT2sn6CObj6a21MuKNRXs3kN5wbys",
|
||||
"clientSecret": "2SlGL2wOGgMEWLI9yUuMAeFxre7qSNJVnXMzyepdNzH1qlxYnC4lKhhQ6a157YQEkYH3vm40KK4RCqbYiF8QIweuPGagPX3oGxEj2exwutoXFfOhtq4hHybQKoFq01Z3",
|
||||
"authUrl": "http://localhost:9000/application/o/authorize/",
|
||||
"tokenUrl": "http://localhost:9000/application/o/token/",
|
||||
"scopes": ["email", "openid", "profile"],
|
||||
"userIdentifier": "email"
|
||||
}
|
||||
}
|
||||
11
bruno/IDP/Generate OIDC URL.bru
Normal file
11
bruno/IDP/Generate OIDC URL.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: Generate OIDC URL
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
3
bruno/IDP/folder.bru
Normal file
3
bruno/IDP/folder.bru
Normal file
@@ -0,0 +1,3 @@
|
||||
meta {
|
||||
name: IDP
|
||||
}
|
||||
11
bruno/Internal/Traefik Config.bru
Normal file
11
bruno/Internal/Traefik Config.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: Traefik Config
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3001/api/v1/traefik-config
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
3
bruno/Internal/folder.bru
Normal file
3
bruno/Internal/folder.bru
Normal file
@@ -0,0 +1,3 @@
|
||||
meta {
|
||||
name: Internal
|
||||
}
|
||||
11
bruno/Newt/Create Newt.bru
Normal file
11
bruno/Newt/Create Newt.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: Create Newt
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1/newt
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
18
bruno/Newt/Get Token.bru
Normal file
18
bruno/Newt/Get Token.bru
Normal file
@@ -0,0 +1,18 @@
|
||||
meta {
|
||||
name: Get Token
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1/auth/newt/get-token
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"newtId": "o0d4rdxq3stnz7b",
|
||||
"secret": "sy7l09fnaesd03iwrfp9m3qf0ryn19g0zf3dqieaazb4k7vk"
|
||||
}
|
||||
}
|
||||
11
bruno/Orgs/Check Id.bru
Normal file
11
bruno/Orgs/Check Id.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: Check Id
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1/org/checkId
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
11
bruno/Orgs/listOrgs.bru
Normal file
11
bruno/Orgs/listOrgs.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: listOrgs
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url:
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
11
bruno/Remote Exit Node/createRemoteExitNode.bru
Normal file
11
bruno/Remote Exit Node/createRemoteExitNode.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: createRemoteExitNode
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
put {
|
||||
url: http://localhost:4000/api/v1/org/org_i21aifypnlyxur2/remote-exit-node
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
11
bruno/Resources/listResourcesByOrg.bru
Normal file
11
bruno/Resources/listResourcesByOrg.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: listResourcesByOrg
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url:
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
16
bruno/Resources/listResourcesBySite.bru
Normal file
16
bruno/Resources/listResourcesBySite.bru
Normal file
@@ -0,0 +1,16 @@
|
||||
meta {
|
||||
name: listResourcesBySite
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1/site/1/resources?limit=10&offset=0
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
params:query {
|
||||
limit: 10
|
||||
offset: 0
|
||||
}
|
||||
11
bruno/Sites/Get Site.bru
Normal file
11
bruno/Sites/Get Site.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: Get Site
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1/org/test/sites/mexican-mole-lizard-windy
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
11
bruno/Sites/listSites.bru
Normal file
11
bruno/Sites/listSites.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: listSites
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url:
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
16
bruno/Targets/listTargets.bru
Normal file
16
bruno/Targets/listTargets.bru
Normal file
@@ -0,0 +1,16 @@
|
||||
meta {
|
||||
name: listTargets
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1/resource/web.main.localhost/targets?limit=10&offset=0
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
params:query {
|
||||
limit: 10
|
||||
offset: 0
|
||||
}
|
||||
11
bruno/Test.bru
Normal file
11
bruno/Test.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: Test
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
11
bruno/Traefik/traefik-config.bru
Normal file
11
bruno/Traefik/traefik-config.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: traefik-config
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3001/api/v1/traefik-config
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
11
bruno/Users/adminListUsers.bru
Normal file
11
bruno/Users/adminListUsers.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: adminListUsers
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1/users
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
11
bruno/Users/adminRemoveUser.bru
Normal file
11
bruno/Users/adminRemoveUser.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: adminRemoveUser
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
delete {
|
||||
url: http://localhost:3000/api/v1/user/ky5r7ivqs8wc7u4
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
11
bruno/Users/getUser.bru
Normal file
11
bruno/Users/getUser.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: getUser
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url:
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
13
bruno/bruno.json
Normal file
13
bruno/bruno.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "Pangolin Saas",
|
||||
"type": "collection",
|
||||
"ignore": [
|
||||
"node_modules",
|
||||
".git"
|
||||
],
|
||||
"presets": {
|
||||
"requestType": "http",
|
||||
"requestUrl": "http://localhost:3000/api/v1"
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { CommandModule } from "yargs";
|
||||
import { db, certificates } from "@server/db";
|
||||
|
||||
type ClearCertificatesArgs = {};
|
||||
|
||||
export const clearCertificates: CommandModule<{}, ClearCertificatesArgs> = {
|
||||
command: "clear-certificates",
|
||||
describe: "Delete all entries from the certificates table",
|
||||
builder: (yargs) => {
|
||||
return yargs;
|
||||
},
|
||||
handler: async (argv: {}) => {
|
||||
try {
|
||||
console.log("Clearing all certificates from the database...");
|
||||
|
||||
const deleted = await db.delete(certificates).returning();
|
||||
|
||||
console.log(
|
||||
`Deleted ${deleted.length} certificate(s) from the database`
|
||||
);
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
import { CommandModule } from "yargs";
|
||||
import { db, exitNodes } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
type ClearExitNodesArgs = { };
|
||||
|
||||
export const clearExitNodes: CommandModule<
|
||||
{},
|
||||
ClearExitNodesArgs
|
||||
> = {
|
||||
command: "clear-exit-nodes",
|
||||
describe:
|
||||
"Clear all exit nodes from the database",
|
||||
// no args
|
||||
builder: (yargs) => {
|
||||
return yargs;
|
||||
},
|
||||
handler: async (argv: {}) => {
|
||||
try {
|
||||
|
||||
console.log(`Clearing all exit nodes from the database`);
|
||||
|
||||
// Delete all exit nodes
|
||||
const deletedCount = await db
|
||||
.delete(exitNodes)
|
||||
.where(eq(exitNodes.exitNodeId, exitNodes.exitNodeId)) .returning();; // delete all
|
||||
|
||||
console.log(`Deleted ${deletedCount.length} exit node(s) from the database`);
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
import { CommandModule } from "yargs";
|
||||
import { db, licenseKey } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
type ClearLicenseKeysArgs = { };
|
||||
|
||||
export const clearLicenseKeys: CommandModule<
|
||||
{},
|
||||
ClearLicenseKeysArgs
|
||||
> = {
|
||||
command: "clear-license-keys",
|
||||
describe:
|
||||
"Clear all license keys from the database",
|
||||
// no args
|
||||
builder: (yargs) => {
|
||||
return yargs;
|
||||
},
|
||||
handler: async (argv: {}) => {
|
||||
try {
|
||||
|
||||
console.log(`Clearing all license keys from the database`);
|
||||
|
||||
// Delete all license keys
|
||||
const deletedCount = await db
|
||||
.delete(licenseKey)
|
||||
.where(eq(licenseKey.licenseKeyId, licenseKey.licenseKeyId)) .returning();; // delete all
|
||||
|
||||
console.log(`Deleted ${deletedCount.length} license key(s) from the database`);
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,123 +0,0 @@
|
||||
import { CommandModule } from "yargs";
|
||||
import { db, clients, olms, currentFingerprint, userClients, approvals } from "@server/db";
|
||||
import { eq, and, inArray } from "drizzle-orm";
|
||||
|
||||
type DeleteClientArgs = {
|
||||
orgId: string;
|
||||
niceId: string;
|
||||
};
|
||||
|
||||
export const deleteClient: CommandModule<{}, DeleteClientArgs> = {
|
||||
command: "delete-client",
|
||||
describe:
|
||||
"Delete a client and all associated data (OLMs, current fingerprint, userClients, approvals). Snapshots are preserved.",
|
||||
builder: (yargs) => {
|
||||
return yargs
|
||||
.option("orgId", {
|
||||
type: "string",
|
||||
demandOption: true,
|
||||
describe: "The organization ID"
|
||||
})
|
||||
.option("niceId", {
|
||||
type: "string",
|
||||
demandOption: true,
|
||||
describe: "The client niceId (identifier)"
|
||||
});
|
||||
},
|
||||
handler: async (argv: { orgId: string; niceId: string }) => {
|
||||
try {
|
||||
const { orgId, niceId } = argv;
|
||||
|
||||
console.log(
|
||||
`Deleting client with orgId: ${orgId}, niceId: ${niceId}...`
|
||||
);
|
||||
|
||||
// Find the client
|
||||
const [client] = await db
|
||||
.select()
|
||||
.from(clients)
|
||||
.where(and(eq(clients.orgId, orgId), eq(clients.niceId, niceId)))
|
||||
.limit(1);
|
||||
|
||||
if (!client) {
|
||||
console.error(
|
||||
`Error: Client with orgId "${orgId}" and niceId "${niceId}" not found.`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const clientId = client.clientId;
|
||||
console.log(`Found client with clientId: ${clientId}`);
|
||||
|
||||
// Find all OLMs associated with this client
|
||||
const associatedOlms = await db
|
||||
.select()
|
||||
.from(olms)
|
||||
.where(eq(olms.clientId, clientId));
|
||||
|
||||
console.log(`Found ${associatedOlms.length} OLM(s) associated with this client`);
|
||||
|
||||
// Delete in a transaction to ensure atomicity
|
||||
await db.transaction(async (trx) => {
|
||||
// Delete currentFingerprint entries for the associated OLMs
|
||||
// Note: We delete these explicitly before deleting OLMs to ensure
|
||||
// we have control, even though cascade would handle it
|
||||
let fingerprintCount = 0;
|
||||
if (associatedOlms.length > 0) {
|
||||
const olmIds = associatedOlms.map((olm) => olm.olmId);
|
||||
const deletedFingerprints = await trx
|
||||
.delete(currentFingerprint)
|
||||
.where(inArray(currentFingerprint.olmId, olmIds))
|
||||
.returning();
|
||||
fingerprintCount = deletedFingerprints.length;
|
||||
}
|
||||
console.log(`Deleted ${fingerprintCount} current fingerprint(s)`);
|
||||
|
||||
// Delete OLMs
|
||||
// Note: OLMs have onDelete: "set null" for clientId, so we need to delete them explicitly
|
||||
const deletedOlms = await trx
|
||||
.delete(olms)
|
||||
.where(eq(olms.clientId, clientId))
|
||||
.returning();
|
||||
console.log(`Deleted ${deletedOlms.length} OLM(s)`);
|
||||
|
||||
// Delete approvals
|
||||
// Note: Approvals have onDelete: "cascade" but we delete explicitly for clarity
|
||||
const deletedApprovals = await trx
|
||||
.delete(approvals)
|
||||
.where(eq(approvals.clientId, clientId))
|
||||
.returning();
|
||||
console.log(`Deleted ${deletedApprovals.length} approval(s)`);
|
||||
|
||||
// Delete userClients
|
||||
// Note: userClients have onDelete: "cascade" but we delete explicitly for clarity
|
||||
const deletedUserClients = await trx
|
||||
.delete(userClients)
|
||||
.where(eq(userClients.clientId, clientId))
|
||||
.returning();
|
||||
console.log(`Deleted ${deletedUserClients.length} userClient association(s)`);
|
||||
|
||||
// Finally, delete the client itself
|
||||
const deletedClients = await trx
|
||||
.delete(clients)
|
||||
.where(eq(clients.clientId, clientId))
|
||||
.returning();
|
||||
console.log(`Deleted client: ${deletedClients[0]?.name || niceId}`);
|
||||
});
|
||||
|
||||
console.log("\nClient deletion completed successfully!");
|
||||
console.log("\nSummary:");
|
||||
console.log(` - Client: ${niceId} (clientId: ${clientId})`);
|
||||
console.log(` - Olm(s): ${associatedOlms.length}`);
|
||||
console.log(` - Current fingerprints: deleted`);
|
||||
console.log(` - Approvals: deleted`);
|
||||
console.log(` - UserClients: deleted`);
|
||||
console.log(` - Snapshots: preserved (not deleted)`);
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error("Error deleting client:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
import { CommandModule } from "yargs";
|
||||
import { db, users } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
/**
|
||||
* Disable 2FA for a user by email address.
|
||||
*/
|
||||
type DisableUser2faArgs = {
|
||||
email: string;
|
||||
};
|
||||
|
||||
export const disableUser2fa: CommandModule<{}, DisableUser2faArgs> = {
|
||||
command: "disable-user-2fa",
|
||||
describe: "Disable 2FA for a user (sets twoFactorEnabled=false, clears secret)",
|
||||
builder: (yargs) => {
|
||||
return yargs.option("email", {
|
||||
type: "string",
|
||||
demandOption: true,
|
||||
describe: "User email address"
|
||||
});
|
||||
},
|
||||
handler: async (argv: { email: string }) => {
|
||||
try {
|
||||
const { email } = argv;
|
||||
console.log(`Looking for user with email: ${email}`);
|
||||
|
||||
// Find the user by email
|
||||
const [user] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.email, email))
|
||||
.limit(1);
|
||||
|
||||
if (!user) {
|
||||
console.error(`User with email '${email}' not found`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!user.twoFactorEnabled) {
|
||||
console.log(`2FA is already disabled for user '${email}'.`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Update user: disable 2FA and clear secret
|
||||
await db.update(users)
|
||||
.set({
|
||||
twoFactorEnabled: false,
|
||||
twoFactorSecret: null,
|
||||
twoFactorSetupRequested: false
|
||||
})
|
||||
.where(eq(users.userId, user.userId));
|
||||
|
||||
console.log(`2FA disabled for user '${email}'.`);
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error("Error disabling 2FA:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,121 +0,0 @@
|
||||
import { CommandModule } from "yargs";
|
||||
import { db, orgs } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { encrypt } from "@server/lib/crypto";
|
||||
import { configFilePath1, configFilePath2 } from "@server/lib/consts";
|
||||
import { generateCA } from "@server/lib/sshCA";
|
||||
import fs from "fs";
|
||||
import yaml from "js-yaml";
|
||||
|
||||
type GenerateOrgCaKeysArgs = {
|
||||
orgId: string;
|
||||
secret?: string;
|
||||
force?: boolean;
|
||||
};
|
||||
|
||||
export const generateOrgCaKeys: CommandModule<{}, GenerateOrgCaKeysArgs> = {
|
||||
command: "generate-org-ca-keys",
|
||||
describe:
|
||||
"Generate SSH CA public/private key pair for an organization and store them in the database (private key encrypted with server secret)",
|
||||
builder: (yargs) => {
|
||||
return yargs
|
||||
.option("orgId", {
|
||||
type: "string",
|
||||
demandOption: true,
|
||||
describe: "The organization ID"
|
||||
})
|
||||
.option("secret", {
|
||||
type: "string",
|
||||
describe:
|
||||
"Server secret used to encrypt the CA private key. If omitted, read from config file (config.yml or config.yaml)."
|
||||
})
|
||||
.option("force", {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
describe:
|
||||
"Overwrite existing CA keys for the org if they already exist"
|
||||
});
|
||||
},
|
||||
handler: async (argv: {
|
||||
orgId: string;
|
||||
secret?: string;
|
||||
force?: boolean;
|
||||
}) => {
|
||||
try {
|
||||
const { orgId, force } = argv;
|
||||
let secret = argv.secret;
|
||||
|
||||
if (!secret) {
|
||||
const configPath = fs.existsSync(configFilePath1)
|
||||
? configFilePath1
|
||||
: fs.existsSync(configFilePath2)
|
||||
? configFilePath2
|
||||
: null;
|
||||
|
||||
if (!configPath) {
|
||||
console.error(
|
||||
"Error: No server secret provided and config file not found. " +
|
||||
"Expected config.yml or config.yaml in the config directory, or pass --secret."
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const configContent = fs.readFileSync(configPath, "utf8");
|
||||
const config = yaml.load(configContent) as {
|
||||
server?: { secret?: string };
|
||||
};
|
||||
|
||||
if (!config?.server?.secret) {
|
||||
console.error(
|
||||
"Error: No server.secret in config file. Pass --secret or set server.secret in config."
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
secret = config.server.secret;
|
||||
}
|
||||
|
||||
const [org] = await db
|
||||
.select({
|
||||
orgId: orgs.orgId,
|
||||
sshCaPrivateKey: orgs.sshCaPrivateKey,
|
||||
sshCaPublicKey: orgs.sshCaPublicKey
|
||||
})
|
||||
.from(orgs)
|
||||
.where(eq(orgs.orgId, orgId))
|
||||
.limit(1);
|
||||
|
||||
if (!org) {
|
||||
console.error(`Error: Organization with orgId "${orgId}" not found.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (org.sshCaPrivateKey != null || org.sshCaPublicKey != null) {
|
||||
if (!force) {
|
||||
console.error(
|
||||
"Error: This organization already has CA keys. Use --force to overwrite."
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const ca = generateCA(`pangolin-ssh-ca-${orgId}`);
|
||||
const encryptedPrivateKey = encrypt(ca.privateKeyPem, secret);
|
||||
|
||||
await db
|
||||
.update(orgs)
|
||||
.set({
|
||||
sshCaPrivateKey: encryptedPrivateKey,
|
||||
sshCaPublicKey: ca.publicKeyOpenSSH
|
||||
})
|
||||
.where(eq(orgs.orgId, orgId));
|
||||
|
||||
console.log("SSH CA keys generated and stored for org:", orgId);
|
||||
console.log("\nPublic key (OpenSSH format):");
|
||||
console.log(ca.publicKeyOpenSSH);
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error("Error generating org CA keys:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,416 +0,0 @@
|
||||
import { CommandModule } from "yargs";
|
||||
import { db, idpOidcConfig, licenseKey, certificates, eventStreamingDestinations, alertWebhookActions } from "@server/db";
|
||||
import { encrypt, decrypt } from "@server/lib/crypto";
|
||||
import { configFilePath1, configFilePath2 } from "@server/lib/consts";
|
||||
import { eq } from "drizzle-orm";
|
||||
import fs from "fs";
|
||||
import yaml from "js-yaml";
|
||||
|
||||
type RotateServerSecretArgs = {
|
||||
"old-secret": string;
|
||||
"new-secret": string;
|
||||
force?: boolean;
|
||||
};
|
||||
|
||||
export const rotateServerSecret: CommandModule<
|
||||
{},
|
||||
RotateServerSecretArgs
|
||||
> = {
|
||||
command: "rotate-server-secret",
|
||||
describe:
|
||||
"Rotate the server secret by decrypting all encrypted values with the old secret and re-encrypting with a new secret",
|
||||
builder: (yargs) => {
|
||||
return yargs
|
||||
.option("old-secret", {
|
||||
type: "string",
|
||||
demandOption: true,
|
||||
describe: "The current server secret (for verification)"
|
||||
})
|
||||
.option("new-secret", {
|
||||
type: "string",
|
||||
demandOption: true,
|
||||
describe: "The new server secret to use"
|
||||
})
|
||||
.option("force", {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
describe:
|
||||
"Force rotation even if the old secret doesn't match the config file. " +
|
||||
"Use this if you know the old secret is correct but the config file is out of sync. " +
|
||||
"WARNING: This will attempt to decrypt all values with the provided old secret. " +
|
||||
"If the old secret is incorrect, the rotation will fail or corrupt data."
|
||||
});
|
||||
},
|
||||
handler: async (argv: {
|
||||
"old-secret": string;
|
||||
"new-secret": string;
|
||||
force?: boolean;
|
||||
}) => {
|
||||
try {
|
||||
// Determine which config file exists
|
||||
const configPath = fs.existsSync(configFilePath1)
|
||||
? configFilePath1
|
||||
: fs.existsSync(configFilePath2)
|
||||
? configFilePath2
|
||||
: null;
|
||||
|
||||
if (!configPath) {
|
||||
console.error(
|
||||
"Error: Config file not found. Expected config.yml or config.yaml in the config directory."
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Read current config
|
||||
const configContent = fs.readFileSync(configPath, "utf8");
|
||||
const config = yaml.load(configContent) as any;
|
||||
|
||||
if (!config?.server?.secret) {
|
||||
console.error(
|
||||
"Error: No server secret found in config file. Cannot rotate."
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const configSecret = config.server.secret;
|
||||
const oldSecret = argv["old-secret"];
|
||||
const newSecret = argv["new-secret"];
|
||||
const force = argv.force || false;
|
||||
|
||||
// Verify that the provided old secret matches the one in config
|
||||
if (configSecret !== oldSecret) {
|
||||
if (!force) {
|
||||
console.error(
|
||||
"Error: The provided old secret does not match the secret in the config file."
|
||||
);
|
||||
console.error(
|
||||
"\nIf you are certain the old secret is correct and the config file is out of sync,"
|
||||
);
|
||||
console.error(
|
||||
"you can use the --force flag to bypass this check."
|
||||
);
|
||||
console.error(
|
||||
"\nWARNING: Using --force with an incorrect old secret will cause the rotation to fail"
|
||||
);
|
||||
console.error(
|
||||
"or corrupt encrypted data. Only use --force if you are absolutely certain."
|
||||
);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.warn(
|
||||
"\nWARNING: Using --force flag. Bypassing old secret verification."
|
||||
);
|
||||
console.warn(
|
||||
"The provided old secret does not match the config file, but proceeding anyway."
|
||||
);
|
||||
console.warn(
|
||||
"If the old secret is incorrect, this operation will fail or corrupt data.\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate new secret
|
||||
if (newSecret.length < 8) {
|
||||
console.error(
|
||||
"Error: New secret must be at least 8 characters long"
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (oldSecret === newSecret) {
|
||||
console.error("Error: New secret must be different from old secret");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("Starting server secret rotation...");
|
||||
console.log("This will decrypt and re-encrypt all encrypted values in the database.");
|
||||
|
||||
// Read all data first
|
||||
console.log("\nReading encrypted data from database...");
|
||||
const idpConfigs = await db.select().from(idpOidcConfig);
|
||||
const licenseKeys = await db.select().from(licenseKey);
|
||||
const certs = await db.select().from(certificates);
|
||||
const streamingDestinations = await db.select().from(eventStreamingDestinations);
|
||||
const webhookActions = await db.select().from(alertWebhookActions);
|
||||
|
||||
console.log(`Found ${idpConfigs.length} OIDC IdP configuration(s)`);
|
||||
console.log(`Found ${licenseKeys.length} license key(s)`);
|
||||
console.log(`Found ${certs.length} certificate(s)`);
|
||||
console.log(`Found ${streamingDestinations.length} event streaming destination(s)`);
|
||||
console.log(`Found ${webhookActions.length} alert webhook action(s)`);
|
||||
|
||||
// Prepare all decrypted and re-encrypted values
|
||||
console.log("\nDecrypting and re-encrypting values...");
|
||||
|
||||
type IdpUpdate = {
|
||||
idpOauthConfigId: number;
|
||||
encryptedClientId: string;
|
||||
encryptedClientSecret: string;
|
||||
};
|
||||
|
||||
type LicenseKeyUpdate = {
|
||||
oldLicenseKeyId: string;
|
||||
newLicenseKeyId: string;
|
||||
encryptedToken: string;
|
||||
encryptedInstanceId: string;
|
||||
};
|
||||
|
||||
type CertUpdate = {
|
||||
certId: number;
|
||||
encryptedCertFile: string | null;
|
||||
encryptedKeyFile: string | null;
|
||||
};
|
||||
|
||||
type StreamingDestinationUpdate = {
|
||||
destinationId: number;
|
||||
encryptedConfig: string;
|
||||
};
|
||||
|
||||
type WebhookActionUpdate = {
|
||||
webhookActionId: number;
|
||||
encryptedConfig: string;
|
||||
};
|
||||
|
||||
const idpUpdates: IdpUpdate[] = [];
|
||||
const licenseKeyUpdates: LicenseKeyUpdate[] = [];
|
||||
const certUpdates: CertUpdate[] = [];
|
||||
const streamingDestinationUpdates: StreamingDestinationUpdate[] = [];
|
||||
const webhookActionUpdates: WebhookActionUpdate[] = [];
|
||||
|
||||
// Process idpOidcConfig entries
|
||||
for (const idpConfig of idpConfigs) {
|
||||
try {
|
||||
// Decrypt with old secret
|
||||
const decryptedClientId = decrypt(idpConfig.clientId, oldSecret);
|
||||
const decryptedClientSecret = decrypt(
|
||||
idpConfig.clientSecret,
|
||||
oldSecret
|
||||
);
|
||||
|
||||
// Re-encrypt with new secret
|
||||
const encryptedClientId = encrypt(decryptedClientId, newSecret);
|
||||
const encryptedClientSecret = encrypt(
|
||||
decryptedClientSecret,
|
||||
newSecret
|
||||
);
|
||||
|
||||
idpUpdates.push({
|
||||
idpOauthConfigId: idpConfig.idpOauthConfigId,
|
||||
encryptedClientId,
|
||||
encryptedClientSecret
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error processing IdP config ${idpConfig.idpOauthConfigId}:`,
|
||||
error
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Process licenseKey entries
|
||||
for (const key of licenseKeys) {
|
||||
try {
|
||||
// Decrypt with old secret
|
||||
const decryptedLicenseKeyId = decrypt(key.licenseKeyId, oldSecret);
|
||||
const decryptedToken = decrypt(key.token, oldSecret);
|
||||
const decryptedInstanceId = decrypt(key.instanceId, oldSecret);
|
||||
|
||||
// Re-encrypt with new secret
|
||||
const encryptedLicenseKeyId = encrypt(
|
||||
decryptedLicenseKeyId,
|
||||
newSecret
|
||||
);
|
||||
const encryptedToken = encrypt(decryptedToken, newSecret);
|
||||
const encryptedInstanceId = encrypt(
|
||||
decryptedInstanceId,
|
||||
newSecret
|
||||
);
|
||||
|
||||
licenseKeyUpdates.push({
|
||||
oldLicenseKeyId: key.licenseKeyId,
|
||||
newLicenseKeyId: encryptedLicenseKeyId,
|
||||
encryptedToken,
|
||||
encryptedInstanceId
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error processing license key ${key.licenseKeyId}:`,
|
||||
error
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Process certificate entries
|
||||
for (const cert of certs) {
|
||||
try {
|
||||
const encryptedCertFile = cert.certFile
|
||||
? encrypt(decrypt(cert.certFile, oldSecret), newSecret)
|
||||
: null;
|
||||
const encryptedKeyFile = cert.keyFile
|
||||
? encrypt(decrypt(cert.keyFile, oldSecret), newSecret)
|
||||
: null;
|
||||
|
||||
certUpdates.push({
|
||||
certId: cert.certId,
|
||||
encryptedCertFile,
|
||||
encryptedKeyFile
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error processing certificate ${cert.certId} (${cert.domain}):`,
|
||||
error
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Process eventStreamingDestinations entries
|
||||
for (const dest of streamingDestinations) {
|
||||
try {
|
||||
const decryptedConfig = decrypt(dest.config, oldSecret);
|
||||
const encryptedConfig = encrypt(decryptedConfig, newSecret);
|
||||
|
||||
streamingDestinationUpdates.push({
|
||||
destinationId: dest.destinationId,
|
||||
encryptedConfig
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error processing event streaming destination ${dest.destinationId}:`,
|
||||
error
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Process alertWebhookActions entries
|
||||
for (const webhook of webhookActions) {
|
||||
try {
|
||||
if (webhook.config == null) continue;
|
||||
|
||||
const decryptedConfig = decrypt(webhook.config, oldSecret);
|
||||
const encryptedConfig = encrypt(decryptedConfig, newSecret);
|
||||
|
||||
webhookActionUpdates.push({
|
||||
webhookActionId: webhook.webhookActionId,
|
||||
encryptedConfig
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error processing alert webhook action ${webhook.webhookActionId}:`,
|
||||
error
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Perform all database updates in a single transaction
|
||||
console.log("\nUpdating database in transaction...");
|
||||
await db.transaction(async (trx) => {
|
||||
// Update idpOidcConfig entries
|
||||
for (const update of idpUpdates) {
|
||||
await trx
|
||||
.update(idpOidcConfig)
|
||||
.set({
|
||||
clientId: update.encryptedClientId,
|
||||
clientSecret: update.encryptedClientSecret
|
||||
})
|
||||
.where(
|
||||
eq(
|
||||
idpOidcConfig.idpOauthConfigId,
|
||||
update.idpOauthConfigId
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Update licenseKey entries (delete old, insert new)
|
||||
for (const update of licenseKeyUpdates) {
|
||||
// Delete old entry
|
||||
await trx
|
||||
.delete(licenseKey)
|
||||
.where(eq(licenseKey.licenseKeyId, update.oldLicenseKeyId));
|
||||
|
||||
// Insert new entry with re-encrypted values
|
||||
await trx.insert(licenseKey).values({
|
||||
licenseKeyId: update.newLicenseKeyId,
|
||||
token: update.encryptedToken,
|
||||
instanceId: update.encryptedInstanceId
|
||||
});
|
||||
}
|
||||
|
||||
// Update certificate entries
|
||||
for (const update of certUpdates) {
|
||||
await trx
|
||||
.update(certificates)
|
||||
.set({
|
||||
certFile: update.encryptedCertFile,
|
||||
keyFile: update.encryptedKeyFile
|
||||
})
|
||||
.where(eq(certificates.certId, update.certId));
|
||||
}
|
||||
|
||||
// Update event streaming destination entries
|
||||
for (const update of streamingDestinationUpdates) {
|
||||
await trx
|
||||
.update(eventStreamingDestinations)
|
||||
.set({ config: update.encryptedConfig })
|
||||
.where(
|
||||
eq(
|
||||
eventStreamingDestinations.destinationId,
|
||||
update.destinationId
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Update alert webhook action entries
|
||||
for (const update of webhookActionUpdates) {
|
||||
await trx
|
||||
.update(alertWebhookActions)
|
||||
.set({ config: update.encryptedConfig })
|
||||
.where(
|
||||
eq(
|
||||
alertWebhookActions.webhookActionId,
|
||||
update.webhookActionId
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Rotated ${idpUpdates.length} OIDC IdP configuration(s)`);
|
||||
console.log(`Rotated ${licenseKeyUpdates.length} license key(s)`);
|
||||
console.log(`Rotated ${certUpdates.length} certificate(s)`);
|
||||
console.log(`Rotated ${streamingDestinationUpdates.length} event streaming destination(s)`);
|
||||
console.log(`Rotated ${webhookActionUpdates.length} alert webhook action(s)`);
|
||||
|
||||
// Update config file with new secret
|
||||
console.log("\nUpdating config file...");
|
||||
config.server.secret = newSecret;
|
||||
const newConfigContent = yaml.dump(config, {
|
||||
indent: 2,
|
||||
lineWidth: -1
|
||||
});
|
||||
fs.writeFileSync(configPath, newConfigContent, "utf8");
|
||||
|
||||
console.log(`Updated config file: ${configPath}`);
|
||||
|
||||
console.log("\nServer secret rotation completed successfully!");
|
||||
console.log(`\nSummary:`);
|
||||
console.log(` - OIDC IdP configurations: ${idpUpdates.length}`);
|
||||
console.log(` - License keys: ${licenseKeyUpdates.length}`);
|
||||
console.log(` - Certificates: ${certUpdates.length}`);
|
||||
console.log(` - Event streaming destinations: ${streamingDestinationUpdates.length}`);
|
||||
console.log(` - Alert webhook actions: ${webhookActionUpdates.length}`);
|
||||
console.log(
|
||||
`\n IMPORTANT: Restart the server for the new secret to take effect.`
|
||||
);
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error("Error rotating server secret:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
14
cli/index.ts
14
cli/index.ts
@@ -4,24 +4,10 @@ import yargs from "yargs";
|
||||
import { hideBin } from "yargs/helpers";
|
||||
import { setAdminCredentials } from "@cli/commands/setAdminCredentials";
|
||||
import { resetUserSecurityKeys } from "@cli/commands/resetUserSecurityKeys";
|
||||
import { clearExitNodes } from "./commands/clearExitNodes";
|
||||
import { rotateServerSecret } from "./commands/rotateServerSecret";
|
||||
import { clearLicenseKeys } from "./commands/clearLicenseKeys";
|
||||
import { deleteClient } from "./commands/deleteClient";
|
||||
import { generateOrgCaKeys } from "./commands/generateOrgCaKeys";
|
||||
import { clearCertificates } from "./commands/clearCertificates";
|
||||
import { disableUser2fa } from "./commands/disableUser2fa";
|
||||
|
||||
yargs(hideBin(process.argv))
|
||||
.scriptName("pangctl")
|
||||
.command(setAdminCredentials)
|
||||
.command(resetUserSecurityKeys)
|
||||
.command(clearExitNodes)
|
||||
.command(rotateServerSecret)
|
||||
.command(clearLicenseKeys)
|
||||
.command(deleteClient)
|
||||
.command(generateOrgCaKeys)
|
||||
.command(clearCertificates)
|
||||
.command(disableUser2fa)
|
||||
.demandCommand()
|
||||
.help().argv;
|
||||
|
||||
@@ -17,4 +17,4 @@
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,28 @@
|
||||
# To see all available options, please visit the docs:
|
||||
# https://docs.pangolin.net/
|
||||
|
||||
gerbil:
|
||||
start_port: 51820
|
||||
base_endpoint: "{{.DashboardDomain}}"
|
||||
# https://docs.pangolin.net/self-host/advanced/config-file
|
||||
|
||||
app:
|
||||
dashboard_url: "https://{{.DashboardDomain}}"
|
||||
log_level: "info"
|
||||
telemetry:
|
||||
anonymous_usage: true
|
||||
dashboard_url: http://localhost:3002
|
||||
log_level: debug
|
||||
|
||||
domains:
|
||||
domain1:
|
||||
base_domain: "{{.BaseDomain}}"
|
||||
domain1:
|
||||
base_domain: example.com
|
||||
|
||||
server:
|
||||
secret: "{{.Secret}}"
|
||||
cors:
|
||||
origins: ["https://{{.DashboardDomain}}"]
|
||||
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
||||
allowed_headers: ["X-CSRF-Token", "Content-Type"]
|
||||
credentials: false
|
||||
secret: my_secret_key
|
||||
|
||||
gerbil:
|
||||
base_endpoint: example.com
|
||||
|
||||
orgs:
|
||||
block_size: 24
|
||||
subnet_group: 100.90.137.0/20
|
||||
|
||||
flags:
|
||||
require_email_verification: false
|
||||
disable_signup_without_invite: true
|
||||
disable_user_create_org: false
|
||||
allow_raw_resources: true
|
||||
require_email_verification: false
|
||||
disable_signup_without_invite: true
|
||||
disable_user_create_org: true
|
||||
allow_raw_resources: true
|
||||
enable_integration_api: true
|
||||
enable_clients: true
|
||||
|
||||
1
config/db/.gitignore
vendored
1
config/db/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
*-journal
|
||||
@@ -1,9 +1,5 @@
|
||||
http:
|
||||
middlewares:
|
||||
badger:
|
||||
plugin:
|
||||
badger:
|
||||
disableForwardAuth: true
|
||||
redirect-to-https:
|
||||
redirectScheme:
|
||||
scheme: https
|
||||
@@ -17,16 +13,14 @@ http:
|
||||
- web
|
||||
middlewares:
|
||||
- redirect-to-https
|
||||
- badger
|
||||
|
||||
# Next.js router (handles everything except API and WebSocket paths)
|
||||
next-router:
|
||||
rule: "Host(`{{.DashboardDomain}}`) && !PathPrefix(`/api/v1`)"
|
||||
rule: "Host(`{{.DashboardDomain}}`)"
|
||||
service: next-service
|
||||
priority: 10
|
||||
entryPoints:
|
||||
- websecure
|
||||
middlewares:
|
||||
- badger
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
@@ -34,10 +28,9 @@ http:
|
||||
api-router:
|
||||
rule: "Host(`{{.DashboardDomain}}`) && PathPrefix(`/api/v1`)"
|
||||
service: api-service
|
||||
priority: 100
|
||||
entryPoints:
|
||||
- websecure
|
||||
middlewares:
|
||||
- badger
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
@@ -51,12 +44,3 @@ http:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://pangolin:3000" # API/WebSocket server
|
||||
|
||||
tcp:
|
||||
serversTransports:
|
||||
pp-transport-v1:
|
||||
proxyProtocol:
|
||||
version: 1
|
||||
pp-transport-v2:
|
||||
proxyProtocol:
|
||||
version: 2
|
||||
|
||||
@@ -3,52 +3,32 @@ api:
|
||||
dashboard: true
|
||||
|
||||
providers:
|
||||
http:
|
||||
endpoint: "http://pangolin:3001/api/v1/traefik-config"
|
||||
pollInterval: "5s"
|
||||
file:
|
||||
filename: "/etc/traefik/dynamic_config.yml"
|
||||
directory: "/var/dynamic"
|
||||
watch: true
|
||||
|
||||
experimental:
|
||||
plugins:
|
||||
badger:
|
||||
moduleName: "github.com/fosrl/badger"
|
||||
version: "{{.BadgerVersion}}"
|
||||
version: "v1.2.0"
|
||||
|
||||
log:
|
||||
level: "INFO"
|
||||
level: "DEBUG"
|
||||
format: "common"
|
||||
maxSize: 100
|
||||
maxBackups: 3
|
||||
maxAge: 3
|
||||
compress: true
|
||||
|
||||
certificatesResolvers:
|
||||
letsencrypt:
|
||||
acme:
|
||||
httpChallenge:
|
||||
entryPoint: web
|
||||
email: "{{.LetsEncryptEmail}}"
|
||||
storage: "/letsencrypt/acme.json"
|
||||
caServer: "https://acme-v02.api.letsencrypt.org/directory"
|
||||
|
||||
entryPoints:
|
||||
web:
|
||||
address: ":80"
|
||||
websecure:
|
||||
address: ":443"
|
||||
address: ":9443"
|
||||
transport:
|
||||
respondingTimeouts:
|
||||
readTimeout: "30m"
|
||||
http:
|
||||
tls:
|
||||
certResolver: "letsencrypt"
|
||||
encodedCharacters:
|
||||
allowEncodedSlash: true
|
||||
allowEncodedQuestionMark: true
|
||||
|
||||
serversTransport:
|
||||
insecureSkipVerify: true
|
||||
|
||||
ping:
|
||||
entryPoint: "web"
|
||||
|
||||
@@ -4,12 +4,6 @@ services:
|
||||
image: fosrl/pangolin:latest
|
||||
container_name: pangolin
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1g
|
||||
reservations:
|
||||
memory: 256m
|
||||
volumes:
|
||||
- ./config:/app/config
|
||||
healthcheck:
|
||||
@@ -41,7 +35,7 @@ services:
|
||||
- 80:80 # Port for traefik because of the network_mode
|
||||
|
||||
traefik:
|
||||
image: traefik:v3.6
|
||||
image: traefik:v3.5
|
||||
container_name: traefik
|
||||
restart: unless-stopped
|
||||
network_mode: service:gerbil # Ports appear on the gerbil service
|
||||
@@ -58,4 +52,4 @@ networks:
|
||||
default:
|
||||
driver: bridge
|
||||
name: pangolin
|
||||
enable_ipv6: true
|
||||
enable_ipv6: true
|
||||
@@ -1,12 +0,0 @@
|
||||
services:
|
||||
mailer:
|
||||
image: axllent/mailpit
|
||||
ports:
|
||||
- 8025:8025
|
||||
- 1025:1025
|
||||
volumes:
|
||||
- mailpit-storage:/data
|
||||
environment:
|
||||
- MP_DATABASE=/data/mailpit.db
|
||||
volumes:
|
||||
mailpit-storage:
|
||||
@@ -1,7 +1,9 @@
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
import path from "path";
|
||||
|
||||
const schema = [path.join("server", "db", "pg", "schema")];
|
||||
const schema = [
|
||||
path.join("server", "db", "pg", "schema"),
|
||||
];
|
||||
|
||||
export default defineConfig({
|
||||
dialect: "postgresql",
|
||||
|
||||
@@ -2,7 +2,9 @@ import { APP_PATH } from "@server/lib/consts";
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
import path from "path";
|
||||
|
||||
const schema = [path.join("server", "db", "sqlite", "schema")];
|
||||
const schema = [
|
||||
path.join("server", "db", "sqlite", "schema"),
|
||||
];
|
||||
|
||||
export default defineConfig({
|
||||
dialect: "sqlite",
|
||||
|
||||
101
esbuild.mjs
101
esbuild.mjs
@@ -6,12 +6,6 @@ import path from "path";
|
||||
import fs from "fs";
|
||||
// import { glob } from "glob";
|
||||
|
||||
// Read default build type from server/build.ts
|
||||
let build = "oss";
|
||||
const buildFile = fs.readFileSync(path.resolve("server/build.ts"), "utf8");
|
||||
const m = buildFile.match(/export\s+const\s+build\s*=\s*["'](oss|saas|enterprise)["']/);
|
||||
if (m) build = m[1];
|
||||
|
||||
const banner = `
|
||||
// patch __dirname
|
||||
// import { fileURLToPath } from "url";
|
||||
@@ -30,20 +24,20 @@ const argv = yargs(hideBin(process.argv))
|
||||
alias: "e",
|
||||
describe: "Entry point file",
|
||||
type: "string",
|
||||
demandOption: true
|
||||
demandOption: true,
|
||||
})
|
||||
.option("out", {
|
||||
alias: "o",
|
||||
describe: "Output file path",
|
||||
type: "string",
|
||||
demandOption: true
|
||||
demandOption: true,
|
||||
})
|
||||
.option("build", {
|
||||
alias: "b",
|
||||
describe: "Build type (oss, saas, enterprise)",
|
||||
type: "string",
|
||||
choices: ["oss", "saas", "enterprise"],
|
||||
default: build
|
||||
default: "oss",
|
||||
})
|
||||
.help()
|
||||
.alias("help", "h").argv;
|
||||
@@ -72,9 +66,7 @@ function privateImportGuardPlugin() {
|
||||
|
||||
// Check if the importing file is NOT in server/private
|
||||
const normalizedImporter = path.normalize(importingFile);
|
||||
const isInServerPrivate = normalizedImporter.includes(
|
||||
path.normalize("server/private")
|
||||
);
|
||||
const isInServerPrivate = normalizedImporter.includes(path.normalize("server/private"));
|
||||
|
||||
if (!isInServerPrivate) {
|
||||
const violation = {
|
||||
@@ -87,8 +79,8 @@ function privateImportGuardPlugin() {
|
||||
console.log(`PRIVATE IMPORT VIOLATION:`);
|
||||
console.log(` File: ${importingFile}`);
|
||||
console.log(` Import: ${args.path}`);
|
||||
console.log(` Resolve dir: ${args.resolveDir || "N/A"}`);
|
||||
console.log("");
|
||||
console.log(` Resolve dir: ${args.resolveDir || 'N/A'}`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Return null to let the default resolver handle it
|
||||
@@ -97,20 +89,16 @@ function privateImportGuardPlugin() {
|
||||
|
||||
build.onEnd((result) => {
|
||||
if (violations.length > 0) {
|
||||
console.log(
|
||||
`\nSUMMARY: Found ${violations.length} private import violation(s):`
|
||||
);
|
||||
console.log(`\nSUMMARY: Found ${violations.length} private import violation(s):`);
|
||||
violations.forEach((v, i) => {
|
||||
console.log(
|
||||
` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}`
|
||||
);
|
||||
console.log(` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}`);
|
||||
});
|
||||
console.log("");
|
||||
console.log('');
|
||||
|
||||
result.errors.push({
|
||||
text: `Private import violations detected: ${violations.length} violation(s) found`,
|
||||
location: null,
|
||||
notes: violations.map((v) => ({
|
||||
notes: violations.map(v => ({
|
||||
text: `${path.relative(process.cwd(), v.file)} imports ${v.importPath}`,
|
||||
location: null
|
||||
}))
|
||||
@@ -133,9 +121,7 @@ function dynamicImportGuardPlugin() {
|
||||
|
||||
// Check if the importing file is NOT in server/private
|
||||
const normalizedImporter = path.normalize(importingFile);
|
||||
const isInServerPrivate = normalizedImporter.includes(
|
||||
path.normalize("server/private")
|
||||
);
|
||||
const isInServerPrivate = normalizedImporter.includes(path.normalize("server/private"));
|
||||
|
||||
if (isInServerPrivate) {
|
||||
const violation = {
|
||||
@@ -148,8 +134,8 @@ function dynamicImportGuardPlugin() {
|
||||
console.log(`DYNAMIC IMPORT VIOLATION:`);
|
||||
console.log(` File: ${importingFile}`);
|
||||
console.log(` Import: ${args.path}`);
|
||||
console.log(` Resolve dir: ${args.resolveDir || "N/A"}`);
|
||||
console.log("");
|
||||
console.log(` Resolve dir: ${args.resolveDir || 'N/A'}`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Return null to let the default resolver handle it
|
||||
@@ -158,20 +144,16 @@ function dynamicImportGuardPlugin() {
|
||||
|
||||
build.onEnd((result) => {
|
||||
if (violations.length > 0) {
|
||||
console.log(
|
||||
`\nSUMMARY: Found ${violations.length} dynamic import violation(s):`
|
||||
);
|
||||
console.log(`\nSUMMARY: Found ${violations.length} dynamic import violation(s):`);
|
||||
violations.forEach((v, i) => {
|
||||
console.log(
|
||||
` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}`
|
||||
);
|
||||
console.log(` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}`);
|
||||
});
|
||||
console.log("");
|
||||
console.log('');
|
||||
|
||||
result.errors.push({
|
||||
text: `Dynamic import violations detected: ${violations.length} violation(s) found`,
|
||||
location: null,
|
||||
notes: violations.map((v) => ({
|
||||
notes: violations.map(v => ({
|
||||
text: `${path.relative(process.cwd(), v.file)} imports ${v.importPath}`,
|
||||
location: null
|
||||
}))
|
||||
@@ -190,28 +172,21 @@ function dynamicImportSwitcherPlugin(buildValue) {
|
||||
const switches = [];
|
||||
|
||||
build.onStart(() => {
|
||||
console.log(
|
||||
`Dynamic import switcher using build type: ${buildValue}`
|
||||
);
|
||||
console.log(`Dynamic import switcher using build type: ${buildValue}`);
|
||||
});
|
||||
|
||||
build.onResolve({ filter: /^#dynamic\// }, (args) => {
|
||||
// Extract the path after #dynamic/
|
||||
const dynamicPath = args.path.replace(/^#dynamic\//, "");
|
||||
const dynamicPath = args.path.replace(/^#dynamic\//, '');
|
||||
|
||||
// Determine the replacement based on build type
|
||||
let replacement;
|
||||
if (buildValue === "oss") {
|
||||
replacement = `#open/${dynamicPath}`;
|
||||
} else if (
|
||||
buildValue === "saas" ||
|
||||
buildValue === "enterprise"
|
||||
) {
|
||||
} else if (buildValue === "saas" || buildValue === "enterprise") {
|
||||
replacement = `#closed/${dynamicPath}`; // We use #closed here so that the route guards dont complain after its been changed but this is the same as #private
|
||||
} else {
|
||||
console.warn(
|
||||
`Unknown build type '${buildValue}', defaulting to #open/`
|
||||
);
|
||||
console.warn(`Unknown build type '${buildValue}', defaulting to #open/`);
|
||||
replacement = `#open/${dynamicPath}`;
|
||||
}
|
||||
|
||||
@@ -226,10 +201,8 @@ function dynamicImportSwitcherPlugin(buildValue) {
|
||||
console.log(`DYNAMIC IMPORT SWITCH:`);
|
||||
console.log(` File: ${args.importer}`);
|
||||
console.log(` Original: ${args.path}`);
|
||||
console.log(
|
||||
` Switched to: ${replacement} (build: ${buildValue})`
|
||||
);
|
||||
console.log("");
|
||||
console.log(` Switched to: ${replacement} (build: ${buildValue})`);
|
||||
console.log('');
|
||||
|
||||
// Rewrite the import path and let the normal resolution continue
|
||||
return build.resolve(replacement, {
|
||||
@@ -242,18 +215,12 @@ function dynamicImportSwitcherPlugin(buildValue) {
|
||||
|
||||
build.onEnd((result) => {
|
||||
if (switches.length > 0) {
|
||||
console.log(
|
||||
`\nDYNAMIC IMPORT SUMMARY: Switched ${switches.length} import(s) for build type '${buildValue}':`
|
||||
);
|
||||
console.log(`\nDYNAMIC IMPORT SUMMARY: Switched ${switches.length} import(s) for build type '${buildValue}':`);
|
||||
switches.forEach((s, i) => {
|
||||
console.log(
|
||||
` ${i + 1}. ${path.relative(process.cwd(), s.file)}`
|
||||
);
|
||||
console.log(
|
||||
` ${s.originalPath} → ${s.replacementPath}`
|
||||
);
|
||||
console.log(` ${i + 1}. ${path.relative(process.cwd(), s.file)}`);
|
||||
console.log(` ${s.originalPath} → ${s.replacementPath}`);
|
||||
});
|
||||
console.log("");
|
||||
console.log('');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -268,7 +235,7 @@ esbuild
|
||||
format: "esm",
|
||||
minify: false,
|
||||
banner: {
|
||||
js: banner
|
||||
js: banner,
|
||||
},
|
||||
platform: "node",
|
||||
external: ["body-parser"],
|
||||
@@ -277,22 +244,20 @@ esbuild
|
||||
dynamicImportGuardPlugin(),
|
||||
dynamicImportSwitcherPlugin(argv.build),
|
||||
nodeExternalsPlugin({
|
||||
packagePath: getPackagePaths()
|
||||
})
|
||||
packagePath: getPackagePaths(),
|
||||
}),
|
||||
],
|
||||
sourcemap: "inline",
|
||||
target: "node24"
|
||||
target: "node22",
|
||||
})
|
||||
.then((result) => {
|
||||
// Check if there were any errors in the build result
|
||||
if (result.errors && result.errors.length > 0) {
|
||||
console.error(
|
||||
`Build failed with ${result.errors.length} error(s):`
|
||||
);
|
||||
console.error(`Build failed with ${result.errors.length} error(s):`);
|
||||
result.errors.forEach((error, i) => {
|
||||
console.error(`${i + 1}. ${error.text}`);
|
||||
if (error.notes) {
|
||||
error.notes.forEach((note) => {
|
||||
error.notes.forEach(note => {
|
||||
console.error(` - ${note.text}`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import tseslint from "typescript-eslint";
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config({
|
||||
files: ["**/*.{ts,tsx,js,jsx}"],
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
}
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
semi: "error",
|
||||
"prefer-const": "warn"
|
||||
files: ["**/*.{ts,tsx,js,jsx}"],
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
rules: {
|
||||
"semi": "error",
|
||||
"prefer-const": "warn"
|
||||
}
|
||||
});
|
||||
@@ -1,24 +1,41 @@
|
||||
all: go-build-release
|
||||
|
||||
# Build with version injection via ldflags
|
||||
# Versions can be passed via: make go-build-release PANGOLIN_VERSION=x.x.x GERBIL_VERSION=x.x.x BADGER_VERSION=x.x.x
|
||||
# Or fetched automatically if not provided (requires curl and jq)
|
||||
|
||||
PANGOLIN_VERSION ?= $(shell curl -s https://api.github.com/repos/fosrl/pangolin/tags | jq -r '.[0].name')
|
||||
GERBIL_VERSION ?= $(shell curl -s https://api.github.com/repos/fosrl/gerbil/tags | jq -r '.[0].name')
|
||||
BADGER_VERSION ?= $(shell curl -s https://api.github.com/repos/fosrl/badger/tags | jq -r '.[0].name')
|
||||
|
||||
LDFLAGS = -X main.pangolinVersion=$(PANGOLIN_VERSION) \
|
||||
-X main.gerbilVersion=$(GERBIL_VERSION) \
|
||||
-X main.badgerVersion=$(BADGER_VERSION)
|
||||
all: update-versions go-build-release put-back
|
||||
dev-all: dev-update-versions dev-build dev-clean
|
||||
|
||||
go-build-release:
|
||||
@echo "Building with versions - Pangolin: $(PANGOLIN_VERSION), Gerbil: $(GERBIL_VERSION), Badger: $(BADGER_VERSION)"
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o bin/installer_linux_amd64
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o bin/installer_linux_arm64
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/installer_linux_amd64
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/installer_linux_arm64
|
||||
|
||||
clean:
|
||||
rm -f bin/installer_linux_amd64
|
||||
rm -f bin/installer_linux_arm64
|
||||
|
||||
.PHONY: all go-build-release clean
|
||||
update-versions:
|
||||
@echo "Fetching latest versions..."
|
||||
cp main.go main.go.bak && \
|
||||
$(MAKE) dev-update-versions
|
||||
|
||||
put-back:
|
||||
mv main.go.bak main.go
|
||||
|
||||
dev-update-versions:
|
||||
if [ -z "$(tag)" ]; then \
|
||||
PANGOLIN_VERSION=$$(curl -s https://api.github.com/repos/fosrl/pangolin/tags | jq -r '.[0].name'); \
|
||||
else \
|
||||
PANGOLIN_VERSION=$(tag); \
|
||||
fi && \
|
||||
GERBIL_VERSION=$$(curl -s https://api.github.com/repos/fosrl/gerbil/tags | jq -r '.[0].name') && \
|
||||
BADGER_VERSION=$$(curl -s https://api.github.com/repos/fosrl/badger/tags | jq -r '.[0].name') && \
|
||||
echo "Latest versions - Pangolin: $$PANGOLIN_VERSION, Gerbil: $$GERBIL_VERSION, Badger: $$BADGER_VERSION" && \
|
||||
sed -i "s/config.PangolinVersion = \".*\"/config.PangolinVersion = \"$$PANGOLIN_VERSION\"/" main.go && \
|
||||
sed -i "s/config.GerbilVersion = \".*\"/config.GerbilVersion = \"$$GERBIL_VERSION\"/" main.go && \
|
||||
sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"$$BADGER_VERSION\"/" main.go && \
|
||||
echo "Updated main.go with latest versions"
|
||||
|
||||
dev-build: go-build-release
|
||||
|
||||
dev-clean:
|
||||
@echo "Restoring version values ..."
|
||||
sed -i "s/config.PangolinVersion = \".*\"/config.PangolinVersion = \"replaceme\"/" main.go && \
|
||||
sed -i "s/config.GerbilVersion = \".*\"/config.GerbilVersion = \"replaceme\"/" main.go && \
|
||||
sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"replaceme\"/" main.go
|
||||
@echo "Restored version strings in main.go"
|
||||
|
||||
@@ -99,6 +99,11 @@ func ReadAppConfig(configPath string) (*AppConfigValues, error) {
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// findPattern finds the start of a pattern in a string
|
||||
func findPattern(s, pattern string) int {
|
||||
return bytes.Index([]byte(s), []byte(pattern))
|
||||
}
|
||||
|
||||
func copyDockerService(sourceFile, destFile, serviceName string) error {
|
||||
// Read source file
|
||||
sourceData, err := os.ReadFile(sourceFile)
|
||||
@@ -113,19 +118,19 @@ func copyDockerService(sourceFile, destFile, serviceName string) error {
|
||||
}
|
||||
|
||||
// Parse source Docker Compose YAML
|
||||
var sourceCompose map[string]any
|
||||
var sourceCompose map[string]interface{}
|
||||
if err := yaml.Unmarshal(sourceData, &sourceCompose); err != nil {
|
||||
return fmt.Errorf("error parsing source Docker Compose file: %w", err)
|
||||
}
|
||||
|
||||
// Parse destination Docker Compose YAML
|
||||
var destCompose map[string]any
|
||||
var destCompose map[string]interface{}
|
||||
if err := yaml.Unmarshal(destData, &destCompose); err != nil {
|
||||
return fmt.Errorf("error parsing destination Docker Compose file: %w", err)
|
||||
}
|
||||
|
||||
// Get services section from source
|
||||
sourceServices, ok := sourceCompose["services"].(map[string]any)
|
||||
sourceServices, ok := sourceCompose["services"].(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("services section not found in source file or has invalid format")
|
||||
}
|
||||
@@ -137,10 +142,10 @@ func copyDockerService(sourceFile, destFile, serviceName string) error {
|
||||
}
|
||||
|
||||
// Get or create services section in destination
|
||||
destServices, ok := destCompose["services"].(map[string]any)
|
||||
destServices, ok := destCompose["services"].(map[string]interface{})
|
||||
if !ok {
|
||||
// If services section doesn't exist, create it
|
||||
destServices = make(map[string]any)
|
||||
destServices = make(map[string]interface{})
|
||||
destCompose["services"] = destServices
|
||||
}
|
||||
|
||||
@@ -182,21 +187,17 @@ func backupConfig() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func MarshalYAMLWithIndent(data any, indent int) (resp []byte, err error) {
|
||||
func MarshalYAMLWithIndent(data interface{}, indent int) ([]byte, error) {
|
||||
buffer := new(bytes.Buffer)
|
||||
encoder := yaml.NewEncoder(buffer)
|
||||
encoder.SetIndent(indent)
|
||||
|
||||
if err := encoder.Encode(data); err != nil {
|
||||
err := encoder.Encode(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if cerr := encoder.Close(); cerr != nil && err == nil {
|
||||
err = cerr
|
||||
}
|
||||
}()
|
||||
|
||||
defer encoder.Close()
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
@@ -208,7 +209,7 @@ func replaceInFile(filepath, oldStr, newStr string) error {
|
||||
}
|
||||
|
||||
// Replace the string
|
||||
newContent := strings.ReplaceAll(string(content), oldStr, newStr)
|
||||
newContent := strings.Replace(string(content), oldStr, newStr, -1)
|
||||
|
||||
// Write the modified content back to the file
|
||||
err = os.WriteFile(filepath, []byte(newContent), 0644)
|
||||
@@ -227,28 +228,28 @@ func CheckAndAddTraefikLogVolume(composePath string) error {
|
||||
}
|
||||
|
||||
// Parse YAML into a generic map
|
||||
var compose map[string]any
|
||||
var compose map[string]interface{}
|
||||
if err := yaml.Unmarshal(data, &compose); err != nil {
|
||||
return fmt.Errorf("error parsing compose file: %w", err)
|
||||
}
|
||||
|
||||
// Get services section
|
||||
services, ok := compose["services"].(map[string]any)
|
||||
services, ok := compose["services"].(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("services section not found or invalid")
|
||||
}
|
||||
|
||||
// Get traefik service
|
||||
traefik, ok := services["traefik"].(map[string]any)
|
||||
traefik, ok := services["traefik"].(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("traefik service not found or invalid")
|
||||
}
|
||||
|
||||
// Check volumes
|
||||
logVolume := "./config/traefik/logs:/var/log/traefik"
|
||||
var volumes []any
|
||||
var volumes []interface{}
|
||||
|
||||
if existingVolumes, ok := traefik["volumes"].([]any); ok {
|
||||
if existingVolumes, ok := traefik["volumes"].([]interface{}); ok {
|
||||
// Check if volume already exists
|
||||
for _, v := range existingVolumes {
|
||||
if v.(string) == logVolume {
|
||||
@@ -294,13 +295,13 @@ func MergeYAML(baseFile, overlayFile string) error {
|
||||
}
|
||||
|
||||
// Parse base YAML into a map
|
||||
var baseMap map[string]any
|
||||
var baseMap map[string]interface{}
|
||||
if err := yaml.Unmarshal(baseContent, &baseMap); err != nil {
|
||||
return fmt.Errorf("error parsing base YAML: %v", err)
|
||||
}
|
||||
|
||||
// Parse overlay YAML into a map
|
||||
var overlayMap map[string]any
|
||||
var overlayMap map[string]interface{}
|
||||
if err := yaml.Unmarshal(overlayContent, &overlayMap); err != nil {
|
||||
return fmt.Errorf("error parsing overlay YAML: %v", err)
|
||||
}
|
||||
@@ -323,8 +324,8 @@ func MergeYAML(baseFile, overlayFile string) error {
|
||||
}
|
||||
|
||||
// mergeMap recursively merges two maps
|
||||
func mergeMap(base, overlay map[string]any) map[string]any {
|
||||
result := make(map[string]any)
|
||||
func mergeMap(base, overlay map[string]interface{}) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
|
||||
// Copy all key-values from base map
|
||||
for k, v := range base {
|
||||
@@ -335,8 +336,8 @@ func mergeMap(base, overlay map[string]any) map[string]any {
|
||||
for k, v := range overlay {
|
||||
// If both maps have the same key and both values are maps, merge recursively
|
||||
if baseVal, ok := base[k]; ok {
|
||||
if baseMap, isBaseMap := baseVal.(map[string]any); isBaseMap {
|
||||
if overlayMap, isOverlayMap := v.(map[string]any); isOverlayMap {
|
||||
if baseMap, isBaseMap := baseVal.(map[string]interface{}); isBaseMap {
|
||||
if overlayMap, isOverlayMap := v.(map[string]interface{}); isOverlayMap {
|
||||
result[k] = mergeMap(baseMap, overlayMap)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -9,15 +9,10 @@ services:
|
||||
PARSERS: crowdsecurity/whitelists
|
||||
ENROLL_TAGS: docker
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- cscli
|
||||
- lapi
|
||||
- status
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
interval: 10s
|
||||
retries: 15
|
||||
timeout: 10s
|
||||
test: ["CMD", "cscli", "capi", "status"]
|
||||
labels:
|
||||
- "traefik.enable=false" # Disable traefik for crowdsec
|
||||
volumes:
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
http:
|
||||
middlewares:
|
||||
badger:
|
||||
plugin:
|
||||
badger:
|
||||
disableForwardAuth: true
|
||||
redirect-to-https:
|
||||
redirectScheme:
|
||||
scheme: https
|
||||
@@ -48,7 +44,7 @@ http:
|
||||
crowdsecAppsecUnreachableBlock: true # Block on unreachable
|
||||
crowdsecAppsecBodyLimit: 10485760
|
||||
crowdsecLapiKey: "PUT_YOUR_BOUNCER_KEY_HERE_OR_IT_WILL_NOT_WORK" # CrowdSec API key which you noted down later
|
||||
crowdsecLapiHost: crowdsec:8080 # CrowdSec
|
||||
crowdsecLapiHost: crowdsec:8080 # CrowdSec
|
||||
crowdsecLapiScheme: http # CrowdSec API scheme
|
||||
forwardedHeadersTrustedIPs: # Forwarded headers trusted IPs
|
||||
- "0.0.0.0/0" # All IP addresses are trusted for forwarded headers (CHANGE MADE HERE)
|
||||
@@ -67,7 +63,6 @@ http:
|
||||
- web
|
||||
middlewares:
|
||||
- redirect-to-https
|
||||
- badger
|
||||
|
||||
# Next.js router (handles everything except API and WebSocket paths)
|
||||
next-router:
|
||||
@@ -77,7 +72,6 @@ http:
|
||||
- websecure
|
||||
middlewares:
|
||||
- security-headers # Add security headers middleware
|
||||
- badger
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
@@ -89,7 +83,6 @@ http:
|
||||
- websecure
|
||||
middlewares:
|
||||
- security-headers # Add security headers middleware
|
||||
- badger
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
@@ -101,7 +94,6 @@ http:
|
||||
- websecure
|
||||
middlewares:
|
||||
- security-headers # Add security headers middleware
|
||||
- badger
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
@@ -114,13 +106,4 @@ http:
|
||||
api-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://pangolin:3000" # API/WebSocket server
|
||||
|
||||
tcp:
|
||||
serversTransports:
|
||||
pp-transport-v1:
|
||||
proxyProtocol:
|
||||
version: 1
|
||||
pp-transport-v2:
|
||||
proxyProtocol:
|
||||
version: 2
|
||||
- url: "http://pangolin:3000" # API/WebSocket server
|
||||
@@ -81,19 +81,11 @@ entryPoints:
|
||||
transport:
|
||||
respondingTimeouts:
|
||||
readTimeout: "30m"
|
||||
http3:
|
||||
advertisedPort: 443
|
||||
http:
|
||||
tls:
|
||||
certResolver: "letsencrypt"
|
||||
middlewares:
|
||||
middlewares:
|
||||
- crowdsec@file
|
||||
encodedCharacters:
|
||||
allowEncodedSlash: true
|
||||
allowEncodedQuestionMark: true
|
||||
|
||||
serversTransport:
|
||||
insecureSkipVerify: true
|
||||
|
||||
ping:
|
||||
entryPoint: "web"
|
||||
insecureSkipVerify: true
|
||||
@@ -1,15 +1,9 @@
|
||||
name: pangolin
|
||||
services:
|
||||
pangolin:
|
||||
image: docker.io/fosrl/pangolin:{{if .IsEnterprise}}ee-{{end}}{{.PangolinVersion}}
|
||||
image: docker.io/fosrl/pangolin:{{.PangolinVersion}}
|
||||
container_name: pangolin
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1g
|
||||
reservations:
|
||||
memory: 256m
|
||||
volumes:
|
||||
- ./config:/app/config
|
||||
healthcheck:
|
||||
@@ -38,14 +32,15 @@ services:
|
||||
- 51820:51820/udp
|
||||
- 21820:21820/udp
|
||||
- 443:443
|
||||
- 443:443/udp # For http3 QUIC if desired
|
||||
- 80:80
|
||||
{{end}}
|
||||
traefik:
|
||||
image: docker.io/traefik:v3.6
|
||||
image: docker.io/traefik:v3.5
|
||||
container_name: traefik
|
||||
restart: unless-stopped
|
||||
{{if .InstallGerbil}} network_mode: service:gerbil # Ports appear on the gerbil service{{end}}{{if not .InstallGerbil}}
|
||||
{{if .InstallGerbil}}
|
||||
network_mode: service:gerbil # Ports appear on the gerbil service
|
||||
{{end}}{{if not .InstallGerbil}}
|
||||
ports:
|
||||
- 443:443
|
||||
- 80:80
|
||||
@@ -64,4 +59,4 @@ networks:
|
||||
default:
|
||||
driver: bridge
|
||||
name: pangolin
|
||||
{{if .EnableIPv6}} enable_ipv6: true{{end}}
|
||||
{{if .EnableIPv6}} enable_ipv6: true{{end}}
|
||||
@@ -1,9 +1,5 @@
|
||||
http:
|
||||
middlewares:
|
||||
badger:
|
||||
plugin:
|
||||
badger:
|
||||
disableForwardAuth: true
|
||||
redirect-to-https:
|
||||
redirectScheme:
|
||||
scheme: https
|
||||
@@ -17,7 +13,6 @@ http:
|
||||
- web
|
||||
middlewares:
|
||||
- redirect-to-https
|
||||
- badger
|
||||
|
||||
# Next.js router (handles everything except API and WebSocket paths)
|
||||
next-router:
|
||||
@@ -25,8 +20,6 @@ http:
|
||||
service: next-service
|
||||
entryPoints:
|
||||
- websecure
|
||||
middlewares:
|
||||
- badger
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
@@ -36,8 +29,6 @@ http:
|
||||
service: api-service
|
||||
entryPoints:
|
||||
- websecure
|
||||
middlewares:
|
||||
- badger
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
@@ -47,8 +38,6 @@ http:
|
||||
service: api-service
|
||||
entryPoints:
|
||||
- websecure
|
||||
middlewares:
|
||||
- badger
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
@@ -70,4 +59,4 @@ tcp:
|
||||
version: 1
|
||||
pp-transport-v2:
|
||||
proxyProtocol:
|
||||
version: 2
|
||||
version: 2
|
||||
@@ -40,17 +40,12 @@ entryPoints:
|
||||
transport:
|
||||
respondingTimeouts:
|
||||
readTimeout: "30m"
|
||||
http3:
|
||||
advertisedPort: 443
|
||||
http:
|
||||
tls:
|
||||
certResolver: "letsencrypt"
|
||||
encodedCharacters:
|
||||
allowEncodedSlash: true
|
||||
allowEncodedQuestionMark: true
|
||||
|
||||
serversTransport:
|
||||
insecureSkipVerify: true
|
||||
|
||||
ping:
|
||||
entryPoint: "web"
|
||||
entryPoint: "web"
|
||||
@@ -73,7 +73,7 @@ func installDocker() error {
|
||||
case strings.Contains(osRelease, "ID=ubuntu"):
|
||||
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
||||
apt-get update &&
|
||||
apt-get install -y apt-transport-https ca-certificates curl gpg &&
|
||||
apt-get install -y apt-transport-https ca-certificates curl &&
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
|
||||
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
|
||||
apt-get update &&
|
||||
@@ -82,7 +82,7 @@ func installDocker() error {
|
||||
case strings.Contains(osRelease, "ID=debian"):
|
||||
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
||||
apt-get update &&
|
||||
apt-get install -y apt-transport-https ca-certificates curl gpg &&
|
||||
apt-get install -y apt-transport-https ca-certificates curl &&
|
||||
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
|
||||
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
|
||||
apt-get update &&
|
||||
@@ -144,13 +144,12 @@ func installDocker() error {
|
||||
}
|
||||
|
||||
func startDockerService() error {
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
if runtime.GOOS == "linux" {
|
||||
cmd := exec.Command("systemctl", "enable", "--now", "docker")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
case "darwin":
|
||||
} else if runtime.GOOS == "darwin" {
|
||||
// On macOS, Docker is usually started via the Docker Desktop application
|
||||
fmt.Println("Please start Docker Desktop manually on macOS.")
|
||||
return nil
|
||||
@@ -211,47 +210,6 @@ func isDockerRunning() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func isPodmanRunning() bool {
|
||||
cmd := exec.Command("podman", "info")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// detectContainerType detects whether the system is currently using Docker or Podman
|
||||
// by checking which container runtime is running and has containers
|
||||
func detectContainerType() SupportedContainer {
|
||||
// Check if we have running containers with podman
|
||||
if isPodmanRunning() {
|
||||
cmd := exec.Command("podman", "ps", "-q")
|
||||
output, err := cmd.Output()
|
||||
if err == nil && len(strings.TrimSpace(string(output))) > 0 {
|
||||
return Podman
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we have running containers with docker
|
||||
if isDockerRunning() {
|
||||
cmd := exec.Command("docker", "ps", "-q")
|
||||
output, err := cmd.Output()
|
||||
if err == nil && len(strings.TrimSpace(string(output))) > 0 {
|
||||
return Docker
|
||||
}
|
||||
}
|
||||
|
||||
// If no containers are running, check which one is installed and running
|
||||
if isPodmanRunning() && isPodmanInstalled() {
|
||||
return Podman
|
||||
}
|
||||
|
||||
if isDockerRunning() && isDockerInstalled() {
|
||||
return Docker
|
||||
}
|
||||
|
||||
return Undefined
|
||||
}
|
||||
|
||||
// executeDockerComposeCommandWithArgs executes the appropriate docker command with arguments supplied
|
||||
func executeDockerComposeCommandWithArgs(args ...string) error {
|
||||
var cmd *exec.Cmd
|
||||
@@ -303,7 +261,7 @@ func pullContainers(containerType SupportedContainer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("unsupported container type: %s", containerType)
|
||||
return fmt.Errorf("Unsupported container type: %s", containerType)
|
||||
}
|
||||
|
||||
// startContainers starts the containers using the appropriate command.
|
||||
@@ -326,7 +284,7 @@ func startContainers(containerType SupportedContainer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("unsupported container type: %s", containerType)
|
||||
return fmt.Errorf("Unsupported container type: %s", containerType)
|
||||
}
|
||||
|
||||
// stopContainers stops the containers using the appropriate command.
|
||||
@@ -348,7 +306,7 @@ func stopContainers(containerType SupportedContainer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("unsupported container type: %s", containerType)
|
||||
return fmt.Errorf("Unsupported container type: %s", containerType)
|
||||
}
|
||||
|
||||
// restartContainer restarts a specific container using the appropriate command.
|
||||
@@ -370,5 +328,5 @@ func restartContainer(container string, containerType SupportedContainer) error
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("unsupported container type: %s", containerType)
|
||||
return fmt.Errorf("Unsupported container type: %s", containerType)
|
||||
}
|
||||
|
||||
@@ -6,13 +6,12 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func installCrowdsec(config Config, installDir string) error {
|
||||
func installCrowdsec(config Config) error {
|
||||
|
||||
if err := stopContainers(config.InstallationContainerType); err != nil {
|
||||
return fmt.Errorf("failed to stop containers: %v", err)
|
||||
@@ -28,20 +27,9 @@ func installCrowdsec(config Config, installDir string) error {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll("config/crowdsec/db", 0755); err != nil {
|
||||
fmt.Printf("Error creating config files: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := os.MkdirAll("config/crowdsec/acquis.d", 0755); err != nil {
|
||||
fmt.Printf("Error creating config files: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := os.MkdirAll("config/traefik/logs", 0755); err != nil {
|
||||
fmt.Printf("Error creating config files: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
setupTraefikLogRotate(installDir)
|
||||
os.MkdirAll("config/crowdsec/db", 0755)
|
||||
os.MkdirAll("config/crowdsec/acquis.d", 0755)
|
||||
os.MkdirAll("config/traefik/logs", 0755)
|
||||
|
||||
if err := copyDockerService("config/crowdsec/docker-compose.yml", "docker-compose.yml", "crowdsec"); err != nil {
|
||||
fmt.Printf("Error copying docker service: %v\n", err)
|
||||
@@ -105,7 +93,7 @@ func installCrowdsec(config Config, installDir string) error {
|
||||
|
||||
if checkIfTextInFile("config/traefik/dynamic_config.yml", "PUT_YOUR_BOUNCER_KEY_HERE_OR_IT_WILL_NOT_WORK") {
|
||||
fmt.Println("Failed to replace bouncer key! Please retrieve the key and replace it in the config/traefik/dynamic_config.yml file using the following command:")
|
||||
fmt.Printf(" %s exec crowdsec cscli bouncers add traefik-bouncer\n", config.InstallationContainerType)
|
||||
fmt.Println(" docker exec crowdsec cscli bouncers add traefik-bouncer")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -129,7 +117,7 @@ func GetCrowdSecAPIKey(containerType SupportedContainer) (string, error) {
|
||||
}
|
||||
|
||||
// Execute the command to get the API key
|
||||
cmd := exec.Command(string(containerType), "exec", "crowdsec", "cscli", "bouncers", "add", "traefik-bouncer", "-o", "raw")
|
||||
cmd := exec.Command("docker", "exec", "crowdsec", "cscli", "bouncers", "add", "traefik-bouncer", "-o", "raw")
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
|
||||
@@ -165,34 +153,34 @@ func CheckAndAddCrowdsecDependency(composePath string) error {
|
||||
}
|
||||
|
||||
// Parse YAML into a generic map
|
||||
var compose map[string]any
|
||||
var compose map[string]interface{}
|
||||
if err := yaml.Unmarshal(data, &compose); err != nil {
|
||||
return fmt.Errorf("error parsing compose file: %w", err)
|
||||
}
|
||||
|
||||
// Get services section
|
||||
services, ok := compose["services"].(map[string]any)
|
||||
services, ok := compose["services"].(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("services section not found or invalid")
|
||||
}
|
||||
|
||||
// Get traefik service
|
||||
traefik, ok := services["traefik"].(map[string]any)
|
||||
traefik, ok := services["traefik"].(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("traefik service not found or invalid")
|
||||
}
|
||||
|
||||
// Get dependencies
|
||||
dependsOn, ok := traefik["depends_on"].(map[string]any)
|
||||
dependsOn, ok := traefik["depends_on"].(map[string]interface{})
|
||||
if ok {
|
||||
// Append the new block for crowdsec
|
||||
dependsOn["crowdsec"] = map[string]any{
|
||||
dependsOn["crowdsec"] = map[string]interface{}{
|
||||
"condition": "service_healthy",
|
||||
}
|
||||
} else {
|
||||
// No dependencies exist, create it
|
||||
traefik["depends_on"] = map[string]any{
|
||||
"crowdsec": map[string]any{
|
||||
traefik["depends_on"] = map[string]interface{}{
|
||||
"crowdsec": map[string]interface{}{
|
||||
"condition": "service_healthy",
|
||||
},
|
||||
}
|
||||
@@ -211,69 +199,3 @@ func CheckAndAddCrowdsecDependency(composePath string) error {
|
||||
fmt.Println("Added dependency of crowdsec to traefik")
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupTraefikLogRotate writes a logrotate config for the Traefik access log
|
||||
// that CrowdSec depends on. This is only needed when CrowdSec is installed
|
||||
// because the default Pangolin install does not enable Traefik access logs.
|
||||
//
|
||||
// copytruncate is used so Traefik does not need to be restarted or sent a
|
||||
// signal after rotation — it keeps writing to the same file descriptor while
|
||||
// the rotated copy is made and the original is truncated in place.
|
||||
func setupTraefikLogRotate(installDir string) {
|
||||
const logrotateDir = "/etc/logrotate.d"
|
||||
const logrotateFile = "/etc/logrotate.d/pangolin-traefik"
|
||||
|
||||
logPath := filepath.Join(installDir, "config/traefik/logs/access.log")
|
||||
|
||||
if os.Geteuid() != 0 {
|
||||
fmt.Println("\n[logrotate] Skipping automatic logrotate setup: not running as root.")
|
||||
fmt.Println("[logrotate] To prevent unbounded growth of the Traefik access log used by CrowdSec,")
|
||||
fmt.Println("[logrotate] create the file /etc/logrotate.d/pangolin-traefik manually with:")
|
||||
printLogrotateConfig(logPath)
|
||||
return
|
||||
}
|
||||
|
||||
config := fmt.Sprintf(`# Logrotate config for Traefik access logs used by CrowdSec.
|
||||
# Generated by the Pangolin installer. Safe to edit.
|
||||
%s {
|
||||
daily
|
||||
rotate 7
|
||||
compress
|
||||
delaycompress
|
||||
missingok
|
||||
notifempty
|
||||
copytruncate
|
||||
}
|
||||
`, logPath)
|
||||
|
||||
if err := os.MkdirAll(logrotateDir, 0755); err != nil {
|
||||
fmt.Printf("[logrotate] Warning: could not create %s: %v\n", logrotateDir, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.WriteFile(logrotateFile, []byte(config), 0644); err != nil {
|
||||
fmt.Printf("[logrotate] Warning: could not write %s: %v\n", logrotateFile, err)
|
||||
fmt.Println("[logrotate] Set it up manually:")
|
||||
printLogrotateConfig(logPath)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("[logrotate] Wrote logrotate config to %s\n", logrotateFile)
|
||||
fmt.Println("[logrotate] Traefik access logs will be rotated daily, keeping 7 compressed copies.")
|
||||
}
|
||||
|
||||
// printLogrotateConfig prints a logrotate config block to stdout so users can
|
||||
// set it up manually when the installer cannot write to /etc.
|
||||
func printLogrotateConfig(logPath string) {
|
||||
fmt.Printf(`
|
||||
%s {
|
||||
daily
|
||||
rotate 7
|
||||
compress
|
||||
delaycompress
|
||||
missingok
|
||||
notifempty
|
||||
copytruncate
|
||||
}
|
||||
`, logPath)
|
||||
}
|
||||
|
||||
@@ -1,38 +1,10 @@
|
||||
module installer
|
||||
|
||||
go 1.25.0
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/huh v1.0.0
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
golang.org/x/term v0.42.0
|
||||
golang.org/x/term v0.37.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/catppuccin/go v0.3.0 // indirect
|
||||
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
|
||||
github.com/charmbracelet/bubbletea v1.3.6 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/x/ansi v0.9.3 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
)
|
||||
require golang.org/x/sys v0.38.0 // indirect
|
||||
|
||||
@@ -1,80 +1,7 @@
|
||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
|
||||
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
|
||||
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
|
||||
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws=
|
||||
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw=
|
||||
github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
|
||||
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||
github.com/charmbracelet/huh v1.0.0 h1:wOnedH8G4qzJbmhftTqrpppyqHakl/zbbNdXIWJyIxw=
|
||||
github.com/charmbracelet/huh v1.0.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
|
||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
|
||||
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
|
||||
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
|
||||
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
|
||||
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
||||
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
||||
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
|
||||
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
248
install/input.go
248
install/input.go
@@ -1,208 +1,74 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// pangolinTheme is the custom theme using brand colors
|
||||
var pangolinTheme = ThemePangolin()
|
||||
|
||||
// isAccessibleMode checks if we should use accessible mode (simple prompts)
|
||||
// This is true for: non-TTY, TERM=dumb, or ACCESSIBLE env var set
|
||||
func isAccessibleMode() bool {
|
||||
// Check if stdin is not a terminal (piped input, CI, etc.)
|
||||
if !term.IsTerminal(int(os.Stdin.Fd())) {
|
||||
return true
|
||||
}
|
||||
// Check for dumb terminal
|
||||
if os.Getenv("TERM") == "dumb" {
|
||||
return true
|
||||
}
|
||||
// Check for explicit accessible mode request
|
||||
if os.Getenv("ACCESSIBLE") != "" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// handleAbort checks if the error is a user abort (Ctrl+C) and exits if so
|
||||
func handleAbort(err error) {
|
||||
if err != nil && errors.Is(err, huh.ErrUserAborted) {
|
||||
fmt.Println("\nInstallation cancelled.")
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
// runField runs a single field with the Pangolin theme, handling accessible mode
|
||||
func runField(field huh.Field) error {
|
||||
if isAccessibleMode() {
|
||||
return field.RunAccessible(os.Stdout, os.Stdin)
|
||||
}
|
||||
form := huh.NewForm(huh.NewGroup(field)).WithTheme(pangolinTheme)
|
||||
return form.Run()
|
||||
}
|
||||
|
||||
func readString(prompt string, defaultValue string) string {
|
||||
var value string
|
||||
|
||||
title := prompt
|
||||
func readString(reader *bufio.Reader, prompt string, defaultValue string) string {
|
||||
if defaultValue != "" {
|
||||
title = fmt.Sprintf("%s (default: %s)", prompt, defaultValue)
|
||||
fmt.Printf("%s (default: %s): ", prompt, defaultValue)
|
||||
} else {
|
||||
fmt.Print(prompt + ": ")
|
||||
}
|
||||
|
||||
input := huh.NewInput().
|
||||
Title(title).
|
||||
Value(&value)
|
||||
|
||||
// If no default value, this field is required
|
||||
if defaultValue == "" {
|
||||
input = input.Validate(func(s string) error {
|
||||
if s == "" {
|
||||
return fmt.Errorf("this field is required")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
err := runField(input)
|
||||
handleAbort(err)
|
||||
|
||||
if value == "" {
|
||||
value = defaultValue
|
||||
}
|
||||
|
||||
// Print the answer so it remains visible in terminal history (skip in accessible mode as it already shows)
|
||||
if !isAccessibleMode() {
|
||||
fmt.Printf("%s: %s\n", prompt, value)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func readPassword(prompt string) string {
|
||||
var value string
|
||||
|
||||
for {
|
||||
input := huh.NewInput().
|
||||
Title(prompt).
|
||||
Value(&value).
|
||||
EchoMode(huh.EchoModePassword).
|
||||
Validate(func(s string) error {
|
||||
if s == "" {
|
||||
return fmt.Errorf("password is required")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
err := runField(input)
|
||||
handleAbort(err)
|
||||
|
||||
if value != "" {
|
||||
// Print confirmation without revealing the password
|
||||
if !isAccessibleMode() {
|
||||
fmt.Printf("%s: %s\n", prompt, "********")
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readBool(prompt string, defaultValue bool) bool {
|
||||
var value = defaultValue
|
||||
|
||||
confirm := huh.NewConfirm().
|
||||
Title(prompt).
|
||||
Value(&value).
|
||||
Affirmative("Yes").
|
||||
Negative("No")
|
||||
|
||||
err := runField(confirm)
|
||||
handleAbort(err)
|
||||
|
||||
// Print the answer so it remains visible in terminal history
|
||||
if !isAccessibleMode() {
|
||||
answer := "No"
|
||||
if value {
|
||||
answer = "Yes"
|
||||
}
|
||||
fmt.Printf("%s: %s\n", prompt, answer)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func readBoolNoDefault(prompt string) bool {
|
||||
var value bool
|
||||
|
||||
confirm := huh.NewConfirm().
|
||||
Title(prompt).
|
||||
Value(&value).
|
||||
Affirmative("Yes").
|
||||
Negative("No")
|
||||
|
||||
err := runField(confirm)
|
||||
handleAbort(err)
|
||||
|
||||
// Print the answer so it remains visible in terminal history
|
||||
if !isAccessibleMode() {
|
||||
answer := "No"
|
||||
if value {
|
||||
answer = "Yes"
|
||||
}
|
||||
fmt.Printf("%s: %s\n", prompt, answer)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func readInt(prompt string, defaultValue int) int {
|
||||
var value string
|
||||
|
||||
title := fmt.Sprintf("%s (default: %d)", prompt, defaultValue)
|
||||
|
||||
input := huh.NewInput().
|
||||
Title(title).
|
||||
Value(&value).
|
||||
Validate(func(s string) error {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
_, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("please enter a valid number")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
err := runField(input)
|
||||
handleAbort(err)
|
||||
|
||||
if value == "" {
|
||||
// Print the answer so it remains visible in terminal history
|
||||
if !isAccessibleMode() {
|
||||
fmt.Printf("%s: %d\n", prompt, defaultValue)
|
||||
}
|
||||
input, _ := reader.ReadString('\n')
|
||||
input = strings.TrimSpace(input)
|
||||
if input == "" {
|
||||
return defaultValue
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
result, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
if !isAccessibleMode() {
|
||||
fmt.Printf("%s: %d\n", prompt, defaultValue)
|
||||
func readStringNoDefault(reader *bufio.Reader, prompt string) string {
|
||||
fmt.Print(prompt + ": ")
|
||||
input, _ := reader.ReadString('\n')
|
||||
return strings.TrimSpace(input)
|
||||
}
|
||||
|
||||
func readPassword(prompt string, reader *bufio.Reader) string {
|
||||
if term.IsTerminal(int(syscall.Stdin)) {
|
||||
fmt.Print(prompt + ": ")
|
||||
// Read password without echo if we're in a terminal
|
||||
password, err := term.ReadPassword(int(syscall.Stdin))
|
||||
fmt.Println() // Add a newline since ReadPassword doesn't add one
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
input := strings.TrimSpace(string(password))
|
||||
if input == "" {
|
||||
return readPassword(prompt, reader)
|
||||
}
|
||||
return input
|
||||
} else {
|
||||
// Fallback to reading from stdin if not in a terminal
|
||||
return readString(reader, prompt, "")
|
||||
}
|
||||
}
|
||||
|
||||
func readBool(reader *bufio.Reader, prompt string, defaultValue bool) bool {
|
||||
defaultStr := "no"
|
||||
if defaultValue {
|
||||
defaultStr = "yes"
|
||||
}
|
||||
input := readString(reader, prompt+" (yes/no)", defaultStr)
|
||||
return strings.ToLower(input) == "yes"
|
||||
}
|
||||
|
||||
func readBoolNoDefault(reader *bufio.Reader, prompt string) bool {
|
||||
input := readStringNoDefault(reader, prompt+" (yes/no)")
|
||||
return strings.ToLower(input) == "yes"
|
||||
}
|
||||
|
||||
func readInt(reader *bufio.Reader, prompt string, defaultValue int) int {
|
||||
input := readString(reader, prompt, fmt.Sprintf("%d", defaultValue))
|
||||
if input == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// Print the answer so it remains visible in terminal history
|
||||
if !isAccessibleMode() {
|
||||
fmt.Printf("%s: %d\n", prompt, result)
|
||||
}
|
||||
|
||||
return result
|
||||
value := defaultValue
|
||||
fmt.Sscanf(input, "%d", &value)
|
||||
return value
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user