diff --git a/cmd/web/wire_gen.go b/cmd/web/wire_gen.go index 536bc51e..6a9aa81a 100644 --- a/cmd/web/wire_gen.go +++ b/cmd/web/wire_gen.go @@ -97,6 +97,8 @@ func initWeb() (*app.Web, error) { sshService := service.NewSSHService(sshRepo) containerRepo := data.NewContainerRepo() containerService := service.NewContainerService(containerRepo) + containerComposeRepo := data.NewContainerComposeRepo() + containerComposeService := service.NewContainerComposeService(containerComposeRepo) containerNetworkRepo := data.NewContainerNetworkRepo() containerNetworkService := service.NewContainerNetworkService(containerNetworkRepo) containerImageRepo := data.NewContainerImageRepo() @@ -132,7 +134,7 @@ func initWeb() (*app.Web, error) { supervisorApp := supervisor.NewApp() toolboxApp := toolbox.NewApp() loader := bootstrap.NewLoader(benchmarkApp, dockerApp, fail2banApp, frpApp, giteaApp, memcachedApp, mysqlApp, nginxApp, php74App, php80App, php81App, php82App, php83App, php84App, phpmyadminApp, podmanApp, postgresqlApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp, toolboxApp) - http := route.NewHttp(userService, dashboardService, taskService, websiteService, databaseService, databaseServerService, databaseUserService, backupService, certService, certDNSService, certAccountService, appService, cronService, processService, safeService, firewallService, sshService, containerService, containerNetworkService, containerImageService, containerVolumeService, fileService, monitorService, settingService, systemctlService, loader) + http := route.NewHttp(userService, dashboardService, taskService, websiteService, databaseService, databaseServerService, databaseUserService, backupService, certService, certDNSService, certAccountService, appService, cronService, processService, safeService, firewallService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, monitorService, settingService, systemctlService, loader) wsService := service.NewWsService(koanf, sshRepo) ws := route.NewWs(wsService) mux, err := bootstrap.NewRouter(middlewares, http, ws) diff --git a/internal/biz/container_compose.go b/internal/biz/container_compose.go new file mode 100644 index 00000000..61028a7e --- /dev/null +++ b/internal/biz/container_compose.go @@ -0,0 +1,10 @@ +package biz + +type ContainerComposeRepo interface { + List() ([]string, error) + Get(name string) (string, error) + Create(name, compose string) error + Up(name string, force bool) error + Down(name string) error + Remove(name string) error +} diff --git a/internal/data/container_compose.go b/internal/data/container_compose.go new file mode 100644 index 00000000..7c72a95d --- /dev/null +++ b/internal/data/container_compose.go @@ -0,0 +1,83 @@ +package data + +import ( + "io/fs" + "os" + "path/filepath" + "strings" + + "github.com/tnb-labs/panel/internal/app" + "github.com/tnb-labs/panel/internal/biz" + "github.com/tnb-labs/panel/pkg/shell" +) + +type containerComposeRepo struct{} + +func NewContainerComposeRepo() biz.ContainerComposeRepo { + return &containerComposeRepo{} +} + +// List 列出所有编排文件名 +func (r *containerComposeRepo) List() ([]string, error) { + dir := filepath.Join(app.Root, "server", "compose") + var files []string + err := filepath.Walk(dir, func(path string, d fs.FileInfo, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + if ext := filepath.Ext(path); ext == ".yml" || ext == ".yaml" { + files = append(files, strings.TrimSuffix(filepath.Base(path), ext)) + } + return nil + }) + if err != nil { + return nil, err + } + + return files, nil +} + +// Get 获取编排文件内容 +func (r *containerComposeRepo) Get(name string) (string, error) { + dir := filepath.Join(app.Root, "server", "compose") + path := filepath.Join(dir, name+".yml") + content, err := os.ReadFile(path) + return string(content), err +} + +// Create 创建编排文件 +func (r *containerComposeRepo) Create(name, compose string) error { + dir := filepath.Join(app.Root, "server", "compose") + path := filepath.Join(dir, name+".yml") + return os.WriteFile(path, []byte(compose), 0644) +} + +// Up 启动编排 +func (r *containerComposeRepo) Up(name string, force bool) error { + dir := filepath.Join(app.Root, "server", "compose") + path := filepath.Join(dir, name+".yml") + cmd := "docker compose -f %s up -d" + if force { + cmd += " --pull always" // 强制拉取镜像 + } + _, err := shell.Execf(cmd, path) + return err +} + +// Down 停止编排 +func (r *containerComposeRepo) Down(name string) error { + dir := filepath.Join(app.Root, "server", "compose") + path := filepath.Join(dir, name+".yml") + _, err := shell.Execf("docker compose -f %s down", path) + return err +} + +// Remove 删除编排 +func (r *containerComposeRepo) Remove(name string) error { + dir := filepath.Join(app.Root, "server", "compose") + path := filepath.Join(dir, name+".yml") + return os.Remove(path) +} diff --git a/internal/data/data.go b/internal/data/data.go index 03895ed0..4b374816 100644 --- a/internal/data/data.go +++ b/internal/data/data.go @@ -11,6 +11,7 @@ var ProviderSet = wire.NewSet( NewCertAccountRepo, NewCertDNSRepo, NewContainerRepo, + NewContainerComposeRepo, NewContainerImageRepo, NewContainerNetworkRepo, NewContainerVolumeRepo, diff --git a/internal/http/request/container_compose.go b/internal/http/request/container_compose.go new file mode 100644 index 00000000..902957ff --- /dev/null +++ b/internal/http/request/container_compose.go @@ -0,0 +1,23 @@ +package request + +type ContainerComposeGet struct { + Name string `uri:"name" validate:"required"` +} + +type ContainerComposeCreate struct { + Name string `json:"name" validate:"required"` + Compose string `json:"compose" validate:"required"` +} + +type ContainerComposeUp struct { + Name string `uri:"name" validate:"required"` + Force bool `json:"force"` +} + +type ContainerComposeDown struct { + Name string `uri:"name" validate:"required"` +} + +type ContainerComposeRemove struct { + Name string `uri:"name" validate:"required"` +} diff --git a/internal/route/http.go b/internal/route/http.go index 8cfd1a6c..5b4ee811 100644 --- a/internal/route/http.go +++ b/internal/route/http.go @@ -33,6 +33,7 @@ type Http struct { firewall *service.FirewallService ssh *service.SSHService container *service.ContainerService + containerCompose *service.ContainerComposeService containerNetwork *service.ContainerNetworkService containerImage *service.ContainerImageService containerVolume *service.ContainerVolumeService @@ -62,6 +63,7 @@ func NewHttp( firewall *service.FirewallService, ssh *service.SSHService, container *service.ContainerService, + containerCompose *service.ContainerComposeService, containerNetwork *service.ContainerNetworkService, containerImage *service.ContainerImageService, containerVolume *service.ContainerVolumeService, @@ -90,6 +92,7 @@ func NewHttp( firewall: firewall, ssh: ssh, container: container, + containerCompose: containerCompose, containerNetwork: containerNetwork, containerImage: containerImage, containerVolume: containerVolume, @@ -284,6 +287,14 @@ func (route *Http) Register(r *chi.Mux) { r.Get("/{id}/logs", route.container.Logs) r.Post("/prune", route.container.Prune) }) + r.Route("/compose", func(r chi.Router) { + r.Get("/", route.containerCompose.List) + r.Get("/{name}", route.containerCompose.Get) + r.Post("/", route.containerCompose.Create) + r.Post("/{name}/up", route.containerCompose.Up) + r.Post("/{name}/down", route.containerCompose.Down) + r.Delete("/{name}", route.containerCompose.Remove) + }) r.Route("/network", func(r chi.Router) { r.Get("/", route.containerNetwork.List) r.Post("/", route.containerNetwork.Create) diff --git a/internal/service/container_compose.go b/internal/service/container_compose.go new file mode 100644 index 00000000..96c8f19d --- /dev/null +++ b/internal/service/container_compose.go @@ -0,0 +1,111 @@ +package service + +import ( + "net/http" + + "github.com/go-rat/chix" + + "github.com/tnb-labs/panel/internal/biz" + "github.com/tnb-labs/panel/internal/http/request" +) + +type ContainerComposeService struct { + containerComposeRepo biz.ContainerComposeRepo +} + +func NewContainerComposeService(containerCompose biz.ContainerComposeRepo) *ContainerComposeService { + return &ContainerComposeService{ + containerComposeRepo: containerCompose, + } +} + +func (s *ContainerComposeService) List(w http.ResponseWriter, r *http.Request) { + files, err := s.containerComposeRepo.List() + if err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + paged, total := Paginate(r, files) + + Success(w, chix.M{ + "total": total, + "items": paged, + }) +} + +func (s *ContainerComposeService) Get(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerComposeGet](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, "%v", err) + return + } + + content, err := s.containerComposeRepo.Get(req.Name) + if err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + Success(w, content) +} + +func (s *ContainerComposeService) Create(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerComposeCreate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, "%v", err) + return + } + + if err = s.containerComposeRepo.Create(req.Name, req.Compose); err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + Success(w, nil) +} + +func (s *ContainerComposeService) Up(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerComposeUp](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, "%v", err) + return + } + + if err = s.containerComposeRepo.Up(req.Name, req.Force); err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + Success(w, nil) +} + +func (s *ContainerComposeService) Down(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerComposeDown](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, "%v", err) + return + } + + if err = s.containerComposeRepo.Down(req.Name); err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + Success(w, nil) +} + +func (s *ContainerComposeService) Remove(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerComposeRemove](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, "%v", err) + return + } + + if err = s.containerComposeRepo.Remove(req.Name); err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + Success(w, nil) +} diff --git a/internal/service/service.go b/internal/service/service.go index acb0d242..0c7ccba7 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -11,6 +11,7 @@ var ProviderSet = wire.NewSet( NewCertDNSService, NewCliService, NewContainerService, + NewContainerComposeService, NewContainerImageService, NewContainerNetworkService, NewContainerVolumeService,