diff --git a/cmd/ace/wire_gen.go b/cmd/ace/wire_gen.go index 92406b9f..7a5189b3 100644 --- a/cmd/ace/wire_gen.go +++ b/cmd/ace/wire_gen.go @@ -128,6 +128,8 @@ func initWeb() (*app.Web, error) { toolboxLogService := service.NewToolboxLogService(locale, db, containerImageRepo, settingRepo) webHookRepo := data.NewWebHookRepo(locale, db, logger) webHookService := service.NewWebHookService(webHookRepo) + templateRepo := data.NewTemplateRepo() + templateService := service.NewTemplateService(locale, templateRepo, settingRepo) apacheApp := apache.NewApp(locale) codeserverApp := codeserver.NewApp() dockerApp := docker.NewApp() @@ -150,7 +152,7 @@ func initWeb() (*app.Web, error) { s3fsApp := s3fs.NewApp(locale) supervisorApp := supervisor.NewApp(locale) loader := bootstrap.NewLoader(apacheApp, codeserverApp, dockerApp, fail2banApp, frpApp, giteaApp, mariadbApp, memcachedApp, minioApp, mysqlApp, nginxApp, openrestyApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp) - http := route.NewHttp(config, userService, userTokenService, homeService, taskService, websiteService, projectService, databaseService, databaseServerService, databaseUserService, backupService, certService, certDNSService, certAccountService, appService, environmentService, environmentPHPService, cronService, processService, safeService, firewallService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, logService, monitorService, settingService, systemctlService, toolboxSystemService, toolboxBenchmarkService, toolboxSSHService, toolboxDiskService, toolboxLogService, webHookService, loader) + http := route.NewHttp(config, userService, userTokenService, homeService, taskService, websiteService, projectService, databaseService, databaseServerService, databaseUserService, backupService, certService, certDNSService, certAccountService, appService, environmentService, environmentPHPService, cronService, processService, safeService, firewallService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, logService, monitorService, settingService, systemctlService, toolboxSystemService, toolboxBenchmarkService, toolboxSSHService, toolboxDiskService, toolboxLogService, webHookService, templateService, loader) wsService := service.NewWsService(locale, config, logger, sshRepo) ws := route.NewWs(wsService) mux, err := bootstrap.NewRouter(locale, middlewares, http, ws) diff --git a/go.mod b/go.mod index a37c791d..820bf80f 100644 --- a/go.mod +++ b/go.mod @@ -49,7 +49,7 @@ require ( github.com/robfig/cron/v3 v3.0.1 github.com/samber/lo v1.52.0 github.com/sethvargo/go-limiter v1.1.0 - github.com/shirou/gopsutil v3.21.11+incompatible + github.com/shirou/gopsutil/v4 v4.25.12 github.com/spf13/cast v1.10.0 github.com/stretchr/testify v1.11.1 github.com/tufanbarisyildirim/gonginx v0.0.0-20250620092546-c3e307e36701 @@ -69,6 +69,7 @@ require ( github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect + github.com/ebitengine/purego v0.9.1 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/gofiber/schema v1.6.0 // indirect @@ -79,17 +80,19 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/libtnb/securecookie v1.2.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/ncruces/julianday v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/stretchr/objx v0.5.3 // indirect github.com/tetratelabs/wazero v1.11.0 // indirect github.com/timtadh/data-structures v0.6.2 // indirect github.com/timtadh/lexmachine v0.2.3 // indirect - github.com/tklauser/go-sysconf v0.3.15 // indirect - github.com/tklauser/numcpus v0.10.0 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect diff --git a/go.sum b/go.sum index 99fa6023..6fb002c6 100644 --- a/go.sum +++ b/go.sum @@ -67,6 +67,8 @@ github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pM github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= +github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8= github.com/expr-lang/expr v1.17.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -114,6 +116,7 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= @@ -233,6 +236,8 @@ github.com/libtnb/testify v0.0.0-20260103194301-c7a63ea79696 h1:GN0Y3DG27mMruX53 github.com/libtnb/testify v0.0.0-20260103194301-c7a63ea79696/go.mod h1:HeQeTfKU6tj2Lx1z79UacwYeDioo6M4ZD7BDDI6+rrg= github.com/libtnb/utils v1.2.1 h1:LJmReRREnpqfHyy9PZtNgBh3ZaIGct81b8ZaAsolMkM= github.com/libtnb/utils v1.2.1/go.mod h1:o6LEDeC42PXI21uLWdWJWTVYvR9BtAZfzzTGJVQoQiU= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= @@ -273,6 +278,8 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -297,8 +304,8 @@ github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRo github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sethvargo/go-limiter v1.1.0 h1:eLeZVQ2zqJOiEs03GguqmBVG6/T6lsZB+6PP1t7J6fA= github.com/sethvargo/go-limiter v1.1.0/go.mod h1:01b6tW25Ap+MeLYBuD4aHunMrJoNO5PVUFdS9rac3II= -github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= -github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shirou/gopsutil/v4 v4.25.12 h1:e7PvW/0RmJ8p8vPGJH4jvNkOyLmbkXgXW4m6ZPic6CY= +github.com/shirou/gopsutil/v4 v4.25.12/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= @@ -329,10 +336,10 @@ github.com/timtadh/getopt v1.0.0/go.mod h1:L3EL6YN2G0eIAhYBo9b7SB9d/kEQmdnwthIlM github.com/timtadh/lexmachine v0.2.2/go.mod h1:GBJvD5OAfRn/gnp92zb9KTgHLB7akKyxmVivoYCcjQI= github.com/timtadh/lexmachine v0.2.3 h1:ZqlfHnfMcAygtbNM5Gv7jQf8hmM8LfVzDjfCrq235NQ= github.com/timtadh/lexmachine v0.2.3/go.mod h1:oK1NW+93fQSIF6s+J6sXBFWsCPCFbNmrwKV1i0aqvW0= -github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= -github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= -github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= -github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tufanbarisyildirim/gonginx v0.0.0-20250620092546-c3e307e36701 h1:JgeHIJzRSEdcuLXufZrni5+a4yDnBhQG+DdKhqCFhq0= github.com/tufanbarisyildirim/gonginx v0.0.0-20250620092546-c3e307e36701/go.mod h1:ALbEe81QPWOZjDKCKNWodG2iqCMtregG8+ebQgjx2+4= @@ -419,6 +426,7 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= @@ -452,6 +460,7 @@ golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= diff --git a/internal/biz/template.go b/internal/biz/template.go new file mode 100644 index 00000000..5a80b95a --- /dev/null +++ b/internal/biz/template.go @@ -0,0 +1,13 @@ +package biz + +import ( + "github.com/acepanel/panel/pkg/api" + "github.com/acepanel/panel/pkg/types" +) + +type TemplateRepo interface { + List() (api.Templates, error) + Get(slug string) (*api.Template, error) + Callback(slug string) error + CreateCompose(name, compose string, envs []types.KV, autoFirewall bool) error +} diff --git a/internal/data/backup.go b/internal/data/backup.go index e4d23d22..f64fc82a 100644 --- a/internal/data/backup.go +++ b/internal/data/backup.go @@ -12,7 +12,7 @@ import ( "time" "github.com/leonelquinteros/gotext" - "github.com/shirou/gopsutil/disk" + "github.com/shirou/gopsutil/v4/disk" "gorm.io/gorm" "github.com/acepanel/panel/internal/app" diff --git a/internal/data/data.go b/internal/data/data.go index bc57b283..433b0e1b 100644 --- a/internal/data/data.go +++ b/internal/data/data.go @@ -27,6 +27,7 @@ var ProviderSet = wire.NewSet( NewSettingRepo, NewSSHRepo, NewTaskRepo, + NewTemplateRepo, NewUserRepo, NewUserTokenRepo, NewWebHookRepo, diff --git a/internal/data/project.go b/internal/data/project.go index 6664071d..7b223881 100644 --- a/internal/data/project.go +++ b/internal/data/project.go @@ -19,6 +19,7 @@ import ( "github.com/acepanel/panel/internal/biz" "github.com/acepanel/panel/internal/http/request" + "github.com/acepanel/panel/pkg/systemctl" "github.com/acepanel/panel/pkg/types" ) @@ -206,6 +207,15 @@ func (r *projectRepo) parseProjectDetail(project *biz.Project) (*types.ProjectDe } } + // 获取运行状态 + if info, err := systemctl.GetServiceInfo(project.Name); err == nil { + detail.Status = info.Status + detail.PID = info.PID + detail.Memory = info.Memory + detail.CPU = info.CPU + detail.Uptime = info.Uptime + } + return detail, nil } diff --git a/internal/data/template.go b/internal/data/template.go new file mode 100644 index 00000000..d4979a5d --- /dev/null +++ b/internal/data/template.go @@ -0,0 +1,156 @@ +package data + +import ( + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/acepanel/panel/internal/app" + "github.com/acepanel/panel/internal/biz" + "github.com/acepanel/panel/pkg/api" + "github.com/acepanel/panel/pkg/firewall" + "github.com/acepanel/panel/pkg/types" +) + +type templateRepo struct { + api *api.API + firewall *firewall.Firewall +} + +func NewTemplateRepo() biz.TemplateRepo { + return &templateRepo{ + api: api.NewAPI(app.Version, app.Locale), + firewall: firewall.NewFirewall(), + } +} + +// List 获取所有模版 +func (r *templateRepo) List() (api.Templates, error) { + templates, err := r.api.Templates() + if err != nil { + return nil, err + } + return *templates, nil +} + +// Get 获取模版详情 +func (r *templateRepo) Get(slug string) (*api.Template, error) { + return r.api.TemplateBySlug(slug) +} + +// Callback 模版下载回调 +func (r *templateRepo) Callback(slug string) error { + return r.api.TemplateCallback(slug) +} + +// CreateCompose 创建编排 +func (r *templateRepo) CreateCompose(name, compose string, envs []types.KV, autoFirewall bool) error { + dir := filepath.Join(app.Root, "server", "compose", name) + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + if err := os.WriteFile(filepath.Join(dir, "docker-compose.yml"), []byte(compose), 0644); err != nil { + return err + } + + var sb strings.Builder + for _, kv := range envs { + sb.WriteString(kv.Key) + sb.WriteString("=") + sb.WriteString(kv.Value) + sb.WriteString("\n") + } + if err := os.WriteFile(filepath.Join(dir, ".env"), []byte(sb.String()), 0644); err != nil { + return err + } + + // 自动放行端口 + if autoFirewall { + ports := r.parsePortsFromCompose(compose) + for _, port := range ports { + _ = r.firewall.Port(firewall.FireInfo{ + Family: "ipv4", + PortStart: port.Port, + PortEnd: port.Port, + Protocol: port.Protocol, + Strategy: firewall.StrategyAccept, + Direction: "in", + }, firewall.OperationAdd) + } + } + + return nil +} + +type composePort struct { + Port uint + Protocol firewall.Protocol +} + +// parsePortsFromCompose 从 compose 文件中解析端口 +func (r *templateRepo) parsePortsFromCompose(compose string) []composePort { + var ports []composePort + seen := make(map[string]bool) + + // 匹配 ports 部分的端口映射 + // 支持格式: "8080:80", "8080:80/tcp", "8080:80/udp", "80", "80/tcp" + portRegex := regexp.MustCompile(`(?m)^\s*-\s*["']?(\d+)(?::\d+)?(?:/(\w+))?["']?\s*$`) + matches := portRegex.FindAllStringSubmatch(compose, -1) + + for _, match := range matches { + if len(match) < 2 { + continue + } + + portStr := match[1] + protocol := firewall.ProtocolTCP + if len(match) > 2 && match[2] != "" { + switch strings.ToLower(match[2]) { + case "udp": + protocol = firewall.ProtocolUDP + case "tcp": + protocol = firewall.ProtocolTCP + } + } + + // 去重 + key := portStr + "/" + string(protocol) + if seen[key] { + continue + } + seen[key] = true + + var port uint + if _, _, found := strings.Cut(portStr, ":"); found { + // 格式: host:container + parts := strings.Split(portStr, ":") + if len(parts) > 0 { + port = parseUint(parts[0]) + } + } else { + port = parseUint(portStr) + } + + if port > 0 && port <= 65535 { + ports = append(ports, composePort{ + Port: port, + Protocol: protocol, + }) + } + } + + return ports +} + +func parseUint(s string) uint { + var n uint + for _, c := range s { + if c >= '0' && c <= '9' { + n = n*10 + uint(c-'0') + } else { + break + } + } + return n +} diff --git a/internal/http/request/template.go b/internal/http/request/template.go new file mode 100644 index 00000000..f8b9128b --- /dev/null +++ b/internal/http/request/template.go @@ -0,0 +1,14 @@ +package request + +import "github.com/acepanel/panel/pkg/types" + +type TemplateSlug struct { + Slug string `uri:"slug" validate:"required"` +} + +type TemplateCreate struct { + Slug string `json:"slug" validate:"required"` + Name string `json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"` + Envs []types.KV `json:"envs"` + AutoFirewall bool `json:"auto_firewall"` +} diff --git a/internal/route/http.go b/internal/route/http.go index 7900c0f9..12cda143 100644 --- a/internal/route/http.go +++ b/internal/route/http.go @@ -54,6 +54,7 @@ type Http struct { toolboxDisk *service.ToolboxDiskService toolboxLog *service.ToolboxLogService webhook *service.WebHookService + template *service.TemplateService apps *apploader.Loader } @@ -96,6 +97,7 @@ func NewHttp( toolboxDisk *service.ToolboxDiskService, toolboxLog *service.ToolboxLogService, webhook *service.WebHookService, + template *service.TemplateService, apps *apploader.Loader, ) *Http { return &Http{ @@ -137,6 +139,7 @@ func NewHttp( toolboxDisk: toolboxDisk, toolboxLog: toolboxLog, webhook: webhook, + template: template, apps: apps, } } @@ -523,6 +526,13 @@ func (route *Http) Register(r *chi.Mux) { r.Delete("/{id}", route.webhook.Delete) }) + r.Route("/template", func(r chi.Router) { + r.Get("/", route.template.List) + r.Get("/{slug}", route.template.Get) + r.Post("/", route.template.Create) + r.Post("/{slug}/callback", route.template.Callback) + }) + r.Route("/apps", func(r chi.Router) { route.apps.Register(r) }) diff --git a/internal/service/cli.go b/internal/service/cli.go index 9575fb69..6d5504e8 100644 --- a/internal/service/cli.go +++ b/internal/service/cli.go @@ -932,6 +932,7 @@ func (s *CliService) Init(ctx context.Context, cmd *cli.Command) error { {Key: biz.SettingKeyMonitorDays, Value: "30"}, {Key: biz.SettingKeyBackupPath, Value: filepath.Join(app.Root, "backup")}, {Key: biz.SettingKeyWebsitePath, Value: filepath.Join(app.Root, "sites")}, + {Key: biz.SettingKeyProjectPath, Value: filepath.Join(app.Root, "projects")}, {Key: biz.SettingKeyWebsiteTLSVersions, Value: `["TLSv1.2","TLSv1.3"]`}, {Key: biz.SettingKeyWebsiteCipherSuites, Value: `ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305`}, {Key: biz.SettingKeyOfflineMode, Value: "false"}, diff --git a/internal/service/home.go b/internal/service/home.go index 6478e094..642ac9bd 100644 --- a/internal/service/home.go +++ b/internal/service/home.go @@ -10,8 +10,8 @@ import ( "github.com/leonelquinteros/gotext" "github.com/libtnb/chix" "github.com/libtnb/utils/collect" - "github.com/shirou/gopsutil/disk" - "github.com/shirou/gopsutil/host" + "github.com/shirou/gopsutil/v4/disk" + "github.com/shirou/gopsutil/v4/host" "github.com/spf13/cast" "github.com/acepanel/panel/internal/app" diff --git a/internal/service/process.go b/internal/service/process.go index ac6c0f08..f2b1c964 100644 --- a/internal/service/process.go +++ b/internal/service/process.go @@ -10,7 +10,7 @@ import ( "time" "github.com/libtnb/chix" - "github.com/shirou/gopsutil/process" + "github.com/shirou/gopsutil/v4/process" "github.com/acepanel/panel/internal/http/request" "github.com/acepanel/panel/pkg/types" @@ -159,7 +159,9 @@ func (s *ProcessService) processProcessBasic(proc *process.Process) types.Proces data.Username = username } data.PPID, _ = proc.Ppid() - data.Status, _ = proc.Status() + if status, err := proc.Status(); err == nil && len(status) > 0 { + data.Status = status[0] + } data.Background, _ = proc.Background() if ct, err := proc.CreateTime(); err == nil { data.StartTime = time.Unix(ct/1000, 0).Format(time.DateTime) @@ -188,7 +190,6 @@ func (s *ProcessService) processProcessFull(proc *process.Process) types.Process data.DiskRead = ioStat.ReadBytes } - data.Nets, _ = proc.NetIOCounters(false) data.Connections, _ = proc.Connections() data.CmdLine, _ = proc.Cmdline() data.OpenFiles, _ = proc.OpenFiles() diff --git a/internal/service/service.go b/internal/service/service.go index d4c05719..bab22d84 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -33,6 +33,7 @@ var ProviderSet = wire.NewSet( NewSSHService, NewSystemctlService, NewTaskService, + NewTemplateService, NewUserService, NewUserTokenService, NewWebHookService, diff --git a/internal/service/template.go b/internal/service/template.go new file mode 100644 index 00000000..20911a92 --- /dev/null +++ b/internal/service/template.go @@ -0,0 +1,110 @@ +package service + +import ( + "net/http" + + "github.com/leonelquinteros/gotext" + + "github.com/acepanel/panel/internal/biz" + "github.com/acepanel/panel/internal/http/request" +) + +type TemplateService struct { + t *gotext.Locale + templateRepo biz.TemplateRepo + settingRepo biz.SettingRepo +} + +func NewTemplateService(t *gotext.Locale, template biz.TemplateRepo, setting biz.SettingRepo) *TemplateService { + return &TemplateService{ + t: t, + templateRepo: template, + settingRepo: setting, + } +} + +// List 获取所有模版 +func (s *TemplateService) List(w http.ResponseWriter, r *http.Request) { + if offline, _ := s.settingRepo.GetBool(biz.SettingKeyOfflineMode); offline { + Error(w, http.StatusForbidden, s.t.Get("Unable to get template list in offline mode")) + return + } + + templates, err := s.templateRepo.List() + if err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + Success(w, templates) +} + +// Get 获取模版详情 +func (s *TemplateService) Get(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.TemplateSlug](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, "%v", err) + return + } + + if offline, _ := s.settingRepo.GetBool(biz.SettingKeyOfflineMode); offline { + Error(w, http.StatusForbidden, s.t.Get("Unable to get template in offline mode")) + return + } + + template, err := s.templateRepo.Get(req.Slug) + if err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + Success(w, template) +} + +// Create 使用模版创建编排 +func (s *TemplateService) Create(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.TemplateCreate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, "%v", err) + return + } + + if offline, _ := s.settingRepo.GetBool(biz.SettingKeyOfflineMode); offline { + Error(w, http.StatusForbidden, s.t.Get("Unable to create compose from template in offline mode")) + return + } + + // 获取模版 + template, err := s.templateRepo.Get(req.Slug) + if err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + // 创建编排 + if err = s.templateRepo.CreateCompose(req.Name, template.Compose, req.Envs, req.AutoFirewall); err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + // 回调 + _ = s.templateRepo.Callback(req.Slug) + + Success(w, nil) +} + +// Callback 模版下载回调 +func (s *TemplateService) Callback(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.TemplateSlug](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, "%v", err) + return + } + + if err = s.templateRepo.Callback(req.Slug); err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + Success(w, nil) +} diff --git a/pkg/api/api.go b/pkg/api/api.go index 413293a4..d5c53712 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -7,7 +7,7 @@ import ( "github.com/go-resty/resty/v2" "github.com/libtnb/utils/copier" - "github.com/shirou/gopsutil/host" + "github.com/shirou/gopsutil/v4/host" ) type API struct { diff --git a/pkg/systemctl/service.go b/pkg/systemctl/service.go index ac3bf792..0b0bf4c5 100644 --- a/pkg/systemctl/service.go +++ b/pkg/systemctl/service.go @@ -1,11 +1,105 @@ package systemctl import ( + "strconv" + "strings" "time" + "github.com/shirou/gopsutil/v4/process" + "github.com/acepanel/panel/pkg/shell" ) +// ServiceInfo 服务详细信息 +type ServiceInfo struct { + Status string // 运行状态 (active, inactive, failed, etc.) + PID int // 主进程 PID + Memory int64 // 内存使用(字节) + CPU float64 // CPU 使用率 + Uptime string // 运行时间 +} + +// GetServiceInfo 获取服务详细信息 +func GetServiceInfo(name string) (*ServiceInfo, error) { + output, err := shell.Execf("systemctl show '%s' --property=ActiveState,MainPID,ExecMainStartTimestamp --no-pager", name) + if err != nil { + return nil, err + } + + info := &ServiceInfo{} + for _, line := range strings.Split(output, "\n") { + parts := strings.SplitN(line, "=", 2) + if len(parts) != 2 { + continue + } + key, value := parts[0], parts[1] + switch key { + case "ActiveState": + info.Status = value + case "MainPID": + if pid, err := strconv.Atoi(value); err == nil { + info.PID = pid + } + case "ExecMainStartTimestamp": + // 格式: Mon 2024-01-01 12:00:00 UTC + if value != "" && value != "n/a" { + info.Uptime = calcUptime(value) + } + } + } + + // 如果有 PID,使用 gopsutil 获取进程信息 + if info.PID > 0 { + if proc, err := process.NewProcess(int32(info.PID)); err == nil { + // 获取内存信息 + if memInfo, err := proc.MemoryInfo(); err == nil && memInfo != nil { + info.Memory = int64(memInfo.RSS) + } + // 获取 CPU 使用率 + if cpu, err := proc.CPUPercent(); err == nil { + info.CPU = cpu + } + } + } + + return info, nil +} + +// calcUptime 计算运行时间 +func calcUptime(startTime string) string { + // 解析时间格式: Mon 2024-01-01 12:00:00 UTC + // 或者: Mon 2024-01-01 12:00:00 CST + layouts := []string{ + "Mon 2006-01-02 15:04:05 MST", + "Mon 2006-01-02 15:04:05 -0700", + } + + var t time.Time + var err error + for _, layout := range layouts { + t, err = time.Parse(layout, startTime) + if err == nil { + break + } + } + if err != nil { + return "" + } + + duration := time.Since(t) + days := int(duration.Hours() / 24) + hours := int(duration.Hours()) % 24 + minutes := int(duration.Minutes()) % 60 + + if days > 0 { + return strconv.Itoa(days) + "d " + strconv.Itoa(hours) + "h " + strconv.Itoa(minutes) + "m" + } + if hours > 0 { + return strconv.Itoa(hours) + "h " + strconv.Itoa(minutes) + "m" + } + return strconv.Itoa(minutes) + "m" +} + // Status 获取服务状态 func Status(name string) (bool, error) { output, _ := shell.Execf("systemctl is-active '%s'", name) // 不判断错误,因为 is-active 在服务未启用时会返回 3 diff --git a/pkg/tools/tools.go b/pkg/tools/tools.go index a8b8b41c..bad91f6c 100644 --- a/pkg/tools/tools.go +++ b/pkg/tools/tools.go @@ -13,12 +13,12 @@ import ( "time" "github.com/go-resty/resty/v2" - "github.com/shirou/gopsutil/cpu" - "github.com/shirou/gopsutil/disk" - "github.com/shirou/gopsutil/host" - "github.com/shirou/gopsutil/load" - "github.com/shirou/gopsutil/mem" - "github.com/shirou/gopsutil/net" + "github.com/shirou/gopsutil/v4/cpu" + "github.com/shirou/gopsutil/v4/disk" + "github.com/shirou/gopsutil/v4/host" + "github.com/shirou/gopsutil/v4/load" + "github.com/shirou/gopsutil/v4/mem" + "github.com/shirou/gopsutil/v4/net" "github.com/acepanel/panel/pkg/shell" "github.com/acepanel/panel/pkg/types" diff --git a/pkg/types/process.go b/pkg/types/process.go index 8d1f36cb..e9841d0b 100644 --- a/pkg/types/process.go +++ b/pkg/types/process.go @@ -1,8 +1,8 @@ package types import ( - "github.com/shirou/gopsutil/net" - "github.com/shirou/gopsutil/process" + "github.com/shirou/gopsutil/v4/net" + "github.com/shirou/gopsutil/v4/process" ) type ProcessData struct { @@ -35,5 +35,4 @@ type ProcessData struct { OpenFiles []process.OpenFilesStat `json:"open_files"` Connections []net.ConnectionStat `json:"connections"` - Nets []net.IOCountersStat `json:"nets"` } diff --git a/pkg/types/system.go b/pkg/types/system.go index 397d4b0f..85680c6a 100644 --- a/pkg/types/system.go +++ b/pkg/types/system.go @@ -3,12 +3,12 @@ package types import ( "time" - "github.com/shirou/gopsutil/cpu" - "github.com/shirou/gopsutil/disk" - "github.com/shirou/gopsutil/host" - "github.com/shirou/gopsutil/load" - "github.com/shirou/gopsutil/mem" - "github.com/shirou/gopsutil/net" + "github.com/shirou/gopsutil/v4/cpu" + "github.com/shirou/gopsutil/v4/disk" + "github.com/shirou/gopsutil/v4/host" + "github.com/shirou/gopsutil/v4/load" + "github.com/shirou/gopsutil/v4/mem" + "github.com/shirou/gopsutil/v4/net" ) // CurrentInfo 监控信息 diff --git a/pkg/webserver/nginx/proxy.go b/pkg/webserver/nginx/proxy.go index ca10a27f..13641861 100644 --- a/pkg/webserver/nginx/proxy.go +++ b/pkg/webserver/nginx/proxy.go @@ -229,7 +229,7 @@ func generateProxyConfig(proxy types.Proxy) string { if proxy.Host != "" { sb.WriteString(fmt.Sprintf(" proxy_set_header Host \"%s\";\n", proxy.Host)) } else { - sb.WriteString(" proxy_set_header Host $host;\n") + sb.WriteString(" proxy_set_header Host $proxy_host;\n") } // 标准代理头 diff --git a/web/src/api/panel/template/index.ts b/web/src/api/panel/template/index.ts new file mode 100644 index 00000000..f2550b14 --- /dev/null +++ b/web/src/api/panel/template/index.ts @@ -0,0 +1,17 @@ +import { http } from '@/utils' + +export default { + // 获取模版列表 + list: (): any => http.Get('/template'), + // 获取模版详情 + get: (slug: string): any => http.Get(`/template/${slug}`), + // 使用模版创建编排 + create: (data: { + slug: string + name: string + envs: { key: string; value: string }[] + auto_firewall: boolean + }): any => http.Post('/template', data), + // 模版下载回调 + callback: (slug: string): any => http.Post(`/template/${slug}/callback`) +} diff --git a/web/src/components/page/CommonPage.vue b/web/src/components/page/CommonPage.vue index 747ef771..93b7ef42 100644 --- a/web/src/components/page/CommonPage.vue +++ b/web/src/components/page/CommonPage.vue @@ -16,7 +16,7 @@ withDefaults(defineProps(), { diff --git a/web/src/views/app/types.ts b/web/src/views/app/types.ts index c9021c00..9f0695f1 100644 --- a/web/src/views/app/types.ts +++ b/web/src/views/app/types.ts @@ -17,3 +17,23 @@ export interface Channel { version: string log: string } + +export interface TemplateEnvironment { + name: string + type: 'text' | 'password' | 'number' | 'port' | 'select' + options?: Record + default: string +} + +export interface Template { + created_at: string + updated_at: string + slug: string + icon: string + name: string + description: string + categories: string[] + version: string + compose: string + environments: TemplateEnvironment[] +} diff --git a/web/src/views/toolbox/ProcessView.vue b/web/src/views/toolbox/ProcessView.vue index 0b5a6c8c..6da5f32a 100644 --- a/web/src/views/toolbox/ProcessView.vue +++ b/web/src/views/toolbox/ProcessView.vue @@ -76,19 +76,21 @@ const dropdownOptions = computed(() => { // 渲染状态标签 const renderStatus = (status: string) => { switch (status) { - case 'R': + case 'running': return h(NTag, { type: 'success' }, { default: () => $gettext('Running') }) - case 'S': + case 'blocked': + return h(NTag, { type: 'error' }, { default: () => $gettext('Blocked') }) + case 'sleep': return h(NTag, { type: 'warning' }, { default: () => $gettext('Sleeping') }) - case 'T': + case 'stop': return h(NTag, { type: 'error' }, { default: () => $gettext('Stopped') }) - case 'I': + case 'idle': return h(NTag, { type: 'primary' }, { default: () => $gettext('Idle') }) - case 'Z': + case 'zombie': return h(NTag, { type: 'error' }, { default: () => $gettext('Zombie') }) - case 'W': + case 'wait': return h(NTag, { type: 'warning' }, { default: () => $gettext('Waiting') }) - case 'L': + case 'lock': return h(NTag, { type: 'info' }, { default: () => $gettext('Locked') }) default: return h(NTag, { type: 'default' }, { default: () => status }) diff --git a/web/src/views/website/EditView.vue b/web/src/views/website/EditView.vue index 5e88127c..a3dc7705 100644 --- a/web/src/views/website/EditView.vue +++ b/web/src/views/website/EditView.vue @@ -666,7 +666,7 @@ const updateTimeoutUnit = (proxy: any, unit: string) => {