Skip to content

Commit 4e5f648

Browse files
committed
feat: add cleanup on exit functionality for IPVS and iptables rules, bump version to 0.3.2
1 parent 58e8368 commit 4e5f648

13 files changed

Lines changed: 620 additions & 11 deletions

File tree

Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM golang:1.25 as builder
1+
FROM golang:1.25 AS builder
22
ARG TARGETOS
33
ARG TARGETARCH
44
ENV GOPROXY="https://goproxy.cn,direct"
@@ -21,7 +21,8 @@ RUN make build
2121

2222
## runtime image
2323
FROM debian:bookworm-slim
24-
ENV LANG C.UTF-8
24+
ENV LANG=C.UTF-8
25+
ENV TZ=Asia/Shanghai
2526

2627
# runtime dependencies
2728
RUN set -eux; \
@@ -49,7 +50,6 @@ RUN set -eux; \
4950
; \
5051
rm -rf /var/lib/apt/lists/*
5152

52-
ENV TZ=Asia/Shanghai
5353
WORKDIR /app
5454

5555
COPY --from=builder /workspace/build/* /app/

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Create a config file `config.yaml`:
3434
```yaml
3535
global:
3636
log_level: info
37+
cleanup_on_exit: true # Remove managed IPVS services and EZLB-SNAT iptables chain on exit (default: true)
3738

3839
services:
3940
- name: web-service

README_CN.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ make build-linux
3434
```yaml
3535
global:
3636
log_level: info
37+
cleanup_on_exit: true # 退出时删除 ezlb 管理的 IPVS 服务和 EZLB-SNAT iptables 链(默认: true)
3738

3839
services:
3940
- name: web-service

cmd/ezlb/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616
var (
1717
BuildTime string
1818
BuildCommit string
19-
Version = "0.3.1"
19+
Version = "0.3.2"
2020
configPath string
2121
showVersion bool
2222
)

docs/deploy/start-container.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ docker run -d \
1818
--cap-add NET_ADMIN \
1919
--cap-add NET_RAW \
2020
--restart unless-stopped \
21+
-v /lib/modules:/lib/modules:ro \
2122
-v ./config.yaml:/app/config.yaml \
2223
easzlab/ezlb:latest \
2324
/app/ezlb start -c /app/config.yaml
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
### cleanup-on-exit ###
2+
在实现 cleanup_on_exit 功能的基础上,补充单元测试(config、reconciler)、e2e 测试(daemon 模式清理/保留行为)以及 README/README_CN 文档更新。
3+
4+
# 进程退出时按需清理 IPVS 和 iptables 规则
5+
6+
添加 `cleanup_on_exit` 全局配置项(默认 `true`),控制 ezlb 退出时是否删除自身管理的 IPVS 虚拟服务/后端及 iptables SNAT 规则。IPVS 精确删除 ezlb 管理的服务,不影响系统中其他 IPVS 规则;iptables 删除整个 `EZLB-SNAT` 链(该链为本项目专用)。同步补充单元测试、e2e 测试和文档。
7+
8+
## Proposed Changes
9+
10+
---
11+
12+
### config — 新增 cleanup_on_exit 配置项
13+
14+
#### [MODIFY] [config.go](file:///Users/jin.gjm/code/go-network/ezlb/pkg/config/config.go)
15+
16+
`GlobalConfig` 中新增字段,并添加 `IsCleanupOnExit()` 方法:
17+
18+
```diff
19+
type GlobalConfig struct {
20+
- LogLevel string `yaml:"log_level" mapstructure:"log_level"`
21+
+ LogLevel string `yaml:"log_level" mapstructure:"log_level"`
22+
+ CleanupOnExit *bool `yaml:"cleanup_on_exit" mapstructure:"cleanup_on_exit"`
23+
}
24+
25+
+// IsCleanupOnExit returns whether to clean up IPVS and iptables rules on exit.
26+
+// Defaults to true if not explicitly set.
27+
+func (g GlobalConfig) IsCleanupOnExit() bool {
28+
+ if g.CleanupOnExit == nil {
29+
+ return true
30+
+ }
31+
+ return *g.CleanupOnExit
32+
+}
33+
```
34+
35+
`NewManager` 中设置 viper 默认值:
36+
37+
```diff
38+
viperInstance.SetDefault("global.log_level", "info")
39+
+viperInstance.SetDefault("global.cleanup_on_exit", true)
40+
```
41+
42+
---
43+
44+
### lvs — Reconciler 新增精确清理方法
45+
46+
#### [MODIFY] [reconciler.go](file:///Users/jin.gjm/code/go-network/ezlb/pkg/lvs/reconciler.go)
47+
48+
新增 `Cleanup()` 方法,遍历 `r.managed` 精确删除 ezlb 管理的 IPVS 服务,不影响系统中其他 IPVS 规则:
49+
50+
```go
51+
// Cleanup removes all IPVS services currently managed by this Reconciler.
52+
// It only deletes services tracked in the managed map, leaving other IPVS
53+
// rules untouched.
54+
func (r *Reconciler) Cleanup() error {
55+
r.mu.Lock()
56+
defer r.mu.Unlock()
57+
58+
actualServices, err := r.manager.GetServices()
59+
if err != nil {
60+
return fmt.Errorf("failed to get IPVS services for cleanup: %w", err)
61+
}
62+
63+
actualMap := make(map[ServiceKey]*Service)
64+
for _, svc := range actualServices {
65+
actualMap[ServiceKeyFromIPVS(svc)] = svc
66+
}
67+
68+
var errs []error
69+
for key := range r.managed {
70+
svc, exists := actualMap[key]
71+
if !exists {
72+
delete(r.managed, key)
73+
continue
74+
}
75+
if err := r.manager.DeleteService(svc); err != nil {
76+
errs = append(errs, fmt.Errorf("delete service %s: %w", key, err))
77+
} else {
78+
delete(r.managed, key)
79+
}
80+
}
81+
82+
if len(errs) > 0 {
83+
return errors.Join(errs...)
84+
}
85+
r.logger.Info("cleaned up all managed IPVS services")
86+
return nil
87+
}
88+
```
89+
90+
---
91+
92+
### server — shutdown 逻辑按配置分支
93+
94+
#### [MODIFY] [server.go](file:///Users/jin.gjm/code/go-network/ezlb/pkg/server/server.go)
95+
96+
```diff
97+
func (s *Server) shutdown() {
98+
s.healthMgr.Stop()
99+
- if err := s.snatMgr.Cleanup(); err != nil {
100+
- s.logger.Error("failed to cleanup SNAT rules", zap.Error(err))
101+
- }
102+
+ cfg := s.configMgr.GetConfig()
103+
+ if cfg.Global.IsCleanupOnExit() {
104+
+ if err := s.reconciler.Cleanup(); err != nil {
105+
+ s.logger.Error("failed to cleanup IPVS rules", zap.Error(err))
106+
+ }
107+
+ if err := s.snatMgr.Cleanup(); err != nil {
108+
+ s.logger.Error("failed to cleanup SNAT rules", zap.Error(err))
109+
+ }
110+
+ } else {
111+
+ s.logger.Info("cleanup_on_exit is false, preserving IPVS and iptables rules")
112+
+ }
113+
s.lvsMgr.Close()
114+
s.logger.Info("server stopped")
115+
}
116+
```
117+
118+
---
119+
120+
### examples — 更新示例配置
121+
122+
#### [MODIFY] [ezlb.yaml](file:///Users/jin.gjm/code/go-network/ezlb/examples/ezlb.yaml)
123+
124+
```diff
125+
global:
126+
log_level: info
127+
+ # cleanup_on_exit: true # Remove managed IPVS services and EZLB-SNAT iptables chain on exit (default: true)
128+
```
129+
130+
---
131+
132+
### 单元测试
133+
134+
#### [MODIFY] [config_test.go](file:///Users/jin.gjm/code/go-network/ezlb/pkg/config/config_test.go)
135+
136+
在现有测试文件末尾追加 `GlobalConfig.IsCleanupOnExit()` 的测试用例:
137+
138+
- `TestGlobalConfig_IsCleanupOnExit_DefaultTrue``CleanupOnExit` 为 nil 时返回 true
139+
- `TestGlobalConfig_IsCleanupOnExit_ExplicitTrue` — 显式设置 true 时返回 true
140+
- `TestGlobalConfig_IsCleanupOnExit_ExplicitFalse` — 显式设置 false 时返回 false
141+
- `TestManager_LoadYAML_CleanupOnExitDefault` — 不配置时加载后默认为 true
142+
- `TestManager_LoadYAML_CleanupOnExitFalse` — 配置 `cleanup_on_exit: false` 后加载正确
143+
144+
#### [MODIFY] [reconciler_test.go](file:///Users/jin.gjm/code/go-network/ezlb/pkg/lvs/reconciler_test.go)
145+
146+
追加 `Reconciler.Cleanup()` 的测试用例:
147+
148+
- `TestReconciler_Cleanup_RemovesManagedServices` — reconcile 后调用 Cleanup,验证 managed 服务被删除
149+
- `TestReconciler_Cleanup_PreservesUnmanagedServices` — 手动创建一个 ezlb 未管理的服务,调用 Cleanup 后该服务仍然存在
150+
- `TestReconciler_Cleanup_EmptyManaged` — managed 为空时调用 Cleanup 不报错
151+
- `TestReconciler_Cleanup_WithFullNATService` — 包含 FullNAT 服务时,Cleanup 正确删除 IPVS 服务(SNAT 清理由 snatMgr 负责,此处验证 IPVS 部分)
152+
153+
---
154+
155+
### e2e 测试
156+
157+
#### [MODIFY] [e2e_test.go](file:///Users/jin.gjm/code/go-network/ezlb/tests/e2e/e2e_test.go)
158+
159+
追加两个 daemon 模式测试用例:
160+
161+
**Test 9: `cleanup_on_exit: true`(默认)— 退出后 IPVS 规则被清理**
162+
163+
```
164+
TestE2E_DaemonMode_CleanupOnExit_True
165+
```
166+
- 启动 daemon,配置 `cleanup_on_exit: true`
167+
- 等待初始 reconcile 完成,验证 IPVS 服务存在
168+
- 发送 SIGTERM,等待进程退出
169+
- 验证 IPVS 中对应服务已被删除
170+
171+
**Test 10: `cleanup_on_exit: false` — 退出后 IPVS 规则保留**
172+
173+
```
174+
TestE2E_DaemonMode_CleanupOnExit_False
175+
```
176+
- 启动 daemon,配置 `cleanup_on_exit: false`
177+
- 等待初始 reconcile 完成,验证 IPVS 服务存在
178+
- 发送 SIGTERM,等待进程退出
179+
- 验证 IPVS 中对应服务仍然存在
180+
181+
---
182+
183+
### 文档更新
184+
185+
#### [MODIFY] [README.md](file:///Users/jin.gjm/code/go-network/ezlb/README.md)
186+
187+
在配置参考章节的 `global` 部分新增 `cleanup_on_exit` 字段说明。
188+
189+
#### [MODIFY] [README_CN.md](file:///Users/jin.gjm/code/go-network/ezlb/README_CN.md)
190+
191+
同步更新中文文档,在 `global` 配置说明中新增 `cleanup_on_exit` 字段。
192+
193+
## Verification Plan
194+
195+
### Automated Tests
196+
197+
```bash
198+
# 单元测试(全平台)
199+
go test ./pkg/config/... ./pkg/lvs/... ./pkg/snat/...
200+
201+
# e2e 测试(需要 Linux + root)
202+
go test -v -tags integration ./tests/e2e/...
203+
```
204+
205+
### Manual Verification
206+
207+
- Linux 环境下启动 ezlb,配置 `cleanup_on_exit: true`,发送 SIGTERM,验证 `ipvsadm -L` 中对应服务已删除,`iptables -t nat -L``EZLB-SNAT` 链已消失
208+
- 配置 `cleanup_on_exit: false`,发送 SIGTERM,验证 IPVS 规则和 `EZLB-SNAT` 链均保留
209+
210+
211+
## Tasks
212+
213+
- [ ] **pkg/config/config.go** — GlobalConfig 新增 `CleanupOnExit *bool` 字段、`IsCleanupOnExit()` 方法,viper 设置默认值 true
214+
- [ ] **pkg/lvs/reconciler.go** — 新增 `Cleanup()` 方法,精确删除 managed 的 IPVS 服务
215+
- [ ] **pkg/server/server.go**`shutdown()``IsCleanupOnExit()` 分支调用清理或跳过
216+
- [ ] **examples/ezlb.yaml** — 添加 `cleanup_on_exit` 注释说明
217+
- [ ] **pkg/config/config_test.go** — 追加 `IsCleanupOnExit()` 和配置加载相关单元测试(5 个用例)
218+
- [ ] **pkg/lvs/reconciler_test.go** — 追加 `Reconciler.Cleanup()` 单元测试(4 个用例)
219+
- [ ] **tests/e2e/e2e_test.go** — 追加 daemon 模式 cleanup_on_exit=true 和 false 的 e2e 测试(2 个用例)
220+
- [ ] **README.md** — 在 global 配置章节新增 `cleanup_on_exit` 字段说明
221+
- [ ] **README_CN.md** — 同步更新中文文档
222+
- [ ] 运行 `go test ./pkg/config/... ./pkg/lvs/... ./pkg/snat/...` 验证无编译错误和测试回归
223+
224+
updateAtTime: 2026/3/16 11:13:24
225+
226+
planId: 138ca5d8-5bcf-4e88-a5e3-ce78d9816ba6

examples/ezlb.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
global:
22
log_level: info
3+
# cleanup_on_exit: true # Remove managed IPVS services and EZLB-SNAT iptables chain on exit (default: true)
34

45
services:
56
- name: web-service

pkg/config/config.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,17 @@ type Config struct {
1919

2020
// GlobalConfig holds global settings.
2121
type GlobalConfig struct {
22-
LogLevel string `yaml:"log_level" mapstructure:"log_level"`
22+
LogLevel string `yaml:"log_level" mapstructure:"log_level"`
23+
CleanupOnExit *bool `yaml:"cleanup_on_exit" mapstructure:"cleanup_on_exit"`
24+
}
25+
26+
// IsCleanupOnExit returns whether to clean up IPVS and iptables rules on exit.
27+
// Defaults to true if not explicitly set.
28+
func (g GlobalConfig) IsCleanupOnExit() bool {
29+
if g.CleanupOnExit == nil {
30+
return true
31+
}
32+
return *g.CleanupOnExit
2333
}
2434

2535
// ServiceConfig defines a virtual service with its backends and health check settings.
@@ -165,6 +175,7 @@ func NewManager(configPath string, logger *zap.Logger) (*Manager, error) {
165175

166176
// Set defaults
167177
viperInstance.SetDefault("global.log_level", "info")
178+
viperInstance.SetDefault("global.cleanup_on_exit", true)
168179

169180
manager := &Manager{
170181
viper: viperInstance,

pkg/config/config_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,3 +654,66 @@ func TestManager_OnChangeChannel(t *testing.T) {
654654
t.Fatal("expected OnChange to return non-nil channel")
655655
}
656656
}
657+
658+
// --- GlobalConfig.IsCleanupOnExit tests ---
659+
660+
func TestGlobalConfig_IsCleanupOnExit_DefaultTrue(t *testing.T) {
661+
g := GlobalConfig{}
662+
if !g.IsCleanupOnExit() {
663+
t.Error("expected IsCleanupOnExit to return true when CleanupOnExit is nil")
664+
}
665+
}
666+
667+
func TestGlobalConfig_IsCleanupOnExit_ExplicitTrue(t *testing.T) {
668+
g := GlobalConfig{CleanupOnExit: boolPtr(true)}
669+
if !g.IsCleanupOnExit() {
670+
t.Error("expected IsCleanupOnExit to return true when CleanupOnExit is explicitly true")
671+
}
672+
}
673+
674+
func TestGlobalConfig_IsCleanupOnExit_ExplicitFalse(t *testing.T) {
675+
g := GlobalConfig{CleanupOnExit: boolPtr(false)}
676+
if g.IsCleanupOnExit() {
677+
t.Error("expected IsCleanupOnExit to return false when CleanupOnExit is explicitly false")
678+
}
679+
}
680+
681+
func TestManager_LoadYAML_CleanupOnExitDefault(t *testing.T) {
682+
// cleanup_on_exit not set in YAML — should default to true
683+
path := writeTestYAML(t, validYAML)
684+
mgr, err := NewManager(path, zap.NewNop())
685+
if err != nil {
686+
t.Fatalf("NewManager failed: %v", err)
687+
}
688+
cfg := mgr.GetConfig()
689+
if !cfg.Global.IsCleanupOnExit() {
690+
t.Error("expected IsCleanupOnExit to return true when not set in config")
691+
}
692+
}
693+
694+
func TestManager_LoadYAML_CleanupOnExitFalse(t *testing.T) {
695+
yaml := `
696+
global:
697+
log_level: info
698+
cleanup_on_exit: false
699+
services:
700+
- name: web-service
701+
listen: 10.0.0.1:80
702+
protocol: tcp
703+
scheduler: rr
704+
health_check:
705+
enabled: false
706+
backends:
707+
- address: 192.168.1.10:8080
708+
weight: 1
709+
`
710+
path := writeTestYAML(t, yaml)
711+
mgr, err := NewManager(path, zap.NewNop())
712+
if err != nil {
713+
t.Fatalf("NewManager failed: %v", err)
714+
}
715+
cfg := mgr.GetConfig()
716+
if cfg.Global.IsCleanupOnExit() {
717+
t.Error("expected IsCleanupOnExit to return false when cleanup_on_exit: false in config")
718+
}
719+
}

0 commit comments

Comments
 (0)