mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-08 07:05:08 +02:00
Compare commits
155 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 37f943edd2 | |||
| a437b8937e | |||
| fa9de6db42 | |||
| 533bd1b68a | |||
| eba84db036 | |||
| 07d848a40e | |||
| 9f54e2efff | |||
| 77f1172640 | |||
| f35946860a | |||
| 5d28c3e50c | |||
| 6d37c419c2 | |||
| bcc0658a20 | |||
| 24e49d85e9 | |||
| cc06503fbe | |||
| 984799b854 | |||
| 141d3db56c | |||
| 1d46a151ff | |||
| b03cb24d1d | |||
| ffbd3931be | |||
| 47f812616a | |||
| b1d7d56a7a | |||
| 4bceb70d87 | |||
| d23fbf2810 | |||
| efb373503c | |||
| 98d8c91f82 | |||
| 04697abc41 | |||
| 6f63292b26 | |||
| c416dd275f | |||
| 603b66780d | |||
| 6bb72a7447 | |||
| a679588d3b | |||
| 56114cb11a | |||
| ccc6a64332 | |||
| aee8d0131e | |||
| 20fc347182 | |||
| 6462b3c482 | |||
| c3d620a9b2 | |||
| 1cfab08262 | |||
| e70919b9e4 | |||
| 157b673b64 | |||
| 2d71293a7c | |||
| ac30d28d7e | |||
| 23cf7bf745 | |||
| d63de9ba40 | |||
| e7ac5c34a2 | |||
| 1f88876e3c | |||
| e0425e2458 | |||
| 10e9f017fb | |||
| 6d2cb69e45 | |||
| 2fab9b65a8 | |||
| 47a7a3f230 | |||
| 61835ca0e6 | |||
| f4b22c5b31 | |||
| 65afe23dd8 | |||
| 371e44e235 | |||
| d349795995 | |||
| 7d106294bc | |||
| 8c3e094534 | |||
| b20a7231bc | |||
| 84998fdb6e | |||
| 0d9ece1329 | |||
| eb7107016b | |||
| a7608424b7 | |||
| 6710657789 | |||
| b25069ab49 | |||
| 6e8d1f9149 | |||
| 825da82e56 | |||
| 8d28063724 | |||
| a7eed886ab | |||
| 4e965f1e83 | |||
| ea3011afca | |||
| b20e5b4af1 | |||
| 577660c656 | |||
| f9a9007ed9 | |||
| 19873a0b3d | |||
| 4f13267bc8 | |||
| c820fcc518 | |||
| c612972f38 | |||
| 395c4eff53 | |||
| 6aa40bfd61 | |||
| f5744d0dcd | |||
| a975b33b7e | |||
| 92be668a55 | |||
| 15153d5e3a | |||
| 212800d365 | |||
| 24ac9a1623 | |||
| 78bcfed668 | |||
| bf8be14c3a | |||
| fe1f7bee60 | |||
| fbe14acdd1 | |||
| 11c0afd9ab | |||
| 52e6b36c2d | |||
| dcce15036c | |||
| fd27682bbb | |||
| 0cffeda5da | |||
| aff486ca57 | |||
| d3eb950888 | |||
| 1b98f44588 | |||
| 8a5a0c7c18 | |||
| d2f0825498 | |||
| ac88b79066 | |||
| 9160d94c7b | |||
| de6ce7aa10 | |||
| 91a4b11632 | |||
| 99ed4ea683 | |||
| 1a0d4870ed | |||
| f1bd315a96 | |||
| 42d8e932ff | |||
| 658ec87d1b | |||
| e9bbee13c0 | |||
| 99164bf7ab | |||
| 6a3cc578a7 | |||
| 720c3d7c41 | |||
| 72733a9b77 | |||
| 1295141eaf | |||
| e8099795b3 | |||
| d65fbcc28b | |||
| ad0a17f642 | |||
| c2ecabab33 | |||
| 7170b14b1f | |||
| 343ba64eb4 | |||
| 242c314e42 | |||
| 16f9191bc5 | |||
| 6ec51e50d1 | |||
| f1d73c5f8c | |||
| 6093add470 | |||
| 4af6bd43ef | |||
| 99ad7c6bef | |||
| fc192d955b | |||
| 914f1b41c6 | |||
| 0601eacfd5 | |||
| 5481eae606 | |||
| d868c85ebe | |||
| ff0f0aa4e0 | |||
| d4f9f20ef9 | |||
| 30808ce2db | |||
| db5f7ed731 | |||
| 6fd69f1293 | |||
| 9f1dd42a85 | |||
| 7b55ef1cc8 | |||
| b988f21b15 | |||
| a688757f5c | |||
| 0ed1852a35 | |||
| a5c3702b8d | |||
| b25f43da9a | |||
| ea0f0de802 | |||
| 476131859d | |||
| 8573c38332 | |||
| bc9d2835be | |||
| df582b0409 | |||
| e3865dcd4d | |||
| 0eba5cdb0b | |||
| 2284f2cafc | |||
| 80215a7de0 | |||
| ddd829064a |
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
alwaysApply: true
|
|
||||||
---
|
|
||||||
|
|
||||||
When adding submit buttons, don't change the text of the button during the loading state. Text should stay static and you should use the loading prop on the button.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
alwaysApply: true
|
|
||||||
---
|
|
||||||
|
|
||||||
When creating UI for popup dialogs or modals, use the Credenza componennt. This component is mobile responsive and works on desktop and wraps the dialog component and sheet into one.
|
|
||||||
@@ -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,5 +0,0 @@
|
|||||||
---
|
|
||||||
alwaysApply: true
|
|
||||||
---
|
|
||||||
|
|
||||||
Don't write or edit migrations in `server/setup` unless specificall instructed to do so.
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
description:
|
|
||||||
alwaysApply: true
|
|
||||||
---
|
|
||||||
|
|
||||||
Proxy resources = public resources
|
|
||||||
Private resources = client resources = site resources
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
alwaysApply: true
|
|
||||||
---
|
|
||||||
|
|
||||||
When writing TypeScript:
|
|
||||||
|
|
||||||
Prefer to use types instead of interfaces.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
alwaysApply: true
|
|
||||||
---
|
|
||||||
|
|
||||||
When creating forms, use React form for validation and use Zod schemas.
|
|
||||||
+1
-5
@@ -28,11 +28,7 @@ LICENSE
|
|||||||
CONTRIBUTING.md
|
CONTRIBUTING.md
|
||||||
dist
|
dist
|
||||||
.git
|
.git
|
||||||
server/migrations/
|
migrations/
|
||||||
config/
|
config/
|
||||||
build.ts
|
build.ts
|
||||||
tsconfig.json
|
tsconfig.json
|
||||||
Dockerfile*
|
|
||||||
drizzle.config.ts
|
|
||||||
allowedDevOrigins.json
|
|
||||||
scratch/
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
* @oschwartz10612 @miloschwartz
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: [fosrl]
|
||||||
@@ -14,13 +14,12 @@ body:
|
|||||||
label: Environment
|
label: Environment
|
||||||
description: Please fill out the relevant details below for your environment.
|
description: Please fill out the relevant details below for your environment.
|
||||||
value: |
|
value: |
|
||||||
- OS Type & Version:
|
- OS Type & Version: (e.g., Ubuntu 22.04)
|
||||||
- Pangolin Version:
|
- Pangolin Version:
|
||||||
- Edition (Community or Enterprise):
|
|
||||||
- Gerbil Version:
|
- Gerbil Version:
|
||||||
- Traefik Version:
|
- Traefik Version:
|
||||||
- Newt Version:
|
- Newt Version:
|
||||||
- Client Version:
|
- Olm Version: (if applicable)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|||||||
+38
-18
@@ -1,42 +1,62 @@
|
|||||||
version: 2
|
version: 2
|
||||||
|
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: "npm"
|
- package-ecosystem: "npm"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "daily"
|
||||||
open-pull-requests-limit: 1
|
|
||||||
groups:
|
groups:
|
||||||
npm-dependencies:
|
dev-patch-updates:
|
||||||
patterns:
|
dependency-type: "development"
|
||||||
- "*"
|
update-types:
|
||||||
|
- "patch"
|
||||||
|
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"
|
||||||
|
|
||||||
- package-ecosystem: "docker"
|
- package-ecosystem: "docker"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "daily"
|
||||||
open-pull-requests-limit: 1
|
|
||||||
groups:
|
groups:
|
||||||
docker-dependencies:
|
patch-updates:
|
||||||
patterns:
|
update-types:
|
||||||
- "*"
|
- "patch"
|
||||||
|
minor-updates:
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
|
||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
open-pull-requests-limit: 1
|
|
||||||
groups:
|
|
||||||
github-actions-dependencies:
|
|
||||||
patterns:
|
|
||||||
- "*"
|
|
||||||
|
|
||||||
- package-ecosystem: "gomod"
|
- package-ecosystem: "gomod"
|
||||||
directory: "/install"
|
directory: "/install"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "daily"
|
||||||
open-pull-requests-limit: 1
|
|
||||||
groups:
|
groups:
|
||||||
go-install-dependencies:
|
dev-patch-updates:
|
||||||
patterns:
|
dependency-type: "development"
|
||||||
- "*"
|
update-types:
|
||||||
|
- "patch"
|
||||||
|
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"
|
||||||
+60
-164
@@ -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.
|
# 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.
|
# Actions are pinned to specific SHAs to reduce supply-chain risk. This workflow triggers on tag push events.
|
||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
permissions: write-all
|
permissions: write-all
|
||||||
steps:
|
steps:
|
||||||
- name: Configure AWS credentials
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v6
|
uses: aws-actions/configure-aws-credentials@v2
|
||||||
with:
|
with:
|
||||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
role-duration-seconds: 3600
|
role-duration-seconds: 3600
|
||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- name: Monitor storage space
|
- name: Monitor storage space
|
||||||
run: |
|
run: |
|
||||||
@@ -77,7 +77,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
with:
|
with:
|
||||||
registry: docker.io
|
registry: docker.io
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
@@ -134,7 +134,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- name: Monitor storage space
|
- name: Monitor storage space
|
||||||
run: |
|
run: |
|
||||||
@@ -149,7 +149,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
with:
|
with:
|
||||||
registry: docker.io
|
registry: docker.io
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
@@ -201,10 +201,10 @@ jobs:
|
|||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
with:
|
with:
|
||||||
registry: docker.io
|
registry: docker.io
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
@@ -256,7 +256,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- name: Extract tag name
|
- name: Extract tag name
|
||||||
id: get-tag
|
id: get-tag
|
||||||
@@ -264,9 +264,9 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.25
|
go-version: 1.24
|
||||||
|
|
||||||
- name: Update version in package.json
|
- name: Update version in package.json
|
||||||
run: |
|
run: |
|
||||||
@@ -289,17 +289,25 @@ jobs:
|
|||||||
echo "LATEST_BADGER_TAG=$LATEST_TAG" >> $GITHUB_ENV
|
echo "LATEST_BADGER_TAG=$LATEST_TAG" >> $GITHUB_ENV
|
||||||
shell: bash
|
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
|
- name: Build installer
|
||||||
working-directory: install
|
working-directory: install
|
||||||
run: |
|
run: |
|
||||||
make go-build-release \
|
make go-build-release
|
||||||
PANGOLIN_VERSION=${{ env.TAG }} \
|
|
||||||
GERBIL_VERSION=${{ env.LATEST_GERBIL_TAG }} \
|
|
||||||
BADGER_VERSION=${{ env.LATEST_BADGER_TAG }}
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Upload artifacts from /install/bin
|
- name: Upload artifacts from /install/bin
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: install-bin
|
name: install-bin
|
||||||
path: install/bin/
|
path: install/bin/
|
||||||
@@ -321,182 +329,70 @@ jobs:
|
|||||||
skopeo login ghcr.io -u "${{ github.actor }}" -p "${{ secrets.GITHUB_TOKEN }}"
|
skopeo login ghcr.io -u "${{ github.actor }}" -p "${{ secrets.GITHUB_TOKEN }}"
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Copy tags from Docker Hub to GHCR
|
- name: Copy tag from Docker Hub to GHCR
|
||||||
# Mirror the already-built images (all architectures) to GHCR so we can sign them
|
# Mirror the already-built image (all architectures) to GHCR so we can sign it
|
||||||
# Wait a bit for both architectures to be available in Docker Hub manifest
|
# Wait a bit for both architectures to be available in Docker Hub manifest
|
||||||
env:
|
env:
|
||||||
REGISTRY_AUTH_FILE: ${{ runner.temp }}/containers/auth.json
|
REGISTRY_AUTH_FILE: ${{ runner.temp }}/containers/auth.json
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
TAG=${{ env.TAG }}
|
TAG=${{ env.TAG }}
|
||||||
MAJOR_TAG=$(echo $TAG | cut -d. -f1)
|
echo "Waiting for multi-arch manifest to be ready..."
|
||||||
MINOR_TAG=$(echo $TAG | cut -d. -f1,2)
|
|
||||||
|
|
||||||
echo "Waiting for multi-arch manifests to be ready..."
|
|
||||||
sleep 30
|
sleep 30
|
||||||
|
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:${TAG} -> ${{ env.GHCR_IMAGE }}:${TAG}"
|
||||||
# Determine if this is an RC release
|
skopeo copy --all --retry-times 3 \
|
||||||
IS_RC="false"
|
docker://$DOCKERHUB_IMAGE:$TAG \
|
||||||
if [[ "$TAG" == *"-rc."* ]]; then
|
docker://$GHCR_IMAGE:$TAG
|
||||||
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!"
|
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry (for cosign)
|
- name: Login to GitHub Container Registry (for cosign)
|
||||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Install cosign
|
- name: Install cosign
|
||||||
# cosign is used to sign container images using keyless (OIDC) signing
|
# cosign is used to sign and verify container images (key and keyless)
|
||||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
||||||
with:
|
|
||||||
cosign-release: v3.0.6
|
|
||||||
|
|
||||||
- name: Sign (GHCR, keyless)
|
- name: Dual-sign and verify (GHCR & Docker Hub)
|
||||||
# Sign each GHCR image by digest using keyless (OIDC) signing via Sigstore/Rekor.
|
# Sign each image by digest using keyless (OIDC) and key-based signing,
|
||||||
# Signatures are stored in the registry alongside the image.
|
# then verify both the public key signature and the keyless OIDC signature.
|
||||||
env:
|
env:
|
||||||
TAG: ${{ env.TAG }}
|
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"
|
COSIGN_YES: "true"
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Determine if this is an RC release
|
issuer="https://token.actions.githubusercontent.com"
|
||||||
IS_RC="false"
|
id_regex="^https://github.com/${{ github.repository }}/.+" # accept this repo (all workflows/refs)
|
||||||
if [[ "$TAG" == *"-rc."* ]]; then
|
|
||||||
IS_RC="true"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Define image variants to sign
|
for IMAGE in "${GHCR_IMAGE}" "${DOCKERHUB_IMAGE}"; do
|
||||||
if [ "$IS_RC" = "true" ]; then
|
echo "Processing ${IMAGE}:${TAG}"
|
||||||
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
|
|
||||||
|
|
||||||
FAILED_TAGS=()
|
DIGEST="$(skopeo inspect --retry-times 3 docker://${IMAGE}:${TAG} | jq -r '.Digest')"
|
||||||
SUCCESSFUL_TAGS=()
|
REF="${IMAGE}@${DIGEST}"
|
||||||
|
echo "Resolved digest: ${REF}"
|
||||||
|
|
||||||
for IMAGE_TAG in "${IMAGE_TAGS[@]}"; do
|
echo "==> cosign sign (keyless) --recursive ${REF}"
|
||||||
echo "Processing ${GHCR_IMAGE}:${IMAGE_TAG}"
|
cosign sign --recursive "${REF}"
|
||||||
TAG_FAILED=false
|
|
||||||
|
|
||||||
(
|
echo "==> cosign sign (key) --recursive ${REF}"
|
||||||
set -e
|
cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${REF}"
|
||||||
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 (keyless) --recursive ${REF}"
|
echo "==> cosign verify (public key) ${REF}"
|
||||||
cosign sign --recursive "${REF}"
|
cosign verify --key env://COSIGN_PUBLIC_KEY "${REF}" -o text
|
||||||
) || TAG_FAILED=true
|
|
||||||
|
|
||||||
if [ "$TAG_FAILED" = "true" ]; then
|
echo "==> cosign verify (keyless policy) ${REF}"
|
||||||
echo "⚠️ WARNING: Failed to sign ${GHCR_IMAGE}:${IMAGE_TAG}"
|
cosign verify \
|
||||||
FAILED_TAGS+=("${GHCR_IMAGE}:${IMAGE_TAG}")
|
--certificate-oidc-issuer "${issuer}" \
|
||||||
else
|
--certificate-identity-regexp "${id_regex}" \
|
||||||
echo "✓ Successfully signed ${GHCR_IMAGE}:${IMAGE_TAG}"
|
"${REF}" -o text
|
||||||
SUCCESSFUL_TAGS+=("${GHCR_IMAGE}:${IMAGE_TAG}")
|
|
||||||
fi
|
|
||||||
done
|
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
|
shell: bash
|
||||||
|
|
||||||
post-run:
|
post-run:
|
||||||
@@ -514,7 +410,7 @@ jobs:
|
|||||||
permissions: write-all
|
permissions: write-all
|
||||||
steps:
|
steps:
|
||||||
- name: Configure AWS credentials
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v6
|
uses: aws-actions/configure-aws-credentials@v2
|
||||||
with:
|
with:
|
||||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
role-duration-seconds: 3600
|
role-duration-seconds: 3600
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version: '24'
|
node-version: '22'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
skopeo --version
|
skopeo --version
|
||||||
|
|
||||||
- name: Install cosign
|
- name: Install cosign
|
||||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
||||||
|
|
||||||
- name: Input check
|
- name: Input check
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
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@v5
|
||||||
|
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"
|
||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
|
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||||
with:
|
with:
|
||||||
days-before-stale: 14
|
days-before-stale: 14
|
||||||
days-before-close: 14
|
days-before-close: 14
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version: '24'
|
node-version: '22'
|
||||||
|
|
||||||
- name: Copy config file
|
- name: Copy config file
|
||||||
run: cp config/config.example.yml config/config.yml
|
run: cp config/config.example.yml config/config.yml
|
||||||
@@ -34,10 +34,10 @@ jobs:
|
|||||||
run: npm run set:oss
|
run: npm run set:oss
|
||||||
|
|
||||||
- name: Generate database migrations
|
- name: Generate database migrations
|
||||||
run: npm run db:generate
|
run: npm run db:sqlite:generate
|
||||||
|
|
||||||
- name: Apply database migrations
|
- name: Apply database migrations
|
||||||
run: npm run db:push
|
run: npm run db:sqlite:push
|
||||||
|
|
||||||
- name: Test with tsc
|
- name: Test with tsc
|
||||||
run: npx tsc --noEmit
|
run: npx tsc --noEmit
|
||||||
@@ -62,7 +62,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
|
- name: Copy config file
|
||||||
|
run: cp config/config.example.yml config/config.yml
|
||||||
|
|
||||||
- name: Build Docker image sqlite
|
- name: Build Docker image sqlite
|
||||||
run: make dev-build-sqlite
|
run: make dev-build-sqlite
|
||||||
@@ -71,7 +74,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
|
- name: Copy config file
|
||||||
|
run: cp config/config.example.yml config/config.yml
|
||||||
|
|
||||||
- name: Build Docker image pg
|
- name: Build Docker image pg
|
||||||
run: make dev-build-pg
|
run: make dev-build-pg
|
||||||
|
|||||||
+2
-7
@@ -17,9 +17,9 @@ yarn-error.log*
|
|||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
*.db
|
*.db
|
||||||
*.sqlite*
|
*.sqlite
|
||||||
!Dockerfile.sqlite
|
!Dockerfile.sqlite
|
||||||
*.sqlite3*
|
*.sqlite3
|
||||||
*.log
|
*.log
|
||||||
.machinelogs*.json
|
.machinelogs*.json
|
||||||
*-audit.json
|
*-audit.json
|
||||||
@@ -51,8 +51,3 @@ dynamic/
|
|||||||
scratch/
|
scratch/
|
||||||
tsconfig.json
|
tsconfig.json
|
||||||
hydrateSaas.ts
|
hydrateSaas.ts
|
||||||
CLAUDE.md
|
|
||||||
drizzle.config.ts
|
|
||||||
server/setup/migrations.ts
|
|
||||||
solo.yml
|
|
||||||
allowedDevOrigins.json
|
|
||||||
Vendored
+2
-5
@@ -4,7 +4,7 @@
|
|||||||
},
|
},
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"[jsonc]": {
|
"[jsonc]": {
|
||||||
"editor.defaultFormatter": "vscode.json-language-features"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
},
|
},
|
||||||
"[javascript]": {
|
"[javascript]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
@@ -18,8 +18,5 @@
|
|||||||
"[json]": {
|
"[json]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
},
|
},
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true
|
||||||
"cSpell.words": [
|
|
||||||
"nessicary"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
+48
-69
@@ -1,93 +1,72 @@
|
|||||||
# FROM node:24-slim AS base
|
FROM node:24-alpine AS builder
|
||||||
FROM public.ecr.aws/docker/library/node:24-slim AS base
|
|
||||||
|
|
||||||
WORKDIR /app
|
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 BUILD=oss
|
||||||
ARG DATABASE=sqlite
|
ARG DATABASE=sqlite
|
||||||
|
|
||||||
RUN if [ "$BUILD" = "oss" ]; then rm -rf server/private; fi && \
|
RUN apk add --no-cache curl tzdata python3 make g++
|
||||||
npm run set:$DATABASE && \
|
|
||||||
npm run set:$BUILD && \
|
|
||||||
npm run db:generate && \
|
|
||||||
npm run build && \
|
|
||||||
npm run build:cli && \
|
|
||||||
test -f dist/server.mjs
|
|
||||||
|
|
||||||
# Create placeholder files for MaxMind databases to avoid COPY errors
|
# COPY package.json package-lock.json ./
|
||||||
# Real files should be present for saas builds, placeholders for oss builds
|
COPY package*.json ./
|
||||||
RUN touch /app/GeoLite2-Country.mmdb /app/GeoLite2-ASN.mmdb
|
RUN npm ci
|
||||||
|
|
||||||
FROM base AS builder
|
COPY . .
|
||||||
|
|
||||||
RUN npm ci --omit=dev
|
RUN echo "export * from \"./$DATABASE\";" > server/db/index.ts
|
||||||
|
RUN echo "export const driver: \"pg\" | \"sqlite\" = \"$DATABASE\";" >> server/db/index.ts
|
||||||
|
|
||||||
# FROM node:24-slim AS runner
|
RUN echo "export const build = \"$BUILD\" as \"saas\" | \"enterprise\" | \"oss\";" > server/build.ts
|
||||||
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
|
||||||
|
|
||||||
|
# Prune dev dependencies and clean up to prepare for copy to runner
|
||||||
|
RUN npm prune --omit=dev && npm cache clean --force
|
||||||
|
|
||||||
|
FROM node:24-alpine AS runner
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y curl tzdata && rm -rf /var/lib/apt/lists/*
|
# Only curl and tzdata needed at runtime - no build tools!
|
||||||
|
RUN apk add --no-cache curl tzdata
|
||||||
|
|
||||||
|
# Copy pre-built node_modules from builder (already pruned to production only)
|
||||||
|
# This includes the compiled native modules like better-sqlite3
|
||||||
COPY --from=builder /app/node_modules ./node_modules
|
COPY --from=builder /app/node_modules ./node_modules
|
||||||
COPY --from=builder /app/package.json ./package.json
|
|
||||||
|
|
||||||
COPY --from=builder-dev /app/.next/standalone ./
|
COPY --from=builder /app/.next/standalone ./
|
||||||
COPY --from=builder-dev /app/.next/static ./.next/static
|
COPY --from=builder /app/.next/static ./.next/static
|
||||||
COPY --from=builder-dev /app/dist ./dist
|
COPY --from=builder /app/dist ./dist
|
||||||
COPY --from=builder-dev /app/server/migrations ./dist/init
|
COPY --from=builder /app/init ./dist/init
|
||||||
|
COPY --from=builder /app/package.json ./package.json
|
||||||
|
|
||||||
COPY ./cli/wrapper.sh /usr/local/bin/pangctl
|
COPY ./cli/wrapper.sh /usr/local/bin/pangctl
|
||||||
RUN chmod +x /usr/local/bin/pangctl ./dist/cli.mjs
|
RUN chmod +x /usr/local/bin/pangctl ./dist/cli.mjs
|
||||||
|
|
||||||
COPY server/db/names.json ./dist/names.json
|
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 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"]
|
CMD ["npm", "run", "start"]
|
||||||
|
|||||||
+1
-3
@@ -1,9 +1,7 @@
|
|||||||
FROM node:24-alpine
|
FROM node:22-alpine
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --no-cache python3 make g++
|
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
|
|||||||
@@ -3,25 +3,6 @@
|
|||||||
major_tag := $(shell echo $(tag) | cut -d. -f1)
|
major_tag := $(shell echo $(tag) | cut -d. -f1)
|
||||||
minor_tag := $(shell echo $(tag) | cut -d. -f1,2)
|
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
|
.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-release: build-sqlite build-postgresql build-ee-sqlite build-ee-postgresql
|
||||||
@@ -34,7 +15,6 @@ build-sqlite:
|
|||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=sqlite \
|
--build-arg DATABASE=sqlite \
|
||||||
$(OCI_ARGS_OSS) \
|
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:latest \
|
--tag fosrl/pangolin:latest \
|
||||||
--tag fosrl/pangolin:$(major_tag) \
|
--tag fosrl/pangolin:$(major_tag) \
|
||||||
@@ -50,7 +30,6 @@ build-postgresql:
|
|||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=pg \
|
--build-arg DATABASE=pg \
|
||||||
$(OCI_ARGS_OSS) \
|
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:postgresql-latest \
|
--tag fosrl/pangolin:postgresql-latest \
|
||||||
--tag fosrl/pangolin:postgresql-$(major_tag) \
|
--tag fosrl/pangolin:postgresql-$(major_tag) \
|
||||||
@@ -66,7 +45,6 @@ build-ee-sqlite:
|
|||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=sqlite \
|
--build-arg DATABASE=sqlite \
|
||||||
$(OCI_ARGS_EE) \
|
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:ee-latest \
|
--tag fosrl/pangolin:ee-latest \
|
||||||
--tag fosrl/pangolin:ee-$(major_tag) \
|
--tag fosrl/pangolin:ee-$(major_tag) \
|
||||||
@@ -82,7 +60,6 @@ build-ee-postgresql:
|
|||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=pg \
|
--build-arg DATABASE=pg \
|
||||||
$(OCI_ARGS_EE) \
|
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:ee-postgresql-latest \
|
--tag fosrl/pangolin:ee-postgresql-latest \
|
||||||
--tag fosrl/pangolin:ee-postgresql-$(major_tag) \
|
--tag fosrl/pangolin:ee-postgresql-$(major_tag) \
|
||||||
@@ -90,18 +67,6 @@ build-ee-postgresql:
|
|||||||
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
||||||
--push .
|
--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:
|
build-release-arm:
|
||||||
@if [ -z "$(tag)" ]; then \
|
@if [ -z "$(tag)" ]; then \
|
||||||
echo "Error: tag is required. Usage: make build-release-arm tag=<tag>"; \
|
echo "Error: tag is required. Usage: make build-release-arm tag=<tag>"; \
|
||||||
@@ -109,16 +74,9 @@ build-release-arm:
|
|||||||
fi
|
fi
|
||||||
@MAJOR_TAG=$$(echo $(tag) | cut -d. -f1); \
|
@MAJOR_TAG=$$(echo $(tag) | cut -d. -f1); \
|
||||||
MINOR_TAG=$$(echo $(tag) | cut -d. -f1,2); \
|
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 \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=sqlite \
|
--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 \
|
--platform linux/arm64 \
|
||||||
--tag fosrl/pangolin:latest-arm64 \
|
--tag fosrl/pangolin:latest-arm64 \
|
||||||
--tag fosrl/pangolin:$$MAJOR_TAG-arm64 \
|
--tag fosrl/pangolin:$$MAJOR_TAG-arm64 \
|
||||||
@@ -128,11 +86,6 @@ build-release-arm:
|
|||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=pg \
|
--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 \
|
--platform linux/arm64 \
|
||||||
--tag fosrl/pangolin:postgresql-latest-arm64 \
|
--tag fosrl/pangolin:postgresql-latest-arm64 \
|
||||||
--tag fosrl/pangolin:postgresql-$$MAJOR_TAG-arm64 \
|
--tag fosrl/pangolin:postgresql-$$MAJOR_TAG-arm64 \
|
||||||
@@ -142,12 +95,6 @@ build-release-arm:
|
|||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=sqlite \
|
--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 \
|
--platform linux/arm64 \
|
||||||
--tag fosrl/pangolin:ee-latest-arm64 \
|
--tag fosrl/pangolin:ee-latest-arm64 \
|
||||||
--tag fosrl/pangolin:ee-$$MAJOR_TAG-arm64 \
|
--tag fosrl/pangolin:ee-$$MAJOR_TAG-arm64 \
|
||||||
@@ -157,12 +104,6 @@ build-release-arm:
|
|||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=pg \
|
--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 \
|
--platform linux/arm64 \
|
||||||
--tag fosrl/pangolin:ee-postgresql-latest-arm64 \
|
--tag fosrl/pangolin:ee-postgresql-latest-arm64 \
|
||||||
--tag fosrl/pangolin:ee-postgresql-$$MAJOR_TAG-arm64 \
|
--tag fosrl/pangolin:ee-postgresql-$$MAJOR_TAG-arm64 \
|
||||||
@@ -177,16 +118,9 @@ build-release-amd:
|
|||||||
fi
|
fi
|
||||||
@MAJOR_TAG=$$(echo $(tag) | cut -d. -f1); \
|
@MAJOR_TAG=$$(echo $(tag) | cut -d. -f1); \
|
||||||
MINOR_TAG=$$(echo $(tag) | cut -d. -f1,2); \
|
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 \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=sqlite \
|
--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 \
|
--platform linux/amd64 \
|
||||||
--tag fosrl/pangolin:latest-amd64 \
|
--tag fosrl/pangolin:latest-amd64 \
|
||||||
--tag fosrl/pangolin:$$MAJOR_TAG-amd64 \
|
--tag fosrl/pangolin:$$MAJOR_TAG-amd64 \
|
||||||
@@ -196,11 +130,6 @@ build-release-amd:
|
|||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=pg \
|
--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 \
|
--platform linux/amd64 \
|
||||||
--tag fosrl/pangolin:postgresql-latest-amd64 \
|
--tag fosrl/pangolin:postgresql-latest-amd64 \
|
||||||
--tag fosrl/pangolin:postgresql-$$MAJOR_TAG-amd64 \
|
--tag fosrl/pangolin:postgresql-$$MAJOR_TAG-amd64 \
|
||||||
@@ -210,12 +139,6 @@ build-release-amd:
|
|||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=sqlite \
|
--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 \
|
--platform linux/amd64 \
|
||||||
--tag fosrl/pangolin:ee-latest-amd64 \
|
--tag fosrl/pangolin:ee-latest-amd64 \
|
||||||
--tag fosrl/pangolin:ee-$$MAJOR_TAG-amd64 \
|
--tag fosrl/pangolin:ee-$$MAJOR_TAG-amd64 \
|
||||||
@@ -225,12 +148,6 @@ build-release-amd:
|
|||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=pg \
|
--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 \
|
--platform linux/amd64 \
|
||||||
--tag fosrl/pangolin:ee-postgresql-latest-amd64 \
|
--tag fosrl/pangolin:ee-postgresql-latest-amd64 \
|
||||||
--tag fosrl/pangolin:ee-postgresql-$$MAJOR_TAG-amd64 \
|
--tag fosrl/pangolin:ee-postgresql-$$MAJOR_TAG-amd64 \
|
||||||
@@ -284,51 +201,27 @@ build-rc:
|
|||||||
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi
|
fi
|
||||||
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
|
||||||
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=sqlite \
|
--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 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:$(tag) \
|
--tag fosrl/pangolin:$(tag) \
|
||||||
--push . && \
|
--push .
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=pg \
|
--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 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:postgresql-$(tag) \
|
--tag fosrl/pangolin:postgresql-$(tag) \
|
||||||
--push . && \
|
--push .
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=sqlite \
|
--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 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:ee-$(tag) \
|
--tag fosrl/pangolin:ee-$(tag) \
|
||||||
--push . && \
|
--push .
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=pg \
|
--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 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
||||||
--push .
|
--push .
|
||||||
@@ -338,51 +231,27 @@ build-rc-arm:
|
|||||||
echo "Error: tag is required. Usage: make build-rc-arm tag=<tag>"; \
|
echo "Error: tag is required. Usage: make build-rc-arm tag=<tag>"; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi
|
fi
|
||||||
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
|
||||||
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=sqlite \
|
--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 \
|
--platform linux/arm64 \
|
||||||
--tag fosrl/pangolin:$(tag)-arm64 \
|
--tag fosrl/pangolin:$(tag)-arm64 \
|
||||||
--push . && \
|
--push . && \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=pg \
|
--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 \
|
--platform linux/arm64 \
|
||||||
--tag fosrl/pangolin:postgresql-$(tag)-arm64 \
|
--tag fosrl/pangolin:postgresql-$(tag)-arm64 \
|
||||||
--push . && \
|
--push . && \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=sqlite \
|
--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 \
|
--platform linux/arm64 \
|
||||||
--tag fosrl/pangolin:ee-$(tag)-arm64 \
|
--tag fosrl/pangolin:ee-$(tag)-arm64 \
|
||||||
--push . && \
|
--push . && \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=pg \
|
--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 \
|
--platform linux/arm64 \
|
||||||
--tag fosrl/pangolin:ee-postgresql-$(tag)-arm64 \
|
--tag fosrl/pangolin:ee-postgresql-$(tag)-arm64 \
|
||||||
--push .
|
--push .
|
||||||
@@ -392,51 +261,27 @@ build-rc-amd:
|
|||||||
echo "Error: tag is required. Usage: make build-rc-amd tag=<tag>"; \
|
echo "Error: tag is required. Usage: make build-rc-amd tag=<tag>"; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi
|
fi
|
||||||
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
|
||||||
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=sqlite \
|
--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 \
|
--platform linux/amd64 \
|
||||||
--tag fosrl/pangolin:$(tag)-amd64 \
|
--tag fosrl/pangolin:$(tag)-amd64 \
|
||||||
--push . && \
|
--push . && \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=pg \
|
--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 \
|
--platform linux/amd64 \
|
||||||
--tag fosrl/pangolin:postgresql-$(tag)-amd64 \
|
--tag fosrl/pangolin:postgresql-$(tag)-amd64 \
|
||||||
--push . && \
|
--push . && \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=sqlite \
|
--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 \
|
--platform linux/amd64 \
|
||||||
--tag fosrl/pangolin:ee-$(tag)-amd64 \
|
--tag fosrl/pangolin:ee-$(tag)-amd64 \
|
||||||
--push . && \
|
--push . && \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=pg \
|
--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 \
|
--platform linux/amd64 \
|
||||||
--tag fosrl/pangolin:ee-postgresql-$(tag)-amd64 \
|
--tag fosrl/pangolin:ee-postgresql-$(tag)-amd64 \
|
||||||
--push .
|
--push .
|
||||||
@@ -469,52 +314,16 @@ create-manifests-rc:
|
|||||||
echo "All RC multi-arch manifests created successfully!"
|
echo "All RC multi-arch manifests created successfully!"
|
||||||
|
|
||||||
build-arm:
|
build-arm:
|
||||||
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest .
|
||||||
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 .
|
|
||||||
|
|
||||||
build-x86:
|
build-x86:
|
||||||
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
docker buildx build --platform linux/amd64 -t fosrl/pangolin:latest .
|
||||||
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 .
|
|
||||||
|
|
||||||
dev-build-sqlite:
|
dev-build-sqlite:
|
||||||
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
docker build --build-arg DATABASE=sqlite -t fosrl/pangolin:latest .
|
||||||
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 .
|
|
||||||
|
|
||||||
dev-build-pg:
|
dev-build-pg:
|
||||||
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
docker build --build-arg DATABASE=pg -t fosrl/pangolin:postgresql-latest .
|
||||||
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 .
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
docker run -it -p 3000:3000 -p 3001:3001 -p 3002:3002 -v ./config:/app/config fosrl/pangolin:latest
|
docker run -it -p 3000:3000 -p 3001:3001 -p 3002:3002 -v ./config:/app/config fosrl/pangolin:latest
|
||||||
|
|||||||
@@ -37,51 +37,35 @@
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<strong>
|
<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>
|
</strong>
|
||||||
</p>
|
</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 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, all with zero-trust security and granular access control.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
- Get started for free with [Pangolin Cloud](https://app.pangolin.net/).
|
- Check out the [quick install guide](https://docs.pangolin.net/self-host/quick-install) for how to install and set up Pangolin.
|
||||||
- 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.
|
||||||
- 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
|
## Deployment Options
|
||||||
|
|
||||||
- **Pangolin Cloud** - Fully managed service - no infrastructure required.
|
| <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 making less than \$100K USD gross annual revenue.
|
| **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
|
## Key Features
|
||||||
|
|
||||||
### Connect remote networks with sites and NAT traversal
|
| <img width=500 /> | <img width=500 /> |
|
||||||
|
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
|
||||||
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.
|
| **Connect remote networks with sites**<br /><br />Pangolin's lightweight site connectors create secure tunnels from remote networks without requiring public IP addresses or open ports. Sites make any network anywhere available for authorized access. | <img src="public/screenshots/sites.png" width=500 /><tr></tr> |
|
||||||
|
| **Browser-based reverse proxy access**<br /><br />Expose web applications through identity and context-aware tunneled reverse proxies. Pangolin handles routing, load balancing, health checking, and automatic SSL certificates without exposing your network directly to the internet. Users access applications through any web browser with authentication and granular access control. | <img src="public/clip.gif" width=500 /><tr></tr> |
|
||||||
<img src="public/screenshots/sites.png" alt="Sites" width="100%" />
|
| **Client-based private resource access**<br /><br />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. | <img src="public/screenshots/private-resources.png" width=500 /><tr></tr> |
|
||||||
|
| **Zero-trust granular access**<br /><br />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 and services you explicitly define, reducing security risk and attack surface. | <img src="public/screenshots/user-devices.png" width=500 /><tr></tr> |
|
||||||
### 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 Clients
|
||||||
|
|
||||||
@@ -90,25 +74,28 @@ Download the Pangolin client for your platform:
|
|||||||
- [Mac](https://pangolin.net/downloads/mac)
|
- [Mac](https://pangolin.net/downloads/mac)
|
||||||
- [Windows](https://pangolin.net/downloads/windows)
|
- [Windows](https://pangolin.net/downloads/windows)
|
||||||
- [Linux](https://pangolin.net/downloads/linux)
|
- [Linux](https://pangolin.net/downloads/linux)
|
||||||
- [iOS](https://pangolin.net/downloads/ios)
|
|
||||||
- [Android](https://pangolin.net/downloads/android)
|
|
||||||
|
|
||||||
## Get Started
|
## 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
|
### Check out the docs
|
||||||
|
|
||||||
We encourage everyone to read the full documentation first, which is
|
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
|
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.
|
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
|
## Licensing
|
||||||
|
|
||||||
Pangolin is dual licensed under the AGPL-3 and the [Fossorial Commercial License](https://pangolin.net/fcl). For inquiries about commercial licensing, please contact us at [contact@pangolin.net](mailto:contact@pangolin.net).
|
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).
|
||||||
|
|
||||||
## Contributions
|
## Contributions
|
||||||
|
|
||||||
Please see [CONTRIBUTING](./CONTRIBUTING.md) in the repository for guidelines and best practices.
|
Please see [CONTRIBUTING](./CONTRIBUTING.md) in the repository for guidelines and best practices.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
WireGuard® is a registered trademark of Jason A. Donenfeld.
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
If you discover a security vulnerability, please follow the steps below to responsibly disclose it to us:
|
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.
|
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.
|
- Description and location of the vulnerability.
|
||||||
- Potential impact of the vulnerability.
|
- Potential impact of the vulnerability.
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
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
|
||||||
|
# auto-login-idp: 1
|
||||||
|
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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
meta {
|
||||||
|
name: API Keys
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
meta {
|
||||||
|
name: login
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: http://localhost:3000/api/v1/auth/login
|
||||||
|
body: json
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"email": "admin@fosrl.io",
|
||||||
|
"password": "Password123!"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
meta {
|
||||||
|
name: logout
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: http://localhost:4000/api/v1/auth/logout
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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!"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
meta {
|
||||||
|
name: Generate OIDC URL
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: http://localhost:3000/api/v1
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
meta {
|
||||||
|
name: IDP
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
meta {
|
||||||
|
name: Internal
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
meta {
|
||||||
|
name: Create Newt
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: http://localhost:3000/api/v1/newt
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
meta {
|
||||||
|
name: createOlm
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
put {
|
||||||
|
url: http://localhost:3000/api/v1/olm
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
encodeUrl: true
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
meta {
|
||||||
|
name: Olm
|
||||||
|
seq: 15
|
||||||
|
}
|
||||||
|
|
||||||
|
auth {
|
||||||
|
mode: inherit
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
meta {
|
||||||
|
name: listOrgs
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url:
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
meta {
|
||||||
|
name: listResourcesByOrg
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url:
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
meta {
|
||||||
|
name: listSites
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url:
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
meta {
|
||||||
|
name: Test
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: http://localhost:3000/api/v1
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
meta {
|
||||||
|
name: adminListUsers
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: http://localhost:3000/api/v1/users
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
meta {
|
||||||
|
name: adminRemoveUser
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
delete {
|
||||||
|
url: http://localhost:3000/api/v1/user/ky5r7ivqs8wc7u4
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
meta {
|
||||||
|
name: getUser
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url:
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"name": "Pangolin",
|
||||||
|
"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, 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,5 +1,5 @@
|
|||||||
import { CommandModule } from "yargs";
|
import { CommandModule } from "yargs";
|
||||||
import { db, idpOidcConfig, licenseKey, certificates, eventStreamingDestinations, alertWebhookActions } from "@server/db";
|
import { db, idpOidcConfig, licenseKey } from "@server/db";
|
||||||
import { encrypt, decrypt } from "@server/lib/crypto";
|
import { encrypt, decrypt } from "@server/lib/crypto";
|
||||||
import { configFilePath1, configFilePath2 } from "@server/lib/consts";
|
import { configFilePath1, configFilePath2 } from "@server/lib/consts";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
@@ -129,15 +129,9 @@ export const rotateServerSecret: CommandModule<
|
|||||||
console.log("\nReading encrypted data from database...");
|
console.log("\nReading encrypted data from database...");
|
||||||
const idpConfigs = await db.select().from(idpOidcConfig);
|
const idpConfigs = await db.select().from(idpOidcConfig);
|
||||||
const licenseKeys = await db.select().from(licenseKey);
|
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 ${idpConfigs.length} OIDC IdP configuration(s)`);
|
||||||
console.log(`Found ${licenseKeys.length} license key(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
|
// Prepare all decrypted and re-encrypted values
|
||||||
console.log("\nDecrypting and re-encrypting values...");
|
console.log("\nDecrypting and re-encrypting values...");
|
||||||
@@ -155,27 +149,8 @@ export const rotateServerSecret: CommandModule<
|
|||||||
encryptedInstanceId: 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 idpUpdates: IdpUpdate[] = [];
|
||||||
const licenseKeyUpdates: LicenseKeyUpdate[] = [];
|
const licenseKeyUpdates: LicenseKeyUpdate[] = [];
|
||||||
const certUpdates: CertUpdate[] = [];
|
|
||||||
const streamingDestinationUpdates: StreamingDestinationUpdate[] = [];
|
|
||||||
const webhookActionUpdates: WebhookActionUpdate[] = [];
|
|
||||||
|
|
||||||
// Process idpOidcConfig entries
|
// Process idpOidcConfig entries
|
||||||
for (const idpConfig of idpConfigs) {
|
for (const idpConfig of idpConfigs) {
|
||||||
@@ -242,70 +217,6 @@ export const rotateServerSecret: CommandModule<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
// Perform all database updates in a single transaction
|
||||||
console.log("\nUpdating database in transaction...");
|
console.log("\nUpdating database in transaction...");
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
@@ -339,50 +250,10 @@ export const rotateServerSecret: CommandModule<
|
|||||||
instanceId: update.encryptedInstanceId
|
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 ${idpUpdates.length} OIDC IdP configuration(s)`);
|
||||||
console.log(`Rotated ${licenseKeyUpdates.length} license key(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
|
// Update config file with new secret
|
||||||
console.log("\nUpdating config file...");
|
console.log("\nUpdating config file...");
|
||||||
@@ -399,9 +270,6 @@ export const rotateServerSecret: CommandModule<
|
|||||||
console.log(`\nSummary:`);
|
console.log(`\nSummary:`);
|
||||||
console.log(` - OIDC IdP configurations: ${idpUpdates.length}`);
|
console.log(` - OIDC IdP configurations: ${idpUpdates.length}`);
|
||||||
console.log(` - License keys: ${licenseKeyUpdates.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(
|
console.log(
|
||||||
`\n IMPORTANT: Restart the server for the new secret to take effect.`
|
`\n IMPORTANT: Restart the server for the new secret to take effect.`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
import { CommandModule } from "yargs";
|
|
||||||
import { db, users } from "@server/db";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
|
|
||||||
type SetServerAdminArgs = {
|
|
||||||
email: string;
|
|
||||||
remove: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setServerAdmin: CommandModule<{}, SetServerAdminArgs> = {
|
|
||||||
command: "set-server-admin",
|
|
||||||
describe: "Add or remove server admin by email address",
|
|
||||||
builder: (yargs) => {
|
|
||||||
return yargs
|
|
||||||
.option("email", {
|
|
||||||
type: "string",
|
|
||||||
demandOption: true,
|
|
||||||
describe: "User email address"
|
|
||||||
})
|
|
||||||
.option("remove", {
|
|
||||||
type: "boolean",
|
|
||||||
default: false,
|
|
||||||
describe: "Remove server admin status from the user"
|
|
||||||
});
|
|
||||||
},
|
|
||||||
handler: async (argv: SetServerAdminArgs) => {
|
|
||||||
try {
|
|
||||||
const email = argv.email.trim().toLowerCase();
|
|
||||||
|
|
||||||
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 (argv.remove) {
|
|
||||||
if (!user.serverAdmin) {
|
|
||||||
console.log(`User '${email}' is not a server admin`);
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const serverAdmins = await db
|
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.where(eq(users.serverAdmin, true));
|
|
||||||
|
|
||||||
if (serverAdmins.length <= 1) {
|
|
||||||
console.error(
|
|
||||||
"Cannot remove server admin: at least one server admin must exist"
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
await db
|
|
||||||
.update(users)
|
|
||||||
.set({ serverAdmin: false })
|
|
||||||
.where(eq(users.userId, user.userId));
|
|
||||||
|
|
||||||
console.log(`Server admin status removed from user '${email}'`);
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.serverAdmin) {
|
|
||||||
console.log(`User '${email}' is already a server admin`);
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
await db
|
|
||||||
.update(users)
|
|
||||||
.set({ serverAdmin: true })
|
|
||||||
.where(eq(users.userId, user.userId));
|
|
||||||
|
|
||||||
console.log(`User '${email}' has been marked as a server admin`);
|
|
||||||
process.exit(0);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error:", error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -6,12 +6,6 @@ import { setAdminCredentials } from "@cli/commands/setAdminCredentials";
|
|||||||
import { resetUserSecurityKeys } from "@cli/commands/resetUserSecurityKeys";
|
import { resetUserSecurityKeys } from "@cli/commands/resetUserSecurityKeys";
|
||||||
import { clearExitNodes } from "./commands/clearExitNodes";
|
import { clearExitNodes } from "./commands/clearExitNodes";
|
||||||
import { rotateServerSecret } from "./commands/rotateServerSecret";
|
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";
|
|
||||||
import { setServerAdmin } from "./commands/setServerAdmin";
|
|
||||||
|
|
||||||
yargs(hideBin(process.argv))
|
yargs(hideBin(process.argv))
|
||||||
.scriptName("pangctl")
|
.scriptName("pangctl")
|
||||||
@@ -19,11 +13,5 @@ yargs(hideBin(process.argv))
|
|||||||
.command(resetUserSecurityKeys)
|
.command(resetUserSecurityKeys)
|
||||||
.command(clearExitNodes)
|
.command(clearExitNodes)
|
||||||
.command(rotateServerSecret)
|
.command(rotateServerSecret)
|
||||||
.command(clearLicenseKeys)
|
|
||||||
.command(deleteClient)
|
|
||||||
.command(generateOrgCaKeys)
|
|
||||||
.command(clearCertificates)
|
|
||||||
.command(disableUser2fa)
|
|
||||||
.command(setServerAdmin)
|
|
||||||
.demandCommand()
|
.demandCommand()
|
||||||
.help().argv;
|
.help().argv;
|
||||||
|
|||||||
@@ -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:
|
|
||||||
+18
-21
@@ -1,30 +1,27 @@
|
|||||||
# To see all available options, please visit the docs:
|
# To see all available options, please visit the docs:
|
||||||
# https://docs.pangolin.net/
|
# https://docs.pangolin.net/self-host/advanced/config-file
|
||||||
|
|
||||||
gerbil:
|
|
||||||
start_port: 51820
|
|
||||||
base_endpoint: "{{.DashboardDomain}}"
|
|
||||||
|
|
||||||
app:
|
app:
|
||||||
dashboard_url: "https://{{.DashboardDomain}}"
|
dashboard_url: http://localhost:3002
|
||||||
log_level: "info"
|
log_level: debug
|
||||||
telemetry:
|
|
||||||
anonymous_usage: true
|
|
||||||
|
|
||||||
domains:
|
domains:
|
||||||
domain1:
|
domain1:
|
||||||
base_domain: "{{.BaseDomain}}"
|
base_domain: example.com
|
||||||
|
|
||||||
server:
|
server:
|
||||||
secret: "{{.Secret}}"
|
secret: my_secret_key
|
||||||
cors:
|
|
||||||
origins: ["https://{{.DashboardDomain}}"]
|
gerbil:
|
||||||
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
base_endpoint: example.com
|
||||||
allowed_headers: ["X-CSRF-Token", "Content-Type"]
|
|
||||||
credentials: false
|
orgs:
|
||||||
|
block_size: 24
|
||||||
|
subnet_group: 100.90.137.0/20
|
||||||
|
|
||||||
flags:
|
flags:
|
||||||
require_email_verification: false
|
require_email_verification: false
|
||||||
disable_signup_without_invite: true
|
disable_signup_without_invite: true
|
||||||
disable_user_create_org: false
|
disable_user_create_org: true
|
||||||
allow_raw_resources: true
|
allow_raw_resources: true
|
||||||
|
enable_integration_api: true
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
*-journal
|
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
http:
|
http:
|
||||||
middlewares:
|
middlewares:
|
||||||
badger:
|
|
||||||
plugin:
|
|
||||||
badger:
|
|
||||||
disableForwardAuth: true
|
|
||||||
redirect-to-https:
|
redirect-to-https:
|
||||||
redirectScheme:
|
redirectScheme:
|
||||||
scheme: https
|
scheme: https
|
||||||
@@ -17,16 +13,14 @@ http:
|
|||||||
- web
|
- web
|
||||||
middlewares:
|
middlewares:
|
||||||
- redirect-to-https
|
- redirect-to-https
|
||||||
- badger
|
|
||||||
|
|
||||||
# Next.js router (handles everything except API and WebSocket paths)
|
# Next.js router (handles everything except API and WebSocket paths)
|
||||||
next-router:
|
next-router:
|
||||||
rule: "Host(`{{.DashboardDomain}}`) && !PathPrefix(`/api/v1`)"
|
rule: "Host(`{{.DashboardDomain}}`)"
|
||||||
service: next-service
|
service: next-service
|
||||||
|
priority: 10
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
middlewares:
|
|
||||||
- badger
|
|
||||||
tls:
|
tls:
|
||||||
certResolver: letsencrypt
|
certResolver: letsencrypt
|
||||||
|
|
||||||
@@ -34,10 +28,9 @@ http:
|
|||||||
api-router:
|
api-router:
|
||||||
rule: "Host(`{{.DashboardDomain}}`) && PathPrefix(`/api/v1`)"
|
rule: "Host(`{{.DashboardDomain}}`) && PathPrefix(`/api/v1`)"
|
||||||
service: api-service
|
service: api-service
|
||||||
|
priority: 100
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
middlewares:
|
|
||||||
- badger
|
|
||||||
tls:
|
tls:
|
||||||
certResolver: letsencrypt
|
certResolver: letsencrypt
|
||||||
|
|
||||||
@@ -51,12 +44,3 @@ http:
|
|||||||
loadBalancer:
|
loadBalancer:
|
||||||
servers:
|
servers:
|
||||||
- url: "http://pangolin:3000" # API/WebSocket server
|
- url: "http://pangolin:3000" # API/WebSocket server
|
||||||
|
|
||||||
tcp:
|
|
||||||
serversTransports:
|
|
||||||
pp-transport-v1:
|
|
||||||
proxyProtocol:
|
|
||||||
version: 1
|
|
||||||
pp-transport-v2:
|
|
||||||
proxyProtocol:
|
|
||||||
version: 2
|
|
||||||
|
|||||||
@@ -1,47 +1,34 @@
|
|||||||
api:
|
api:
|
||||||
insecure: true
|
insecure: true
|
||||||
dashboard: true
|
dashboard: true
|
||||||
|
|
||||||
providers:
|
providers:
|
||||||
http:
|
|
||||||
endpoint: http://pangolin:3001/api/v1/traefik-config
|
|
||||||
pollInterval: 5s
|
|
||||||
file:
|
file:
|
||||||
filename: /etc/traefik/dynamic_config.yml
|
directory: "/var/dynamic"
|
||||||
|
watch: true
|
||||||
|
|
||||||
experimental:
|
experimental:
|
||||||
plugins:
|
plugins:
|
||||||
badger:
|
badger:
|
||||||
moduleName: github.com/fosrl/badger
|
moduleName: "github.com/fosrl/badger"
|
||||||
version: v1.4.1
|
version: "v1.3.0"
|
||||||
|
|
||||||
log:
|
log:
|
||||||
level: INFO
|
level: "DEBUG"
|
||||||
format: common
|
format: "common"
|
||||||
maxSize: 100
|
maxSize: 100
|
||||||
maxBackups: 3
|
maxBackups: 3
|
||||||
maxAge: 3
|
maxAge: 3
|
||||||
compress: true
|
compress: true
|
||||||
certificatesResolvers:
|
|
||||||
letsencrypt:
|
|
||||||
acme:
|
|
||||||
httpChallenge:
|
|
||||||
entryPoint: web
|
|
||||||
email: '{{.LetsEncryptEmail}}'
|
|
||||||
storage: /letsencrypt/acme.json
|
|
||||||
caServer: https://acme-v02.api.letsencrypt.org/directory
|
|
||||||
entryPoints:
|
entryPoints:
|
||||||
web:
|
web:
|
||||||
address: ':80'
|
address: ":80"
|
||||||
websecure:
|
websecure:
|
||||||
address: ':443'
|
address: ":9443"
|
||||||
transport:
|
transport:
|
||||||
respondingTimeouts:
|
respondingTimeouts:
|
||||||
readTimeout: 30m
|
readTimeout: "30m"
|
||||||
http:
|
|
||||||
tls:
|
|
||||||
certResolver: letsencrypt
|
|
||||||
encodedCharacters:
|
|
||||||
allowEncodedSlash: true
|
|
||||||
allowEncodedQuestionMark: true
|
|
||||||
serversTransport:
|
serversTransport:
|
||||||
insecureSkipVerify: true
|
insecureSkipVerify: true
|
||||||
ping:
|
|
||||||
entryPoint: web
|
|
||||||
|
|||||||
@@ -4,12 +4,6 @@ services:
|
|||||||
image: fosrl/pangolin:latest
|
image: fosrl/pangolin:latest
|
||||||
container_name: pangolin
|
container_name: pangolin
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 1g
|
|
||||||
reservations:
|
|
||||||
memory: 256m
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./config:/app/config
|
- ./config:/app/config
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@@ -41,7 +35,7 @@ services:
|
|||||||
- 80:80 # Port for traefik because of the network_mode
|
- 80:80 # Port for traefik because of the network_mode
|
||||||
|
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:v3.7
|
image: traefik:v3.6
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
network_mode: service:gerbil # Ports appear on the gerbil service
|
network_mode: service:gerbil # Ports appear on the gerbil service
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { APP_PATH } from "./server/lib/consts";
|
import { APP_PATH } from "@server/lib/consts";
|
||||||
import { defineConfig } from "drizzle-kit";
|
import { defineConfig } from "drizzle-kit";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
|
|||||||
+2
-8
@@ -6,12 +6,6 @@ import path from "path";
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
// import { glob } from "glob";
|
// 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 = `
|
const banner = `
|
||||||
// patch __dirname
|
// patch __dirname
|
||||||
// import { fileURLToPath } from "url";
|
// import { fileURLToPath } from "url";
|
||||||
@@ -43,7 +37,7 @@ const argv = yargs(hideBin(process.argv))
|
|||||||
describe: "Build type (oss, saas, enterprise)",
|
describe: "Build type (oss, saas, enterprise)",
|
||||||
type: "string",
|
type: "string",
|
||||||
choices: ["oss", "saas", "enterprise"],
|
choices: ["oss", "saas", "enterprise"],
|
||||||
default: build
|
default: "oss"
|
||||||
})
|
})
|
||||||
.help()
|
.help()
|
||||||
.alias("help", "h").argv;
|
.alias("help", "h").argv;
|
||||||
@@ -281,7 +275,7 @@ esbuild
|
|||||||
})
|
})
|
||||||
],
|
],
|
||||||
sourcemap: "inline",
|
sourcemap: "inline",
|
||||||
target: "node24"
|
target: "node22"
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
// Check if there were any errors in the build result
|
// Check if there were any errors in the build result
|
||||||
|
|||||||
+34
-17
@@ -1,24 +1,41 @@
|
|||||||
all: go-build-release
|
all: update-versions go-build-release put-back
|
||||||
|
dev-all: dev-update-versions dev-build dev-clean
|
||||||
# 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)
|
|
||||||
|
|
||||||
go-build-release:
|
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 -o bin/installer_linux_amd64
|
||||||
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 -o bin/installer_linux_arm64
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o bin/installer_linux_arm64
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f bin/installer_linux_amd64
|
rm -f bin/installer_linux_amd64
|
||||||
rm -f bin/installer_linux_arm64
|
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"
|
||||||
|
|||||||
+26
-25
@@ -99,6 +99,11 @@ func ReadAppConfig(configPath string) (*AppConfigValues, error) {
|
|||||||
return values, nil
|
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 {
|
func copyDockerService(sourceFile, destFile, serviceName string) error {
|
||||||
// Read source file
|
// Read source file
|
||||||
sourceData, err := os.ReadFile(sourceFile)
|
sourceData, err := os.ReadFile(sourceFile)
|
||||||
@@ -113,19 +118,19 @@ func copyDockerService(sourceFile, destFile, serviceName string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse source Docker Compose YAML
|
// Parse source Docker Compose YAML
|
||||||
var sourceCompose map[string]any
|
var sourceCompose map[string]interface{}
|
||||||
if err := yaml.Unmarshal(sourceData, &sourceCompose); err != nil {
|
if err := yaml.Unmarshal(sourceData, &sourceCompose); err != nil {
|
||||||
return fmt.Errorf("error parsing source Docker Compose file: %w", err)
|
return fmt.Errorf("error parsing source Docker Compose file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse destination Docker Compose YAML
|
// Parse destination Docker Compose YAML
|
||||||
var destCompose map[string]any
|
var destCompose map[string]interface{}
|
||||||
if err := yaml.Unmarshal(destData, &destCompose); err != nil {
|
if err := yaml.Unmarshal(destData, &destCompose); err != nil {
|
||||||
return fmt.Errorf("error parsing destination Docker Compose file: %w", err)
|
return fmt.Errorf("error parsing destination Docker Compose file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get services section from source
|
// Get services section from source
|
||||||
sourceServices, ok := sourceCompose["services"].(map[string]any)
|
sourceServices, ok := sourceCompose["services"].(map[string]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("services section not found in source file or has invalid format")
|
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
|
// Get or create services section in destination
|
||||||
destServices, ok := destCompose["services"].(map[string]any)
|
destServices, ok := destCompose["services"].(map[string]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
// If services section doesn't exist, create it
|
// If services section doesn't exist, create it
|
||||||
destServices = make(map[string]any)
|
destServices = make(map[string]interface{})
|
||||||
destCompose["services"] = destServices
|
destCompose["services"] = destServices
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,21 +187,17 @@ func backupConfig() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func MarshalYAMLWithIndent(data any, indent int) (resp []byte, err error) {
|
func MarshalYAMLWithIndent(data interface{}, indent int) ([]byte, error) {
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(bytes.Buffer)
|
||||||
encoder := yaml.NewEncoder(buffer)
|
encoder := yaml.NewEncoder(buffer)
|
||||||
encoder.SetIndent(indent)
|
encoder.SetIndent(indent)
|
||||||
|
|
||||||
if err := encoder.Encode(data); err != nil {
|
err := encoder.Encode(data)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer encoder.Close()
|
||||||
if cerr := encoder.Close(); cerr != nil && err == nil {
|
|
||||||
err = cerr
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return buffer.Bytes(), nil
|
return buffer.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,7 +209,7 @@ func replaceInFile(filepath, oldStr, newStr string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Replace the string
|
// 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
|
// Write the modified content back to the file
|
||||||
err = os.WriteFile(filepath, []byte(newContent), 0644)
|
err = os.WriteFile(filepath, []byte(newContent), 0644)
|
||||||
@@ -227,28 +228,28 @@ func CheckAndAddTraefikLogVolume(composePath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse YAML into a generic map
|
// Parse YAML into a generic map
|
||||||
var compose map[string]any
|
var compose map[string]interface{}
|
||||||
if err := yaml.Unmarshal(data, &compose); err != nil {
|
if err := yaml.Unmarshal(data, &compose); err != nil {
|
||||||
return fmt.Errorf("error parsing compose file: %w", err)
|
return fmt.Errorf("error parsing compose file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get services section
|
// Get services section
|
||||||
services, ok := compose["services"].(map[string]any)
|
services, ok := compose["services"].(map[string]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("services section not found or invalid")
|
return fmt.Errorf("services section not found or invalid")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get traefik service
|
// Get traefik service
|
||||||
traefik, ok := services["traefik"].(map[string]any)
|
traefik, ok := services["traefik"].(map[string]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("traefik service not found or invalid")
|
return fmt.Errorf("traefik service not found or invalid")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check volumes
|
// Check volumes
|
||||||
logVolume := "./config/traefik/logs:/var/log/traefik"
|
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
|
// Check if volume already exists
|
||||||
for _, v := range existingVolumes {
|
for _, v := range existingVolumes {
|
||||||
if v.(string) == logVolume {
|
if v.(string) == logVolume {
|
||||||
@@ -294,13 +295,13 @@ func MergeYAML(baseFile, overlayFile string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse base YAML into a map
|
// Parse base YAML into a map
|
||||||
var baseMap map[string]any
|
var baseMap map[string]interface{}
|
||||||
if err := yaml.Unmarshal(baseContent, &baseMap); err != nil {
|
if err := yaml.Unmarshal(baseContent, &baseMap); err != nil {
|
||||||
return fmt.Errorf("error parsing base YAML: %v", err)
|
return fmt.Errorf("error parsing base YAML: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse overlay YAML into a map
|
// Parse overlay YAML into a map
|
||||||
var overlayMap map[string]any
|
var overlayMap map[string]interface{}
|
||||||
if err := yaml.Unmarshal(overlayContent, &overlayMap); err != nil {
|
if err := yaml.Unmarshal(overlayContent, &overlayMap); err != nil {
|
||||||
return fmt.Errorf("error parsing overlay YAML: %v", err)
|
return fmt.Errorf("error parsing overlay YAML: %v", err)
|
||||||
}
|
}
|
||||||
@@ -323,8 +324,8 @@ func MergeYAML(baseFile, overlayFile string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// mergeMap recursively merges two maps
|
// mergeMap recursively merges two maps
|
||||||
func mergeMap(base, overlay map[string]any) map[string]any {
|
func mergeMap(base, overlay map[string]interface{}) map[string]interface{} {
|
||||||
result := make(map[string]any)
|
result := make(map[string]interface{})
|
||||||
|
|
||||||
// Copy all key-values from base map
|
// Copy all key-values from base map
|
||||||
for k, v := range base {
|
for k, v := range base {
|
||||||
@@ -335,8 +336,8 @@ func mergeMap(base, overlay map[string]any) map[string]any {
|
|||||||
for k, v := range overlay {
|
for k, v := range overlay {
|
||||||
// If both maps have the same key and both values are maps, merge recursively
|
// If both maps have the same key and both values are maps, merge recursively
|
||||||
if baseVal, ok := base[k]; ok {
|
if baseVal, ok := base[k]; ok {
|
||||||
if baseMap, isBaseMap := baseVal.(map[string]any); isBaseMap {
|
if baseMap, isBaseMap := baseVal.(map[string]interface{}); isBaseMap {
|
||||||
if overlayMap, isOverlayMap := v.(map[string]any); isOverlayMap {
|
if overlayMap, isOverlayMap := v.(map[string]interface{}); isOverlayMap {
|
||||||
result[k] = mergeMap(baseMap, overlayMap)
|
result[k] = mergeMap(baseMap, overlayMap)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ server:
|
|||||||
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
||||||
allowed_headers: ["X-CSRF-Token", "Content-Type"]
|
allowed_headers: ["X-CSRF-Token", "Content-Type"]
|
||||||
credentials: false
|
credentials: false
|
||||||
{{if .EnableMaxMind}}maxmind_db_path: "./config/GeoLite2-Country.mmdb"{{end}}
|
{{if .EnableGeoblocking}}maxmind_db_path: "./config/GeoLite2-Country.mmdb"{{end}}
|
||||||
{{if .EnableMaxMind}}maxmind_asn_path: "./config/GeoLite2-ASN.mmdb"{{end}}
|
|
||||||
{{if .EnableEmail}}
|
{{if .EnableEmail}}
|
||||||
email:
|
email:
|
||||||
smtp_host: "{{.EmailSMTPHost}}"
|
smtp_host: "{{.EmailSMTPHost}}"
|
||||||
@@ -37,6 +36,3 @@ flags:
|
|||||||
disable_signup_without_invite: true
|
disable_signup_without_invite: true
|
||||||
disable_user_create_org: false
|
disable_user_create_org: false
|
||||||
allow_raw_resources: true
|
allow_raw_resources: true
|
||||||
|
|
||||||
{{if .IsPostgreSQL}}postgres:
|
|
||||||
connection_string: postgresql://pangolin:{{.IsPostgreSQLPass}}@postgres:5432/pangolin{{end}}
|
|
||||||
|
|||||||
@@ -81,19 +81,11 @@ entryPoints:
|
|||||||
transport:
|
transport:
|
||||||
respondingTimeouts:
|
respondingTimeouts:
|
||||||
readTimeout: "30m"
|
readTimeout: "30m"
|
||||||
http3:
|
|
||||||
advertisedPort: 443
|
|
||||||
http:
|
http:
|
||||||
tls:
|
tls:
|
||||||
certResolver: "letsencrypt"
|
certResolver: "letsencrypt"
|
||||||
middlewares:
|
middlewares:
|
||||||
- crowdsec@file
|
- crowdsec@file
|
||||||
encodedCharacters:
|
|
||||||
allowEncodedSlash: true
|
|
||||||
allowEncodedQuestionMark: true
|
|
||||||
|
|
||||||
serversTransport:
|
serversTransport:
|
||||||
insecureSkipVerify: true
|
insecureSkipVerify: true
|
||||||
|
|
||||||
ping:
|
|
||||||
entryPoint: "web"
|
|
||||||
|
|||||||
@@ -1,23 +1,9 @@
|
|||||||
name: pangolin
|
name: pangolin
|
||||||
services:
|
services:
|
||||||
pangolin:
|
pangolin:
|
||||||
image: docker.io/fosrl/pangolin:{{if .IsEnterprise}}ee-{{end}}{{if .IsPostgreSQL}}postgresql-{{end}}{{.PangolinVersion}}
|
image: docker.io/fosrl/pangolin:{{if .IsEnterprise}}ee-{{end}}{{.PangolinVersion}}
|
||||||
container_name: pangolin
|
container_name: pangolin
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 2g
|
|
||||||
reservations:
|
|
||||||
memory: 512m
|
|
||||||
{{if or .IsPostgreSQL .IsRedis}}depends_on:
|
|
||||||
{{if .IsPostgreSQL}}postgres:
|
|
||||||
condition: service_healthy{{end}}
|
|
||||||
{{if .IsRedis}}redis:
|
|
||||||
condition: service_healthy{{end}}
|
|
||||||
networks:
|
|
||||||
- default
|
|
||||||
- backend{{end}}
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./config:/app/config
|
- ./config:/app/config
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@@ -25,8 +11,8 @@ services:
|
|||||||
interval: "10s"
|
interval: "10s"
|
||||||
timeout: "10s"
|
timeout: "10s"
|
||||||
retries: 15
|
retries: 15
|
||||||
|
{{if .InstallGerbil}}
|
||||||
{{if .InstallGerbil}}gerbil:
|
gerbil:
|
||||||
image: docker.io/fosrl/gerbil:{{.GerbilVersion}}
|
image: docker.io/fosrl/gerbil:{{.GerbilVersion}}
|
||||||
container_name: gerbil
|
container_name: gerbil
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -46,17 +32,19 @@ services:
|
|||||||
- 51820:51820/udp
|
- 51820:51820/udp
|
||||||
- 21820:21820/udp
|
- 21820:21820/udp
|
||||||
- 443:443
|
- 443:443
|
||||||
- 443:443/udp # For http3 QUIC if desired
|
- 80:80
|
||||||
- 80:80{{end}}
|
{{end}}
|
||||||
|
|
||||||
traefik:
|
traefik:
|
||||||
image: docker.io/traefik:v3.7
|
image: docker.io/traefik:v3.6
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
restart: unless-stopped
|
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:
|
ports:
|
||||||
- 443:443
|
- 443:443
|
||||||
- 80:80{{end}}
|
- 80:80
|
||||||
|
{{end}}
|
||||||
depends_on:
|
depends_on:
|
||||||
pangolin:
|
pangolin:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -67,50 +55,8 @@ services:
|
|||||||
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
|
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
|
||||||
- ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs
|
- ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs
|
||||||
|
|
||||||
{{if .IsPostgreSQL}}postgres:
|
|
||||||
image: postgres:18
|
|
||||||
container_name: postgres
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: pangolin
|
|
||||||
POSTGRES_PASSWORD: {{.IsPostgreSQLPass}}
|
|
||||||
POSTGRES_DB: pangolin
|
|
||||||
volumes:
|
|
||||||
- ./postgres18:/var/lib/postgresql
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U pangolin"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
networks:
|
|
||||||
- backend{{end}}
|
|
||||||
|
|
||||||
{{if .IsRedis}}redis:
|
|
||||||
image: redis:8-trixie
|
|
||||||
container_name: redis
|
|
||||||
restart: unless-stopped
|
|
||||||
command: >
|
|
||||||
redis-server
|
|
||||||
--save 3600 1000
|
|
||||||
--appendonly yes
|
|
||||||
--requirepass {{.IsRedisPass}}
|
|
||||||
volumes:
|
|
||||||
- ./redis8:/data
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "redis-cli", "-a", "{{.IsRedisPass}}", "ping"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 3s
|
|
||||||
retries: 3
|
|
||||||
start_period: 10s
|
|
||||||
networks:
|
|
||||||
- backend{{end}}
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
name: pangolin_frontend
|
name: pangolin
|
||||||
{{if .EnableIPv6}} enable_ipv6: true{{end}}
|
{{if .EnableIPv6}} enable_ipv6: true{{end}}
|
||||||
{{if or .IsPostgreSQL .IsRedis}} backend:
|
|
||||||
driver: bridge
|
|
||||||
name: pangolin_backend
|
|
||||||
internal: true{{end}}
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
{{if .IsRedis}}redis:
|
|
||||||
host: "redis"
|
|
||||||
port: 6379
|
|
||||||
password: "{{.IsRedisPass}}"{{end}}
|
|
||||||
@@ -40,14 +40,9 @@ entryPoints:
|
|||||||
transport:
|
transport:
|
||||||
respondingTimeouts:
|
respondingTimeouts:
|
||||||
readTimeout: "30m"
|
readTimeout: "30m"
|
||||||
http3:
|
|
||||||
advertisedPort: 443
|
|
||||||
http:
|
http:
|
||||||
tls:
|
tls:
|
||||||
certResolver: "letsencrypt"
|
certResolver: "letsencrypt"
|
||||||
encodedCharacters:
|
|
||||||
allowEncodedSlash: true
|
|
||||||
allowEncodedQuestionMark: true
|
|
||||||
|
|
||||||
serversTransport:
|
serversTransport:
|
||||||
insecureSkipVerify: true
|
insecureSkipVerify: true
|
||||||
|
|||||||
+6
-48
@@ -144,13 +144,12 @@ func installDocker() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func startDockerService() error {
|
func startDockerService() error {
|
||||||
switch runtime.GOOS {
|
if runtime.GOOS == "linux" {
|
||||||
case "linux":
|
|
||||||
cmd := exec.Command("systemctl", "enable", "--now", "docker")
|
cmd := exec.Command("systemctl", "enable", "--now", "docker")
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
return cmd.Run()
|
return cmd.Run()
|
||||||
case "darwin":
|
} else if runtime.GOOS == "darwin" {
|
||||||
// On macOS, Docker is usually started via the Docker Desktop application
|
// On macOS, Docker is usually started via the Docker Desktop application
|
||||||
fmt.Println("Please start Docker Desktop manually on macOS.")
|
fmt.Println("Please start Docker Desktop manually on macOS.")
|
||||||
return nil
|
return nil
|
||||||
@@ -211,47 +210,6 @@ func isDockerRunning() bool {
|
|||||||
return true
|
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
|
// executeDockerComposeCommandWithArgs executes the appropriate docker command with arguments supplied
|
||||||
func executeDockerComposeCommandWithArgs(args ...string) error {
|
func executeDockerComposeCommandWithArgs(args ...string) error {
|
||||||
var cmd *exec.Cmd
|
var cmd *exec.Cmd
|
||||||
@@ -303,7 +261,7 @@ func pullContainers(containerType SupportedContainer) error {
|
|||||||
return nil
|
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.
|
// startContainers starts the containers using the appropriate command.
|
||||||
@@ -326,7 +284,7 @@ func startContainers(containerType SupportedContainer) error {
|
|||||||
return nil
|
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.
|
// stopContainers stops the containers using the appropriate command.
|
||||||
@@ -348,7 +306,7 @@ func stopContainers(containerType SupportedContainer) error {
|
|||||||
return nil
|
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.
|
// restartContainer restarts a specific container using the appropriate command.
|
||||||
@@ -370,5 +328,5 @@ func restartContainer(container string, containerType SupportedContainer) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("unsupported container type: %s", containerType)
|
return fmt.Errorf("Unsupported container type: %s", containerType)
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-91
@@ -6,13 +6,12 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func installCrowdsec(config Config, installDir string) error {
|
func installCrowdsec(config Config) error {
|
||||||
|
|
||||||
if err := stopContainers(config.InstallationContainerType); err != nil {
|
if err := stopContainers(config.InstallationContainerType); err != nil {
|
||||||
return fmt.Errorf("failed to stop containers: %v", err)
|
return fmt.Errorf("failed to stop containers: %v", err)
|
||||||
@@ -28,20 +27,9 @@ func installCrowdsec(config Config, installDir string) error {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll("config/crowdsec/db", 0755); err != nil {
|
os.MkdirAll("config/crowdsec/db", 0755)
|
||||||
fmt.Printf("Error creating config files: %v\n", err)
|
os.MkdirAll("config/crowdsec/acquis.d", 0755)
|
||||||
os.Exit(1)
|
os.MkdirAll("config/traefik/logs", 0755)
|
||||||
}
|
|
||||||
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)
|
|
||||||
|
|
||||||
if err := copyDockerService("config/crowdsec/docker-compose.yml", "docker-compose.yml", "crowdsec"); err != nil {
|
if err := copyDockerService("config/crowdsec/docker-compose.yml", "docker-compose.yml", "crowdsec"); err != nil {
|
||||||
fmt.Printf("Error copying docker service: %v\n", err)
|
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") {
|
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.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
|
return nil
|
||||||
@@ -129,7 +117,7 @@ func GetCrowdSecAPIKey(containerType SupportedContainer) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Execute the command to get the API key
|
// 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
|
var out bytes.Buffer
|
||||||
cmd.Stdout = &out
|
cmd.Stdout = &out
|
||||||
|
|
||||||
@@ -165,34 +153,34 @@ func CheckAndAddCrowdsecDependency(composePath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse YAML into a generic map
|
// Parse YAML into a generic map
|
||||||
var compose map[string]any
|
var compose map[string]interface{}
|
||||||
if err := yaml.Unmarshal(data, &compose); err != nil {
|
if err := yaml.Unmarshal(data, &compose); err != nil {
|
||||||
return fmt.Errorf("error parsing compose file: %w", err)
|
return fmt.Errorf("error parsing compose file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get services section
|
// Get services section
|
||||||
services, ok := compose["services"].(map[string]any)
|
services, ok := compose["services"].(map[string]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("services section not found or invalid")
|
return fmt.Errorf("services section not found or invalid")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get traefik service
|
// Get traefik service
|
||||||
traefik, ok := services["traefik"].(map[string]any)
|
traefik, ok := services["traefik"].(map[string]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("traefik service not found or invalid")
|
return fmt.Errorf("traefik service not found or invalid")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get dependencies
|
// Get dependencies
|
||||||
dependsOn, ok := traefik["depends_on"].(map[string]any)
|
dependsOn, ok := traefik["depends_on"].(map[string]interface{})
|
||||||
if ok {
|
if ok {
|
||||||
// Append the new block for crowdsec
|
// Append the new block for crowdsec
|
||||||
dependsOn["crowdsec"] = map[string]any{
|
dependsOn["crowdsec"] = map[string]interface{}{
|
||||||
"condition": "service_healthy",
|
"condition": "service_healthy",
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No dependencies exist, create it
|
// No dependencies exist, create it
|
||||||
traefik["depends_on"] = map[string]any{
|
traefik["depends_on"] = map[string]interface{}{
|
||||||
"crowdsec": map[string]any{
|
"crowdsec": map[string]interface{}{
|
||||||
"condition": "service_healthy",
|
"condition": "service_healthy",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -211,69 +199,3 @@ func CheckAndAddCrowdsecDependency(composePath string) error {
|
|||||||
fmt.Println("Added dependency of crowdsec to traefik")
|
fmt.Println("Added dependency of crowdsec to traefik")
|
||||||
return nil
|
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)
|
|
||||||
}
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user