mirror of
https://github.com/hhftechnology/middleware-manager.git
synced 2026-04-28 03:29:42 +00:00
Initial commit
This commit is contained in:
commit
f6e0ce616a
26 changed files with 3945 additions and 0 deletions
14
.env.example
Normal file
14
.env.example
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# API connection
|
||||
PANGOLIN_API_URL=http://pangolin:3001/api/v1
|
||||
|
||||
# Traefik configuration
|
||||
TRAEFIK_CONF_DIR=/conf
|
||||
|
||||
# Database
|
||||
DB_PATH=/data/middleware.db
|
||||
|
||||
# Server
|
||||
PORT=3456
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=info
|
||||
65
.github/workflows/main.yml
vendored
Normal file
65
.github/workflows/main.yml
vendored
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
name: Build and Push to Docker Hub
|
||||
|
||||
# Trigger the workflow on push to main branch or when creating a tag (release)
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch: # Allows manual trigger from GitHub UI
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
name: Build and Push Docker image
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history for proper versioning
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23.x' # Match your go.mod version
|
||||
cache: true
|
||||
|
||||
- name: Verify dependencies
|
||||
run: go mod verify
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ secrets.DOCKERHUB_USERNAME }}/middleware-manager
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=ref,event=branch
|
||||
type=sha,format=short
|
||||
latest
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile # Points to your Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
48
.gitignore
vendored
Normal file
48
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
middleware-manager
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
# IDE files
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Node modules and builds
|
||||
ui/node_modules/
|
||||
ui/build/
|
||||
|
||||
# SQLite database
|
||||
*.db
|
||||
|
||||
# Generated configuration files
|
||||
/conf/
|
||||
|
||||
# Log files
|
||||
*.log
|
||||
|
||||
# Data directories
|
||||
/data/
|
||||
98
Dockerfile
Normal file
98
Dockerfile
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
# Build UI stage
|
||||
FROM node:18-alpine AS ui-builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package manifests first from host's ui/src
|
||||
COPY ui/src/package.json ui/src/package-lock.json* ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install
|
||||
|
||||
# Create the target directories for source and public files within the container
|
||||
RUN mkdir src public
|
||||
|
||||
# Copy contents of host's ui/public into container's /app/public
|
||||
# Assumes host has ui/public/index.html etc.
|
||||
COPY ui/public/ ./public/
|
||||
|
||||
# Copy *specific* source files and directories from host's ui/src into container's /app/src
|
||||
# Add any other top-level .js/.jsx files or necessary subdirectories from ui/src here
|
||||
COPY ui/src/styles ./src/styles
|
||||
COPY ui/src/App.js ./src/App.js
|
||||
COPY ui/src/index.js ./src/index.js
|
||||
# If you have other files like setupTests.js, reportWebVitals.js etc. directly in ui/src, copy them too:
|
||||
# COPY ui/src/setupTests.js ./src/setupTests.js
|
||||
# COPY ui/src/reportWebVitals.js ./src/reportWebVitals.js
|
||||
|
||||
|
||||
# Verify structure (optional)
|
||||
RUN echo "--- Contents of /app/public ---"
|
||||
RUN ls -la public
|
||||
RUN echo "--- Contents of /app/src ---"
|
||||
RUN ls -la src
|
||||
|
||||
# Build the UI (runs in /app, expects ./src, ./public relative to package.json)
|
||||
RUN npm run build
|
||||
|
||||
# Verify build output (optional but good practice)
|
||||
RUN echo "--- Contents of build ---"
|
||||
RUN ls -la build/
|
||||
|
||||
|
||||
# Build Go stage
|
||||
FROM golang:1.19-alpine AS go-builder
|
||||
|
||||
# Install build dependencies for Go
|
||||
RUN apk add --no-cache gcc musl-dev
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy go.mod and go.sum files
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
# Download Go dependencies
|
||||
RUN go mod download
|
||||
|
||||
# Copy the Go source code
|
||||
COPY . .
|
||||
|
||||
# Build the Go application
|
||||
RUN CGO_ENABLED=1 GOOS=linux go build -o middleware-manager .
|
||||
|
||||
# Final stage
|
||||
FROM alpine:3.16
|
||||
|
||||
RUN apk add --no-cache ca-certificates sqlite curl tzdata
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the binary from the builder stage
|
||||
COPY --from=go-builder /app/middleware-manager /app/middleware-manager
|
||||
|
||||
# Copy UI build files from UI builder stage
|
||||
# The build output is in /app/build in the ui-builder stage
|
||||
COPY --from=ui-builder /app/build /app/ui/build
|
||||
|
||||
# Copy configuration files
|
||||
COPY --from=go-builder /app/config/templates.yaml /app/config/templates.yaml
|
||||
|
||||
# Copy database migrations file
|
||||
COPY --from=go-builder /app/database/migrations.sql /app/database/migrations.sql
|
||||
# Also copy to root as fallback
|
||||
COPY --from=go-builder /app/database/migrations.sql /app/migrations.sql
|
||||
|
||||
# Create directories for data
|
||||
RUN mkdir -p /data /conf
|
||||
|
||||
# Set environment variables
|
||||
ENV PANGOLIN_API_URL=http://pangolin:3001/api/v1 \
|
||||
TRAEFIK_CONF_DIR=/conf \
|
||||
DB_PATH=/data/middleware.db \
|
||||
PORT=3456
|
||||
|
||||
# Expose the port
|
||||
EXPOSE 3456
|
||||
|
||||
# Run the application
|
||||
CMD ["/app/middleware-manager"]
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2025 HHF Technology
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
66
Makefile
Normal file
66
Makefile
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
.PHONY: build build-ui build-backend run clean docker-build docker-push test
|
||||
|
||||
# Variables
|
||||
APP_NAME := middleware-manager
|
||||
DOCKER_REPO := hhftechnology
|
||||
DOCKER_TAG := latest
|
||||
GO_FILES := $(shell find . -name "*.go" -not -path "./vendor/*")
|
||||
|
||||
# Default target
|
||||
all: build
|
||||
|
||||
# Build everything
|
||||
build: build-ui build-backend
|
||||
|
||||
# Build UI
|
||||
build-ui:
|
||||
@echo "Building UI..."
|
||||
cd ui && npm install && npm run build
|
||||
|
||||
# Build backend
|
||||
build-backend:
|
||||
@echo "Building backend..."
|
||||
go build -o $(APP_NAME) .
|
||||
|
||||
# Run the application
|
||||
run: build
|
||||
@echo "Running application..."
|
||||
./$(APP_NAME)
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
@echo "Cleaning..."
|
||||
rm -f $(APP_NAME)
|
||||
rm -rf ui/build
|
||||
|
||||
# Build Docker image
|
||||
docker-build: build
|
||||
@echo "Building Docker image..."
|
||||
docker build -t $(DOCKER_REPO)/$(APP_NAME):$(DOCKER_TAG) .
|
||||
|
||||
# Push Docker image
|
||||
docker-push: docker-build
|
||||
@echo "Pushing Docker image..."
|
||||
docker push $(DOCKER_REPO)/$(APP_NAME):$(DOCKER_TAG)
|
||||
|
||||
# Run tests
|
||||
test:
|
||||
@echo "Running tests..."
|
||||
go test -v ./...
|
||||
|
||||
# Run the application in development mode
|
||||
dev:
|
||||
@echo "Running in development mode..."
|
||||
go run main.go
|
||||
|
||||
# Run the UI in development mode
|
||||
dev-ui:
|
||||
@echo "Running UI in development mode..."
|
||||
cd ui && npm start
|
||||
|
||||
# Install dependencies
|
||||
deps:
|
||||
@echo "Installing Go dependencies..."
|
||||
go mod download
|
||||
@echo "Installing UI dependencies..."
|
||||
cd ui && npm install
|
||||
140
README.md
Normal file
140
README.md
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
# Pangolin Middleware Manager
|
||||
|
||||
A microservice that allows you to add custom middleware to Pangolin resources without modifying Pangolin itself.
|
||||
|
||||
## Overview
|
||||
|
||||
Middleware Manager watches for resources created in Pangolin and allows you to attach additional Traefik middlewares such as authentication providers (Authelia, Authentik) to these resources through a simple UI.
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- Automatically synchronizes with Pangolin resources
|
||||
- Add authentication middlewares to specific resources
|
||||
- Support for various middleware types (ForwardAuth, BasicAuth, etc.)
|
||||
- Template library for common middleware configurations
|
||||
- Web UI for easy management
|
||||
- Compatible with Authelia, Authentik, and other Traefik-supported authentication providers
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A running Pangolin setup with Traefik
|
||||
- Docker and Docker Compose
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Clone this repository:
|
||||
```
|
||||
git clone https://github.com/hhftechnology/middleware-manager.git
|
||||
cd middleware-manager
|
||||
```
|
||||
|
||||
2. Configure the environment:
|
||||
```
|
||||
cp .env.example .env
|
||||
# Edit .env with your specific configuration
|
||||
```
|
||||
|
||||
3. Start the service:
|
||||
```
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
4. Access the UI:
|
||||
```
|
||||
http://your-server:3456
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
- `PANGOLIN_API_URL`: URL of your Pangolin API (default: `http://pangolin:3001/api/v1`)
|
||||
- `TRAEFIK_CONF_DIR`: Directory to output Traefik configuration (default: `/conf`)
|
||||
- `DB_PATH`: Path to SQLite database file (default: `/data/middleware.db`)
|
||||
- `PORT`: Port to run the API server on (default: `3456`)
|
||||
|
||||
### Middleware Templates
|
||||
|
||||
Custom middleware templates can be added to `config/templates.yaml`. Several default templates are included:
|
||||
|
||||
- Authelia
|
||||
- Authentik
|
||||
- Basic Auth
|
||||
- JWT Auth
|
||||
- Custom ForwardAuth
|
||||
|
||||
## Usage
|
||||
|
||||
1. Create resources in Pangolin as usual
|
||||
2. In the Middleware Manager UI, select a resource
|
||||
3. Choose a middleware type and configure it
|
||||
4. Save the configuration
|
||||
5. The middleware will be automatically applied to the resource
|
||||
|
||||
## Docker Compose Integration
|
||||
|
||||
Add this service to your existing Pangolin `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
middleware-manager:
|
||||
image: hhftechmology/middleware-manager:latest
|
||||
container_name: middleware-manager
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- pangolin
|
||||
volumes:
|
||||
- ./config/traefik/conf:/conf
|
||||
- ./data/middleware:/data
|
||||
environment:
|
||||
- PANGOLIN_API_URL=http://pangolin:3001/api/v1
|
||||
- TRAEFIK_CONF_DIR=/conf
|
||||
depends_on:
|
||||
- pangolin
|
||||
- traefik
|
||||
ports:
|
||||
- "3456:3456"
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Go 1.19+
|
||||
- Node.js 16+
|
||||
- npm or yarn
|
||||
|
||||
### Backend Development
|
||||
|
||||
```bash
|
||||
# Run backend in development mode
|
||||
go run main.go
|
||||
|
||||
# Build backend
|
||||
go build -o middleware-manager
|
||||
```
|
||||
|
||||
### Frontend Development
|
||||
|
||||
```bash
|
||||
cd ui
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
### Build Docker Image
|
||||
|
||||
```bash
|
||||
make build
|
||||
# or
|
||||
docker build -t middleware-manager .
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
503
api/handlers.go
Normal file
503
api/handlers.go
Normal file
|
|
@ -0,0 +1,503 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// APIError represents a standardized error response
|
||||
type APIError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// ResponseWithError sends a standardized error response
|
||||
func ResponseWithError(c *gin.Context, statusCode int, message string) {
|
||||
c.JSON(statusCode, APIError{
|
||||
Code: statusCode,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// getMiddlewares returns all middleware configurations
|
||||
func (s *Server) getMiddlewares(c *gin.Context) {
|
||||
middlewares, err := s.db.GetMiddlewares()
|
||||
if err != nil {
|
||||
log.Printf("Error fetching middlewares: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Failed to fetch middlewares")
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, middlewares)
|
||||
}
|
||||
|
||||
// createMiddleware creates a new middleware configuration
|
||||
func (s *Server) createMiddleware(c *gin.Context) {
|
||||
var middleware struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Type string `json:"type" binding:"required"`
|
||||
Config map[string]interface{} `json:"config" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&middleware); err != nil {
|
||||
ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Validate middleware type
|
||||
if !isValidMiddlewareType(middleware.Type) {
|
||||
ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid middleware type: %s", middleware.Type))
|
||||
return
|
||||
}
|
||||
|
||||
// Generate a unique ID
|
||||
id, err := generateID()
|
||||
if err != nil {
|
||||
log.Printf("Error generating ID: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Failed to generate ID")
|
||||
return
|
||||
}
|
||||
|
||||
// Convert config to JSON string
|
||||
configJSON, err := json.Marshal(middleware.Config)
|
||||
if err != nil {
|
||||
log.Printf("Error encoding config: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Failed to encode config")
|
||||
return
|
||||
}
|
||||
|
||||
// Insert into database using a transaction
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
log.Printf("Error beginning transaction: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Database error")
|
||||
return
|
||||
}
|
||||
|
||||
// If something goes wrong, rollback
|
||||
defer func() {
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = tx.Exec(
|
||||
"INSERT INTO middlewares (id, name, type, config) VALUES (?, ?, ?, ?)",
|
||||
id, middleware.Name, middleware.Type, string(configJSON),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Error inserting middleware: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Failed to save middleware")
|
||||
return
|
||||
}
|
||||
|
||||
// Commit the transaction
|
||||
if err = tx.Commit(); err != nil {
|
||||
log.Printf("Error committing transaction: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Database error")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"id": id,
|
||||
"name": middleware.Name,
|
||||
"type": middleware.Type,
|
||||
"config": middleware.Config,
|
||||
})
|
||||
}
|
||||
|
||||
// getMiddleware returns a specific middleware configuration
|
||||
func (s *Server) getMiddleware(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
ResponseWithError(c, http.StatusBadRequest, "Middleware ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
middleware, err := s.db.GetMiddleware(id)
|
||||
if err != nil {
|
||||
if err.Error() == fmt.Sprintf("middleware not found: %s", id) {
|
||||
ResponseWithError(c, http.StatusNotFound, "Middleware not found")
|
||||
return
|
||||
}
|
||||
log.Printf("Error fetching middleware: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Failed to fetch middleware")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, middleware)
|
||||
}
|
||||
|
||||
// updateMiddleware updates a middleware configuration
|
||||
func (s *Server) updateMiddleware(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
ResponseWithError(c, http.StatusBadRequest, "Middleware ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
var middleware struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Type string `json:"type" binding:"required"`
|
||||
Config map[string]interface{} `json:"config" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&middleware); err != nil {
|
||||
ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Validate middleware type
|
||||
if !isValidMiddlewareType(middleware.Type) {
|
||||
ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid middleware type: %s", middleware.Type))
|
||||
return
|
||||
}
|
||||
|
||||
// Check if middleware exists
|
||||
var exists int
|
||||
err := s.db.QueryRow("SELECT 1 FROM middlewares WHERE id = ?", id).Scan(&exists)
|
||||
if err == sql.ErrNoRows {
|
||||
ResponseWithError(c, http.StatusNotFound, "Middleware not found")
|
||||
return
|
||||
} else if err != nil {
|
||||
log.Printf("Error checking middleware existence: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Database error")
|
||||
return
|
||||
}
|
||||
|
||||
// Convert config to JSON string
|
||||
configJSON, err := json.Marshal(middleware.Config)
|
||||
if err != nil {
|
||||
log.Printf("Error encoding config: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Failed to encode config")
|
||||
return
|
||||
}
|
||||
|
||||
// Update in database using a transaction
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
log.Printf("Error beginning transaction: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Database error")
|
||||
return
|
||||
}
|
||||
|
||||
// If something goes wrong, rollback
|
||||
defer func() {
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = tx.Exec(
|
||||
"UPDATE middlewares SET name = ?, type = ?, config = ?, updated_at = ? WHERE id = ?",
|
||||
middleware.Name, middleware.Type, string(configJSON), time.Now(), id,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Error updating middleware: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Failed to update middleware")
|
||||
return
|
||||
}
|
||||
|
||||
// Commit the transaction
|
||||
if err = tx.Commit(); err != nil {
|
||||
log.Printf("Error committing transaction: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Database error")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": id,
|
||||
"name": middleware.Name,
|
||||
"type": middleware.Type,
|
||||
"config": middleware.Config,
|
||||
})
|
||||
}
|
||||
|
||||
// deleteMiddleware deletes a middleware configuration
|
||||
func (s *Server) deleteMiddleware(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
ResponseWithError(c, http.StatusBadRequest, "Middleware ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Check for dependencies first
|
||||
var count int
|
||||
err := s.db.QueryRow("SELECT COUNT(*) FROM resource_middlewares WHERE middleware_id = ?", id).Scan(&count)
|
||||
if err != nil {
|
||||
log.Printf("Error checking middleware dependencies: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Database error")
|
||||
return
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
ResponseWithError(c, http.StatusConflict, fmt.Sprintf("Cannot delete middleware because it is used by %d resources", count))
|
||||
return
|
||||
}
|
||||
|
||||
// Delete from database using a transaction
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
log.Printf("Error beginning transaction: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Database error")
|
||||
return
|
||||
}
|
||||
|
||||
// If something goes wrong, rollback
|
||||
defer func() {
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
result, err := tx.Exec("DELETE FROM middlewares WHERE id = ?", id)
|
||||
if err != nil {
|
||||
log.Printf("Error deleting middleware: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Failed to delete middleware")
|
||||
return
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
log.Printf("Error getting rows affected: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Database error")
|
||||
return
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
ResponseWithError(c, http.StatusNotFound, "Middleware not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Commit the transaction
|
||||
if err = tx.Commit(); err != nil {
|
||||
log.Printf("Error committing transaction: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Database error")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Middleware deleted successfully"})
|
||||
}
|
||||
|
||||
// getResources returns all resources and their assigned middlewares
|
||||
func (s *Server) getResources(c *gin.Context) {
|
||||
resources, err := s.db.GetResources()
|
||||
if err != nil {
|
||||
log.Printf("Error fetching resources: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Failed to fetch resources")
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, resources)
|
||||
}
|
||||
|
||||
// getResource returns a specific resource
|
||||
func (s *Server) getResource(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
ResponseWithError(c, http.StatusBadRequest, "Resource ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
resource, err := s.db.GetResource(id)
|
||||
if err != nil {
|
||||
if err.Error() == fmt.Sprintf("resource not found: %s", id) {
|
||||
ResponseWithError(c, http.StatusNotFound, "Resource not found")
|
||||
return
|
||||
}
|
||||
log.Printf("Error fetching resource: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Failed to fetch resource")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, resource)
|
||||
}
|
||||
|
||||
// assignMiddleware assigns a middleware to a resource
|
||||
func (s *Server) assignMiddleware(c *gin.Context) {
|
||||
resourceID := c.Param("id")
|
||||
if resourceID == "" {
|
||||
ResponseWithError(c, http.StatusBadRequest, "Resource ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
var input struct {
|
||||
MiddlewareID string `json:"middleware_id" binding:"required"`
|
||||
Priority int `json:"priority"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Default priority is 100 if not specified
|
||||
if input.Priority <= 0 {
|
||||
input.Priority = 100
|
||||
}
|
||||
|
||||
// Verify resource exists
|
||||
var exists int
|
||||
err := s.db.QueryRow("SELECT 1 FROM resources WHERE id = ?", resourceID).Scan(&exists)
|
||||
if err == sql.ErrNoRows {
|
||||
ResponseWithError(c, http.StatusNotFound, "Resource not found")
|
||||
return
|
||||
} else if err != nil {
|
||||
log.Printf("Error checking resource existence: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Database error")
|
||||
return
|
||||
}
|
||||
|
||||
// Verify middleware exists
|
||||
err = s.db.QueryRow("SELECT 1 FROM middlewares WHERE id = ?", input.MiddlewareID).Scan(&exists)
|
||||
if err == sql.ErrNoRows {
|
||||
ResponseWithError(c, http.StatusNotFound, "Middleware not found")
|
||||
return
|
||||
} else if err != nil {
|
||||
log.Printf("Error checking middleware existence: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Database error")
|
||||
return
|
||||
}
|
||||
|
||||
// Insert or update the resource middleware relationship using a transaction
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
log.Printf("Error beginning transaction: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Database error")
|
||||
return
|
||||
}
|
||||
|
||||
// If something goes wrong, rollback
|
||||
defer func() {
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
// First delete any existing relationship
|
||||
_, err = tx.Exec(
|
||||
"DELETE FROM resource_middlewares WHERE resource_id = ? AND middleware_id = ?",
|
||||
resourceID, input.MiddlewareID,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("Error removing existing relationship: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Database error")
|
||||
return
|
||||
}
|
||||
|
||||
// Then insert the new relationship
|
||||
_, err = tx.Exec(
|
||||
"INSERT INTO resource_middlewares (resource_id, middleware_id, priority) VALUES (?, ?, ?)",
|
||||
resourceID, input.MiddlewareID, input.Priority,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("Error assigning middleware: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Failed to assign middleware")
|
||||
return
|
||||
}
|
||||
|
||||
// Commit the transaction
|
||||
if err = tx.Commit(); err != nil {
|
||||
log.Printf("Error committing transaction: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Database error")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"resource_id": resourceID,
|
||||
"middleware_id": input.MiddlewareID,
|
||||
"priority": input.Priority,
|
||||
})
|
||||
}
|
||||
|
||||
// removeMiddleware removes a middleware from a resource
|
||||
func (s *Server) removeMiddleware(c *gin.Context) {
|
||||
resourceID := c.Param("resourceId")
|
||||
middlewareID := c.Param("middlewareId")
|
||||
|
||||
if resourceID == "" || middlewareID == "" {
|
||||
ResponseWithError(c, http.StatusBadRequest, "Resource ID and Middleware ID are required")
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the relationship using a transaction
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
log.Printf("Error beginning transaction: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Database error")
|
||||
return
|
||||
}
|
||||
|
||||
// If something goes wrong, rollback
|
||||
defer func() {
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
result, err := tx.Exec(
|
||||
"DELETE FROM resource_middlewares WHERE resource_id = ? AND middleware_id = ?",
|
||||
resourceID, middlewareID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Error removing middleware: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Failed to remove middleware")
|
||||
return
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
log.Printf("Error getting rows affected: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Database error")
|
||||
return
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
ResponseWithError(c, http.StatusNotFound, "Resource middleware relationship not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Commit the transaction
|
||||
if err = tx.Commit(); err != nil {
|
||||
log.Printf("Error committing transaction: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Database error")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Middleware removed from resource successfully"})
|
||||
}
|
||||
|
||||
// generateID generates a random 16-character hex string
|
||||
func generateID() (string, error) {
|
||||
bytes := make([]byte, 8)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return "", fmt.Errorf("failed to generate random bytes: %w", err)
|
||||
}
|
||||
return hex.EncodeToString(bytes), nil
|
||||
}
|
||||
|
||||
// isValidMiddlewareType checks if a middleware type is valid
|
||||
func isValidMiddlewareType(typ string) bool {
|
||||
validTypes := map[string]bool{
|
||||
"basicAuth": true,
|
||||
"forwardAuth": true,
|
||||
"ipWhiteList": true,
|
||||
"rateLimit": true,
|
||||
"headers": true,
|
||||
"stripPrefix": true,
|
||||
"addPrefix": true,
|
||||
"redirectRegex": true,
|
||||
"redirectScheme": true,
|
||||
}
|
||||
|
||||
return validTypes[typ]
|
||||
}
|
||||
231
api/routes.go
Normal file
231
api/routes.go
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-contrib/static"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/hhftechnology/middleware-manager/database"
|
||||
)
|
||||
|
||||
// Server represents the API server
|
||||
type Server struct {
|
||||
db *database.DB
|
||||
router *gin.Engine
|
||||
srv *http.Server
|
||||
}
|
||||
|
||||
// ServerConfig contains configuration options for the server
|
||||
type ServerConfig struct {
|
||||
Port string
|
||||
UIPath string
|
||||
Debug bool
|
||||
AllowCORS bool
|
||||
CORSOrigin string
|
||||
}
|
||||
|
||||
// NewServer creates a new API server
|
||||
func NewServer(db *database.DB, config ServerConfig) *Server {
|
||||
// Set gin mode based on debug flag
|
||||
if !config.Debug {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
router := gin.New()
|
||||
|
||||
// Use recovery and logger middleware
|
||||
router.Use(gin.Recovery())
|
||||
if config.Debug {
|
||||
router.Use(gin.Logger())
|
||||
} else {
|
||||
// In production, use a custom minimal logger
|
||||
router.Use(minimalLogger())
|
||||
}
|
||||
|
||||
// CORS middleware if enabled
|
||||
if config.AllowCORS {
|
||||
corsConfig := cors.DefaultConfig()
|
||||
|
||||
// If a specific origin is provided, use it
|
||||
if config.CORSOrigin != "" {
|
||||
corsConfig.AllowOrigins = []string{config.CORSOrigin}
|
||||
} else {
|
||||
corsConfig.AllowAllOrigins = true
|
||||
}
|
||||
|
||||
corsConfig.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}
|
||||
corsConfig.AllowHeaders = []string{"Origin", "Content-Type", "Accept", "Authorization"}
|
||||
corsConfig.ExposeHeaders = []string{"Content-Length"}
|
||||
corsConfig.AllowCredentials = true
|
||||
corsConfig.MaxAge = 12 * time.Hour
|
||||
|
||||
router.Use(cors.New(corsConfig))
|
||||
}
|
||||
|
||||
// Setup server
|
||||
server := &Server{
|
||||
db: db,
|
||||
router: router,
|
||||
srv: &http.Server{
|
||||
Addr: ":" + config.Port,
|
||||
Handler: router,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
// Configure routes
|
||||
server.setupRoutes(config.UIPath)
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
// setupRoutes configures all the routes for the API server
|
||||
func (s *Server) setupRoutes(uiPath string) {
|
||||
// Health check endpoint
|
||||
s.router.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
// API routes
|
||||
api := s.router.Group("/api")
|
||||
{
|
||||
// Middleware routes
|
||||
middlewares := api.Group("/middlewares")
|
||||
{
|
||||
middlewares.GET("", s.getMiddlewares)
|
||||
middlewares.POST("", s.createMiddleware)
|
||||
middlewares.GET("/:id", s.getMiddleware)
|
||||
middlewares.PUT("/:id", s.updateMiddleware)
|
||||
middlewares.DELETE("/:id", s.deleteMiddleware)
|
||||
}
|
||||
|
||||
// Resource routes
|
||||
resources := api.Group("/resources")
|
||||
{
|
||||
resources.GET("", s.getResources)
|
||||
resources.GET("/:id", s.getResource)
|
||||
resources.POST("/:id/middlewares", s.assignMiddleware)
|
||||
resources.DELETE("/:resourceId/middlewares/:middlewareId", s.removeMiddleware)
|
||||
}
|
||||
}
|
||||
|
||||
// Serve the React app
|
||||
uiPathToUse := uiPath
|
||||
if uiPathToUse == "" {
|
||||
// Default UI path
|
||||
uiPathToUse = "/app/ui/build"
|
||||
}
|
||||
|
||||
// Check if UI path exists and is a directory
|
||||
if stat, err := os.Stat(uiPathToUse); err == nil && stat.IsDir() {
|
||||
s.router.Use(static.Serve("/", static.LocalFile(uiPathToUse, false)))
|
||||
|
||||
// Handle all other routes by serving the index.html file
|
||||
s.router.NoRoute(func(c *gin.Context) {
|
||||
// API routes should 404 when not found
|
||||
if len(c.Request.URL.Path) >= 4 && c.Request.URL.Path[:4] == "/api" {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "API endpoint not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Non-API routes serve the SPA
|
||||
c.File(uiPathToUse + "/index.html")
|
||||
})
|
||||
} else {
|
||||
log.Printf("Warning: UI path %s doesn't exist or is not a directory. Web UI will not be available.", uiPathToUse)
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the API server with graceful shutdown
|
||||
func (s *Server) Start() error {
|
||||
// Channel to listen for errors coming from the listener.
|
||||
serverErrors := make(chan error, 1)
|
||||
|
||||
// Start the server
|
||||
go func() {
|
||||
log.Printf("API server listening on %s", s.srv.Addr)
|
||||
serverErrors <- s.srv.ListenAndServe()
|
||||
}()
|
||||
|
||||
// Channel to listen for an interrupt or terminate signal from the OS.
|
||||
shutdown := make(chan os.Signal, 1)
|
||||
signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
// Blocking select waiting for either a server error or a signal.
|
||||
select {
|
||||
case err := <-serverErrors:
|
||||
// Non-nil error from ListenAndServe.
|
||||
return err
|
||||
|
||||
case <-shutdown:
|
||||
log.Println("Shutdown signal received")
|
||||
|
||||
// Give outstanding requests a deadline for completion.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Asking listener to shut down and shed load.
|
||||
if err := s.srv.Shutdown(ctx); err != nil {
|
||||
// Error from closing listeners, or context timeout.
|
||||
log.Printf("Graceful shutdown failed: %v", err)
|
||||
if err := s.srv.Close(); err != nil {
|
||||
log.Printf("Error during forced shutdown: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println("API server stopped gracefully")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop gracefully stops the API server
|
||||
func (s *Server) Stop() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := s.srv.Shutdown(ctx); err != nil {
|
||||
log.Printf("Failed to gracefully shutdown server: %v", err)
|
||||
if err := s.srv.Close(); err != nil {
|
||||
log.Printf("Error during forced shutdown: %v", err)
|
||||
}
|
||||
} else {
|
||||
log.Println("API server stopped gracefully")
|
||||
}
|
||||
}
|
||||
|
||||
// minimalLogger returns a Gin middleware for minimal request logging
|
||||
func minimalLogger() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Start timer
|
||||
start := time.Now()
|
||||
|
||||
// Process request
|
||||
c.Next()
|
||||
|
||||
// Log only when path is not being probed by health checkers
|
||||
if c.Request.URL.Path != "/health" && c.Request.URL.Path != "/ping" {
|
||||
// Log only requests with errors or non-standard responses
|
||||
if c.Writer.Status() >= 400 || len(c.Errors) > 0 {
|
||||
log.Printf("[GIN] %s | %d | %v | %s | %s",
|
||||
c.Request.Method,
|
||||
c.Writer.Status(),
|
||||
time.Since(start),
|
||||
c.ClientIP(),
|
||||
c.Request.URL.Path,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
171
config/defaults.go
Normal file
171
config/defaults.go
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/hhftechnology/middleware-manager/database"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// DefaultMiddleware represents a default middleware template
|
||||
type DefaultMiddleware struct {
|
||||
ID string `yaml:"id"`
|
||||
Name string `yaml:"name"`
|
||||
Type string `yaml:"type"`
|
||||
Config map[string]interface{} `yaml:"config"`
|
||||
}
|
||||
|
||||
// DefaultTemplates represents the structure of the templates.yaml file
|
||||
type DefaultTemplates struct {
|
||||
Middlewares []DefaultMiddleware `yaml:"middlewares"`
|
||||
}
|
||||
|
||||
// LoadDefaultTemplates loads the default middleware templates
|
||||
func LoadDefaultTemplates(db *database.DB) error {
|
||||
// Determine the path to the templates file
|
||||
templatesFile := "config/templates.yaml"
|
||||
|
||||
// Check if the file exists in the current directory
|
||||
if _, err := os.Stat(templatesFile); os.IsNotExist(err) {
|
||||
// Try to find it in different locations
|
||||
possiblePaths := []string{
|
||||
"/app/config/templates.yaml", // Docker container path
|
||||
"templates.yaml", // Current directory
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, path := range possiblePaths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
templatesFile = path
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
log.Printf("Warning: templates.yaml not found, skipping default templates")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Read the templates file
|
||||
data, err := ioutil.ReadFile(templatesFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse the YAML
|
||||
var templates DefaultTemplates
|
||||
if err := yaml.Unmarshal(data, &templates); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add templates to the database if they don't exist
|
||||
for _, middleware := range templates.Middlewares {
|
||||
// Check if the middleware already exists
|
||||
var exists int
|
||||
err := db.QueryRow("SELECT 1 FROM middlewares WHERE id = ?", middleware.ID).Scan(&exists)
|
||||
if err == nil {
|
||||
// Middleware exists, skip
|
||||
continue
|
||||
}
|
||||
|
||||
// Convert config to JSON string
|
||||
configJSON, err := json.Marshal(middleware.Config)
|
||||
if err != nil {
|
||||
log.Printf("Failed to marshal config for %s: %v", middleware.Name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Insert into database
|
||||
_, err = db.Exec(
|
||||
"INSERT INTO middlewares (id, name, type, config) VALUES (?, ?, ?, ?)",
|
||||
middleware.ID, middleware.Name, middleware.Type, string(configJSON),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Failed to insert middleware %s: %v", middleware.Name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("Added default middleware: %s", middleware.Name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsureConfigDirectory ensures the configuration directory exists
|
||||
func EnsureConfigDirectory(path string) error {
|
||||
return os.MkdirAll(path, 0755)
|
||||
}
|
||||
|
||||
// SaveTemplateFile saves the default templates file if it doesn't exist
|
||||
func SaveTemplateFile(templatesDir string) error {
|
||||
templatesFile := filepath.Join(templatesDir, "templates.yaml")
|
||||
|
||||
// Check if file already exists
|
||||
if _, err := os.Stat(templatesFile); err == nil {
|
||||
// File exists, skip
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create default templates
|
||||
templates := DefaultTemplates{
|
||||
Middlewares: []DefaultMiddleware{
|
||||
{
|
||||
ID: "authelia",
|
||||
Name: "Authelia",
|
||||
Type: "forwardAuth",
|
||||
Config: map[string]interface{}{
|
||||
"address": "http://authelia:9091/api/verify?rd=https://auth.yourdomain.com",
|
||||
"trustForwardHeader": true,
|
||||
"authResponseHeaders": []string{
|
||||
"Remote-User",
|
||||
"Remote-Groups",
|
||||
"Remote-Name",
|
||||
"Remote-Email",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "authentik",
|
||||
Name: "Authentik",
|
||||
Type: "forwardAuth",
|
||||
Config: map[string]interface{}{
|
||||
"address": "http://authentik:9000/outpost.goauthentik.io/auth/traefik",
|
||||
"trustForwardHeader": true,
|
||||
"authResponseHeaders": []string{
|
||||
"X-authentik-username",
|
||||
"X-authentik-groups",
|
||||
"X-authentik-email",
|
||||
"X-authentik-name",
|
||||
"X-authentik-uid",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "basic-auth",
|
||||
Name: "Basic Auth",
|
||||
Type: "basicAuth",
|
||||
Config: map[string]interface{}{
|
||||
"users": []string{
|
||||
"admin:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Convert to YAML
|
||||
data, err := yaml.Marshal(templates)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write to file
|
||||
return ioutil.WriteFile(templatesFile, data, 0644)
|
||||
}
|
||||
60
config/templates.yaml
Normal file
60
config/templates.yaml
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# Default middleware templates
|
||||
middlewares:
|
||||
- id: authelia
|
||||
name: Authelia
|
||||
type: forwardAuth
|
||||
config:
|
||||
address: "http://authelia:9091/api/verify?rd=https://auth.yourdomain.com"
|
||||
trustForwardHeader: true
|
||||
authResponseHeaders:
|
||||
- "Remote-User"
|
||||
- "Remote-Groups"
|
||||
- "Remote-Name"
|
||||
- "Remote-Email"
|
||||
|
||||
- id: authentik
|
||||
name: Authentik
|
||||
type: forwardAuth
|
||||
config:
|
||||
address: "http://authentik:9000/outpost.goauthentik.io/auth/traefik"
|
||||
trustForwardHeader: true
|
||||
authResponseHeaders:
|
||||
- "X-authentik-username"
|
||||
- "X-authentik-groups"
|
||||
- "X-authentik-email"
|
||||
- "X-authentik-name"
|
||||
- "X-authentik-uid"
|
||||
|
||||
- id: basic-auth
|
||||
name: Basic Auth
|
||||
type: basicAuth
|
||||
config:
|
||||
users:
|
||||
- "admin:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/"
|
||||
|
||||
- id: jwt-auth
|
||||
name: JWT Authentication
|
||||
type: forwardAuth
|
||||
config:
|
||||
address: "http://jwt-auth:8080/verify"
|
||||
trustForwardHeader: true
|
||||
authResponseHeaders:
|
||||
- "X-JWT-Sub"
|
||||
- "X-JWT-Name"
|
||||
- "X-JWT-Email"
|
||||
|
||||
- id: ip-whitelist
|
||||
name: IP Whitelist
|
||||
type: ipWhiteList
|
||||
config:
|
||||
sourceRange:
|
||||
- "127.0.0.1/32"
|
||||
- "192.168.1.0/24"
|
||||
- "10.0.0.0/8"
|
||||
|
||||
- id: rate-limit
|
||||
name: Rate Limit
|
||||
type: rateLimit
|
||||
config:
|
||||
average: 100
|
||||
burst: 50
|
||||
277
database/db.go
Normal file
277
database/db.go
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
// DB is a wrapper around sql.DB
|
||||
type DB struct {
|
||||
*sql.DB
|
||||
}
|
||||
|
||||
// InitDB initializes the database connection
|
||||
func InitDB(dbPath string) (*DB, error) {
|
||||
// Create parent directory if it doesn't exist
|
||||
dir := filepath.Dir(dbPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create directory %s: %w", dir, err)
|
||||
}
|
||||
|
||||
// Open the database with pragmas for better reliability
|
||||
db, err := sql.Open("sqlite3", dbPath+"?_journal=WAL&_busy_timeout=5000")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||
}
|
||||
|
||||
// Test the connection
|
||||
if err := db.Ping(); err != nil {
|
||||
db.Close() // Close the connection on failure
|
||||
return nil, fmt.Errorf("failed to ping database: %w", err)
|
||||
}
|
||||
|
||||
// Set connection limits
|
||||
db.SetMaxOpenConns(25)
|
||||
db.SetMaxIdleConns(5)
|
||||
db.SetConnMaxLifetime(30 * time.Minute)
|
||||
|
||||
log.Printf("Connected to database at %s", dbPath)
|
||||
|
||||
// Run migrations
|
||||
if err := runMigrations(db); err != nil {
|
||||
db.Close() // Close the connection on failure
|
||||
return nil, fmt.Errorf("failed to run migrations: %w", err)
|
||||
}
|
||||
|
||||
return &DB{db}, nil
|
||||
}
|
||||
|
||||
// runMigrations executes the database migrations
|
||||
func runMigrations(db *sql.DB) error {
|
||||
// Try to find migrations file in different locations
|
||||
migrationsFile := findMigrationsFile()
|
||||
if migrationsFile == "" {
|
||||
return fmt.Errorf("migrations file not found")
|
||||
}
|
||||
|
||||
// Read migrations file
|
||||
migrations, err := ioutil.ReadFile(migrationsFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read migrations file: %w", err)
|
||||
}
|
||||
|
||||
// Execute migrations in a transaction
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to begin transaction: %w", err)
|
||||
}
|
||||
|
||||
// If something goes wrong, rollback
|
||||
defer func() {
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
// Execute migrations
|
||||
if _, err = tx.Exec(string(migrations)); err != nil {
|
||||
return fmt.Errorf("failed to execute migrations: %w", err)
|
||||
}
|
||||
|
||||
// Commit the transaction
|
||||
if err = tx.Commit(); err != nil {
|
||||
return fmt.Errorf("failed to commit transaction: %w", err)
|
||||
}
|
||||
|
||||
log.Println("Migrations completed successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// findMigrationsFile tries to find the migrations file in different locations
|
||||
func findMigrationsFile() string {
|
||||
possiblePaths := []string{
|
||||
"database/migrations.sql",
|
||||
"migrations.sql",
|
||||
"/app/database/migrations.sql",
|
||||
"/app/migrations.sql",
|
||||
}
|
||||
|
||||
for _, path := range possiblePaths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetMiddlewares fetches all middleware definitions
|
||||
func (db *DB) GetMiddlewares() ([]map[string]interface{}, error) {
|
||||
rows, err := db.Query("SELECT id, name, type, config FROM middlewares")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query failed: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var middlewares []map[string]interface{}
|
||||
for rows.Next() {
|
||||
var id, name, typ, configStr string
|
||||
if err := rows.Scan(&id, &name, &typ, &configStr); err != nil {
|
||||
return nil, fmt.Errorf("row scan failed: %w", err)
|
||||
}
|
||||
|
||||
// Parse the config JSON
|
||||
var configMap map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(configStr), &configMap); err != nil {
|
||||
// If we can't parse the JSON, just return it as a string
|
||||
middleware := map[string]interface{}{
|
||||
"id": id,
|
||||
"name": name,
|
||||
"type": typ,
|
||||
"config": configStr,
|
||||
}
|
||||
middlewares = append(middlewares, middleware)
|
||||
continue
|
||||
}
|
||||
|
||||
middleware := map[string]interface{}{
|
||||
"id": id,
|
||||
"name": name,
|
||||
"type": typ,
|
||||
"config": configMap,
|
||||
}
|
||||
middlewares = append(middlewares, middleware)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("rows iteration error: %w", err)
|
||||
}
|
||||
|
||||
return middlewares, nil
|
||||
}
|
||||
|
||||
// GetResources fetches all resources
|
||||
func (db *DB) GetResources() ([]map[string]interface{}, error) {
|
||||
rows, err := db.Query(`
|
||||
SELECT r.id, r.host, r.service_id, r.org_id, r.site_id,
|
||||
GROUP_CONCAT(m.id || ':' || m.name || ':' || rm.priority, ',') as middlewares
|
||||
FROM resources r
|
||||
LEFT JOIN resource_middlewares rm ON r.id = rm.resource_id
|
||||
LEFT JOIN middlewares m ON rm.middleware_id = m.id
|
||||
GROUP BY r.id
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query failed: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var resources []map[string]interface{}
|
||||
for rows.Next() {
|
||||
var id, host, serviceID, orgID, siteID string
|
||||
var middlewares sql.NullString
|
||||
if err := rows.Scan(&id, &host, &serviceID, &orgID, &siteID, &middlewares); err != nil {
|
||||
return nil, fmt.Errorf("row scan failed: %w", err)
|
||||
}
|
||||
|
||||
resource := map[string]interface{}{
|
||||
"id": id,
|
||||
"host": host,
|
||||
"service_id": serviceID,
|
||||
"org_id": orgID,
|
||||
"site_id": siteID,
|
||||
}
|
||||
|
||||
if middlewares.Valid {
|
||||
resource["middlewares"] = middlewares.String
|
||||
} else {
|
||||
resource["middlewares"] = ""
|
||||
}
|
||||
|
||||
resources = append(resources, resource)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("rows iteration error: %w", err)
|
||||
}
|
||||
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
// GetResource fetches a specific resource by ID
|
||||
func (db *DB) GetResource(id string) (map[string]interface{}, error) {
|
||||
var host, serviceID, orgID, siteID string
|
||||
var middlewares sql.NullString
|
||||
|
||||
err := db.QueryRow(`
|
||||
SELECT r.host, r.service_id, r.org_id, r.site_id,
|
||||
GROUP_CONCAT(m.id || ':' || m.name || ':' || rm.priority, ',') as middlewares
|
||||
FROM resources r
|
||||
LEFT JOIN resource_middlewares rm ON r.id = rm.resource_id
|
||||
LEFT JOIN middlewares m ON rm.middleware_id = m.id
|
||||
WHERE r.id = ?
|
||||
GROUP BY r.id
|
||||
`, id).Scan(&host, &serviceID, &orgID, &siteID, &middlewares)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, fmt.Errorf("resource not found: %s", id)
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("query failed: %w", err)
|
||||
}
|
||||
|
||||
resource := map[string]interface{}{
|
||||
"id": id,
|
||||
"host": host,
|
||||
"service_id": serviceID,
|
||||
"org_id": orgID,
|
||||
"site_id": siteID,
|
||||
}
|
||||
|
||||
if middlewares.Valid {
|
||||
resource["middlewares"] = middlewares.String
|
||||
} else {
|
||||
resource["middlewares"] = ""
|
||||
}
|
||||
|
||||
return resource, nil
|
||||
}
|
||||
|
||||
// GetMiddleware fetches a specific middleware by ID
|
||||
func (db *DB) GetMiddleware(id string) (map[string]interface{}, error) {
|
||||
var name, typ, configStr string
|
||||
|
||||
err := db.QueryRow(
|
||||
"SELECT name, type, config FROM middlewares WHERE id = ?", id,
|
||||
).Scan(&name, &typ, &configStr)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, fmt.Errorf("middleware not found: %s", id)
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("query failed: %w", err)
|
||||
}
|
||||
|
||||
var configMap map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(configStr), &configMap); err != nil {
|
||||
// If we can't parse the JSON, just return the string
|
||||
return map[string]interface{}{
|
||||
"id": id,
|
||||
"name": name,
|
||||
"type": typ,
|
||||
"config": configStr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"id": id,
|
||||
"name": name,
|
||||
"type": typ,
|
||||
"config": configMap,
|
||||
}, nil
|
||||
}
|
||||
37
database/migrations.sql
Normal file
37
database/migrations.sql
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
-- Middlewares table stores middleware definitions
|
||||
CREATE TABLE IF NOT EXISTS middlewares (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
config TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Resources table stores Pangolin resources
|
||||
CREATE TABLE IF NOT EXISTS resources (
|
||||
id TEXT PRIMARY KEY,
|
||||
host TEXT NOT NULL,
|
||||
service_id TEXT NOT NULL,
|
||||
org_id TEXT NOT NULL,
|
||||
site_id TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Resource_middlewares table stores the relationship between resources and middlewares
|
||||
CREATE TABLE IF NOT EXISTS resource_middlewares (
|
||||
resource_id TEXT NOT NULL,
|
||||
middleware_id TEXT NOT NULL,
|
||||
priority INTEGER NOT NULL DEFAULT 100,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (resource_id, middleware_id),
|
||||
FOREIGN KEY (resource_id) REFERENCES resources(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (middleware_id) REFERENCES middlewares(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Insert default middlewares
|
||||
INSERT OR IGNORE INTO middlewares (id, name, type, config) VALUES
|
||||
('authelia', 'Authelia', 'forwardAuth', '{"address":"http://authelia:9091/api/verify?rd=https://auth.yourdomain.com","trustForwardHeader":true,"authResponseHeaders":["Remote-User","Remote-Groups","Remote-Name","Remote-Email"]}'),
|
||||
('authentik', 'Authentik', 'forwardAuth', '{"address":"http://authentik:9000/outpost.goauthentik.io/auth/traefik","trustForwardHeader":true,"authResponseHeaders":["X-authentik-username","X-authentik-groups","X-authentik-email","X-authentik-name","X-authentik-uid"]}'),
|
||||
('basic-auth', 'Basic Auth', 'basicAuth', '{"users":["admin:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/"]}');
|
||||
37
docker-compose.yml
Normal file
37
docker-compose.yml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
middleware-manager:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: middleware-manager
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./data:/data
|
||||
- ./config/traefik/conf:/conf
|
||||
environment:
|
||||
- PANGOLIN_API_URL=http://pangolin:3001/api/v1
|
||||
- TRAEFIK_CONF_DIR=/conf
|
||||
- DB_PATH=/data/middleware.db
|
||||
- PORT=3456
|
||||
ports:
|
||||
- "3456:3456"
|
||||
networks:
|
||||
- pangolin
|
||||
|
||||
# For development/testing, you can include a mock Pangolin API
|
||||
mock-pangolin:
|
||||
image: nginx:alpine
|
||||
container_name: mock-pangolin
|
||||
volumes:
|
||||
- ./test/mock-api:/usr/share/nginx/html
|
||||
ports:
|
||||
- "3001:80"
|
||||
networks:
|
||||
- pangolin
|
||||
|
||||
networks:
|
||||
pangolin:
|
||||
external: true # In production, this should connect to your existing Pangolin network
|
||||
# When testing standalone, remove the 'external' flag and it will create a new network
|
||||
32
go.mod
Normal file
32
go.mod
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
module github.com/hhftechnology/middleware-manager
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/cors v1.4.0
|
||||
github.com/gin-contrib/static v0.0.1
|
||||
github.com/gin-gonic/gin v1.8.2
|
||||
github.com/mattn/go-sqlite3 v1.14.16
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.11.1 // indirect
|
||||
github.com/goccy/go-json v0.10.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
|
||||
github.com/ugorji/go/codec v1.2.8 // indirect
|
||||
golang.org/x/crypto v0.5.0 // indirect
|
||||
golang.org/x/net v0.5.0 // indirect
|
||||
golang.org/x/sys v0.4.0 // indirect
|
||||
golang.org/x/text v0.6.0 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
130
go.sum
Normal file
130
go.sum
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g=
|
||||
github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-contrib/static v0.0.1 h1:JVxuvHPuUfkoul12N7dtQw7KRn/pSMq7Ue1Va9Swm1U=
|
||||
github.com/gin-contrib/static v0.0.1/go.mod h1:CSxeF+wep05e0kCOsqWdAWbSszmc31zTIbD8TvWl7Hs=
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
||||
github.com/gin-gonic/gin v1.8.2 h1:UzKToD9/PoFj/V4rvlKqTRKnQYyz8Sc1MJlv4JHPtvY=
|
||||
github.com/gin-gonic/gin v1.8.2/go.mod h1:qw5AYuDrzRTnhvusDsrov+fDIxp9Dleuu12h8nfB398=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||
github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
|
||||
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
|
||||
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
|
||||
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/ugorji/go/codec v1.2.8 h1:sgBJS6COt0b/P40VouWKdseidkDgHxYGm0SAglUHfP0=
|
||||
github.com/ugorji/go/codec v1.2.8/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
|
||||
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
|
||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
|
||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
163
main.go
Normal file
163
main.go
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/hhftechnology/middleware-manager/api"
|
||||
"github.com/hhftechnology/middleware-manager/config"
|
||||
"github.com/hhftechnology/middleware-manager/database"
|
||||
"github.com/hhftechnology/middleware-manager/services"
|
||||
)
|
||||
|
||||
// Configuration represents the application configuration
|
||||
type Configuration struct {
|
||||
PangolinAPIURL string
|
||||
TraefikConfDir string
|
||||
DBPath string
|
||||
Port string
|
||||
UIPath string
|
||||
CheckInterval time.Duration
|
||||
GenerateInterval time.Duration
|
||||
Debug bool
|
||||
AllowCORS bool
|
||||
CORSOrigin string
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.Println("Starting Middleware Manager...")
|
||||
|
||||
// Parse command line flags
|
||||
var debug bool
|
||||
flag.BoolVar(&debug, "debug", false, "Enable debug mode")
|
||||
flag.Parse()
|
||||
|
||||
// Load configuration
|
||||
cfg := loadConfiguration(debug)
|
||||
|
||||
// Initialize database
|
||||
db, err := database.InitDB(cfg.DBPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize database: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Ensure config directory exists
|
||||
configDir := getEnv("CONFIG_DIR", "/app/config")
|
||||
if err := config.EnsureConfigDirectory(configDir); err != nil {
|
||||
log.Printf("Warning: Failed to create config directory: %v", err)
|
||||
}
|
||||
|
||||
// Save default templates file if it doesn't exist
|
||||
if err := config.SaveTemplateFile(configDir); err != nil {
|
||||
log.Printf("Warning: Failed to save default templates: %v", err)
|
||||
}
|
||||
|
||||
// Load default middleware templates
|
||||
if err := config.LoadDefaultTemplates(db); err != nil {
|
||||
log.Printf("Warning: Failed to load default templates: %v", err)
|
||||
}
|
||||
|
||||
// Create stop channel for graceful shutdown
|
||||
stopChan := make(chan struct{})
|
||||
|
||||
// Start resource watcher
|
||||
resourceWatcher := services.NewResourceWatcher(db, cfg.PangolinAPIURL)
|
||||
go resourceWatcher.Start(cfg.CheckInterval)
|
||||
|
||||
// Start configuration generator
|
||||
configGenerator := services.NewConfigGenerator(db, cfg.TraefikConfDir)
|
||||
go configGenerator.Start(cfg.GenerateInterval)
|
||||
|
||||
// Start API server
|
||||
serverConfig := api.ServerConfig{
|
||||
Port: cfg.Port,
|
||||
UIPath: cfg.UIPath,
|
||||
Debug: cfg.Debug,
|
||||
AllowCORS: cfg.AllowCORS,
|
||||
CORSOrigin: cfg.CORSOrigin,
|
||||
}
|
||||
|
||||
server := api.NewServer(db, serverConfig)
|
||||
go func() {
|
||||
if err := server.Start(); err != nil {
|
||||
log.Printf("Server error: %v", err)
|
||||
close(stopChan)
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for shutdown signal or server error
|
||||
signalChan := make(chan os.Signal, 1)
|
||||
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
select {
|
||||
case <-signalChan:
|
||||
log.Println("Received shutdown signal")
|
||||
case <-stopChan:
|
||||
log.Println("Received stop signal from server")
|
||||
}
|
||||
|
||||
// Graceful shutdown
|
||||
log.Println("Shutting down...")
|
||||
resourceWatcher.Stop()
|
||||
configGenerator.Stop()
|
||||
server.Stop()
|
||||
log.Println("Middleware Manager stopped")
|
||||
}
|
||||
|
||||
// loadConfiguration loads configuration from environment variables
|
||||
func loadConfiguration(debug bool) Configuration {
|
||||
// Default check interval is 30 seconds
|
||||
checkInterval := 30 * time.Second
|
||||
if intervalStr := getEnv("CHECK_INTERVAL_SECONDS", "30"); intervalStr != "" {
|
||||
if interval, err := strconv.Atoi(intervalStr); err == nil && interval > 0 {
|
||||
checkInterval = time.Duration(interval) * time.Second
|
||||
}
|
||||
}
|
||||
|
||||
// Default generate interval is 10 seconds
|
||||
generateInterval := 10 * time.Second
|
||||
if intervalStr := getEnv("GENERATE_INTERVAL_SECONDS", "10"); intervalStr != "" {
|
||||
if interval, err := strconv.Atoi(intervalStr); err == nil && interval > 0 {
|
||||
generateInterval = time.Duration(interval) * time.Second
|
||||
}
|
||||
}
|
||||
|
||||
// Allow CORS if specified
|
||||
allowCORS := false
|
||||
if corsStr := getEnv("ALLOW_CORS", "false"); corsStr != "" {
|
||||
allowCORS = strings.ToLower(corsStr) == "true"
|
||||
}
|
||||
|
||||
// Override debug mode from environment if specified
|
||||
if debugStr := getEnv("DEBUG", ""); debugStr != "" {
|
||||
debug = strings.ToLower(debugStr) == "true"
|
||||
}
|
||||
|
||||
return Configuration{
|
||||
PangolinAPIURL: getEnv("PANGOLIN_API_URL", "http://pangolin:3001/api/v1"),
|
||||
TraefikConfDir: getEnv("TRAEFIK_CONF_DIR", "/conf"),
|
||||
DBPath: getEnv("DB_PATH", "/data/middleware.db"),
|
||||
Port: getEnv("PORT", "3456"),
|
||||
UIPath: getEnv("UI_PATH", "/app/ui/build"),
|
||||
CheckInterval: checkInterval,
|
||||
GenerateInterval: generateInterval,
|
||||
Debug: debug,
|
||||
AllowCORS: allowCORS,
|
||||
CORSOrigin: getEnv("CORS_ORIGIN", ""),
|
||||
}
|
||||
}
|
||||
|
||||
// getEnv gets an environment variable or returns a default value
|
||||
func getEnv(key, fallback string) string {
|
||||
if value, exists := os.LookupEnv(key); exists {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
33
models/middleware.go
Normal file
33
models/middleware.go
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Middleware represents a Traefik middleware configuration
|
||||
type Middleware struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Config string `json:"config"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// ConfigMap returns the middleware config as a map
|
||||
func (m *Middleware) ConfigMap() (map[string]interface{}, error) {
|
||||
var config map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(m.Config), &config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// ResourceMiddleware represents the relationship between a resource and a middleware
|
||||
type ResourceMiddleware struct {
|
||||
ResourceID string `json:"resource_id"`
|
||||
MiddlewareID string `json:"middleware_id"`
|
||||
Priority int `json:"priority"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
54
models/resource.go
Normal file
54
models/resource.go
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Resource represents a Pangolin resource
|
||||
type Resource struct {
|
||||
ID string `json:"id"`
|
||||
Host string `json:"host"`
|
||||
ServiceID string `json:"service_id"`
|
||||
OrgID string `json:"org_id"`
|
||||
SiteID string `json:"site_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
// Middlewares is a list of associated middlewares, populated when needed
|
||||
Middlewares []ResourceMiddleware `json:"middlewares,omitempty"`
|
||||
}
|
||||
|
||||
// PangolinResource represents the format of a resource from Pangolin API
|
||||
type PangolinResource struct {
|
||||
ID string `json:"id"`
|
||||
Host string `json:"host"`
|
||||
OrgID string `json:"org_id"`
|
||||
SiteID string `json:"site_id"`
|
||||
}
|
||||
|
||||
// PangolinTraefikConfig represents the Traefik configuration from Pangolin API
|
||||
type PangolinTraefikConfig struct {
|
||||
HTTP struct {
|
||||
Routers map[string]PangolinRouter `json:"routers"`
|
||||
Services map[string]PangolinService `json:"services"`
|
||||
} `json:"http"`
|
||||
}
|
||||
|
||||
// PangolinRouter represents a router configuration from Pangolin API
|
||||
type PangolinRouter struct {
|
||||
Rule string `json:"rule"`
|
||||
Service string `json:"service"`
|
||||
EntryPoints []string `json:"entryPoints"`
|
||||
Middlewares []string `json:"middlewares"`
|
||||
TLS struct {
|
||||
CertResolver string `json:"certResolver"`
|
||||
} `json:"tls"`
|
||||
}
|
||||
|
||||
// PangolinService represents a service configuration from Pangolin API
|
||||
type PangolinService struct {
|
||||
LoadBalancer struct {
|
||||
Servers []struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"servers"`
|
||||
} `json:"loadBalancer"`
|
||||
}
|
||||
273
services/config_generator.go
Normal file
273
services/config_generator.go
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/hhftechnology/middleware-manager/database"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// ConfigGenerator generates Traefik configuration files
|
||||
type ConfigGenerator struct {
|
||||
db *database.DB
|
||||
confDir string
|
||||
stopChan chan struct{}
|
||||
isRunning bool
|
||||
mutex sync.Mutex // Protects isRunning
|
||||
}
|
||||
|
||||
// TraefikConfig represents the structure of the Traefik configuration
|
||||
type TraefikConfig struct {
|
||||
HTTP struct {
|
||||
Middlewares map[string]interface{} `yaml:"middlewares,omitempty"`
|
||||
Routers map[string]interface{} `yaml:"routers,omitempty"`
|
||||
} `yaml:"http"`
|
||||
}
|
||||
|
||||
// NewConfigGenerator creates a new config generator
|
||||
func NewConfigGenerator(db *database.DB, confDir string) *ConfigGenerator {
|
||||
return &ConfigGenerator{
|
||||
db: db,
|
||||
confDir: confDir,
|
||||
stopChan: make(chan struct{}),
|
||||
isRunning: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins generating configuration files
|
||||
func (cg *ConfigGenerator) Start(interval time.Duration) {
|
||||
cg.mutex.Lock()
|
||||
if cg.isRunning {
|
||||
cg.mutex.Unlock()
|
||||
return
|
||||
}
|
||||
cg.isRunning = true
|
||||
cg.mutex.Unlock()
|
||||
|
||||
log.Printf("Config generator started, generating every %v", interval)
|
||||
|
||||
// Create conf directory if it doesn't exist
|
||||
if err := os.MkdirAll(cg.confDir, 0755); err != nil {
|
||||
log.Printf("Failed to create conf directory: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
// Generate initial configuration
|
||||
if err := cg.generateConfig(); err != nil {
|
||||
log.Printf("Initial config generation failed: %v", err)
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if err := cg.generateConfig(); err != nil {
|
||||
log.Printf("Config generation failed: %v", err)
|
||||
}
|
||||
case <-cg.stopChan:
|
||||
log.Println("Config generator stopped")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop stops the config generator
|
||||
func (cg *ConfigGenerator) Stop() {
|
||||
cg.mutex.Lock()
|
||||
defer cg.mutex.Unlock()
|
||||
|
||||
if !cg.isRunning {
|
||||
return
|
||||
}
|
||||
|
||||
close(cg.stopChan)
|
||||
cg.isRunning = false
|
||||
}
|
||||
|
||||
// generateConfig generates Traefik configuration files
|
||||
func (cg *ConfigGenerator) generateConfig() error {
|
||||
log.Println("Generating Traefik configuration...")
|
||||
|
||||
// Create a new configuration
|
||||
config := TraefikConfig{}
|
||||
config.HTTP.Middlewares = make(map[string]interface{})
|
||||
config.HTTP.Routers = make(map[string]interface{})
|
||||
|
||||
// Process middlewares
|
||||
if err := cg.processMiddlewares(&config); err != nil {
|
||||
return fmt.Errorf("failed to process middlewares: %w", err)
|
||||
}
|
||||
|
||||
// Process resources
|
||||
if err := cg.processResources(&config); err != nil {
|
||||
return fmt.Errorf("failed to process resources: %w", err)
|
||||
}
|
||||
|
||||
// Write configuration to file
|
||||
return cg.writeConfigToFile(&config)
|
||||
}
|
||||
|
||||
// processMiddlewares fetches and processes all middleware definitions
|
||||
func (cg *ConfigGenerator) processMiddlewares(config *TraefikConfig) error {
|
||||
// Fetch all middlewares
|
||||
rows, err := cg.db.Query("SELECT id, name, type, config FROM middlewares")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch middlewares: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// Process middlewares
|
||||
for rows.Next() {
|
||||
var id, name, typ, configStr string
|
||||
if err := rows.Scan(&id, &name, &typ, &configStr); err != nil {
|
||||
log.Printf("Failed to scan middleware: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse middleware config
|
||||
var middlewareConfig map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(configStr), &middlewareConfig); err != nil {
|
||||
log.Printf("Failed to parse middleware config for %s: %v", name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Add middleware to config
|
||||
config.HTTP.Middlewares[id] = map[string]interface{}{
|
||||
typ: middlewareConfig,
|
||||
}
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return fmt.Errorf("error during middleware rows iteration: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// processResources fetches and processes all resources and their middlewares
|
||||
func (cg *ConfigGenerator) processResources(config *TraefikConfig) error {
|
||||
// Fetch all resources and their middlewares
|
||||
rows, err := cg.db.Query(`
|
||||
SELECT r.id, r.host, r.service_id, rm.middleware_id, rm.priority
|
||||
FROM resources r
|
||||
JOIN resource_middlewares rm ON r.id = rm.resource_id
|
||||
ORDER BY rm.priority DESC
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch resources: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// Group middlewares by resource
|
||||
resourceMiddlewares := make(map[string][]string)
|
||||
resourceInfo := make(map[string]struct {
|
||||
Host string
|
||||
ServiceID string
|
||||
})
|
||||
|
||||
for rows.Next() {
|
||||
var resourceID, host, serviceID, middlewareID string
|
||||
var priority int
|
||||
if err := rows.Scan(&resourceID, &host, &serviceID, &middlewareID, &priority); err != nil {
|
||||
log.Printf("Failed to scan resource middleware: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
resourceMiddlewares[resourceID] = append(resourceMiddlewares[resourceID], middlewareID)
|
||||
resourceInfo[resourceID] = struct {
|
||||
Host string
|
||||
ServiceID string
|
||||
}{
|
||||
Host: host,
|
||||
ServiceID: serviceID,
|
||||
}
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return fmt.Errorf("error during resource rows iteration: %w", err)
|
||||
}
|
||||
|
||||
// Create routers for resources with custom middlewares
|
||||
for resourceID, middlewares := range resourceMiddlewares {
|
||||
info, exists := resourceInfo[resourceID]
|
||||
if !exists {
|
||||
log.Printf("Warning: Resource info not found for %s", resourceID)
|
||||
continue
|
||||
}
|
||||
|
||||
// Add "badger" middleware with http provider suffix if not already present
|
||||
if !stringSliceContains(middlewares, "badger@http") {
|
||||
middlewares = append(middlewares, "badger@http")
|
||||
}
|
||||
|
||||
// Process middleware references to add provider suffixes
|
||||
for i, middleware := range middlewares {
|
||||
// If this is not already a fully qualified middleware reference and not the Pangolin badger middleware
|
||||
if !strings.Contains(middleware, "@") && middleware != "badger@http" {
|
||||
// Assume it's from our file provider
|
||||
middlewares[i] = fmt.Sprintf("%s@file", middleware)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a router with higher priority
|
||||
customRouterID := fmt.Sprintf("%s-auth", resourceID)
|
||||
|
||||
config.HTTP.Routers[customRouterID] = map[string]interface{}{
|
||||
"rule": fmt.Sprintf("Host(`%s`)", info.Host),
|
||||
"service": fmt.Sprintf("%s@http", info.ServiceID), // Reference service from http provider
|
||||
"entryPoints": []string{"websecure"},
|
||||
"middlewares": middlewares,
|
||||
"priority": 100, // Higher than Pangolin's default
|
||||
"tls": map[string]interface{}{
|
||||
"certResolver": "letsencrypt",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeConfigToFile writes the configuration to a file
|
||||
func (cg *ConfigGenerator) writeConfigToFile(config *TraefikConfig) error {
|
||||
// Create temporary file first to ensure atomic write
|
||||
configFile := filepath.Join(cg.confDir, "resource-overrides.yml")
|
||||
tempFile := configFile + ".tmp"
|
||||
|
||||
// Convert to YAML
|
||||
yamlData, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert config to YAML: %w", err)
|
||||
}
|
||||
|
||||
// Write to temporary file
|
||||
if err := os.WriteFile(tempFile, yamlData, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write temp config file: %w", err)
|
||||
}
|
||||
|
||||
// Rename temp file to final file (atomic operation)
|
||||
if err := os.Rename(tempFile, configFile); err != nil {
|
||||
return fmt.Errorf("failed to rename temp config file: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Generated Traefik configuration at %s", configFile)
|
||||
return nil
|
||||
}
|
||||
|
||||
// stringSliceContains checks if a string is in a slice
|
||||
func stringSliceContains(slice []string, str string) bool {
|
||||
for _, s := range slice {
|
||||
if s == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
230
services/resource_watcher.go
Normal file
230
services/resource_watcher.go
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hhftechnology/middleware-manager/database"
|
||||
"github.com/hhftechnology/middleware-manager/models"
|
||||
)
|
||||
|
||||
// ResourceWatcher watches for resources in Pangolin
|
||||
type ResourceWatcher struct {
|
||||
db *database.DB
|
||||
pangolinAPI string
|
||||
stopChan chan struct{}
|
||||
isRunning bool
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// NewResourceWatcher creates a new resource watcher
|
||||
func NewResourceWatcher(db *database.DB, pangolinAPI string) *ResourceWatcher {
|
||||
// Create HTTP client with timeout
|
||||
httpClient := &http.Client{
|
||||
Timeout: 10 * time.Second, // Set reasonable timeout
|
||||
}
|
||||
|
||||
return &ResourceWatcher{
|
||||
db: db,
|
||||
pangolinAPI: pangolinAPI,
|
||||
stopChan: make(chan struct{}),
|
||||
isRunning: false,
|
||||
httpClient: httpClient,
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins watching for resources
|
||||
func (rw *ResourceWatcher) Start(interval time.Duration) {
|
||||
if rw.isRunning {
|
||||
return
|
||||
}
|
||||
|
||||
rw.isRunning = true
|
||||
log.Printf("Resource watcher started, checking every %v", interval)
|
||||
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
// Do an initial check
|
||||
if err := rw.checkResources(); err != nil {
|
||||
log.Printf("Initial resource check failed: %v", err)
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if err := rw.checkResources(); err != nil {
|
||||
log.Printf("Resource check failed: %v", err)
|
||||
}
|
||||
case <-rw.stopChan:
|
||||
log.Println("Resource watcher stopped")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop stops the resource watcher
|
||||
func (rw *ResourceWatcher) Stop() {
|
||||
if !rw.isRunning {
|
||||
return
|
||||
}
|
||||
|
||||
close(rw.stopChan)
|
||||
rw.isRunning = false
|
||||
}
|
||||
|
||||
// checkResources fetches resources from Pangolin and updates the database
|
||||
func (rw *ResourceWatcher) checkResources() error {
|
||||
log.Println("Checking for resources in Pangolin...")
|
||||
|
||||
// Create a context with timeout for the operation
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Fetch Traefik configuration from Pangolin
|
||||
config, err := rw.fetchTraefikConfig(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch Traefik config: %w", err)
|
||||
}
|
||||
|
||||
// Process routers to find resources
|
||||
for routerID, router := range config.HTTP.Routers {
|
||||
// Skip non-SSL routers (usually HTTP redirects)
|
||||
if router.TLS.CertResolver == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract host from rule (e.g., "Host(`example.com`)")
|
||||
host := extractHostFromRule(router.Rule)
|
||||
if host == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip Pangolin's own routers
|
||||
if isSystemRouter(routerID) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get or create the resource
|
||||
serviceID := router.Service
|
||||
|
||||
if err := rw.updateOrCreateResource(routerID, host, serviceID); err != nil {
|
||||
log.Printf("Error processing resource %s: %v", routerID, err)
|
||||
// Continue processing other resources even if one fails
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateOrCreateResource updates an existing resource or creates a new one
|
||||
func (rw *ResourceWatcher) updateOrCreateResource(resourceID, host, serviceID string) error {
|
||||
// Check if resource already exists
|
||||
var exists int
|
||||
err := rw.db.QueryRow("SELECT 1 FROM resources WHERE id = ?", resourceID).Scan(&exists)
|
||||
if err == nil {
|
||||
// Resource exists, update if needed
|
||||
_, err = rw.db.Exec(
|
||||
"UPDATE resources SET host = ?, service_id = ?, updated_at = ? WHERE id = ?",
|
||||
host, serviceID, time.Now(), resourceID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update resource %s: %w", resourceID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create new resource (with placeholder org_id and site_id)
|
||||
_, err = rw.db.Exec(
|
||||
"INSERT INTO resources (id, host, service_id, org_id, site_id) VALUES (?, ?, ?, ?, ?)",
|
||||
resourceID, host, serviceID, "unknown", "unknown",
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create resource %s: %w", resourceID, err)
|
||||
}
|
||||
|
||||
log.Printf("Added new resource: %s (%s)", host, resourceID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetchTraefikConfig fetches the Traefik configuration from Pangolin
|
||||
func (rw *ResourceWatcher) fetchTraefikConfig(ctx context.Context) (*models.PangolinTraefikConfig, error) {
|
||||
// Build the URL
|
||||
url := fmt.Sprintf("%s/traefik-config", rw.pangolinAPI)
|
||||
|
||||
// Create a request with context
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
// Make the request
|
||||
resp, err := rw.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("HTTP request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check status code
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP request returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Read response body with a limit to prevent memory issues
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 10*1024*1024)) // 10MB limit
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
// Parse JSON
|
||||
var config models.PangolinTraefikConfig
|
||||
if err := json.Unmarshal(body, &config); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse JSON: %w", err)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// isSystemRouter checks if a router is a system router (to be skipped)
|
||||
func isSystemRouter(routerID string) bool {
|
||||
systemPrefixes := []string{
|
||||
"api-router",
|
||||
"next-router",
|
||||
"ws-router",
|
||||
}
|
||||
|
||||
for _, prefix := range systemPrefixes {
|
||||
if strings.Contains(routerID, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// extractHostFromRule extracts the host from a Traefik rule
|
||||
// Example: "Host(`example.com`) && PathPrefix(`/api`)" -> "example.com"
|
||||
func extractHostFromRule(rule string) string {
|
||||
if !strings.Contains(rule, "Host(`") {
|
||||
return ""
|
||||
}
|
||||
|
||||
parts := strings.Split(rule, "Host(`")
|
||||
if len(parts) < 2 {
|
||||
return ""
|
||||
}
|
||||
|
||||
host := strings.Split(parts[1], "`)")
|
||||
if len(host) < 1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return host[0]
|
||||
}
|
||||
13
ui/public/index.html
Normal file
13
ui/public/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Middleware Manager</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
1042
ui/src/App.js
Normal file
1042
ui/src/App.js
Normal file
File diff suppressed because it is too large
Load diff
12
ui/src/index.js
Normal file
12
ui/src/index.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// ui/src/index.js
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import './styles/main.css';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
40
ui/src/package.json
Normal file
40
ui/src/package.json
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"name": "middleware-manager-ui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^1.2.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"proxy": "http://localhost:3456"
|
||||
}
|
||||
155
ui/src/styles/main.css
Normal file
155
ui/src/styles/main.css
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
/* Base reset and font settings */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
line-height: 1.5;
|
||||
color: #333;
|
||||
background-color: #f5f7f9;
|
||||
}
|
||||
|
||||
/* Typography basics */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Links */
|
||||
a {
|
||||
color: #1677ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Button styling */
|
||||
button, .btn {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
border-radius: 0.25rem;
|
||||
transition: background-color 0.2s, color 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
button:disabled, .btn:disabled {
|
||||
opacity: 0.65;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Form controls */
|
||||
input, select, textarea {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
/* Table basics */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Status indicators */
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
.status-badge.protected {
|
||||
background-color: #ecfdf5;
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
.status-badge.not-protected {
|
||||
background-color: #ffedd5;
|
||||
color: #c2410c;
|
||||
}
|
||||
|
||||
/* Modal overlay */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
/* Loading animation */
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #1677ff;
|
||||
border-radius: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Container utilities */
|
||||
.container {
|
||||
width: 100%;
|
||||
padding-right: 1rem;
|
||||
padding-left: 1rem;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (min-width: 640px) {
|
||||
.container {
|
||||
max-width: 640px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
max-width: 768px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.container {
|
||||
max-width: 1024px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.container {
|
||||
max-width: 1280px;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue