Skip to content

Commit e71293c

Browse files
committed
feat: add deploy_mode field to namespace and implement sandbox testing for nginx config #1350
1 parent de0467b commit e71293c

File tree

44 files changed

+1448
-387
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1448
-387
lines changed

api/cluster/namespace.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ func AddNamespace(c *gin.Context) {
8282
"sync_node_ids": "omitempty",
8383
"post_sync_action": "omitempty,oneof=" + model.PostSyncActionNone + " " + model.PostSyncActionReloadNginx,
8484
"upstream_test_type": "omitempty,oneof=" + model.UpstreamTestLocal + " " + model.UpstreamTestRemote + " " + model.UpstreamTestMirror,
85+
"deploy_mode": "omitempty,oneof=" + model.DeployModeLocal + " " + model.DeployModeRemote,
8586
}).
8687
Create()
8788
}
@@ -93,6 +94,7 @@ func ModifyNamespace(c *gin.Context) {
9394
"sync_node_ids": "omitempty",
9495
"post_sync_action": "omitempty,oneof=" + model.PostSyncActionNone + " " + model.PostSyncActionReloadNginx,
9596
"upstream_test_type": "omitempty,oneof=" + model.UpstreamTestLocal + " " + model.UpstreamTestRemote + " " + model.UpstreamTestMirror,
97+
"deploy_mode": "omitempty,oneof=" + model.DeployModeLocal + " " + model.DeployModeRemote,
9698
}).
9799
Modify()
98100
}

api/nginx/control.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import (
44
"net/http"
55

66
"github.com/0xJacky/Nginx-UI/internal/nginx"
7+
"github.com/0xJacky/Nginx-UI/query"
78
"github.com/gin-gonic/gin"
9+
"github.com/uozi-tech/cosy"
810
)
911

1012
// Reload reloads the nginx
@@ -21,6 +23,67 @@ func TestConfig(c *gin.Context) {
2123
})
2224
}
2325

