Compare commits
2 Commits
feature/da
...
e831640540
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e831640540 | ||
|
|
a023cbc082 |
@@ -30,8 +30,7 @@
|
|||||||
"ghcr.io/guiyomh/features/just:0": {},
|
"ghcr.io/guiyomh/features/just:0": {},
|
||||||
"ghcr.io/devcontainers-extra/features/bun": {
|
"ghcr.io/devcontainers-extra/features/bun": {
|
||||||
"version": "latest"
|
"version": "latest"
|
||||||
},
|
}
|
||||||
"ghcr.io/devcontainers-extra/features/act": {}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"customizations": {
|
"customizations": {
|
||||||
|
|||||||
1
.github/.gitignore
vendored
1
.github/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
*.env
|
|
||||||
4
.github/.secrets.env.template
vendored
4
.github/.secrets.env.template
vendored
@@ -1,4 +0,0 @@
|
|||||||
# This is an example environment variable file for GitHub Actions. You can copy this file to .github/.secrets.env and fill in the values to override the default registry and GitHub token used in the CI workflow. This is useful for testing with a private registry or using a different GitHub account for authentication.
|
|
||||||
OVERRIDE_REGISTRY=<your-registry-url>
|
|
||||||
OVERRIDE_GITHUB_TOKEN=<your-github-token>
|
|
||||||
GITHUB_USERNAME=<your-github-username>
|
|
||||||
70
.github/actions/setup-ci-metadata/action.yaml
vendored
70
.github/actions/setup-ci-metadata/action.yaml
vendored
@@ -1,70 +0,0 @@
|
|||||||
name: 'Setup CI metadata'
|
|
||||||
description: 'Composite action to derive the registry and CI image tag for the current repository.'
|
|
||||||
inputs:
|
|
||||||
registry:
|
|
||||||
description: 'Container registry derived from the current GitHub server URL'
|
|
||||||
required: false
|
|
||||||
default: ''
|
|
||||||
repository:
|
|
||||||
description: 'GitHub repository in the format owner/repo'
|
|
||||||
required: false
|
|
||||||
default: ${{ github.repository }}
|
|
||||||
image_tag:
|
|
||||||
description: 'Tag for the CI image'
|
|
||||||
required: false
|
|
||||||
default: 'latest'
|
|
||||||
outputs:
|
|
||||||
registry:
|
|
||||||
description: 'Container registry derived from the current GitHub server URL'
|
|
||||||
value: ${{ steps.setup.outputs.registry }}
|
|
||||||
image_tag:
|
|
||||||
description: 'Fully qualified CI image tag'
|
|
||||||
value: ${{ steps.setup.outputs.image_tag }}
|
|
||||||
latest_tag:
|
|
||||||
description: 'Fully qualified latest CI image tag'
|
|
||||||
value: ${{ steps.setup.outputs.latest_tag }}
|
|
||||||
runs:
|
|
||||||
using: 'composite'
|
|
||||||
steps:
|
|
||||||
- name: Setup Dynamic Metadata
|
|
||||||
id: setup
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
# Extract the domain from server_url, handling both https:// and ssh:// schemes
|
|
||||||
SERVER_URL="${{ github.server_url }}"
|
|
||||||
|
|
||||||
if [[ "$SERVER_URL" =~ ^ssh:// ]]; then
|
|
||||||
# For SSH URLs like ssh://git@host:port/path, extract just the hostname
|
|
||||||
SERVER_DOMAIN=$(echo "$SERVER_URL" | sed -e 's|^ssh://||' -e 's|^[^@]*@||' -e 's|:[0-9]*.*||')
|
|
||||||
else
|
|
||||||
# For HTTPS URLs, extract domain without scheme
|
|
||||||
SERVER_DOMAIN=$(echo "$SERVER_URL" | sed -e 's|^[^/]*//||' -e 's|/.*$||')
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Extracted server domain: $SERVER_DOMAIN"
|
|
||||||
|
|
||||||
if [[ -n "${{ inputs.registry }}" ]]; then
|
|
||||||
REGISTRY="${{ inputs.registry }}"
|
|
||||||
elif [[ "$SERVER_DOMAIN" == "github.com" ]]; then
|
|
||||||
REGISTRY="ghcr.io"
|
|
||||||
else
|
|
||||||
REGISTRY="$SERVER_DOMAIN"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Extract owner/repo from github.repository, handling SSH URLs
|
|
||||||
REPO="${{ inputs.repository }}"
|
|
||||||
if [[ "$REPO" =~ ^ssh:// ]] || [[ "$REPO" =~ ^https:// ]]; then
|
|
||||||
# Extract owner/repo from URLs like ssh://git@host/owner/repo.git or https://host/owner/repo.git
|
|
||||||
REPO=$(echo "$REPO" | sed -e 's|^[^/]*/||' -e 's|\.git$||' | rev | cut -d'/' -f1,2 | rev)
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Docker image names must be lowercase
|
|
||||||
REGISTRY="${REGISTRY,,}"
|
|
||||||
REPO="${REPO,,}"
|
|
||||||
|
|
||||||
IMAGE_TAG="${REGISTRY}/${REPO}/ci:${{ inputs.image_tag }}"
|
|
||||||
LATEST_TAG="${REGISTRY}/${REPO}/ci:latest"
|
|
||||||
|
|
||||||
echo "registry=$REGISTRY" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "image_tag=$IMAGE_TAG" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "latest_tag=$LATEST_TAG" >> "$GITHUB_OUTPUT"
|
|
||||||
74
.github/actions/setup-rust/action.yaml
vendored
74
.github/actions/setup-rust/action.yaml
vendored
@@ -1,74 +0,0 @@
|
|||||||
name: 'Setup Rust environment'
|
|
||||||
description: 'Composite action to checkout the repo, restore cargo caches and set up the Rust toolchain. Use this from job steps to keep setup DRY across jobs.'
|
|
||||||
inputs:
|
|
||||||
toolchain:
|
|
||||||
description: 'Rust toolchain to install'
|
|
||||||
required: false
|
|
||||||
default: 'stable'
|
|
||||||
override:
|
|
||||||
description: 'Whether to override the default toolchain'
|
|
||||||
required: false
|
|
||||||
default: 'true'
|
|
||||||
components:
|
|
||||||
description: 'Comma-separated list of additional rust components to install'
|
|
||||||
required: false
|
|
||||||
default: 'clippy, rustfmt'
|
|
||||||
skip_cache:
|
|
||||||
description: 'Whether to skip restoring and uploading caches (useful for testing the workflow without cache interference)'
|
|
||||||
required: false
|
|
||||||
default: 'false'
|
|
||||||
runs:
|
|
||||||
using: 'composite'
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Cache cargo registry
|
|
||||||
uses: actions/cache@v4
|
|
||||||
if: inputs.skip_cache != 'true'
|
|
||||||
with:
|
|
||||||
path: ~/.cargo/registry
|
|
||||||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
|
|
||||||
- name: Cache cargo index
|
|
||||||
uses: actions/cache@v4
|
|
||||||
if: inputs.skip_cache != 'true'
|
|
||||||
with:
|
|
||||||
path: ~/.cargo/index
|
|
||||||
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
|
|
||||||
- name: Sanitize components input
|
|
||||||
shell: bash
|
|
||||||
run: echo "SANITIZED_COMPONENTS=${{ inputs.components }}" | sed -E 's/, ?| /-/g' >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Cache Rust toolchain
|
|
||||||
uses: actions/cache@v3
|
|
||||||
if: inputs.skip_cache != 'true'
|
|
||||||
with:
|
|
||||||
path: ~/.rustup
|
|
||||||
# Key includes the OS and the toolchain version (e.g., 'stable')
|
|
||||||
key: ${{ runner.os }}-rustup-${{ hashFiles('rust-toolchain.toml') }}-v1-${{ inputs.toolchain }}-${{ env.SANITIZED_COMPONENTS }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-rustup-
|
|
||||||
|
|
||||||
- name: Cache cargo build (target)
|
|
||||||
uses: actions/cache@v3
|
|
||||||
if: inputs.skip_cache != 'true'
|
|
||||||
with:
|
|
||||||
path: target
|
|
||||||
key: ${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
|
|
||||||
- name: Set up rust toolchain
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
toolchain: ${{ inputs.toolchain }}
|
|
||||||
override: ${{ inputs.override }}
|
|
||||||
components: ${{ inputs.components }}
|
|
||||||
31
.github/docker/ci.Dockerfile
vendored
31
.github/docker/ci.Dockerfile
vendored
@@ -1,31 +0,0 @@
|
|||||||
FROM node:24-bookworm-slim
|
|
||||||
|
|
||||||
# Install necessary dependencies for building Rust projects and running tests
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
curl \
|
|
||||||
git \
|
|
||||||
zstd \
|
|
||||||
build-essential \
|
|
||||||
pkg-config \
|
|
||||||
libssl-dev \
|
|
||||||
gnupg \
|
|
||||||
unzip \
|
|
||||||
tar \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
postgresql-client \
|
|
||||||
protobuf-compiler \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# install bun
|
|
||||||
RUN curl -fsSL https://bun.sh/install | bash
|
|
||||||
ENV PATH="/root/.bun/bin:${PATH}"
|
|
||||||
|
|
||||||
# install rust and cargo
|
|
||||||
RUN apt-get update && apt-get install -y curl build-essential
|
|
||||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
|
||||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
|
||||||
|
|
||||||
# Set the working directory
|
|
||||||
WORKDIR /app
|
|
||||||
54
.github/workflows/build-ci.yaml
vendored
54
.github/workflows/build-ci.yaml
vendored
@@ -1,54 +0,0 @@
|
|||||||
name: Build CI Environment
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
image_tag:
|
|
||||||
description: 'Tag for the CI image (e.g., latest)'
|
|
||||||
required: true
|
|
||||||
default: 'latest'
|
|
||||||
|
|
||||||
env:
|
|
||||||
# OVERRIDE_REGISTRY can be set as a secret to override the default registry (e.g., for testing with a private registry). Else '' will be used, which defaults to ghcr.io for github.com and the GitHub server domain for self-hosted GitHub instances.
|
|
||||||
OVERRIDE_REGISTRY: ${{ secrets.OVERRIDE_REGISTRY }}
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: build-ci
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-ci-image:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup CI metadata
|
|
||||||
id: setup
|
|
||||||
uses: ./.github/actions/setup-ci-metadata
|
|
||||||
with:
|
|
||||||
registry: ${{ env.OVERRIDE_REGISTRY }}
|
|
||||||
image_tag: ${{ github.event.inputs.image_tag }}
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@v4
|
|
||||||
with:
|
|
||||||
registry: ${{ steps.setup.outputs.registry }}
|
|
||||||
username: ${{ secrets.GITHUB_USERNAME || github.actor }}
|
|
||||||
password: ${{ secrets.OVERRIDE_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build and push Docker image for CI
|
|
||||||
uses: docker/build-push-action@v3
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: .github/docker/ci.Dockerfile
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
${{ steps.setup.outputs.image_tag }}
|
|
||||||
${{ steps.setup.outputs.latest_tag }}
|
|
||||||
200
.github/workflows/test.yaml
vendored
200
.github/workflows/test.yaml
vendored
@@ -1,200 +0,0 @@
|
|||||||
# this workflow runs tests on pull request and push events targeting master branch
|
|
||||||
# it also verify the generated code is up to date and valid
|
|
||||||
|
|
||||||
name: Test
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
get-ci-image:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
image_tag: ${{ steps.setup.outputs.image_tag }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup CI metadata
|
|
||||||
id: setup
|
|
||||||
uses: ./.github/actions/setup-ci-metadata
|
|
||||||
with:
|
|
||||||
registry: ${{ secrets.OVERRIDE_REGISTRY }}
|
|
||||||
image_tag: latest
|
|
||||||
|
|
||||||
test-crates:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs:
|
|
||||||
- frontend-build
|
|
||||||
- get-ci-image
|
|
||||||
container:
|
|
||||||
image: ${{ needs.get-ci-image.outputs.image_tag }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup Rust, checkout and restore caches
|
|
||||||
uses: ./.github/actions/setup-rust
|
|
||||||
|
|
||||||
- name: Restore frontend build cache
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: apps/nxmesh-frontend/dist
|
|
||||||
key: frontend-build-${{ runner.os }}-run-${{ github.run_id }}
|
|
||||||
restore-keys: |
|
|
||||||
frontend-build-${{ runner.os }}-
|
|
||||||
|
|
||||||
# TODO: uncomment until artifact hanlding fixed in gitea
|
|
||||||
# - name: Download frontend build artifact
|
|
||||||
# uses: actions/download-artifact@v4
|
|
||||||
# with:
|
|
||||||
# name: frontend-dist
|
|
||||||
# path: apps/nxmesh-frontend/dist
|
|
||||||
|
|
||||||
# - name: Copy frontend build to expected location
|
|
||||||
# run: |
|
|
||||||
# # unlink frontend-dist
|
|
||||||
# rm -f apps/nxmesh-master/frontend-dist || true
|
|
||||||
# rm -rf apps/nxmesh-master/frontend-dist || true
|
|
||||||
# cp -r apps/nxmesh-frontend/dist apps/nxmesh-master/frontend-dist
|
|
||||||
|
|
||||||
# ls -la apps/nxmesh-master/frontend-dist
|
|
||||||
|
|
||||||
- name: Create dummy build artifacts
|
|
||||||
run: |
|
|
||||||
rm -f apps/nxmesh-master/frontend-dist || true
|
|
||||||
mkdir -p apps/nxmesh-master/frontend-dist
|
|
||||||
echo "<html><body><h1>Dummy Build</h1></body></html>" > apps/nxmesh-master/frontend-dist/index.html
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: cargo test --all-features -- --show-output
|
|
||||||
|
|
||||||
lint-crates:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs:
|
|
||||||
- frontend-build
|
|
||||||
- get-ci-image
|
|
||||||
container:
|
|
||||||
image: ${{ needs.get-ci-image.outputs.image_tag }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup Rust, checkout and restore caches
|
|
||||||
uses: ./.github/actions/setup-rust
|
|
||||||
with:
|
|
||||||
components: clippy, rustfmt
|
|
||||||
|
|
||||||
- name: Restore frontend build cache
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: apps/nxmesh-frontend/dist
|
|
||||||
key: frontend-build-${{ runner.os }}-run-${{ github.run_id }}
|
|
||||||
restore-keys: |
|
|
||||||
frontend-build-${{ runner.os }}-
|
|
||||||
|
|
||||||
# TODO: uncomment until artifact hanlding fixed in gitea
|
|
||||||
# - name: Download frontend build artifact
|
|
||||||
# uses: actions/download-artifact@v4
|
|
||||||
# with:
|
|
||||||
# name: frontend-dist
|
|
||||||
# path: apps/nxmesh-frontend/dist
|
|
||||||
|
|
||||||
# - name: Copy frontend build to expected location
|
|
||||||
# run: |
|
|
||||||
# # unlink frontend-dist
|
|
||||||
# rm -f apps/nxmesh-master/frontend-dist || true
|
|
||||||
# rm -rf apps/nxmesh-master/frontend-dist || true
|
|
||||||
# cp -r apps/nxmesh-frontend/dist apps/nxmesh-master/frontend-dist
|
|
||||||
|
|
||||||
- name: Create dummy build artifacts
|
|
||||||
run: |
|
|
||||||
rm -f apps/nxmesh-master/frontend-dist || true
|
|
||||||
mkdir -p apps/nxmesh-master/frontend-dist
|
|
||||||
echo "<html><body><h1>Dummy Build</h1></body></html>" > apps/nxmesh-master/frontend-dist/index.html
|
|
||||||
|
|
||||||
- name: Run clippy
|
|
||||||
run: cargo clippy --all-features
|
|
||||||
|
|
||||||
- name: Check code formatting
|
|
||||||
run: cargo fmt --all -- --check
|
|
||||||
|
|
||||||
lint-frontend:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- uses: oven-sh/setup-bun@v2
|
|
||||||
name: Install Bun
|
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
|
||||||
run: |
|
|
||||||
cd apps/nxmesh-frontend
|
|
||||||
bun install
|
|
||||||
|
|
||||||
- name: Run frontend linter
|
|
||||||
run: |
|
|
||||||
cd apps/nxmesh-frontend
|
|
||||||
bun run lint
|
|
||||||
|
|
||||||
test-frontend:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- uses: oven-sh/setup-bun@v2
|
|
||||||
name: Install Bun
|
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
|
||||||
run: |
|
|
||||||
cd apps/nxmesh-frontend
|
|
||||||
bun install
|
|
||||||
|
|
||||||
- name: Run frontend tests
|
|
||||||
run: |
|
|
||||||
cd apps/nxmesh-frontend
|
|
||||||
bun run test
|
|
||||||
|
|
||||||
frontend-build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- uses: oven-sh/setup-bun@v2
|
|
||||||
name: Install Bun
|
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
|
||||||
run: |
|
|
||||||
cd apps/nxmesh-frontend
|
|
||||||
bun install
|
|
||||||
|
|
||||||
- name: Build frontend
|
|
||||||
run: |
|
|
||||||
cd apps/nxmesh-frontend
|
|
||||||
bun run build
|
|
||||||
|
|
||||||
- name: Cache frontend build
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: apps/nxmesh-frontend/dist
|
|
||||||
key: frontend-build-${{ runner.os }}-run-${{ github.run_id }}
|
|
||||||
# IGNORE restore-keys
|
|
||||||
# TODO: uncomment until artifact hanlding fixed in gitea
|
|
||||||
# - name: Upload frontend build artifact
|
|
||||||
# uses: actions/upload-artifact@v4
|
|
||||||
# with:
|
|
||||||
# name: frontend-dist
|
|
||||||
# path: apps/nxmesh-frontend/dist
|
|
||||||
139
.github/workflows/verify.yaml
vendored
139
.github/workflows/verify.yaml
vendored
@@ -1,139 +0,0 @@
|
|||||||
# this workflow verifies the generated code is up to date and valid
|
|
||||||
|
|
||||||
name: Verify
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
env:
|
|
||||||
# OVERRIDE_REGISTRY can be set as a secret to override the default registry (e.g., for testing with a private registry). Else '' will be used, which defaults to ghcr.io for github.com and the GitHub server domain for self-hosted GitHub instances.
|
|
||||||
OVERRIDE_REGISTRY: ${{ secrets.OVERRIDE_REGISTRY }}
|
|
||||||
ACTIONS_STEP_DEBUG: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
get-ci-image:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
image_tag: ${{ steps.setup.outputs.image_tag }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup CI metadata
|
|
||||||
id: setup
|
|
||||||
uses: ./.github/actions/setup-ci-metadata
|
|
||||||
with:
|
|
||||||
registry: ${{ secrets.OVERRIDE_REGISTRY }}
|
|
||||||
image_tag: latest
|
|
||||||
|
|
||||||
verify-generated-db-entities:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs:
|
|
||||||
- get-ci-image
|
|
||||||
container:
|
|
||||||
image: ${{ needs.get-ci-image.outputs.image_tag }}
|
|
||||||
services:
|
|
||||||
postgres:
|
|
||||||
image: postgres:16
|
|
||||||
env:
|
|
||||||
POSTGRES_USER: postgres
|
|
||||||
POSTGRES_PASSWORD: postgres
|
|
||||||
POSTGRES_DB: nxmesh
|
|
||||||
# ! do not set a fixed port to avoid conflicts when running multiple jobs in parallel, use Docker's internal networking instead
|
|
||||||
# ports:
|
|
||||||
# - 5432:5432
|
|
||||||
options: >-
|
|
||||||
--health-cmd "pg_isready -U postgres -d nxmesh"
|
|
||||||
--health-interval 10s
|
|
||||||
--health-timeout 5s
|
|
||||||
--health-retries 5
|
|
||||||
|
|
||||||
env:
|
|
||||||
DATABASE_URL: postgres://postgres:postgres@postgres:5432/nxmesh
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Check whether migrations/entities changed
|
|
||||||
id: check_changes
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
|
||||||
BASE_SHA=${{ github.event.pull_request.base.sha }}
|
|
||||||
HEAD_SHA=${{ github.event.pull_request.head.sha }}
|
|
||||||
else
|
|
||||||
BASE_SHA=${{ github.event.before }}
|
|
||||||
HEAD_SHA=${{ github.sha }}
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$HEAD_SHA" ]; then
|
|
||||||
HEAD_SHA=$(git rev-parse --verify HEAD 2>/dev/null || echo "")
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$BASE_SHA" ]; then
|
|
||||||
PREV=$(git rev-parse --verify "${HEAD_SHA}^" 2>/dev/null || true)
|
|
||||||
if [ -n "$PREV" ]; then
|
|
||||||
BASE_SHA=$PREV
|
|
||||||
else
|
|
||||||
BASE_SHA=$HEAD_SHA
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Comparing $BASE_SHA..$HEAD_SHA"
|
|
||||||
CHANGED_FILES=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" || true)
|
|
||||||
echo "$CHANGED_FILES"
|
|
||||||
|
|
||||||
echo "$CHANGED_FILES" | grep -E '^(crates/migration/src/|apps/nxmesh-master/src/db/entities/)' >/dev/null 2>&1 \
|
|
||||||
&& echo "changed=true" >> $GITHUB_OUTPUT \
|
|
||||||
|| echo "changed=true" >> $GITHUB_OUTPUT
|
|
||||||
# || echo "changed=false" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Setup Rust, checkout and restore caches
|
|
||||||
if: steps.check_changes.outputs.changed == 'true'
|
|
||||||
uses: ./.github/actions/setup-rust
|
|
||||||
with:
|
|
||||||
skip_cache: ${{ vars.SKIP_CACHE }}
|
|
||||||
|
|
||||||
- name: Install SeaORM CLI
|
|
||||||
if: steps.check_changes.outputs.changed == 'true'
|
|
||||||
run: |
|
|
||||||
cargo install sea-orm-cli@^2.0.0-rc --features "sqlx-postgres runtime-tokio-rustls"
|
|
||||||
|
|
||||||
- name: Apply migrations
|
|
||||||
if: steps.check_changes.outputs.changed == 'true'
|
|
||||||
run: |
|
|
||||||
cd crates && sea-orm-cli migrate up
|
|
||||||
|
|
||||||
- name: Regenerate entities
|
|
||||||
if: steps.check_changes.outputs.changed == 'true'
|
|
||||||
run: |
|
|
||||||
cd crates && sea-orm-cli generate entity \
|
|
||||||
--database-url "$DATABASE_URL" \
|
|
||||||
--output-dir ../apps/nxmesh-master/src/db/entities \
|
|
||||||
--with-serde both \
|
|
||||||
--with-copy-enums \
|
|
||||||
--date-time-crate chrono
|
|
||||||
|
|
||||||
- name: Check for uncommitted changes in entities
|
|
||||||
if: steps.check_changes.outputs.changed == 'true'
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
if [[ -n $(git status --porcelain --untracked-files=all | grep 'apps/nxmesh-master/src/db/entities/') ]]; then
|
|
||||||
echo "Generated SeaORM entities are not up to date."
|
|
||||||
echo "Run 'just db-generate' after applying migrations and commit the result."
|
|
||||||
git status --porcelain --untracked-files=all | grep 'apps/nxmesh-master/src/db/entities/'
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "Generated SeaORM entities are up to date."
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Skip entity generation (no relevant changes)
|
|
||||||
if: steps.check_changes.outputs.changed == 'false'
|
|
||||||
run: echo "No changes in migrations/entities, skipping SeaORM entity verification."
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -68,7 +68,6 @@ web_modules/
|
|||||||
# dotenv environment variable files
|
# dotenv environment variable files
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
*.env
|
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
|||||||
279
Cargo.lock
generated
279
Cargo.lock
generated
@@ -538,7 +538,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
|
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum-core",
|
"axum-core",
|
||||||
"axum-macros",
|
|
||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
@@ -587,45 +586,6 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "axum-macros"
|
|
||||||
version = "0.5.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.117",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "axum-test"
|
|
||||||
version = "20.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3a86bfe2ef15bee102ac34912f7f4542b0bb37dc464fa55461763999c4d625e7"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"axum",
|
|
||||||
"bytes",
|
|
||||||
"bytesize",
|
|
||||||
"cookie",
|
|
||||||
"expect-json",
|
|
||||||
"http",
|
|
||||||
"http-body-util",
|
|
||||||
"hyper",
|
|
||||||
"hyper-util",
|
|
||||||
"mime",
|
|
||||||
"pretty_assertions",
|
|
||||||
"reserve-port",
|
|
||||||
"rust-multipart-rfc7578_2",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"serde_urlencoded",
|
|
||||||
"tokio",
|
|
||||||
"tower",
|
|
||||||
"url",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
@@ -767,12 +727,6 @@ version = "1.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bytesize"
|
|
||||||
version = "2.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bzip2"
|
name = "bzip2"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
@@ -957,16 +911,6 @@ dependencies = [
|
|||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cookie"
|
|
||||||
version = "0.18.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
|
||||||
dependencies = [
|
|
||||||
"time",
|
|
||||||
"version_check",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
@@ -1191,12 +1135,6 @@ dependencies = [
|
|||||||
"unicode-xid",
|
"unicode-xid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "diff"
|
|
||||||
version = "0.1.13"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
@@ -1250,15 +1188,6 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "email_address"
|
|
||||||
version = "0.2.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encoding_rs"
|
name = "encoding_rs"
|
||||||
version = "0.8.35"
|
version = "0.8.35"
|
||||||
@@ -1333,35 +1262,6 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "expect-json"
|
|
||||||
version = "1.10.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "869f97f4abe8e78fc812a94ad6b721d72c4fb5532877c79610f2c238d7ccf6c4"
|
|
||||||
dependencies = [
|
|
||||||
"chrono",
|
|
||||||
"email_address",
|
|
||||||
"expect-json-macros",
|
|
||||||
"num",
|
|
||||||
"regex",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"thiserror",
|
|
||||||
"typetag",
|
|
||||||
"uuid",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "expect-json-macros"
|
|
||||||
version = "1.10.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6e6fdf550180a6c29a28cb9aac262dc0064c25735641d2317f670075e9a469d9"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.117",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
@@ -1443,6 +1343,17 @@ version = "2.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619"
|
checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fs4"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4"
|
||||||
|
dependencies = [
|
||||||
|
"rustix",
|
||||||
|
"tokio",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "funty"
|
name = "funty"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
@@ -2064,15 +1975,6 @@ dependencies = [
|
|||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "inventory"
|
|
||||||
version = "0.3.24"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b"
|
|
||||||
dependencies = [
|
|
||||||
"rustversion",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "is_terminal_polyfill"
|
name = "is_terminal_polyfill"
|
||||||
version = "1.70.2"
|
version = "1.70.2"
|
||||||
@@ -2443,20 +2345,6 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num"
|
|
||||||
version = "0.4.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
|
|
||||||
dependencies = [
|
|
||||||
"num-bigint",
|
|
||||||
"num-complex",
|
|
||||||
"num-integer",
|
|
||||||
"num-iter",
|
|
||||||
"num-rational",
|
|
||||||
"num-traits",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-bigint"
|
name = "num-bigint"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
@@ -2533,17 +2421,6 @@ dependencies = [
|
|||||||
"num-modular",
|
"num-modular",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-rational"
|
|
||||||
version = "0.4.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
|
|
||||||
dependencies = [
|
|
||||||
"num-bigint",
|
|
||||||
"num-integer",
|
|
||||||
"num-traits",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
@@ -2558,10 +2435,12 @@ dependencies = [
|
|||||||
name = "nxmesh-agent"
|
name = "nxmesh-agent"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"config",
|
"config",
|
||||||
|
"fs4",
|
||||||
"futures",
|
"futures",
|
||||||
"hex",
|
"hex",
|
||||||
"hostname",
|
"hostname",
|
||||||
@@ -2604,7 +2483,6 @@ dependencies = [
|
|||||||
"async-stream",
|
"async-stream",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
"axum-test",
|
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"config",
|
"config",
|
||||||
@@ -2612,14 +2490,12 @@ dependencies = [
|
|||||||
"handlebars",
|
"handlebars",
|
||||||
"hex",
|
"hex",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"mime_guess",
|
|
||||||
"mockall",
|
"mockall",
|
||||||
"nxmesh-core",
|
"nxmesh-core",
|
||||||
"nxmesh-migration",
|
"nxmesh-migration",
|
||||||
"nxmesh-proto",
|
"nxmesh-proto",
|
||||||
"rand 0.10.0",
|
"rand 0.10.0",
|
||||||
"rcgen",
|
"rcgen",
|
||||||
"rust-embed",
|
|
||||||
"sea-orm",
|
"sea-orm",
|
||||||
"sea-orm-migration",
|
"sea-orm-migration",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -3069,16 +2945,6 @@ dependencies = [
|
|||||||
"termtree",
|
"termtree",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pretty_assertions"
|
|
||||||
version = "1.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
|
|
||||||
dependencies = [
|
|
||||||
"diff",
|
|
||||||
"yansi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prettyplease"
|
name = "prettyplease"
|
||||||
version = "0.2.37"
|
version = "0.2.37"
|
||||||
@@ -3408,15 +3274,6 @@ dependencies = [
|
|||||||
"bytecheck",
|
"bytecheck",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "reserve-port"
|
|
||||||
version = "2.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "94070964579245eb2f76e62a7668fe87bd9969ed6c41256f3bf614e3323dd3cc"
|
|
||||||
dependencies = [
|
|
||||||
"thiserror",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.14"
|
version = "0.17.14"
|
||||||
@@ -3494,40 +3351,6 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rust-embed"
|
|
||||||
version = "8.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27"
|
|
||||||
dependencies = [
|
|
||||||
"rust-embed-impl",
|
|
||||||
"rust-embed-utils",
|
|
||||||
"walkdir",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rust-embed-impl"
|
|
||||||
version = "8.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"rust-embed-utils",
|
|
||||||
"syn 2.0.117",
|
|
||||||
"walkdir",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rust-embed-utils"
|
|
||||||
version = "8.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1"
|
|
||||||
dependencies = [
|
|
||||||
"sha2",
|
|
||||||
"walkdir",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-ini"
|
name = "rust-ini"
|
||||||
version = "0.21.3"
|
version = "0.21.3"
|
||||||
@@ -3538,21 +3361,6 @@ dependencies = [
|
|||||||
"ordered-multimap",
|
"ordered-multimap",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rust-multipart-rfc7578_2"
|
|
||||||
version = "0.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "00bdaa068902270ca7fa8619775e1838e23a63620abac0947ce0f715819b8cec"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"futures-core",
|
|
||||||
"futures-util",
|
|
||||||
"http",
|
|
||||||
"mime",
|
|
||||||
"rand 0.10.0",
|
|
||||||
"thiserror",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust_decimal"
|
name = "rust_decimal"
|
||||||
version = "1.40.0"
|
version = "1.40.0"
|
||||||
@@ -3659,15 +3467,6 @@ version = "1.0.23"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "same-file"
|
|
||||||
version = "1.0.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
|
||||||
dependencies = [
|
|
||||||
"winapi-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schannel"
|
name = "schannel"
|
||||||
version = "0.1.28"
|
version = "0.1.28"
|
||||||
@@ -4913,30 +4712,6 @@ version = "1.19.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "typetag"
|
|
||||||
version = "0.2.21"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "be2212c8a9b9bcfca32024de14998494cf9a5dfa59ea1b829de98bac374b86bf"
|
|
||||||
dependencies = [
|
|
||||||
"erased-serde",
|
|
||||||
"inventory",
|
|
||||||
"once_cell",
|
|
||||||
"serde",
|
|
||||||
"typetag-impl",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "typetag-impl"
|
|
||||||
version = "0.2.21"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "27a7a9b72ba121f6f1f6c3632b85604cac41aedb5ddc70accbebb6cac83de846"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.117",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ucd-trie"
|
name = "ucd-trie"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
@@ -5114,16 +4889,6 @@ version = "0.9.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "walkdir"
|
|
||||||
version = "2.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
|
||||||
dependencies = [
|
|
||||||
"same-file",
|
|
||||||
"winapi-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "want"
|
name = "want"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -5294,15 +5059,6 @@ dependencies = [
|
|||||||
"wasite",
|
"wasite",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winapi-util"
|
|
||||||
version = "0.1.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
|
||||||
dependencies = [
|
|
||||||
"windows-sys 0.61.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-core"
|
name = "windows-core"
|
||||||
version = "0.62.2"
|
version = "0.62.2"
|
||||||
@@ -5380,6 +5136,15 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.59.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.60.2"
|
version = "0.60.2"
|
||||||
|
|||||||
@@ -56,9 +56,6 @@ futures = "0.3"
|
|||||||
toml = "0.9"
|
toml = "0.9"
|
||||||
config = "0.15"
|
config = "0.15"
|
||||||
|
|
||||||
# HTTP client
|
|
||||||
reqwest = { version = "0.13.2", default-features = false, features = ["json"] }
|
|
||||||
|
|
||||||
# Crypto
|
# Crypto
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ zip = { workspace = true }
|
|||||||
|
|
||||||
# CLI
|
# CLI
|
||||||
clap = { workspace = true, features = ["derive"] }
|
clap = { workspace = true, features = ["derive"] }
|
||||||
|
anyhow = "1.0.102"
|
||||||
|
fs4 = { version = "0.13.1", features = ["tokio"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio-test.workspace = true
|
tokio-test.workspace = true
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use crate::connector::master::{MasterConnector, MasterConnectorTrait, ssh::SshMa
|
|||||||
mod cli;
|
mod cli;
|
||||||
mod config;
|
mod config;
|
||||||
mod connector;
|
mod connector;
|
||||||
|
mod service;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
@@ -60,13 +61,13 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// send a dummy heartbeat to verify the connection is working
|
// send a dummy heartbeat to verify the connection is working
|
||||||
let client = master_connector.get_client();
|
let mut client = master_connector.get_client().lock().await.clone();
|
||||||
|
|
||||||
let request = nxmesh_proto::HealthReport {
|
let request = nxmesh_proto::HealthReport {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
match client.lock().await.report_health(request).await {
|
match client.report_health(request).await {
|
||||||
Ok(_) => info!("Successfully sent health report to master."),
|
Ok(_) => info!("Successfully sent health report to master."),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to send health report to master: {}", e);
|
error!("Failed to send health report to master: {}", e);
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use nxmesh_proto::ConfigUpdate;
|
|
||||||
use tracing::info;
|
|
||||||
|
|
||||||
use crate::connector::master::MasterConnector;
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
pub trait MasterHandler {
|
|
||||||
async fn on_config_update(
|
|
||||||
&self,
|
|
||||||
config_info: ConfigUpdate,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct MasterHandlerImpl {
|
|
||||||
settings: Arc<crate::config::settings::Settings>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MasterHandlerImpl {
|
|
||||||
pub fn new(settings: impl Into<Arc<crate::config::settings::Settings>>) -> Self {
|
|
||||||
Self {
|
|
||||||
settings: settings.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl MasterHandler for MasterHandlerImpl {
|
|
||||||
async fn on_config_update(
|
|
||||||
&self,
|
|
||||||
config_info: ConfigUpdate,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
||||||
info!("Received config update from master: {:?}", config_info);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1
apps/nxmesh-agent/src/service/mod.rs
Normal file
1
apps/nxmesh-agent/src/service/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod nginx_handler;
|
||||||
308
apps/nxmesh-agent/src/service/nginx_handler.rs
Normal file
308
apps/nxmesh-agent/src/service/nginx_handler.rs
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use fs4::tokio::AsyncFileExt;
|
||||||
|
use tokio::{io::AsyncWriteExt, process::Command};
|
||||||
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
|
use crate::config::settings::NginxSettings;
|
||||||
|
|
||||||
|
// TODO: custom error type
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait NginxHandler {
|
||||||
|
// Reload nginx to apply new config. The config_path is an optional parameter that specifies the path to the nginx config file to be used for this reload operation. If not provided, the default config path will be used.
|
||||||
|
async fn reload(&self, config_path: Option<&str>) -> Result<()>;
|
||||||
|
async fn stop(&self) -> Result<()>;
|
||||||
|
async fn validate(&self, config_path: Option<&str>) -> Result<()>;
|
||||||
|
async fn get_version(&self) -> Result<String>;
|
||||||
|
async fn get_status(&self) -> Result<String>;
|
||||||
|
// Write a new config file for nginx.
|
||||||
|
// The output_path is a relative path to the nginx config directory of the deployment folder. The actual path to the config should not be assumed by the caller, as it can be different in different environments, but will be promised to be relative to the deployment folder for each the corresponding deployment_id. Path traversal is not allowed.
|
||||||
|
async fn write_config(
|
||||||
|
&self,
|
||||||
|
deployment_id: &str,
|
||||||
|
config_content: &str,
|
||||||
|
output_path: &str,
|
||||||
|
) -> Result<()>;
|
||||||
|
// Append a new config content to an existing config file for nginx. This is useful for some use cases where we want to keep the existing config and just add some new config content to it. The output_path is a relative path to the nginx config directory of the deployment folder, which should be the same as the one used in write_config function. Path traversal is not allowed.
|
||||||
|
async fn append_config(
|
||||||
|
&self,
|
||||||
|
deployment_id: &str,
|
||||||
|
config_content: &str,
|
||||||
|
output_path: &str,
|
||||||
|
) -> Result<()>;
|
||||||
|
|
||||||
|
// clean up old config files that are applied to nginx
|
||||||
|
// keep only latest n deployments.
|
||||||
|
async fn cleanup_config(&self, n: usize) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NginxHandlerImpl {
|
||||||
|
settings: Arc<NginxSettings>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NginxHandlerImpl {
|
||||||
|
pub fn new(settings: Arc<NginxSettings>) -> Self {
|
||||||
|
Self { settings }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_nginx_command(&self) -> String {
|
||||||
|
// TODO: rename the setting for better clarity, it can be a binary path or a custom command
|
||||||
|
self.settings
|
||||||
|
.nginx_binary_path
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| "nginx".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_config_path(config_path: &str) -> Result<()> {
|
||||||
|
if !std::path::Path::new(config_path).exists() {
|
||||||
|
anyhow::bail!("Config file not found at path: {}", config_path);
|
||||||
|
}
|
||||||
|
if !std::path::Path::new(config_path).is_file() {
|
||||||
|
anyhow::bail!("Config path is not a file: {}", config_path);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_config_path_to_command_vecs<'a>(
|
||||||
|
command: &'a mut Vec<String>,
|
||||||
|
config_path: &str,
|
||||||
|
) -> Result<&'a mut Vec<String>> {
|
||||||
|
// if given a config path, add it to the end of the command arguments to override the default config path used
|
||||||
|
Self::validate_config_path(config_path)?;
|
||||||
|
let parent_dir = match std::path::Path::new(config_path).parent() {
|
||||||
|
Some(dir) => dir,
|
||||||
|
// return root
|
||||||
|
None => std::path::Path::new("/"),
|
||||||
|
};
|
||||||
|
// set prefix path to the parent directory of the config file to ensure nginx can find all related files (e.g. certs, conf.d, etc.)
|
||||||
|
command.push("-p".to_string());
|
||||||
|
command.push(parent_dir.to_string_lossy().to_string());
|
||||||
|
// add the config file path to the command arguments to override the default config path used by nginx
|
||||||
|
command.push("-c".to_string());
|
||||||
|
command.push(config_path.to_string());
|
||||||
|
Ok(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_deployment_dir(&self) -> std::path::PathBuf {
|
||||||
|
std::path::Path::new(&self.settings.nginx_config_path).join("deployments")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_deployment_dir_path(&self, deployment_id: &str) -> std::path::PathBuf {
|
||||||
|
self.get_deployment_dir().join(deployment_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_deployment_config_path(
|
||||||
|
&self,
|
||||||
|
deployment_id: &str,
|
||||||
|
output_path: &str,
|
||||||
|
create_dir_if_not_exists: bool,
|
||||||
|
) -> Result<std::path::PathBuf> {
|
||||||
|
let output_path_obj = std::path::Path::new(output_path);
|
||||||
|
if output_path_obj.is_absolute() {
|
||||||
|
anyhow::bail!("Output path must be a relative path");
|
||||||
|
}
|
||||||
|
if output_path_obj
|
||||||
|
.components()
|
||||||
|
.any(|comp| comp == std::path::Component::ParentDir)
|
||||||
|
{
|
||||||
|
anyhow::bail!("Output path must not contain parent directory traversal");
|
||||||
|
}
|
||||||
|
|
||||||
|
let deployment_config_dir = self.get_deployment_dir_path(deployment_id);
|
||||||
|
if create_dir_if_not_exists {
|
||||||
|
tokio::fs::create_dir_all(&deployment_config_dir).await?;
|
||||||
|
}
|
||||||
|
Ok(deployment_config_dir.join(output_path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl NginxHandler for NginxHandlerImpl {
|
||||||
|
async fn reload(&self, config_path: Option<&str>) -> Result<()> {
|
||||||
|
// TODO: add timeout for the command execution
|
||||||
|
let reload_command_str = self.settings.override_nginx_reload_command.clone();
|
||||||
|
let program = match reload_command_str.first() {
|
||||||
|
Some(cmd) => cmd,
|
||||||
|
None => &self.get_nginx_command(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut reload_command_vec = reload_command_str[1..].to_vec();
|
||||||
|
// if given a config path, add it to the end of the command arguments to override the default config path used
|
||||||
|
if let Some(path) = config_path {
|
||||||
|
Self::apply_config_path_to_command_vecs(&mut reload_command_vec, path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = Command::new(program)
|
||||||
|
.args(&reload_command_vec)
|
||||||
|
.output()
|
||||||
|
.await?;
|
||||||
|
if !output.status.success() {
|
||||||
|
let error_info = String::from_utf8_lossy(&output.stderr);
|
||||||
|
anyhow::bail!("Failed to reload nginx: {}", error_info.trim());
|
||||||
|
}
|
||||||
|
let success_info = String::from_utf8_lossy(&output.stdout);
|
||||||
|
debug!("Nginx reloaded successfully: {}", success_info.trim());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn stop(&self) -> Result<()> {
|
||||||
|
let output = Command::new(self.get_nginx_command())
|
||||||
|
.arg("-s")
|
||||||
|
.arg("stop")
|
||||||
|
.output()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
let error_info = String::from_utf8_lossy(&output.stderr);
|
||||||
|
anyhow::bail!("Failed to stop nginx: {}", error_info.trim());
|
||||||
|
}
|
||||||
|
let success_info = String::from_utf8_lossy(&output.stdout);
|
||||||
|
debug!("Nginx stopped successfully: {}", success_info.trim());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn validate(&self, config_path: Option<&str>) -> Result<()> {
|
||||||
|
// TODO: add timeout for the command execution
|
||||||
|
let validate_command_str = self.settings.override_nginx_test_command.clone();
|
||||||
|
let program = match validate_command_str.first() {
|
||||||
|
Some(cmd) => cmd,
|
||||||
|
None => &self.get_nginx_command(),
|
||||||
|
};
|
||||||
|
let mut validate_args = validate_command_str[1..].to_vec();
|
||||||
|
// if given a config path, add it to the end of the command arguments to override the default config path used
|
||||||
|
if let Some(path) = config_path {
|
||||||
|
Self::apply_config_path_to_command_vecs(&mut validate_args, path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = Command::new(program).args(&validate_args).output().await?;
|
||||||
|
if !output.status.success() {
|
||||||
|
let error_info = String::from_utf8_lossy(&output.stderr);
|
||||||
|
anyhow::bail!("Nginx config validation failed: {}", error_info.trim());
|
||||||
|
}
|
||||||
|
let success_info = String::from_utf8_lossy(&output.stdout);
|
||||||
|
debug!("Nginx config validation succeeded: {}", success_info.trim());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_version(&self) -> Result<String> {
|
||||||
|
let output = Command::new(self.get_nginx_command())
|
||||||
|
.arg("-v")
|
||||||
|
.output()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
let error_info = String::from_utf8_lossy(&output.stderr);
|
||||||
|
anyhow::bail!("Failed to get nginx version: {}", error_info.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
let version_info = String::from_utf8_lossy(&output.stderr);
|
||||||
|
Ok(version_info.trim().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_status(&self) -> Result<String> {
|
||||||
|
let output = Command::new(self.get_nginx_command())
|
||||||
|
.arg("-t")
|
||||||
|
.output()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
let error_info = String::from_utf8_lossy(&output.stderr);
|
||||||
|
anyhow::bail!("Failed to get nginx status: {}", error_info.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
let status_info = String::from_utf8_lossy(&output.stderr);
|
||||||
|
Ok(status_info.trim().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write_config(
|
||||||
|
&self,
|
||||||
|
deployment_id: &str,
|
||||||
|
config_content: &str,
|
||||||
|
output_path: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
let full_output_path = self
|
||||||
|
.get_deployment_config_path(deployment_id, output_path, true)
|
||||||
|
.await?;
|
||||||
|
let mut file = tokio::fs::OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.truncate(true)
|
||||||
|
.open(full_output_path)
|
||||||
|
.await?;
|
||||||
|
// lock the file for writing to prevent concurrent write issue
|
||||||
|
file.allocate(config_content.len() as u64).await?;
|
||||||
|
file.lock_exclusive()?;
|
||||||
|
file.write_all(config_content.as_bytes()).await?;
|
||||||
|
file.unlock()?;
|
||||||
|
file.flush().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn append_config(
|
||||||
|
&self,
|
||||||
|
deployment_id: &str,
|
||||||
|
config_content: &str,
|
||||||
|
output_path: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
let full_output_path = self
|
||||||
|
.get_deployment_config_path(deployment_id, output_path, true)
|
||||||
|
.await?;
|
||||||
|
let mut file = tokio::fs::OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(full_output_path)
|
||||||
|
.await?;
|
||||||
|
// lock the file for writing to prevent concurrent write issue
|
||||||
|
file.allocate(file.metadata().await?.len() + config_content.len() as u64)
|
||||||
|
.await?;
|
||||||
|
file.lock_exclusive()?;
|
||||||
|
file.write_all(config_content.as_bytes()).await?;
|
||||||
|
file.unlock()?;
|
||||||
|
file.flush().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cleanup_config(&self, n: usize) -> Result<()> {
|
||||||
|
let deployment_dir = self.get_deployment_dir();
|
||||||
|
// loop through all files in the deployment dir and delete them
|
||||||
|
let mut entries = tokio::fs::read_dir(&deployment_dir).await?;
|
||||||
|
let mut deployment_ids = Vec::new();
|
||||||
|
while let Some(entry) = entries.next_entry().await? {
|
||||||
|
let file_type = entry.file_type().await?;
|
||||||
|
if file_type.is_dir()
|
||||||
|
&& let Some(deployment_id) = entry.file_name().to_str()
|
||||||
|
{
|
||||||
|
deployment_ids.push(deployment_id.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// sort the deployment ids by modified time in descending order and keep the latest n deployments, delete the rest
|
||||||
|
deployment_ids.sort_by_key(|id| {
|
||||||
|
let path = self.get_deployment_dir_path(id);
|
||||||
|
std::fs::metadata(path)
|
||||||
|
.and_then(|meta| meta.modified())
|
||||||
|
.unwrap_or(std::time::SystemTime::UNIX_EPOCH)
|
||||||
|
});
|
||||||
|
for deployment_id in deployment_ids.into_iter().skip(n) {
|
||||||
|
let path = self.get_deployment_dir_path(&deployment_id);
|
||||||
|
// ensure path is within the deplyment and nginx directory to prevent accidental deletion of other files
|
||||||
|
if !path.starts_with(&deployment_dir)
|
||||||
|
|| !path.starts_with(&self.settings.nginx_config_path)
|
||||||
|
{
|
||||||
|
warn!(
|
||||||
|
"Skipping deletion of path outside of deployment or nginx config directory: {:?}",
|
||||||
|
path
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
tokio::fs::remove_dir_all(path).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
2
apps/nxmesh-frontend/.gitignore
vendored
2
apps/nxmesh-frontend/.gitignore
vendored
@@ -22,5 +22,3 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
.react-router/
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,24 +4,17 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "react-router build",
|
"dev": "vite",
|
||||||
"dev": "react-router dev",
|
"build": "tsc -b && vite build",
|
||||||
"start": "react-router-serve ./build/server/index.js",
|
"lint": "eslint .",
|
||||||
"typecheck": "react-router typegen && tsc"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^6.2.2",
|
"react": "^19.2.0",
|
||||||
"@react-router/node": "7.15.0",
|
"react-dom": "^19.2.0"
|
||||||
"@react-router/serve": "7.15.0",
|
|
||||||
"antd": "^6.3.7",
|
|
||||||
"isbot": "^5.1.36",
|
|
||||||
"react": "^19.2.5",
|
|
||||||
"react-dom": "^19.2.5",
|
|
||||||
"react-router": "7.15.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
"@react-router/dev": "7.15.0",
|
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
@@ -32,6 +25,6 @@
|
|||||||
"globals": "^16.5.0",
|
"globals": "^16.5.0",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.48.0",
|
"typescript-eslint": "^8.48.0",
|
||||||
"vite": "^8.0.3"
|
"vite": "^7.3.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import type { Config } from '@react-router/dev/config';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
// Config options...
|
|
||||||
// Server-side render by default, to enable SPA mode set this to `false`
|
|
||||||
ssr: false,
|
|
||||||
appDirectory: 'src',
|
|
||||||
} satisfies Config;
|
|
||||||
@@ -1,156 +1,42 @@
|
|||||||
/* Light Professional Theme - nxMesh Dashboard */
|
|
||||||
|
|
||||||
* {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
width: 100%;
|
max-width: 1280px;
|
||||||
height: 100vh;
|
margin: 0 auto;
|
||||||
margin: 0;
|
padding: 2rem;
|
||||||
padding: 0;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Header Styles */
|
.logo {
|
||||||
.ant-layout-header {
|
height: 6em;
|
||||||
background: #ffffff !important;
|
padding: 1.5em;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
will-change: filter;
|
||||||
|
transition: filter 300ms;
|
||||||
|
}
|
||||||
|
.logo:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #646cffaa);
|
||||||
|
}
|
||||||
|
.logo.react:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sidebar Styles */
|
@keyframes logo-spin {
|
||||||
.ant-layout-sider {
|
from {
|
||||||
background: #fafafa !important;
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-menu {
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
background: #fafafa !important;
|
a:nth-of-type(2) .logo {
|
||||||
|
animation: logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-menu-item,
|
.card {
|
||||||
.ant-menu-submenu-title {
|
padding: 2em;
|
||||||
color: #333 !important;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-menu-item-selected {
|
.read-the-docs {
|
||||||
background-color: #e6f7ff !important;
|
color: #888;
|
||||||
color: #1890ff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-menu-item:hover,
|
|
||||||
.ant-menu-submenu-title:hover {
|
|
||||||
color: #1890ff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Content Area */
|
|
||||||
.ant-layout-content {
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Card Styles */
|
|
||||||
.ant-card {
|
|
||||||
border-color: #f0f0f0;
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-card:hover {
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-card-head {
|
|
||||||
border-bottom: 1px solid #f0f0f0;
|
|
||||||
background: #fafafa;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Statistic Styles */
|
|
||||||
.ant-statistic-title {
|
|
||||||
color: #666;
|
|
||||||
font-size: 14px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-statistic-content {
|
|
||||||
color: #1890ff;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Button Styles */
|
|
||||||
.ant-btn-primary {
|
|
||||||
background-color: #1890ff;
|
|
||||||
border-color: #1890ff;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-btn-primary:hover {
|
|
||||||
background-color: #0050b3;
|
|
||||||
border-color: #0050b3;
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Input Styles */
|
|
||||||
.ant-input {
|
|
||||||
border-radius: 4px;
|
|
||||||
border-color: #d9d9d9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-input:hover {
|
|
||||||
border-color: #1890ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-input-focused {
|
|
||||||
border-color: #1890ff;
|
|
||||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Badge Styles */
|
|
||||||
.ant-badge-success {
|
|
||||||
color: #52c41a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-badge-processing {
|
|
||||||
color: #1890ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Typography */
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5,
|
|
||||||
h6 {
|
|
||||||
color: #333;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
color: #666;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Design */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.ant-layout-sider {
|
|
||||||
width: 100% !important;
|
|
||||||
max-width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-layout-content {
|
|
||||||
padding: 16px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-card {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
35
apps/nxmesh-frontend/src/App.tsx
Normal file
35
apps/nxmesh-frontend/src/App.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import reactLogo from './assets/react.svg'
|
||||||
|
import viteLogo from '/vite.svg'
|
||||||
|
import './App.css'
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [count, setCount] = useState(0)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<a href="https://vite.dev" target="_blank">
|
||||||
|
<img src={viteLogo} className="logo" alt="Vite logo" />
|
||||||
|
</a>
|
||||||
|
<a href="https://react.dev" target="_blank">
|
||||||
|
<img src={reactLogo} className="logo react" alt="React logo" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<h1>Vite + React</h1>
|
||||||
|
<div className="card">
|
||||||
|
<button onClick={() => setCount((count) => count + 1)}>
|
||||||
|
count is {count}
|
||||||
|
</button>
|
||||||
|
<p>
|
||||||
|
Edit <code>src/App.tsx</code> and save to test HMR
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p className="read-the-docs">
|
||||||
|
Click on the Vite and React logos to learn more
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
1
apps/nxmesh-frontend/src/assets/react.svg
Normal file
1
apps/nxmesh-frontend/src/assets/react.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -1,36 +0,0 @@
|
|||||||
import type { PropsWithChildren } from 'react';
|
|
||||||
import { Layout, ConfigProvider, theme } from 'antd';
|
|
||||||
import { LayoutHeader } from './layout/Header';
|
|
||||||
import { LayoutSidebar } from './layout/Sidebar';
|
|
||||||
import { LayoutFooter } from './layout/Footer';
|
|
||||||
const { Content } = Layout;
|
|
||||||
|
|
||||||
export const RootLayout = ({ children }: PropsWithChildren) => {
|
|
||||||
return (
|
|
||||||
<ConfigProvider
|
|
||||||
theme={{
|
|
||||||
token: {
|
|
||||||
colorPrimary: '#1890ff',
|
|
||||||
borderRadius: 6,
|
|
||||||
},
|
|
||||||
algorithm: theme.defaultAlgorithm,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Layout style={{ minHeight: '100vh' }}>
|
|
||||||
{/* Header */}
|
|
||||||
|
|
||||||
<LayoutHeader />
|
|
||||||
|
|
||||||
<Layout>
|
|
||||||
{/* Sidebar */}
|
|
||||||
<LayoutSidebar />
|
|
||||||
|
|
||||||
{/* Main Content */}
|
|
||||||
<Content style={{ padding: '24px' }}>{children}</Content>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<LayoutFooter />
|
|
||||||
</Layout>
|
|
||||||
</ConfigProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { Layout } from 'antd';
|
|
||||||
const { Footer } = Layout;
|
|
||||||
|
|
||||||
export const LayoutFooter = () => {
|
|
||||||
return (
|
|
||||||
<Footer
|
|
||||||
style={{
|
|
||||||
textAlign: 'center',
|
|
||||||
color: '#666',
|
|
||||||
borderTop: '1px solid #f0f0f0',
|
|
||||||
background: '#fafafa',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
nxMesh © 2026 All Rights Reserved
|
|
||||||
</Footer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import { Space, Avatar, Input, Button, Badge, Dropdown, Layout, type DropdownProps } from 'antd';
|
|
||||||
import { UserOutlined, SearchOutlined, BellOutlined, LogoutOutlined } from '@ant-design/icons';
|
|
||||||
const { Header } = Layout;
|
|
||||||
|
|
||||||
const userMenuItems: Exclude<DropdownProps['menu'], undefined>['items'] = [
|
|
||||||
{ key: 'profile', label: 'Profile' },
|
|
||||||
{ key: 'settings', label: 'Settings' },
|
|
||||||
{ type: 'divider' },
|
|
||||||
{ key: 'logout', label: 'Logout', icon: <LogoutOutlined /> },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const LayoutHeader = () => {
|
|
||||||
return (
|
|
||||||
<Header
|
|
||||||
style={{
|
|
||||||
background: '#fff',
|
|
||||||
padding: '0 24px',
|
|
||||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.06)',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
position: 'sticky',
|
|
||||||
top: 0,
|
|
||||||
zIndex: 999,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
|
|
||||||
<h1 style={{ margin: 0, fontSize: '20px', fontWeight: 'bold', color: '#1890ff' }}>nxMesh</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Input placeholder="Search agents, workspaces..." prefix={<SearchOutlined />} style={{ width: '300px' }} />
|
|
||||||
|
|
||||||
<Space size="large">
|
|
||||||
<Badge count={3} style={{ backgroundColor: '#ff4d4f' }}>
|
|
||||||
<Button type="text" icon={<BellOutlined style={{ fontSize: '18px' }} />} />
|
|
||||||
</Badge>
|
|
||||||
<Dropdown menu={{ items: userMenuItems }} trigger={['click']}>
|
|
||||||
<Space style={{ cursor: 'pointer' }}>
|
|
||||||
<Avatar icon={<UserOutlined />} style={{ backgroundColor: '#1890ff' }} />
|
|
||||||
{/* TODO: from store */}
|
|
||||||
<span>Admin User</span>
|
|
||||||
</Space>
|
|
||||||
</Dropdown>
|
|
||||||
</Space>
|
|
||||||
</Header>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import { Button, Menu, Layout } from 'antd';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { DashboardOutlined, AppstoreOutlined, TeamOutlined, SettingOutlined } from '@ant-design/icons';
|
|
||||||
|
|
||||||
const { Sider } = Layout;
|
|
||||||
|
|
||||||
const menuItems = [
|
|
||||||
{ key: '1', icon: <DashboardOutlined />, label: 'Dashboard' },
|
|
||||||
{ key: '2', icon: <AppstoreOutlined />, label: 'Agents' },
|
|
||||||
{ key: '3', icon: <AppstoreOutlined />, label: 'Workspaces' },
|
|
||||||
{ key: '4', icon: <TeamOutlined />, label: 'Organizations' },
|
|
||||||
{
|
|
||||||
key: '5',
|
|
||||||
icon: <SettingOutlined />,
|
|
||||||
label: 'Infrastructure',
|
|
||||||
children: [
|
|
||||||
{ key: '5-1', label: 'Virtual Hosts' },
|
|
||||||
{ key: '5-2', label: 'Upstreams' },
|
|
||||||
{ key: '5-3', label: 'Certificates' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{ key: '6', icon: <TeamOutlined />, label: 'Users & Access' },
|
|
||||||
{ key: '7', icon: <SettingOutlined />, label: 'Settings' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const LayoutSidebar = () => {
|
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
|
||||||
const [selectedKey, setSelectedKey] = useState('1');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Sider
|
|
||||||
width={250}
|
|
||||||
trigger={null}
|
|
||||||
collapsible
|
|
||||||
collapsed={collapsed}
|
|
||||||
style={{
|
|
||||||
background: '#fafafa',
|
|
||||||
borderRight: '1px solid #f0f0f0',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
onClick={() => setCollapsed(!collapsed)}
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
height: '64px',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{collapsed ? '☰' : '☰'}
|
|
||||||
</Button>
|
|
||||||
<Menu mode="inline" selectedKeys={[selectedKey]} onSelect={(e) => setSelectedKey(e.key)} items={menuItems} style={{ border: 'none' }} />
|
|
||||||
</Sider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
68
apps/nxmesh-frontend/src/index.css
Normal file
68
apps/nxmesh-frontend/src/index.css
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
:root {
|
||||||
|
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #242424;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #646cff;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #535bf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3.2em;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.6em 1.2em;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.25s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
border-color: #646cff;
|
||||||
|
}
|
||||||
|
button:focus,
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
color: #213547;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #747bff;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
apps/nxmesh-frontend/src/main.tsx
Normal file
10
apps/nxmesh-frontend/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import './index.css'
|
||||||
|
import App from './App.tsx'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import './App.css';
|
|
||||||
import { isRouteErrorResponse, Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router';
|
|
||||||
import type { Route } from './+types/root';
|
|
||||||
import { RootLayout } from './components/Layout.tsx';
|
|
||||||
|
|
||||||
export function Layout({ children }: { children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charSet="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<Meta />
|
|
||||||
<Links />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
{children}
|
|
||||||
<ScrollRestoration />
|
|
||||||
<Scripts />
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function App() {
|
|
||||||
return (
|
|
||||||
<RootLayout>
|
|
||||||
<Outlet />
|
|
||||||
</RootLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
|
||||||
let message = 'Oops!';
|
|
||||||
let details = 'An unexpected error occurred.';
|
|
||||||
let stack: string | undefined;
|
|
||||||
|
|
||||||
if (isRouteErrorResponse(error)) {
|
|
||||||
message = error.status === 404 ? '404' : 'Error';
|
|
||||||
details = error.status === 404 ? 'The requested page could not be found.' : error.statusText || details;
|
|
||||||
} else if (import.meta.env.DEV && error && error instanceof Error) {
|
|
||||||
details = error.message;
|
|
||||||
stack = error.stack;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className="pt-16 p-4 container mx-auto">
|
|
||||||
<h1>{message}</h1>
|
|
||||||
<p>{details}</p>
|
|
||||||
{stack && (
|
|
||||||
<pre className="w-full p-4 overflow-x-auto">
|
|
||||||
<code>{stack}</code>
|
|
||||||
</pre>
|
|
||||||
)}
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { type RouteConfig, index } from "@react-router/dev/routes";
|
|
||||||
|
|
||||||
export default [index("routes/home.tsx")] satisfies RouteConfig;
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import type { Route } from './+types/home';
|
|
||||||
import { Row, Col, Card, Statistic, Space, Badge } from 'antd';
|
|
||||||
|
|
||||||
export function meta({}: Route.MetaArgs) {
|
|
||||||
return [{ title: 'New React Router App' }, { name: 'description', content: 'Welcome to React Router!' }];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
|
||||||
<div style={{ background: '#fff', padding: '24px', borderRadius: '6px', minHeight: '400px' }}>
|
|
||||||
<h2 style={{ marginBottom: '24px', fontSize: '24px', fontWeight: '600' }}>Dashboard</h2>
|
|
||||||
|
|
||||||
{/* Key Metrics */}
|
|
||||||
<Row gutter={[16, 16]} style={{ marginBottom: '32px' }}>
|
|
||||||
<Col xs={24} sm={12} lg={6}>
|
|
||||||
<Card hoverable>
|
|
||||||
<Statistic title="Active Agents" value={12} prefix="📡" />
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
<Col xs={24} sm={12} lg={6}>
|
|
||||||
<Card hoverable>
|
|
||||||
<Statistic title="Workspaces" value={5} prefix="📁" />
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
<Col xs={24} sm={12} lg={6}>
|
|
||||||
<Card hoverable>
|
|
||||||
<Statistic title="Organizations" value={3} prefix="🏢" />
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
<Col xs={24} sm={12} lg={6}>
|
|
||||||
<Card hoverable>
|
|
||||||
<Statistic title="System Health" value={98} suffix="%" valueStyle={{ color: '#52c41a' }} />
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
{/* Content Sections */}
|
|
||||||
<Row gutter={[16, 16]}>
|
|
||||||
<Col xs={24} lg={12}>
|
|
||||||
<Card title="Recent Activities" hoverable>
|
|
||||||
<Space direction="vertical" style={{ width: '100%' }}>
|
|
||||||
<p>✓ Agent-01 connected successfully</p>
|
|
||||||
<p>✓ Workspace "Production" updated</p>
|
|
||||||
<p>✓ Certificate renewed for api.example.com</p>
|
|
||||||
<p>✓ New user invited to organization</p>
|
|
||||||
</Space>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
<Col xs={24} lg={12}>
|
|
||||||
<Card title="System Status" hoverable>
|
|
||||||
<Space direction="vertical" style={{ width: '100%' }}>
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
||||||
<span>API Server</span>
|
|
||||||
<Badge status="success" text="Online" />
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
||||||
<span>Database</span>
|
|
||||||
<Badge status="success" text="Online" />
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
||||||
<span>Message Queue</span>
|
|
||||||
<Badge status="processing" text="Operational" />
|
|
||||||
</div>
|
|
||||||
</Space>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,7 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite'
|
||||||
import { reactRouter } from '@react-router/dev/vite';
|
import react from '@vitejs/plugin-react-swc'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [reactRouter()],
|
plugins: [react()],
|
||||||
ssr: {
|
})
|
||||||
noExternal: ['@ant-design/icons', '@rc-component/util'],
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
tsconfigPaths: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -84,13 +84,7 @@ time = "0.3"
|
|||||||
|
|
||||||
# Cert handling
|
# Cert handling
|
||||||
zip = { workspace = true }
|
zip = { workspace = true }
|
||||||
rust-embed = { version = "8.11.0", features = [] }
|
|
||||||
mime_guess = "2.0.5"
|
|
||||||
axum-test = "20.0.0"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio-test.workspace = true
|
tokio-test.workspace = true
|
||||||
mockall.workspace = true
|
mockall.workspace = true
|
||||||
|
|
||||||
[features]
|
|
||||||
dev-tools = ["axum/macros"]
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
../nxmesh-frontend/dist/
|
|
||||||
@@ -6,7 +6,6 @@ use nxmesh_proto::{
|
|||||||
};
|
};
|
||||||
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
|
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
|
||||||
use tonic::transport::Server;
|
use tonic::transport::Server;
|
||||||
use tracing::info;
|
|
||||||
|
|
||||||
use crate::{db::entities::public_key_revocations, service::agent::AgentServerService};
|
use crate::{db::entities::public_key_revocations, service::agent::AgentServerService};
|
||||||
|
|
||||||
@@ -70,14 +69,7 @@ impl AgentConnectorTrait for SshAgentConnector {
|
|||||||
.layer(ssh_interceptor)
|
.layer(ssh_interceptor)
|
||||||
.add_service(agent_server_service);
|
.add_service(agent_server_service);
|
||||||
|
|
||||||
info!("SSH Agent gRPC server is listening on {}", addr);
|
router.serve(addr).await?;
|
||||||
router
|
|
||||||
.serve(addr)
|
|
||||||
.await
|
|
||||||
.inspect(|_| info!("SSH Agent gRPC server stopped gracefully."))
|
|
||||||
.inspect_err(|e| {
|
|
||||||
tracing::error!("SSH Agent gRPC server failed: {}", e);
|
|
||||||
})?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ mod cli;
|
|||||||
mod config;
|
mod config;
|
||||||
mod connector;
|
mod connector;
|
||||||
mod db;
|
mod db;
|
||||||
mod routes;
|
|
||||||
mod service;
|
mod service;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
|||||||
@@ -1,160 +0,0 @@
|
|||||||
use axum::{Router, response::IntoResponse};
|
|
||||||
use tracing::error;
|
|
||||||
|
|
||||||
// In development, build the frontend from the source directory, the soft link will handle the path resolution
|
|
||||||
// In deployment, pre-build the frontend and replace the frontend-dist folder with the built assets, the rust-embed will handle the embedding and path resolution
|
|
||||||
#[derive(rust_embed::Embed)]
|
|
||||||
#[folder = "./frontend-dist/"]
|
|
||||||
struct FrontendAssets;
|
|
||||||
|
|
||||||
const INDEX_HTML: &str = "index.html";
|
|
||||||
|
|
||||||
pub async fn get_router() -> Router {
|
|
||||||
Router::new()
|
|
||||||
.route(
|
|
||||||
"/",
|
|
||||||
axum::routing::get(get_fallback_handler)
|
|
||||||
.head(get_fallback_handler)
|
|
||||||
.options(get_fallback_handler),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/{*path}",
|
|
||||||
axum::routing::get(get_file_handler)
|
|
||||||
.head(get_file_handler)
|
|
||||||
.options(get_file_handler),
|
|
||||||
)
|
|
||||||
//
|
|
||||||
.fallback(get_fallback_handler().await)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_fallback_handler() -> Result<axum::response::Html<Vec<u8>>, axum::http::StatusCode>
|
|
||||||
{
|
|
||||||
let index_html = get_index_html();
|
|
||||||
match index_html {
|
|
||||||
Some(html) => Ok(axum::response::Html(html)),
|
|
||||||
None => Err(axum::http::StatusCode::NOT_FOUND),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_index_html() -> Option<Vec<u8>> {
|
|
||||||
FrontendAssets::get(INDEX_HTML).map(|asset| asset.data.as_ref().to_owned())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_file_handler(
|
|
||||||
axum::extract::Path(path): axum::extract::Path<String>,
|
|
||||||
) -> Result<axum::response::Response, axum::http::StatusCode> {
|
|
||||||
let file_path = if path.is_empty() {
|
|
||||||
INDEX_HTML.to_string()
|
|
||||||
} else {
|
|
||||||
path
|
|
||||||
};
|
|
||||||
|
|
||||||
match FrontendAssets::get(&file_path) {
|
|
||||||
Some(asset) => {
|
|
||||||
let content_type = mime_guess::from_path(&file_path).first_or_octet_stream();
|
|
||||||
let response = axum::response::Response::builder()
|
|
||||||
.header(axum::http::header::CONTENT_TYPE, content_type.as_ref())
|
|
||||||
.body(asset.data.into_owned().into())
|
|
||||||
.map_err(|e| {
|
|
||||||
error!("Failed to build response for {}: {}", file_path, e);
|
|
||||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR
|
|
||||||
})?;
|
|
||||||
Ok(response)
|
|
||||||
}
|
|
||||||
// return index.html for any file not found to support client-side routing in the frontend
|
|
||||||
None => get_fallback_handler()
|
|
||||||
.await
|
|
||||||
.map(|html| html.into_response()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[allow(clippy::expect_used)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_asset() {
|
|
||||||
// list all embedded assets for debugging
|
|
||||||
let assets = FrontendAssets::iter().collect::<Vec<_>>();
|
|
||||||
println!("Embedded assets: {:?}", assets);
|
|
||||||
assert!(
|
|
||||||
!assets.is_empty(),
|
|
||||||
"Expected to find embedded assets, but found none"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_get_index_html() {
|
|
||||||
let index_html = get_index_html();
|
|
||||||
assert!(
|
|
||||||
index_html.is_some(),
|
|
||||||
"Expected to find index.html in embedded assets"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_get_file_handler_existing_file() {
|
|
||||||
let response = get_file_handler(axum::extract::Path("index.html".to_string())).await;
|
|
||||||
assert!(
|
|
||||||
response.is_ok(),
|
|
||||||
"Expected to successfully retrieve index.html"
|
|
||||||
);
|
|
||||||
let response = response.expect("Expected response to be Ok");
|
|
||||||
assert_eq!(response.status(), axum::http::StatusCode::OK);
|
|
||||||
assert!(
|
|
||||||
response
|
|
||||||
.headers()
|
|
||||||
.get(axum::http::header::CONTENT_TYPE)
|
|
||||||
.map(|ct| ct.to_str().unwrap_or(""))
|
|
||||||
.expect("Content-Type header should be present")
|
|
||||||
.starts_with("text/html")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_get_file_handler_nonexistent_file() {
|
|
||||||
let response = get_file_handler(axum::extract::Path("nonexistent.txt".to_string())).await;
|
|
||||||
assert!(
|
|
||||||
response.is_ok(),
|
|
||||||
"Expected to fallback to index.html for nonexistent file"
|
|
||||||
);
|
|
||||||
let response = response.expect("Expected response to be Ok");
|
|
||||||
assert_eq!(response.status(), axum::http::StatusCode::OK);
|
|
||||||
assert!(
|
|
||||||
response
|
|
||||||
.headers()
|
|
||||||
.get(axum::http::header::CONTENT_TYPE)
|
|
||||||
.map(|ct| ct.to_str().unwrap_or(""))
|
|
||||||
.expect("Content-Type header should be present")
|
|
||||||
.starts_with("text/html")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod axum_tests {
|
|
||||||
use super::*;
|
|
||||||
use axum_test::TestServer;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_should_return_index_html_for_root_path() {
|
|
||||||
let router = get_router().await;
|
|
||||||
let server = TestServer::new(router);
|
|
||||||
let response = server.get("/").await;
|
|
||||||
assert_eq!(response.status_code(), 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_should_return_index_html_for_nonexistent_path() {
|
|
||||||
let router = get_router().await;
|
|
||||||
let server = TestServer::new(router);
|
|
||||||
let fallback_response = server.get("/nonexistent").await;
|
|
||||||
assert_eq!(fallback_response.status_code(), 200);
|
|
||||||
|
|
||||||
let index_response = server.get("/").await;
|
|
||||||
assert_eq!(index_response.status_code(), 200);
|
|
||||||
|
|
||||||
assert_eq!(fallback_response.text(), index_response.text());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
use axum::Router;
|
|
||||||
|
|
||||||
mod frontend;
|
|
||||||
|
|
||||||
pub async fn get_root_router() -> Router {
|
|
||||||
Router::new()
|
|
||||||
.merge(frontend::get_router().await)
|
|
||||||
.fallback(frontend::get_fallback_handler().await)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use axum_test::TestServer;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_should_return_index_html_for_root_path() {
|
|
||||||
let router = get_root_router().await;
|
|
||||||
let server = TestServer::new(router);
|
|
||||||
let response = server.get("/").await;
|
|
||||||
assert_eq!(response.status_code(), 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_should_return_index_html_for_nonexistent_path() {
|
|
||||||
let router = get_root_router().await;
|
|
||||||
let server = TestServer::new(router);
|
|
||||||
let fallback_response = server.get("/nonexistent").await;
|
|
||||||
assert_eq!(fallback_response.status_code(), 200);
|
|
||||||
|
|
||||||
let index_response = server.get("/").await;
|
|
||||||
assert_eq!(index_response.status_code(), 200);
|
|
||||||
|
|
||||||
assert_eq!(fallback_response.text(), index_response.text());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
use std::{net::ToSocketAddrs, sync::Arc};
|
use std::sync::Arc;
|
||||||
|
|
||||||
use tracing::info;
|
|
||||||
|
|
||||||
use crate::{connector::agent::AgentConnectorTrait, service::certificate::CertificateService};
|
use crate::{connector::agent::AgentConnectorTrait, service::certificate::CertificateService};
|
||||||
|
|
||||||
@@ -28,38 +26,15 @@ pub async fn start_master_server(
|
|||||||
println!("Certificate generated and stored successfully.");
|
println!("Certificate generated and stored successfully.");
|
||||||
}
|
}
|
||||||
|
|
||||||
let ssh_connector = crate::connector::agent::ssh::SshAgentConnector::new(settings.clone())?;
|
// Initialize agent connector
|
||||||
let cert_service_for_agent = cert_service.clone();
|
let mut agent_connector = crate::connector::agent::AgentConnector::new(Box::new(
|
||||||
let settings_for_agent = settings.clone();
|
crate::connector::agent::ssh::SshAgentConnector::new(settings.clone())?,
|
||||||
let connection_for_agent = db_connection.clone();
|
));
|
||||||
|
|
||||||
tokio::spawn(async move {
|
// Start the agent server
|
||||||
let mut connector = ssh_connector;
|
agent_connector
|
||||||
tracing::info!("Starting agent server...");
|
.start_server(&settings, cert_service, db_connection)
|
||||||
if let Err(e) = connector
|
.await?;
|
||||||
.start_server(
|
|
||||||
&settings_for_agent,
|
|
||||||
cert_service_for_agent,
|
|
||||||
connection_for_agent,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
tracing::error!("Agent server failed: {}", e);
|
|
||||||
} else {
|
|
||||||
tracing::info!("Agent server stopped.");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let axum_router = crate::routes::get_root_router().await;
|
|
||||||
|
|
||||||
// Start the HTTP server
|
|
||||||
let addr = format!("{}:{}", settings.server.bind_address, settings.server.port)
|
|
||||||
.to_socket_addrs()?
|
|
||||||
.next()
|
|
||||||
.ok_or("Invalid bind address")?;
|
|
||||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
|
||||||
info!("Web/API server is listening on {}", addr);
|
|
||||||
axum::serve(listener, axum_router).await?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ pub mod agent {
|
|||||||
|
|
||||||
pub use agent::*;
|
pub use agent::*;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
#[allow(ambiguous_glob_reexports)]
|
|
||||||
pub use tonic_async_interceptor::*;
|
pub use tonic_async_interceptor::*;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
12
justfile
12
justfile
@@ -25,6 +25,7 @@ setup-rust-tools:
|
|||||||
cargo install sea-orm-cli@^2.0.0-rc --features "sqlx-postgres runtime-tokio-rustls"
|
cargo install sea-orm-cli@^2.0.0-rc --features "sqlx-postgres runtime-tokio-rustls"
|
||||||
cargo install cargo-watch
|
cargo install cargo-watch
|
||||||
|
|
||||||
|
|
||||||
# Setup frontend dependencies
|
# Setup frontend dependencies
|
||||||
setup-frontend:
|
setup-frontend:
|
||||||
@echo "📦 Installing frontend dependencies..."
|
@echo "📦 Installing frontend dependencies..."
|
||||||
@@ -34,12 +35,6 @@ setup-frontend:
|
|||||||
# Development Commands
|
# Development Commands
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
# act
|
|
||||||
act *ARGS:
|
|
||||||
# run act with custom secret-file
|
|
||||||
@echo "🎬 Running act with custom secrets file..."
|
|
||||||
act --env-file .github/.env --secret-file .github/.secrets.env --var-file .github/.var.env --network host {{ ARGS }}
|
|
||||||
|
|
||||||
# Start all services for development
|
# Start all services for development
|
||||||
dev:
|
dev:
|
||||||
@echo "🚀 Starting all development services..."
|
@echo "🚀 Starting all development services..."
|
||||||
@@ -210,11 +205,6 @@ docker-run:
|
|||||||
@echo "🐳 Running Docker container..."
|
@echo "🐳 Running Docker container..."
|
||||||
docker run -p 8080:8080 --env-file .env nxmesh:latest
|
docker run -p 8080:8080 --env-file .env nxmesh:latest
|
||||||
|
|
||||||
# Build Docker image for CI
|
|
||||||
docker-build-ci REGISTRY="ghcr.io/nxmesh":
|
|
||||||
@echo "🐳 Building Docker image for CI..."
|
|
||||||
docker build -t {{ REGISTRY }}/ci:latest -f ./.github/docker/ci.Dockerfile .
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Nginx Commands (Shared PID Namespace + Docker Fallback)
|
# Nginx Commands (Shared PID Namespace + Docker Fallback)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user