Skip to content

Commit 381d716

Browse files
committed
Merge remote-tracking branch 'origin/pr@dev-v2@fix-cleanDockerNetwork' into pr@dev-v2@fix-cleanDockerNetwork
# Conflicts: # agent/app/api/v2/container.go # agent/app/dto/container_network_cleanup.go # agent/app/service/container_network_cleanup.go # agent/utils/docker/network_cleanup.go
2 parents f7f4135 + dbf6d9e commit 381d716

3 files changed

Lines changed: 203 additions & 0 deletions

File tree

‎agent/app/api/v2/container.go‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -965,6 +965,24 @@ func (b *BaseApi) ContainerStreamLogs(c *gin.Context) {
965965
containerService.StreamLogs(c, streamLog)
966966
}
967967

968+
// CleanNetworks submits a network cleanup task.
969+
// @Tags Container Network
970+
// @Summary Submit unused network cleanup task
971+
// @Produce json
972+
// @Success 200 {object} dto.NetworkCleanupTask
973+
// @Security ApiKeyAuth
974+
// @Security Timestamp
975+
// @Router /containers/network/clean [post]
976+
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清理未使用的容器网络","formatEN":"Clean unused container networks"}
977+
func (b *BaseApi) CleanNetworks(c *gin.Context) {
978+
result, err := containerService.CleanNetworks()
979+
if err != nil {
980+
helper.InternalServer(c, err)
981+
return
982+
}
983+
helper.SuccessWithData(c, result)
984+
}
985+
968986
func (b *BaseApi) CleanNetworks(c *gin.Context) {
969987
result, err := containerService.CleanNetworks()
970988
if err != nil {
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package service
2+
3+
import (
4+
"context"
5+
"errors"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
"testing"
10+
11+
"github.com/1Panel-dev/1Panel/agent/app/model"
12+
"github.com/1Panel-dev/1Panel/agent/app/task"
13+
"github.com/1Panel-dev/1Panel/agent/constant"
14+
"github.com/1Panel-dev/1Panel/agent/global"
15+
"github.com/1Panel-dev/1Panel/agent/i18n"
16+
"github.com/docker/docker/api/types/container"
17+
"github.com/docker/docker/api/types/network"
18+
"github.com/docker/docker/errdefs"
19+
"github.com/glebarez/sqlite"
20+
"gorm.io/gorm"
21+
)
22+
23+
type networkTaskClient struct{ fail bool }
24+
25+
func (f networkTaskClient) NetworkList(context.Context, network.ListOptions) ([]network.Inspect, error) {
26+
return []network.Inspect{{ID: "reserved", Name: "1panel-network", Scope: "local"}, {ID: "connected", Name: "busy", Scope: "local"}, {ID: "unused", Name: "free", Scope: "local"}, {ID: "race", Name: "race", Scope: "local"}}, nil
27+
}
28+
func (f networkTaskClient) ContainerList(context.Context, container.ListOptions) ([]container.Summary, error) {
29+
return nil, nil
30+
}
31+
func (f networkTaskClient) NetworkInspect(_ context.Context, id string, _ network.InspectOptions) (network.Inspect, error) {
32+
n := network.Inspect{}
33+
if id == "connected" {
34+
n.Containers = map[string]network.EndpointResource{"container-id": {}}
35+
}
36+
return n, nil
37+
}
38+
func (f networkTaskClient) NetworkRemove(_ context.Context, id string) error {
39+
if id == "race" {
40+
return errdefs.Conflict(errors.New("has active endpoints"))
41+
}
42+
if id != "unused" {
43+
return errors.New("unexpected removal")
44+
}
45+
if f.fail {
46+
return errors.New("remove failed")
47+
}
48+
return nil
49+
}
50+
51+
func TestNetworkCleanupTaskPersistsLogsAndStatus(t *testing.T) {
52+
oldDB, oldTaskDB, oldDir, oldI18n := global.DB, global.TaskDB, global.Dir, global.I18n
53+
t.Cleanup(func() { global.DB = oldDB; global.TaskDB = oldTaskDB; global.Dir = oldDir; global.I18n = oldI18n })
54+
dir := t.TempDir()
55+
db, err := gorm.Open(sqlite.Open(filepath.Join(dir, "tasks.db")), &gorm.Config{})
56+
if err != nil {
57+
t.Fatal(err)
58+
}
59+
sqlDB, err := db.DB()
60+
if err != nil {
61+
t.Fatal(err)
62+
}
63+
defer sqlDB.Close()
64+
if err := db.AutoMigrate(&model.Task{}); err != nil {
65+
t.Fatal(err)
66+
}
67+
global.DB = nil
68+
global.TaskDB = db
69+
global.Dir.TaskDir = dir
70+
i18n.Init()
71+
for _, fail := range []bool{false, true} {
72+
name := "success"
73+
wantStatus := constant.StatusSuccess
74+
if fail {
75+
name = "partial failure"
76+
wantStatus = constant.StatusFailed
77+
}
78+
t.Run(name, func(t *testing.T) {
79+
item, err := task.NewTaskWithOps("Network", task.TaskClean, task.TaskScopeContainer, "", 0)
80+
if err != nil {
81+
t.Fatal(err)
82+
}
83+
item.AddSubTask("Clean", func(t *task.Task) error { return executeNetworkCleanup(t, networkTaskClient{fail: fail}) }, nil)
84+
err = item.Execute()
85+
if (err != nil) != fail {
86+
t.Fatalf("unexpected execution error: %v", err)
87+
}
88+
var saved model.Task
89+
if err := db.First(&saved, "id = ?", item.TaskID).Error; err != nil {
90+
t.Fatal(err)
91+
}
92+
if saved.Status != wantStatus {
93+
t.Fatalf("status %s, want %s", saved.Status, wantStatus)
94+
}
95+
content, err := os.ReadFile(saved.LogFile)
96+
if err != nil {
97+
t.Fatal(err)
98+
}
99+
for _, want := range []string{"[busy] (connected): containers connected", "[race] (race): containers connected", "[1panel-network] (reserved): reserved network", "Network cleanup finished:", "[TASK-END]"} {
100+
if !strings.Contains(string(content), want) {
101+
t.Fatalf("missing %q in log: %s", want, content)
102+
}
103+
}
104+
want := "Deleted network [free]"
105+
if fail {
106+
want = "Failed to remove network [free]"
107+
}
108+
if !strings.Contains(string(content), want) {
109+
t.Fatalf("missing %q", want)
110+
}
111+
})
112+
}
113+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package docker
2+
3+
import (
4+
"context"
5+
"errors"
6+
"reflect"
7+
"testing"
8+
9+
"github.com/docker/docker/api/types/container"
10+
"github.com/docker/docker/api/types/network"
11+
"github.com/docker/docker/errdefs"
12+
)
13+
14+
type cleanupFake struct {
15+
networks []network.Inspect
16+
containers []container.Summary
17+
inspected map[string]network.Inspect
18+
inspectErrors, removeErrors map[string]error
19+
listError error
20+
removed []string
21+
}
22+
23+
func (f *cleanupFake) NetworkList(context.Context, network.ListOptions) ([]network.Inspect, error) {
24+
return f.networks, nil
25+
}
26+
func (f *cleanupFake) ContainerList(_ context.Context, o container.ListOptions) ([]container.Summary, error) {
27+
if !o.All {
28+
panic("must include stopped containers")
29+
}
30+
return f.containers, f.listError
31+
}
32+
func (f *cleanupFake) NetworkInspect(_ context.Context, id string, _ network.InspectOptions) (network.Inspect, error) {
33+
return f.inspected[id], f.inspectErrors[id]
34+
}
35+
func (f *cleanupFake) NetworkRemove(_ context.Context, id string) error {
36+
f.removed = append(f.removed, id)
37+
return f.removeErrors[id]
38+
}
39+
40+
func TestNetworkCleanupProtectsAndReports(t *testing.T) {
41+
f := &cleanupFake{inspected: map[string]network.Inspect{"attached": {Containers: map[string]network.EndpointResource{"container": {}}}}, inspectErrors: map[string]error{"unknown": errors.New("inspect failure")}, removeErrors: map[string]error{"race": errdefs.Conflict(errors.New("has active endpoints")), "gone": errdefs.NotFound(errors.New("gone")), "failed": errors.New("denied")}}
42+
for _, name := range []string{"none", "host", "bridge", "1panel-network", "configured", "attached", "stopped", "free", "unknown", "race", "gone", "failed"} {
43+
f.networks = append(f.networks, network.Inspect{ID: name, Name: name, Scope: "local"})
44+
}
45+
// Compose ownership labels do not protect an otherwise unused network.
46+
for i := range f.networks {
47+
if f.networks[i].Name == "configured" {
48+
f.networks[i].Labels = map[string]string{"com.docker.compose.project": "demo", "com.docker.compose.network": "default"}
49+
}
50+
}
51+
f.containers = []container.Summary{{State: "exited", NetworkSettings: &container.NetworkSettingsSummary{Networks: map[string]*network.EndpointSettings{"stopped": {NetworkID: "stopped"}}}}}
52+
report, err := CleanUnusedNetworks(context.Background(), f)
53+
if err != nil {
54+
t.Fatal(err)
55+
}
56+
if !reflect.DeepEqual(f.removed, []string{"configured", "failed", "free", "gone", "race"}) {
57+
t.Fatal(f.removed)
58+
}
59+
if len(report.Deleted) != 2 || report.Deleted[0].Name != "configured" || report.Deleted[1].Name != "free" || len(report.Failed) != 2 || len(report.Skipped) != 8 {
60+
t.Fatalf("%+v", report)
61+
}
62+
}
63+
64+
func TestNetworkCleanupDiscoveryFailureDeletesNothing(t *testing.T) {
65+
f := &cleanupFake{networks: []network.Inspect{{ID: "free", Name: "free", Scope: "local"}}, listError: errors.New("cannot list containers")}
66+
if _, err := CleanUnusedNetworks(context.Background(), f); err == nil {
67+
t.Fatal("expected error")
68+
}
69+
if len(f.removed) > 0 {
70+
t.Fatal(f.removed)
71+
}
72+
}

0 commit comments

Comments
 (0)