26+
// TestConfigWithNamespace tests nginx config in isolated sandbox for a specific namespace
27+
func TestConfigWithNamespace(c *gin.Context) {
28+
var req struct {
29+
NamespaceID uint64 `json:"namespace_id" form:"namespace_id"`
30+
}
31+
32+
if !cosy.BindAndValid(c, &req) {
33+
return
34+
}
35+
36+
// Get namespace and related configs
37+
var namespaceInfo *nginx.NamespaceInfo
38+
var sitePaths []string
39+
var streamPaths []string
40+
41+
if req.NamespaceID > 0 {
42+
// Fetch namespace
43+
ns := query.Namespace
44+
namespace, err := ns.Where(ns.ID.Eq(req.NamespaceID)).First()
45+
if err != nil {
46+
cosy.ErrHandler(c, err)
47+
return
48+
}
49+
50+
namespaceInfo = &nginx.NamespaceInfo{
51+
ID: namespace.ID,
52+
Name: namespace.Name,
53+
DeployMode: namespace.DeployMode,
54+
}
55+
56+
// Fetch sites belonging to this namespace
57+
s := query.Site
58+
sites, err := s.Where(s.NamespaceID.Eq(req.NamespaceID)).Find()
59+
if err == nil {
60+
for _, site := range sites {
61+
sitePaths = append(sitePaths, site.Path)
62+
}
63+
}
64+
65+
// Fetch streams belonging to this namespace
66+
st := query.Stream
67+
streams, err := st.Where(st.NamespaceID.Eq(req.NamespaceID)).Find()
68+
if err == nil {
69+
for _, stream := range streams {
70+
streamPaths = append(streamPaths, stream.Path)
71+
}
72+
}
73+
}
74+
75+
// Use sandbox test with namespace-specific paths
76+
result := nginx.Control(func() (string, error) {
77+
return nginx.SandboxTestConfigWithPaths(namespaceInfo, sitePaths, streamPaths)
78+
})
79+
80+
c.JSON(http.StatusOK, gin.H{
81+
"message": result.GetOutput(),
82+
"level": result.GetLevel(),
83+
"namespace_id": req.NamespaceID,
84+
})
85+
}
86+
2487
// Restart restarts the nginx
2588
func Restart(c *gin.Context) {
2689
c.JSON(http.StatusOK, gin.H{

api/nginx/router.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ func InitRouter(r *gin.RouterGroup) {
1212
r.POST("nginx/reload", Reload)
1313
r.POST("nginx/restart", Restart)
1414
r.POST("nginx/test", TestConfig)
15+
r.POST("nginx/test_namespace", TestConfigWithNamespace)
1516
r.GET("nginx/status", Status)
1617
// Get detailed Nginx status information, including connection count, process information, etc. (Issue #850)
1718
r.GET("nginx/detail_status", GetDetailStatus)

api/sites/list.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"net/http"
55

66
"github.com/0xJacky/Nginx-UI/internal/site"
7+
"github.com/0xJacky/Nginx-UI/model"
78
"github.com/0xJacky/Nginx-UI/query"
89
"github.com/gin-gonic/gin"
910
"github.com/spf13/cast"
@@ -23,12 +24,20 @@ func GetSiteList(c *gin.Context) {
2324

2425
// Get sites from database
2526
s := query.Site
26-
sTx := s.Preload(s.Namespace)
27-
if options.NamespaceID != 0 {
28-
sTx = sTx.Where(s.NamespaceID.Eq(options.NamespaceID))
29-
}
27+
db := cosy.UseDB(c)
28+
29+
var sites []*model.Site
30+
var err error
3031

31-
sites, err := sTx.Find()
32+
if options.NamespaceID == 0 {
33+
// Local tab: no namespace OR deploy_mode='local'
34+
err = db.Where("namespace_id IS NULL OR namespace_id IN (?)",
35+
db.Model(&model.Namespace{}).Where("deploy_mode = ?", "local").Select("id"),
36+
).Preload("Namespace").Find(&sites).Error
37+
} else {
38+
// Remote tab: specific namespace
39+
sites, err = s.Where(s.NamespaceID.Eq(options.NamespaceID)).Preload(s.Namespace).Find()
40+
}
3241
if err != nil {
3342
cosy.ErrHandler(c, err)
3443
return

api/streams/streams.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,23 @@ func GetStreams(c *gin.Context) {
9393

9494
// Get streams with optional filtering
9595
var streams []*model.Stream
96-
if options.NamespaceID != 0 {
97-
streams, err = s.Where(s.NamespaceID.Eq(options.NamespaceID)).Find()
96+
if options.NamespaceID == 0 {
97+
// Local tab: no namespace OR deploy_mode='local'
98+
localNamespaceIDs := lo.Map(lo.Filter(namespaces, func(item *model.Namespace, _ int) bool {
99+
return item.DeployMode == "local"
100+
}), func(item *model.Namespace, _ int) uint64 {
101+
return item.ID
102+
})
103+
104+
db := cosy.UseDB(c)
105+
if len(localNamespaceIDs) > 0 {
106+
err = db.Where("namespace_id IS NULL OR namespace_id IN (?)", localNamespaceIDs).Find(&streams).Error
107+
} else {
108+
err = db.Where("namespace_id IS NULL").Find(&streams).Error
109+
}
98110
} else {
99-
streams, err = s.Find()
111+
// Remote tab: specific namespace
112+
streams, err = s.Where(s.NamespaceID.Eq(options.NamespaceID)).Find()
100113
}
101114
if err != nil {
102115
cosy.ErrHandler(c, err)

app/components.d.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,28 @@ declare module 'vue' {
1010
export interface GlobalComponents {
1111
AAlert: typeof import('ant-design-vue/es')['Alert']
1212
AApp: typeof import('ant-design-vue/es')['App']
13+
AAutoComplete: typeof import('ant-design-vue/es')['AutoComplete']
1314
ABadge: typeof import('ant-design-vue/es')['Badge']
1415
ABreadcrumb: typeof import('ant-design-vue/es')['Breadcrumb']
1516
ABreadcrumbItem: typeof import('ant-design-vue/es')['BreadcrumbItem']
1617
AButton: typeof import('ant-design-vue/es')['Button']
1718
ACard: typeof import('ant-design-vue/es')['Card']
19+
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
20+
ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup']
21+
ACol: typeof import('ant-design-vue/es')['Col']
22+
ACollapse: typeof import('ant-design-vue/es')['Collapse']
23+
ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel']
24+
AComment: typeof import('ant-design-vue/es')['Comment']
1825
AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
1926
ADivider: typeof import('ant-design-vue/es')['Divider']
2027
ADrawer: typeof import('ant-design-vue/es')['Drawer']
28+
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
29+
AEmpty: typeof import('ant-design-vue/es')['Empty']
30+
AForm: typeof import('ant-design-vue/es')['Form']
31+
AFormItem: typeof import('ant-design-vue/es')['FormItem']
2132
AInput: typeof import('ant-design-vue/es')['Input']
2233
AInputGroup: typeof import('ant-design-vue/es')['InputGroup']
34+
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
2335
ALayout: typeof import('ant-design-vue/es')['Layout']
2436
ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']
2537
ALayoutFooter: typeof import('ant-design-vue/es')['LayoutFooter']
@@ -29,17 +41,28 @@ declare module 'vue' {
2941
AListItem: typeof import('ant-design-vue/es')['ListItem']
3042
AListItemMeta: typeof import('ant-design-vue/es')['ListItemMeta']
3143
AMenu: typeof import('ant-design-vue/es')['Menu']
44+
AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider']
3245
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
46+
AModal: typeof import('ant-design-vue/es')['Modal']
3347
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
3448
APopover: typeof import('ant-design-vue/es')['Popover']
35-
AppProvider: typeof import('./src/components/AppProvider.vue')['default']
49+
AppProviderAppProvider: typeof import('./src/components/AppProvider/AppProvider.vue')['default']
50+
AProgress: typeof import('ant-design-vue/es')['Progress']
51+
AResult: typeof import('ant-design-vue/es')['Result']
52+
ARow: typeof import('ant-design-vue/es')['Row']
3653
ASelect: typeof import('ant-design-vue/es')['Select']
3754
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
3855
ASpace: typeof import('ant-design-vue/es')['Space']
56+
ASpin: typeof import('ant-design-vue/es')['Spin']
57+
AStep: typeof import('ant-design-vue/es')['Step']
58+
ASteps: typeof import('ant-design-vue/es')['Steps']
3959
ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
4060
ASwitch: typeof import('ant-design-vue/es')['Switch']
4161
ATable: typeof import('ant-design-vue/es')['Table']
62+
ATabPane: typeof import('ant-design-vue/es')['TabPane']
63+
ATabs: typeof import('ant-design-vue/es')['Tabs']
4264
ATag: typeof import('ant-design-vue/es')['Tag']
65+
ATextarea: typeof import('ant-design-vue/es')['Textarea']
4366
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
4467
AutoCertFormAutoCertForm: typeof import('./src/components/AutoCertForm/AutoCertForm.vue')['default']
4568
AutoCertFormDNSChallenge: typeof import('./src/components/AutoCertForm/DNSChallenge.vue')['default']
@@ -55,6 +78,7 @@ declare module 'vue' {
5578
DevDebugPanelDevDebugPanel: typeof import('./src/components/DevDebugPanel/DevDebugPanel.vue')['default']
5679
FooterToolbarFooterToolBar: typeof import('./src/components/FooterToolbar/FooterToolBar.vue')['default']
5780
ICPICP: typeof import('./src/components/ICP/ICP.vue')['default']
81+
InspectConfigInspectConfig: typeof import('./src/components/InspectConfig/InspectConfig.vue')['default']
5882
LLMChatMessage: typeof import('./src/components/LLM/ChatMessage.vue')['default']
5983
LLMChatMessageInput: typeof import('./src/components/LLM/ChatMessageInput.vue')['default']
6084
LLMChatMessageList: typeof import('./src/components/LLM/ChatMessageList.vue')['default']

app/src/App.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import en_US from 'ant-design-vue/es/locale/en_US'
44
import zh_CN from 'ant-design-vue/es/locale/zh_CN'
55
import zh_TW from 'ant-design-vue/es/locale/zh_TW'
66
import loadTranslations from '@/api/translations'
7-
import AppProvider from '@/components/AppProvider.vue'
7+
import AppProvider from '@/components/AppProvider'
88
import gettext from '@/gettext'
99
import { useSettingsStore } from '@/pinia'
1010

app/src/api/namespace.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,18 @@ export const UpstreamTestType = {
1414
Mirror: 'mirror',
1515
}
1616

17+
// Deploy mode types
18+
export const DeployMode = {
19+
Local: 'local',
20+
Remote: 'remote',
21+
} as const
22+
1723
export interface Namespace extends ModelBase {
1824
name: string
1925
sync_node_ids: number[]
2026
post_sync_action?: string
2127
upstream_test_type?: string
28+
deploy_mode?: string
2229
}
2330

2431
const baseUrl = '/namespaces'

app/src/api/ngx.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,10 @@ const ngx = {
148148
return http.post('/nginx/test')
149149
},
150150

151+
test_namespace(namespace_id?: number): Promise<{ message: string, level: number, namespace_id?: number }> {
152+
return http.post('/nginx/test_namespace', { namespace_id })
153+
},
154+
151155
get_directives(): Promise<DirectiveMap> {
152156
return http.get('/nginx/directives')
153157
},
File renamed without changes.

0 commit comments

Comments
 (0)