mirror of
https://github.com/hhftechnology/middleware-manager.git
synced 2026-04-28 03:29:42 +00:00
Add support for external (Traefik) middlewares
Introduce support for Traefik-native external middlewares referenced by resources. - Database: add resource_external_middlewares table in migrations and ensure creation in post-migration updates. - API: add handlers and routes to assign, remove and list external middlewares; include external_middlewares field in GetResources/GetResource responses (comma-separated name:priority:provider entries). Handlers validate resource status, use transactions, and log errors. - Services: ConfigProxy now loads external middleware refs, merges them with internal middlewares sorted by priority when building resource config. - UI: Resource detail component, API client, store and types updated to expose listing, assigning and removing external middlewares with UI controls and confirmation modal. - Tests: add unit tests for assign/remove/list handlers and inclusion on GetResource. This enables referencing middlewares defined outside MW-manager (e.g., Traefik dynamic config or plugins) and honors priority/provider metadata.
This commit is contained in:
parent
a66930b676
commit
fca08e470b
11 changed files with 842 additions and 9 deletions
|
|
@ -171,6 +171,7 @@ func (h *ResourceHandler) GetResources(c *gin.Context) {
|
|||
resource["middlewares"] = ""
|
||||
}
|
||||
|
||||
resource["external_middlewares"] = ""
|
||||
resources = append(resources, resource)
|
||||
}
|
||||
|
||||
|
|
@ -180,6 +181,33 @@ func (h *ResourceHandler) GetResources(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// Batch-load external middlewares for all resources
|
||||
if len(resources) > 0 {
|
||||
extRows, err := h.DB.Query(
|
||||
"SELECT resource_id, middleware_name, priority, provider FROM resource_external_middlewares ORDER BY resource_id, priority DESC",
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("Warning: failed to fetch external middlewares: %v", err)
|
||||
} else {
|
||||
defer extRows.Close()
|
||||
extMap := make(map[string][]string)
|
||||
for extRows.Next() {
|
||||
var resID, name, provider string
|
||||
var priority int
|
||||
if err := extRows.Scan(&resID, &name, &priority, &provider); err != nil {
|
||||
log.Printf("Error scanning external middleware: %v", err)
|
||||
continue
|
||||
}
|
||||
extMap[resID] = append(extMap[resID], fmt.Sprintf("%s:%d:%s", name, priority, provider))
|
||||
}
|
||||
for i, res := range resources {
|
||||
if parts, ok := extMap[res["id"].(string)]; ok {
|
||||
resources[i]["external_middlewares"] = strings.Join(parts, ",")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return paginated or regular response
|
||||
if usePagination {
|
||||
c.JSON(http.StatusOK, NewPaginatedResponse(resources, total, params))
|
||||
|
|
@ -287,6 +315,29 @@ func (h *ResourceHandler) GetResource(c *gin.Context) {
|
|||
resource["middlewares"] = ""
|
||||
}
|
||||
|
||||
// Fetch external (Traefik-native) middlewares assigned to this resource
|
||||
extRows, err := h.DB.Query(
|
||||
"SELECT middleware_name, priority, provider FROM resource_external_middlewares WHERE resource_id = ? ORDER BY priority DESC",
|
||||
id,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("Error fetching external middlewares for resource %s: %v", id, err)
|
||||
resource["external_middlewares"] = ""
|
||||
} else {
|
||||
defer extRows.Close()
|
||||
var extParts []string
|
||||
for extRows.Next() {
|
||||
var name, provider string
|
||||
var priority int
|
||||
if err := extRows.Scan(&name, &priority, &provider); err != nil {
|
||||
log.Printf("Error scanning external middleware row: %v", err)
|
||||
continue
|
||||
}
|
||||
extParts = append(extParts, fmt.Sprintf("%s:%d:%s", name, priority, provider))
|
||||
}
|
||||
resource["external_middlewares"] = strings.Join(extParts, ",")
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, resource)
|
||||
}
|
||||
|
||||
|
|
@ -808,3 +859,209 @@ func (h *ResourceHandler) RemoveMiddleware(c *gin.Context) {
|
|||
log.Printf("Successfully removed middleware %s from resource %s", middlewareID, resourceID)
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Middleware removed from resource successfully"})
|
||||
}
|
||||
|
||||
// AssignExternalMiddleware assigns a Traefik-native middleware to a resource by name
|
||||
func (h *ResourceHandler) AssignExternalMiddleware(c *gin.Context) {
|
||||
resourceID := c.Param("id")
|
||||
if resourceID == "" {
|
||||
ResponseWithError(c, http.StatusBadRequest, "Resource ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
var input struct {
|
||||
MiddlewareName string `json:"middleware_name" binding:"required"`
|
||||
Priority int `json:"priority"`
|
||||
Provider string `json:"provider"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Validate middleware name is not empty after trimming
|
||||
input.MiddlewareName = strings.TrimSpace(input.MiddlewareName)
|
||||
if input.MiddlewareName == "" {
|
||||
ResponseWithError(c, http.StatusBadRequest, "Middleware name is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Default priority
|
||||
if input.Priority <= 0 {
|
||||
input.Priority = 100
|
||||
}
|
||||
|
||||
// Verify resource exists and is active
|
||||
var exists int
|
||||
var status string
|
||||
err := h.DB.QueryRow("SELECT 1, status FROM resources WHERE id = ?", resourceID).Scan(&exists, &status)
|
||||
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
|
||||
}
|
||||
|
||||
if status == "disabled" {
|
||||
ResponseWithError(c, http.StatusBadRequest, "Cannot assign middleware to a disabled resource")
|
||||
return
|
||||
}
|
||||
|
||||
// Insert or update using a transaction
|
||||
tx, err := h.DB.Begin()
|
||||
if err != nil {
|
||||
log.Printf("Error beginning transaction: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Database error")
|
||||
return
|
||||
}
|
||||
|
||||
var txErr error
|
||||
defer func() {
|
||||
if txErr != nil {
|
||||
tx.Rollback()
|
||||
log.Printf("Transaction rolled back due to error: %v", txErr)
|
||||
}
|
||||
}()
|
||||
|
||||
// Delete any existing relationship first
|
||||
_, txErr = tx.Exec(
|
||||
"DELETE FROM resource_external_middlewares WHERE resource_id = ? AND middleware_name = ?",
|
||||
resourceID, input.MiddlewareName,
|
||||
)
|
||||
if txErr != nil {
|
||||
log.Printf("Error removing existing external middleware: %v", txErr)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Database error")
|
||||
return
|
||||
}
|
||||
|
||||
// Insert new relationship
|
||||
_, txErr = tx.Exec(
|
||||
"INSERT INTO resource_external_middlewares (resource_id, middleware_name, priority, provider) VALUES (?, ?, ?, ?)",
|
||||
resourceID, input.MiddlewareName, input.Priority, input.Provider,
|
||||
)
|
||||
if txErr != nil {
|
||||
log.Printf("Error assigning external middleware: %v", txErr)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Failed to assign external middleware")
|
||||
return
|
||||
}
|
||||
|
||||
if txErr = tx.Commit(); txErr != nil {
|
||||
log.Printf("Error committing transaction: %v", txErr)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Database error")
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Successfully assigned external middleware %s to resource %s with priority %d",
|
||||
input.MiddlewareName, resourceID, input.Priority)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"resource_id": resourceID,
|
||||
"middleware_name": input.MiddlewareName,
|
||||
"priority": input.Priority,
|
||||
"provider": input.Provider,
|
||||
})
|
||||
}
|
||||
|
||||
// RemoveExternalMiddleware removes a Traefik-native middleware from a resource
|
||||
func (h *ResourceHandler) RemoveExternalMiddleware(c *gin.Context) {
|
||||
resourceID := c.Param("id")
|
||||
middlewareName := c.Param("name")
|
||||
|
||||
if resourceID == "" || middlewareName == "" {
|
||||
ResponseWithError(c, http.StatusBadRequest, "Resource ID and Middleware name are required")
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Removing external middleware %s from resource %s", middlewareName, resourceID)
|
||||
|
||||
tx, err := h.DB.Begin()
|
||||
if err != nil {
|
||||
log.Printf("Error beginning transaction: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Database error")
|
||||
return
|
||||
}
|
||||
|
||||
var txErr error
|
||||
defer func() {
|
||||
if txErr != nil {
|
||||
tx.Rollback()
|
||||
log.Printf("Transaction rolled back due to error: %v", txErr)
|
||||
}
|
||||
}()
|
||||
|
||||
result, txErr := tx.Exec(
|
||||
"DELETE FROM resource_external_middlewares WHERE resource_id = ? AND middleware_name = ?",
|
||||
resourceID, middlewareName,
|
||||
)
|
||||
if txErr != nil {
|
||||
log.Printf("Error removing external middleware: %v", txErr)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Failed to remove external 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, "External middleware assignment not found")
|
||||
return
|
||||
}
|
||||
|
||||
if txErr = tx.Commit(); txErr != nil {
|
||||
log.Printf("Error committing transaction: %v", txErr)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Database error")
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Successfully removed external middleware %s from resource %s", middlewareName, resourceID)
|
||||
c.JSON(http.StatusOK, gin.H{"message": "External middleware removed from resource successfully"})
|
||||
}
|
||||
|
||||
// GetExternalMiddlewares returns all external middlewares assigned to a resource
|
||||
func (h *ResourceHandler) GetExternalMiddlewares(c *gin.Context) {
|
||||
resourceID := c.Param("id")
|
||||
if resourceID == "" {
|
||||
ResponseWithError(c, http.StatusBadRequest, "Resource ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := h.DB.Query(
|
||||
"SELECT middleware_name, priority, provider, created_at FROM resource_external_middlewares WHERE resource_id = ? ORDER BY priority DESC",
|
||||
resourceID,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("Error fetching external middlewares: %v", err)
|
||||
ResponseWithError(c, http.StatusInternalServerError, "Failed to fetch external middlewares")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var externalMiddlewares []gin.H
|
||||
for rows.Next() {
|
||||
var name, provider string
|
||||
var priority int
|
||||
var createdAt string
|
||||
if err := rows.Scan(&name, &priority, &provider, &createdAt); err != nil {
|
||||
log.Printf("Error scanning external middleware row: %v", err)
|
||||
continue
|
||||
}
|
||||
externalMiddlewares = append(externalMiddlewares, gin.H{
|
||||
"resource_id": resourceID,
|
||||
"middleware_name": name,
|
||||
"priority": priority,
|
||||
"provider": provider,
|
||||
"created_at": createdAt,
|
||||
})
|
||||
}
|
||||
|
||||
if externalMiddlewares == nil {
|
||||
externalMiddlewares = []gin.H{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, externalMiddlewares)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package handlers
|
|||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
|
@ -289,3 +290,229 @@ func TestResourceHandler_GetResources_Empty(t *testing.T) {
|
|||
|
||||
// Empty result is acceptable (nil or empty slice)
|
||||
}
|
||||
|
||||
// TestResourceHandler_AssignExternalMiddleware tests assigning a Traefik-native middleware
|
||||
func TestResourceHandler_AssignExternalMiddleware(t *testing.T) {
|
||||
db := testutil.NewTempDB(t)
|
||||
handler := NewResourceHandler(db.DB)
|
||||
|
||||
testutil.MustExec(t, db, `
|
||||
INSERT INTO resources (id, pangolin_router_id, host, service_id, org_id, site_id, status, source_type)
|
||||
VALUES ('ext-res-1', 'pangolin-ext-1', 'ext.example.com', 'svc-1', 'org-1', 'site-1', 'active', 'pangolin')
|
||||
`)
|
||||
|
||||
body := `{"middleware_name": "my-auth@file", "priority": 150, "provider": "file"}`
|
||||
c, rec := testutil.NewContext(t, http.MethodPost, "/api/resources/ext-res-1/external-middlewares", strings.NewReader(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
c.Params = gin.Params{{Key: "id", Value: "ext-res-1"}}
|
||||
handler.AssignExternalMiddleware(c)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("failed to parse response: %v", err)
|
||||
}
|
||||
|
||||
if resp["middleware_name"] != "my-auth@file" {
|
||||
t.Errorf("expected middleware_name 'my-auth@file', got %v", resp["middleware_name"])
|
||||
}
|
||||
if int(resp["priority"].(float64)) != 150 {
|
||||
t.Errorf("expected priority 150, got %v", resp["priority"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestResourceHandler_AssignExternalMiddleware_MissingName tests empty middleware name
|
||||
func TestResourceHandler_AssignExternalMiddleware_MissingName(t *testing.T) {
|
||||
db := testutil.NewTempDB(t)
|
||||
handler := NewResourceHandler(db.DB)
|
||||
|
||||
testutil.MustExec(t, db, `
|
||||
INSERT INTO resources (id, pangolin_router_id, host, service_id, org_id, site_id, status, source_type)
|
||||
VALUES ('ext-res-2', 'pangolin-ext-2', 'ext2.example.com', 'svc-1', 'org-1', 'site-1', 'active', 'pangolin')
|
||||
`)
|
||||
|
||||
body := `{"priority": 100}`
|
||||
c, rec := testutil.NewContext(t, http.MethodPost, "/api/resources/ext-res-2/external-middlewares", strings.NewReader(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
c.Params = gin.Params{{Key: "id", Value: "ext-res-2"}}
|
||||
handler.AssignExternalMiddleware(c)
|
||||
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestResourceHandler_AssignExternalMiddleware_DisabledResource tests assigning to disabled resource
|
||||
func TestResourceHandler_AssignExternalMiddleware_DisabledResource(t *testing.T) {
|
||||
db := testutil.NewTempDB(t)
|
||||
handler := NewResourceHandler(db.DB)
|
||||
|
||||
testutil.MustExec(t, db, `
|
||||
INSERT INTO resources (id, pangolin_router_id, host, service_id, org_id, site_id, status, source_type)
|
||||
VALUES ('ext-res-3', 'pangolin-ext-3', 'ext3.example.com', 'svc-1', 'org-1', 'site-1', 'disabled', 'pangolin')
|
||||
`)
|
||||
|
||||
body := `{"middleware_name": "test-mw@file", "priority": 100}`
|
||||
c, rec := testutil.NewContext(t, http.MethodPost, "/api/resources/ext-res-3/external-middlewares", strings.NewReader(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
c.Params = gin.Params{{Key: "id", Value: "ext-res-3"}}
|
||||
handler.AssignExternalMiddleware(c)
|
||||
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestResourceHandler_RemoveExternalMiddleware tests removing an external middleware
|
||||
func TestResourceHandler_RemoveExternalMiddleware(t *testing.T) {
|
||||
db := testutil.NewTempDB(t)
|
||||
handler := NewResourceHandler(db.DB)
|
||||
|
||||
testutil.MustExec(t, db, `
|
||||
INSERT INTO resources (id, pangolin_router_id, host, service_id, org_id, site_id, status, source_type)
|
||||
VALUES ('ext-res-4', 'pangolin-ext-4', 'ext4.example.com', 'svc-1', 'org-1', 'site-1', 'active', 'pangolin')
|
||||
`)
|
||||
testutil.MustExec(t, db, `
|
||||
INSERT INTO resource_external_middlewares (resource_id, middleware_name, priority, provider)
|
||||
VALUES ('ext-res-4', 'test-mw@file', 100, 'file')
|
||||
`)
|
||||
|
||||
c, rec := testutil.NewContext(t, http.MethodDelete, "/api/resources/ext-res-4/external-middlewares/test-mw@file", nil)
|
||||
c.Params = gin.Params{
|
||||
{Key: "id", Value: "ext-res-4"},
|
||||
{Key: "name", Value: "test-mw@file"},
|
||||
}
|
||||
handler.RemoveExternalMiddleware(c)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
// Verify it was removed
|
||||
var count int
|
||||
db.DB.QueryRow("SELECT COUNT(*) FROM resource_external_middlewares WHERE resource_id = 'ext-res-4'").Scan(&count)
|
||||
if count != 0 {
|
||||
t.Error("external middleware was not removed from database")
|
||||
}
|
||||
}
|
||||
|
||||
// TestResourceHandler_RemoveExternalMiddleware_NotFound tests removing non-existent assignment
|
||||
func TestResourceHandler_RemoveExternalMiddleware_NotFound(t *testing.T) {
|
||||
db := testutil.NewTempDB(t)
|
||||
handler := NewResourceHandler(db.DB)
|
||||
|
||||
c, rec := testutil.NewContext(t, http.MethodDelete, "/api/resources/ext-res-5/external-middlewares/nonexistent", nil)
|
||||
c.Params = gin.Params{
|
||||
{Key: "id", Value: "ext-res-5"},
|
||||
{Key: "name", Value: "nonexistent"},
|
||||
}
|
||||
handler.RemoveExternalMiddleware(c)
|
||||
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Errorf("expected 404, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestResourceHandler_GetExternalMiddlewares tests listing external middlewares
|
||||
func TestResourceHandler_GetExternalMiddlewares(t *testing.T) {
|
||||
db := testutil.NewTempDB(t)
|
||||
handler := NewResourceHandler(db.DB)
|
||||
|
||||
testutil.MustExec(t, db, `
|
||||
INSERT INTO resources (id, pangolin_router_id, host, service_id, org_id, site_id, status, source_type)
|
||||
VALUES ('ext-res-6', 'pangolin-ext-6', 'ext6.example.com', 'svc-1', 'org-1', 'site-1', 'active', 'pangolin')
|
||||
`)
|
||||
testutil.MustExec(t, db, `
|
||||
INSERT INTO resource_external_middlewares (resource_id, middleware_name, priority, provider)
|
||||
VALUES ('ext-res-6', 'auth@file', 200, 'file')
|
||||
`)
|
||||
testutil.MustExec(t, db, `
|
||||
INSERT INTO resource_external_middlewares (resource_id, middleware_name, priority, provider)
|
||||
VALUES ('ext-res-6', 'rate-limit@docker', 100, 'docker')
|
||||
`)
|
||||
|
||||
c, rec := testutil.NewContext(t, http.MethodGet, "/api/resources/ext-res-6/external-middlewares", nil)
|
||||
c.Params = gin.Params{{Key: "id", Value: "ext-res-6"}}
|
||||
handler.GetExternalMiddlewares(c)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
var extMiddlewares []map[string]interface{}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &extMiddlewares); err != nil {
|
||||
t.Fatalf("failed to parse response: %v", err)
|
||||
}
|
||||
|
||||
if len(extMiddlewares) != 2 {
|
||||
t.Fatalf("expected 2 external middlewares, got %d", len(extMiddlewares))
|
||||
}
|
||||
|
||||
// Should be sorted by priority DESC (200 first, then 100)
|
||||
if extMiddlewares[0]["middleware_name"] != "auth@file" {
|
||||
t.Errorf("expected first middleware to be 'auth@file', got %v", extMiddlewares[0]["middleware_name"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestResourceHandler_GetExternalMiddlewares_Empty tests empty result
|
||||
func TestResourceHandler_GetExternalMiddlewares_Empty(t *testing.T) {
|
||||
db := testutil.NewTempDB(t)
|
||||
handler := NewResourceHandler(db.DB)
|
||||
|
||||
c, rec := testutil.NewContext(t, http.MethodGet, "/api/resources/ext-res-empty/external-middlewares", nil)
|
||||
c.Params = gin.Params{{Key: "id", Value: "ext-res-empty"}}
|
||||
handler.GetExternalMiddlewares(c)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
var extMiddlewares []map[string]interface{}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &extMiddlewares); err != nil {
|
||||
t.Fatalf("failed to parse response: %v", err)
|
||||
}
|
||||
|
||||
if len(extMiddlewares) != 0 {
|
||||
t.Errorf("expected 0 external middlewares, got %d", len(extMiddlewares))
|
||||
}
|
||||
}
|
||||
|
||||
// TestResourceHandler_GetResource_IncludesExternalMiddlewares tests that GetResource includes external_middlewares
|
||||
func TestResourceHandler_GetResource_IncludesExternalMiddlewares(t *testing.T) {
|
||||
db := testutil.NewTempDB(t)
|
||||
handler := NewResourceHandler(db.DB)
|
||||
|
||||
testutil.MustExec(t, db, `
|
||||
INSERT INTO resources (id, pangolin_router_id, host, service_id, org_id, site_id, status, source_type, router_priority)
|
||||
VALUES ('ext-res-7', 'pangolin-ext-7', 'ext7.example.com', 'svc-1', 'org-1', 'site-1', 'active', 'pangolin', 100)
|
||||
`)
|
||||
testutil.MustExec(t, db, `
|
||||
INSERT INTO resource_external_middlewares (resource_id, middleware_name, priority, provider)
|
||||
VALUES ('ext-res-7', 'my-plugin@file', 150, 'file')
|
||||
`)
|
||||
|
||||
c, rec := testutil.NewContext(t, http.MethodGet, "/api/resources/ext-res-7", nil)
|
||||
c.Params = gin.Params{{Key: "id", Value: "ext-res-7"}}
|
||||
handler.GetResource(c)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
var resource map[string]interface{}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resource); err != nil {
|
||||
t.Fatalf("failed to parse response: %v", err)
|
||||
}
|
||||
|
||||
extMws, ok := resource["external_middlewares"].(string)
|
||||
if !ok {
|
||||
t.Fatalf("external_middlewares field not found or not a string")
|
||||
}
|
||||
|
||||
if extMws != "my-plugin@file:150:file" {
|
||||
t.Errorf("expected 'my-plugin@file:150:file', got '%s'", extMws)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -186,6 +186,11 @@ func (s *Server) setupRoutes(uiPath string) {
|
|||
resources.POST("/:id/middlewares/bulk", s.resourceHandler.AssignMultipleMiddlewares)
|
||||
resources.DELETE("/:id/middlewares/:middlewareId", s.resourceHandler.RemoveMiddleware)
|
||||
|
||||
// External (Traefik-native) middleware assignments
|
||||
resources.GET("/:id/external-middlewares", s.resourceHandler.GetExternalMiddlewares)
|
||||
resources.POST("/:id/external-middlewares", s.resourceHandler.AssignExternalMiddleware)
|
||||
resources.DELETE("/:id/external-middlewares/:name", s.resourceHandler.RemoveExternalMiddleware)
|
||||
|
||||
// Service assignments
|
||||
resources.GET("/:id/service", s.serviceHandler.GetResourceService)
|
||||
resources.POST("/:id/service", s.serviceHandler.AssignServiceToResource)
|
||||
|
|
|
|||
|
|
@ -617,6 +617,35 @@ func runPostMigrationUpdates(db *sql.DB) error {
|
|||
// Create index on services status for faster filtering
|
||||
_, _ = db.Exec("CREATE INDEX IF NOT EXISTS idx_services_status ON services(status)")
|
||||
|
||||
// Check for resource_external_middlewares table (for Traefik-native middleware references)
|
||||
var hasExternalMiddlewaresTable bool
|
||||
err = db.QueryRow(`
|
||||
SELECT COUNT(*) > 0
|
||||
FROM sqlite_master
|
||||
WHERE type='table' AND name='resource_external_middlewares'
|
||||
`).Scan(&hasExternalMiddlewaresTable)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check if resource_external_middlewares table exists: %w", err)
|
||||
}
|
||||
if !hasExternalMiddlewaresTable {
|
||||
log.Println("Creating resource_external_middlewares table")
|
||||
_, err = db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS resource_external_middlewares (
|
||||
resource_id TEXT NOT NULL,
|
||||
middleware_name TEXT NOT NULL,
|
||||
priority INTEGER NOT NULL DEFAULT 100,
|
||||
provider TEXT DEFAULT '',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (resource_id, middleware_name),
|
||||
FOREIGN KEY (resource_id) REFERENCES resources(id) ON DELETE CASCADE
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create resource_external_middlewares table: %w", err)
|
||||
}
|
||||
log.Println("Successfully created resource_external_middlewares table")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -155,4 +155,16 @@ CREATE TABLE IF NOT EXISTS security_config (
|
|||
);
|
||||
|
||||
-- Initialize security config singleton row
|
||||
INSERT OR IGNORE INTO security_config (id) VALUES (1);
|
||||
INSERT OR IGNORE INTO security_config (id) VALUES (1);
|
||||
|
||||
-- External middlewares table stores references to Traefik-native middlewares assigned to resources
|
||||
-- These are middlewares defined in Traefik dynamic config or plugins (not managed by MW-manager)
|
||||
CREATE TABLE IF NOT EXISTS resource_external_middlewares (
|
||||
resource_id TEXT NOT NULL,
|
||||
middleware_name TEXT NOT NULL,
|
||||
priority INTEGER NOT NULL DEFAULT 100,
|
||||
provider TEXT DEFAULT '',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (resource_id, middleware_name),
|
||||
FOREIGN KEY (resource_id) REFERENCES resources(id) ON DELETE CASCADE
|
||||
);
|
||||
|
|
@ -80,6 +80,11 @@ type middlewareWithPriority struct {
|
|||
Priority int
|
||||
}
|
||||
|
||||
type externalMiddlewareRef struct {
|
||||
Name string
|
||||
Priority int
|
||||
}
|
||||
|
||||
type mtlsConfigData struct {
|
||||
CACertPath string
|
||||
Rules []interface{}
|
||||
|
|
@ -109,6 +114,7 @@ type resourceData struct {
|
|||
TLSHardeningEnabled bool
|
||||
SecureHeadersEnabled bool
|
||||
Middlewares []middlewareWithPriority
|
||||
ExternalMiddlewares []externalMiddlewareRef
|
||||
CustomServiceID sql.NullString
|
||||
}
|
||||
|
||||
|
|
@ -501,11 +507,6 @@ func (cp *ConfigProxy) applyResourceOverrides(config *ProxiedTraefikConfig, reso
|
|||
continue
|
||||
}
|
||||
|
||||
// Sort middlewares by priority (highest first)
|
||||
sort.SliceStable(resource.Middlewares, func(i, j int) bool {
|
||||
return resource.Middlewares[i].Priority > resource.Middlewares[j].Priority
|
||||
})
|
||||
|
||||
// Build middleware list (mTLS first, then secure headers, then custom headers, then assigned)
|
||||
var newMiddlewares []string
|
||||
|
||||
|
|
@ -561,9 +562,26 @@ func (cp *ConfigProxy) applyResourceOverrides(config *ProxiedTraefikConfig, reso
|
|||
}
|
||||
}
|
||||
|
||||
// Add assigned middlewares
|
||||
// Build a combined list of internal + external middlewares sorted by priority
|
||||
type middlewareEntry struct {
|
||||
Name string
|
||||
Priority int
|
||||
}
|
||||
var allAssigned []middlewareEntry
|
||||
for _, mw := range resource.Middlewares {
|
||||
newMiddlewares = append(newMiddlewares, mw.ID)
|
||||
allAssigned = append(allAssigned, middlewareEntry{Name: mw.ID, Priority: mw.Priority})
|
||||
}
|
||||
for _, ext := range resource.ExternalMiddlewares {
|
||||
allAssigned = append(allAssigned, middlewareEntry{Name: ext.Name, Priority: ext.Priority})
|
||||
}
|
||||
// Sort by priority (highest first) for consistent ordering
|
||||
sort.SliceStable(allAssigned, func(i, j int) bool {
|
||||
return allAssigned[i].Priority > allAssigned[j].Priority
|
||||
})
|
||||
|
||||
// Add all assigned middlewares (internal by ID, external by name)
|
||||
for _, entry := range allAssigned {
|
||||
newMiddlewares = append(newMiddlewares, entry.Name)
|
||||
}
|
||||
|
||||
// Get existing middlewares from router
|
||||
|
|
@ -796,6 +814,30 @@ func (cp *ConfigProxy) fetchResourceData() ([]*resourceData, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Load external (Traefik-native) middleware assignments
|
||||
extRows, err := cp.db.Query(
|
||||
"SELECT resource_id, middleware_name, priority FROM resource_external_middlewares ORDER BY resource_id, priority DESC",
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("Warning: failed to fetch external middlewares: %v", err)
|
||||
} else {
|
||||
defer extRows.Close()
|
||||
for extRows.Next() {
|
||||
var resID, name string
|
||||
var priority int
|
||||
if err := extRows.Scan(&resID, &name, &priority); err != nil {
|
||||
log.Printf("Failed to scan external middleware: %v", err)
|
||||
continue
|
||||
}
|
||||
if data, ok := resourceMap[resID]; ok {
|
||||
data.ExternalMiddlewares = append(data.ExternalMiddlewares, externalMiddlewareRef{
|
||||
Name: name,
|
||||
Priority: priority,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resources := make([]*resourceData, 0, len(resourceMap))
|
||||
for _, r := range resourceMap {
|
||||
resources = append(resources, r)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useMiddlewareStore } from '@/stores/middlewareStore'
|
|||
import { useServiceStore } from '@/stores/serviceStore'
|
||||
import { useMTLSStore } from '@/stores/mtlsStore'
|
||||
import { useSecurityStore } from '@/stores/securityStore'
|
||||
import { useTraefikStore } from '@/stores/traefikStore'
|
||||
import { useAppStore } from '@/stores/appStore'
|
||||
import { resourceApi } from '@/services/api'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
|
|
@ -51,6 +52,7 @@ import {
|
|||
Save,
|
||||
X,
|
||||
Loader2,
|
||||
Globe,
|
||||
} from 'lucide-react'
|
||||
import { parseJSON } from '@/lib/utils'
|
||||
import type { MTLSWhitelistExternalData } from '@/types'
|
||||
|
|
@ -71,12 +73,15 @@ export function ResourceDetail() {
|
|||
updateTCPConfig,
|
||||
updateRouterPriority,
|
||||
clearError,
|
||||
assignExternalMiddleware,
|
||||
removeExternalMiddleware,
|
||||
} = useResourceStore()
|
||||
|
||||
const { middlewares, fetchMiddlewares } = useMiddlewareStore()
|
||||
const { services, fetchServices } = useServiceStore()
|
||||
const { config: mtlsConfig, fetchConfig: fetchMTLSConfig } = useMTLSStore()
|
||||
const { config: securityConfig, fetchConfig: fetchSecurityConfig } = useSecurityStore()
|
||||
const { httpMiddlewares, fetchMiddlewares: fetchTraefikMiddlewares } = useTraefikStore()
|
||||
const { updateMTLSConfig, updateMTLSWhitelistConfig } = useResourceStore()
|
||||
|
||||
const [selectedMiddlewareId, setSelectedMiddlewareId] = useState('')
|
||||
|
|
@ -84,6 +89,13 @@ export function ResourceDetail() {
|
|||
const [selectedServiceId, setSelectedServiceId] = useState('')
|
||||
const [removeMiddlewareModal, setRemoveMiddlewareModal] = useState<string | null>(null)
|
||||
|
||||
// External middleware state
|
||||
const [selectedExternalMw, setSelectedExternalMw] = useState('')
|
||||
const [customExternalMwName, setCustomExternalMwName] = useState('')
|
||||
const [externalMwPriority, setExternalMwPriority] = useState('100')
|
||||
const [removeExternalMwModal, setRemoveExternalMwModal] = useState<string | null>(null)
|
||||
const [useCustomExternalName, setUseCustomExternalName] = useState(false)
|
||||
|
||||
// Edit priority dialog state
|
||||
const [editPriorityDialog, setEditPriorityDialog] = useState<{ middlewareId: string; currentPriority: number } | null>(null)
|
||||
const [newPriorityValue, setNewPriorityValue] = useState('')
|
||||
|
|
@ -118,8 +130,9 @@ export function ResourceDetail() {
|
|||
fetchServices()
|
||||
fetchMTLSConfig()
|
||||
fetchSecurityConfig()
|
||||
fetchTraefikMiddlewares('http')
|
||||
}
|
||||
}, [resourceId, fetchResource, fetchMiddlewares, fetchServices, fetchMTLSConfig, fetchSecurityConfig])
|
||||
}, [resourceId, fetchResource, fetchMiddlewares, fetchServices, fetchMTLSConfig, fetchSecurityConfig, fetchTraefikMiddlewares])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedResource) return
|
||||
|
|
@ -173,6 +186,18 @@ export function ResourceDetail() {
|
|||
}).sort((a, b) => b.priority - a.priority) // Sort by priority descending (higher priority first)
|
||||
: []
|
||||
|
||||
// Parse external middleware assignments from the format "name:priority:provider,..."
|
||||
const assignedExternalMiddlewares = selectedResource.external_middlewares
|
||||
? selectedResource.external_middlewares.split(',').filter(Boolean).map(mwStr => {
|
||||
const parts = mwStr.split(':')
|
||||
return {
|
||||
name: parts[0] || '',
|
||||
priority: parseInt(parts[1] || '100', 10) || 100,
|
||||
provider: parts[2] || '',
|
||||
}
|
||||
}).sort((a, b) => b.priority - a.priority)
|
||||
: []
|
||||
|
||||
const customHeaders = parseJSON<Record<string, string>>(
|
||||
selectedResource.custom_headers,
|
||||
{}
|
||||
|
|
@ -208,11 +233,40 @@ export function ResourceDetail() {
|
|||
}
|
||||
}
|
||||
|
||||
const handleAssignExternalMiddleware = async () => {
|
||||
const name = useCustomExternalName ? customExternalMwName.trim() : selectedExternalMw
|
||||
if (!name || !resourceId) return
|
||||
|
||||
// Try to find provider info from Traefik data
|
||||
const traefikMw = httpMiddlewares.find(m => m.name === name)
|
||||
await assignExternalMiddleware(resourceId, {
|
||||
middleware_name: name,
|
||||
priority: parseInt(externalMwPriority, 10) || 100,
|
||||
provider: traefikMw?.provider || '',
|
||||
})
|
||||
setSelectedExternalMw('')
|
||||
setCustomExternalMwName('')
|
||||
setExternalMwPriority('100')
|
||||
}
|
||||
|
||||
const handleRemoveExternalMiddleware = async (name: string) => {
|
||||
if (resourceId) {
|
||||
await removeExternalMiddleware(resourceId, name)
|
||||
}
|
||||
setRemoveExternalMwModal(null)
|
||||
}
|
||||
|
||||
const assignedMiddlewareIds = assignedMiddlewares.map(m => m.id)
|
||||
const availableMiddlewares = middlewares.filter(
|
||||
(m) => !assignedMiddlewareIds.includes(m.id)
|
||||
)
|
||||
|
||||
// Filter Traefik middlewares: exclude already-assigned external ones and MW-manager's own
|
||||
const assignedExternalNames = assignedExternalMiddlewares.map(m => m.name)
|
||||
const availableExternalMiddlewares = httpMiddlewares.filter(
|
||||
(m) => !assignedExternalNames.includes(m.name)
|
||||
)
|
||||
|
||||
// Start editing configuration
|
||||
const handleStartEditConfig = () => {
|
||||
if (selectedResource) {
|
||||
|
|
@ -1061,6 +1115,132 @@ export function ResourceDetail() {
|
|||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* External / Traefik-Native Middlewares Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Globe className="h-5 w-5" />
|
||||
External Middlewares
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{assignedExternalMiddlewares.length} Traefik-native middleware(s) assigned
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Add External Middleware */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-sm font-medium whitespace-nowrap">
|
||||
{useCustomExternalName ? 'Custom name:' : 'From Traefik:'}
|
||||
</Label>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setUseCustomExternalName(!useCustomExternalName)
|
||||
setSelectedExternalMw('')
|
||||
setCustomExternalMwName('')
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
{useCustomExternalName ? 'Switch to dropdown' : 'Enter custom name'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{useCustomExternalName ? (
|
||||
<Input
|
||||
placeholder="e.g., my-auth@file or plugin-ratelimit@docker"
|
||||
value={customExternalMwName}
|
||||
onChange={(e) => setCustomExternalMwName(e.target.value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
) : (
|
||||
<Select value={selectedExternalMw} onValueChange={setSelectedExternalMw}>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue placeholder="Select Traefik middleware" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableExternalMiddlewares.map((mw) => (
|
||||
<SelectItem key={mw.name} value={mw.name}>
|
||||
{mw.name} {mw.provider ? `(${mw.provider})` : ''} {mw.type ? `[${mw.type}]` : ''}
|
||||
</SelectItem>
|
||||
))}
|
||||
{availableExternalMiddlewares.length === 0 && (
|
||||
<div className="px-2 py-1.5 text-sm text-muted-foreground">
|
||||
No external middlewares available
|
||||
</div>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Priority"
|
||||
value={externalMwPriority}
|
||||
onChange={(e) => setExternalMwPriority(e.target.value)}
|
||||
className="w-24"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleAssignExternalMiddleware}
|
||||
disabled={useCustomExternalName ? !customExternalMwName.trim() : !selectedExternalMw}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Assigned External Middlewares Table */}
|
||||
{assignedExternalMiddlewares.length > 0 ? (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-12">#</TableHead>
|
||||
<TableHead>Middleware Name</TableHead>
|
||||
<TableHead>Provider</TableHead>
|
||||
<TableHead>Priority</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{assignedExternalMiddlewares.map((mw, index) => (
|
||||
<TableRow key={mw.name}>
|
||||
<TableCell className="text-muted-foreground">{index + 1}</TableCell>
|
||||
<TableCell className="font-mono text-sm">{mw.name}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline" className="bg-blue-50 dark:bg-blue-950 text-blue-700 dark:text-blue-300">
|
||||
{mw.provider || 'external'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="secondary">{mw.priority}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setRemoveExternalMwModal(mw.name)}
|
||||
title="Remove external middleware"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : (
|
||||
<div className="text-center py-8 text-muted-foreground border rounded-lg">
|
||||
No external middlewares assigned. Use this section to reference middlewares defined in Traefik's dynamic config or plugins.
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Custom Headers Card */}
|
||||
{Object.keys(customHeaders).length > 0 && (
|
||||
<Card>
|
||||
|
|
@ -1102,6 +1282,17 @@ export function ResourceDetail() {
|
|||
onConfirm={() => removeMiddlewareModal && handleRemoveMiddleware(removeMiddlewareModal)}
|
||||
/>
|
||||
|
||||
{/* Remove External Middleware Confirmation */}
|
||||
<ConfirmationModal
|
||||
open={!!removeExternalMwModal}
|
||||
onOpenChange={(open) => !open && setRemoveExternalMwModal(null)}
|
||||
title="Remove External Middleware"
|
||||
description={`Are you sure you want to remove the external middleware "${removeExternalMwModal}" from this resource?`}
|
||||
confirmLabel="Remove"
|
||||
variant="destructive"
|
||||
onConfirm={() => removeExternalMwModal && handleRemoveExternalMiddleware(removeExternalMwModal)}
|
||||
/>
|
||||
|
||||
{/* Edit Priority Dialog */}
|
||||
<Dialog
|
||||
open={!!editPriorityDialog}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import type {
|
|||
CreateServiceRequest,
|
||||
UpdateServiceRequest,
|
||||
AssignMiddlewareRequest,
|
||||
AssignExternalMiddlewareRequest,
|
||||
ExternalMiddleware,
|
||||
AssignServiceRequest,
|
||||
HTTPConfig,
|
||||
TLSConfig,
|
||||
|
|
@ -141,6 +143,24 @@ export const resourceApi = {
|
|||
{ method: 'DELETE' }
|
||||
),
|
||||
|
||||
// External (Traefik-native) middleware assignment
|
||||
getExternalMiddlewares: (resourceId: string) =>
|
||||
request<ExternalMiddleware[]>(
|
||||
`${API_BASE}/resources/${encodeURIComponent(resourceId)}/external-middlewares`
|
||||
),
|
||||
|
||||
assignExternalMiddleware: (resourceId: string, data: AssignExternalMiddlewareRequest) =>
|
||||
request<void>(`${API_BASE}/resources/${encodeURIComponent(resourceId)}/external-middlewares`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
removeExternalMiddleware: (resourceId: string, name: string) =>
|
||||
request<void>(
|
||||
`${API_BASE}/resources/${encodeURIComponent(resourceId)}/external-middlewares/${encodeURIComponent(name)}`,
|
||||
{ method: 'DELETE' }
|
||||
),
|
||||
|
||||
// Service assignment
|
||||
getService: (resourceId: string) =>
|
||||
request<Service>(`${API_BASE}/resources/${encodeURIComponent(resourceId)}/service`),
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { resourceApi } from '@/services/api'
|
|||
import type {
|
||||
Resource,
|
||||
AssignMiddlewareRequest,
|
||||
AssignExternalMiddlewareRequest,
|
||||
HTTPConfig,
|
||||
TLSConfig,
|
||||
TCPConfig,
|
||||
|
|
@ -29,6 +30,8 @@ interface ResourceState {
|
|||
deleteDisabledResources: (ids: string[]) => Promise<boolean>
|
||||
assignMiddleware: (resourceId: string, data: AssignMiddlewareRequest) => Promise<boolean>
|
||||
removeMiddleware: (resourceId: string, middlewareId: string) => Promise<boolean>
|
||||
assignExternalMiddleware: (resourceId: string, data: AssignExternalMiddlewareRequest) => Promise<boolean>
|
||||
removeExternalMiddleware: (resourceId: string, name: string) => Promise<boolean>
|
||||
assignService: (resourceId: string, serviceId: string) => Promise<boolean>
|
||||
removeService: (resourceId: string) => Promise<boolean>
|
||||
updateHTTPConfig: (resourceId: string, config: HTTPConfig) => Promise<boolean>
|
||||
|
|
@ -149,6 +152,36 @@ export const useResourceStore = create<ResourceState>((set, get) => ({
|
|||
}
|
||||
},
|
||||
|
||||
// Assign external (Traefik-native) middleware to resource
|
||||
assignExternalMiddleware: async (resourceId, data) => {
|
||||
set({ error: null })
|
||||
try {
|
||||
await resourceApi.assignExternalMiddleware(resourceId, data)
|
||||
await get().fetchResource(resourceId)
|
||||
return true
|
||||
} catch (err) {
|
||||
set({
|
||||
error: err instanceof Error ? err.message : 'Failed to assign external middleware',
|
||||
})
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
// Remove external middleware from resource
|
||||
removeExternalMiddleware: async (resourceId, name) => {
|
||||
set({ error: null })
|
||||
try {
|
||||
await resourceApi.removeExternalMiddleware(resourceId, name)
|
||||
await get().fetchResource(resourceId)
|
||||
return true
|
||||
} catch (err) {
|
||||
set({
|
||||
error: err instanceof Error ? err.message : 'Failed to remove external middleware',
|
||||
})
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
// Assign service to resource
|
||||
assignService: async (resourceId, serviceId) => {
|
||||
set({ error: null })
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ export type {
|
|||
TCPConfig,
|
||||
HeadersConfig,
|
||||
AssignMiddlewareRequest,
|
||||
AssignExternalMiddlewareRequest,
|
||||
ExternalMiddleware,
|
||||
AssignServiceRequest,
|
||||
MTLSWhitelistConfigRequest,
|
||||
MTLSWhitelistExternalData,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export interface Resource {
|
|||
tls_hardening_enabled: boolean
|
||||
secure_headers_enabled: boolean
|
||||
middlewares: string
|
||||
external_middlewares: string
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
|
@ -80,6 +81,20 @@ export interface AssignMiddlewareRequest {
|
|||
priority: number
|
||||
}
|
||||
|
||||
export interface AssignExternalMiddlewareRequest {
|
||||
middleware_name: string
|
||||
priority: number
|
||||
provider?: string
|
||||
}
|
||||
|
||||
export interface ExternalMiddleware {
|
||||
resource_id: string
|
||||
middleware_name: string
|
||||
priority: number
|
||||
provider: string
|
||||
created_at?: string
|
||||
}
|
||||
|
||||
export interface AssignServiceRequest {
|
||||
service_id: string
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue