Scert/backend/internal/cli/server.go
xuans 7284958819 完善域名服务器项目归属与数据库关联约束
保存资源项目、所有者和操作人,接口从会话记录归属,普通更新保留创建信息。
新增三库第十一版迁移、同项目绑定约束和 SQLite 触发器保护,补充迁移回滚重试、仓储、接口及备份恢复测试。
本次整理验证:后端构建、静态检查、关联改造针对性测试和 CLI 包测试通过。
范围说明:项目成员权限及其他资源归属尚未完成;本次未重新执行外部数据库实库、独立程序及生产部署验收。
2026-09-28 10:45:00 +08:00

238 lines
8.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package cli
import (
"fmt"
"strings"
"time"
"github.com/spf13/cobra"
"git.pcux.cn/xuans/Scert/backend/internal/app"
"git.pcux.cn/xuans/Scert/backend/internal/deployment"
"git.pcux.cn/xuans/Scert/backend/internal/domain"
)
func newServerCommand() *cobra.Command {
command := &cobra.Command{Use: "server", Short: "管理部署服务器"}
command.AddCommand(newServerListCommand(), newServerShowCommand(), newServerAddCommand(), newServerEditCommand(), newServerDeleteCommand())
return command
}
func newServerListCommand() *cobra.Command {
return &cobra.Command{Use: "list", Short: "查看部署服务器列表", RunE: func(cmd *cobra.Command, args []string) error {
service, err := openApplication(cmd)
if err != nil {
return err
}
defer service.Close()
items, err := service.Servers.ListServers(cmd.Context())
if err != nil {
return err
}
return printJSON(cmd, items)
}}
}
func newServerShowCommand() *cobra.Command {
var id string
command := &cobra.Command{Use: "show", Short: "查看部署服务器详情", RunE: func(cmd *cobra.Command, args []string) error {
service, err := openApplication(cmd)
if err != nil {
return err
}
defer service.Close()
item, err := service.Servers.GetServerByID(cmd.Context(), id)
if err != nil {
return err
}
return printJSON(cmd, item)
}}
command.Flags().StringVar(&id, "id", "", "服务器 ID")
_ = command.MarkFlagRequired("id")
return command
}
func newServerAddCommand() *cobra.Command {
var value serverFlags
command := &cobra.Command{Use: "add", Short: "添加部署服务器", RunE: func(cmd *cobra.Command, args []string) error {
service, err := openApplication(cmd)
if err != nil {
return err
}
defer service.Close()
item := value.server()
now := time.Now().UTC()
item.CreatedAt = now
item.UpdatedAt = now
item.ID = domain.NewID()
if _, err := service.SSHKeys.ValidateReference(cmd.Context(), item.PrivateKeyRef); err != nil {
return fmt.Errorf("SSH 私钥引用无效:%w", err)
}
if err := service.Servers.CreateServer(cmd.Context(), item); err != nil {
return err
}
return printJSON(cmd, item.Normalize())
}}
value.bind(command)
return command
}
func newServerEditCommand() *cobra.Command {
var id string
var value serverFlags
command := &cobra.Command{Use: "edit", Short: "编辑部署服务器", RunE: func(cmd *cobra.Command, args []string) error {
service, err := openApplication(cmd)
if err != nil {
return err
}
defer service.Close()
existing, err := service.Servers.GetServerByID(cmd.Context(), id)
if err != nil {
return err
}
item := value.server()
item.ID = existing.ID
item.ResourceScope = existing.ResourceScope
item.UpdatedBy = ""
item.CreatedAt = existing.CreatedAt
item.UpdatedAt = time.Now().UTC()
if _, err := service.SSHKeys.ValidateReference(cmd.Context(), item.PrivateKeyRef); err != nil {
return fmt.Errorf("SSH 私钥引用无效:%w", err)
}
if err := service.Servers.UpdateServer(cmd.Context(), item); err != nil {
return err
}
return printJSON(cmd, item.Normalize())
}}
command.Flags().StringVar(&id, "id", "", "服务器 ID")
_ = command.MarkFlagRequired("id")
value.bind(command)
return command
}
func newServerDeleteCommand() *cobra.Command {
var id string
command := &cobra.Command{Use: "delete", Short: "删除部署服务器", RunE: func(cmd *cobra.Command, args []string) error {
service, err := openApplication(cmd)
if err != nil {
return err
}
defer service.Close()
if err := service.Servers.DeleteServer(cmd.Context(), id); err != nil {
return err
}
fmt.Fprintf(cmd.OutOrStdout(), "部署服务器已删除:%s\n", id)
return nil
}}
command.Flags().StringVar(&id, "id", "", "服务器 ID")
_ = command.MarkFlagRequired("id")
return command
}
func newDomainServersCommand() *cobra.Command {
command := &cobra.Command{Use: "servers", Short: "管理域名关联的部署服务器"}
command.AddCommand(newDomainServersListCommand(), newDomainServersSetCommand())
return command
}
func newDomainServersListCommand() *cobra.Command {
var domainID string
command := &cobra.Command{Use: "list", Short: "查看域名关联的部署服务器", RunE: func(cmd *cobra.Command, args []string) error {
service, err := openApplication(cmd)
if err != nil {
return err
}
defer service.Close()
if _, err := service.Repository.GetByID(cmd.Context(), domainID); err != nil {
return err
}
items, err := service.Servers.ListServersByDomainID(cmd.Context(), domainID)
if err != nil {
return err
}
return printJSON(cmd, items)
}}
command.Flags().StringVar(&domainID, "domain-id", "", "域名 ID")
_ = command.MarkFlagRequired("domain-id")
return command
}
func newDomainServersSetCommand() *cobra.Command {
var domainID string
var serverIDs []string
command := &cobra.Command{Use: "set", Short: "替换域名关联的部署服务器", RunE: func(cmd *cobra.Command, args []string) error {
service, err := openApplication(cmd)
if err != nil {
return err
}
defer service.Close()
if _, err := service.Repository.GetByID(cmd.Context(), domainID); err != nil {
return err
}
if err := service.Servers.ReplaceDomainServers(cmd.Context(), domainID, serverIDs); err != nil {
return err
}
items, err := service.Servers.ListServersByDomainID(cmd.Context(), domainID)
if err != nil {
return err
}
return printJSON(cmd, items)
}}
command.Flags().StringVar(&domainID, "domain-id", "", "域名 ID")
command.Flags().StringSliceVar(&serverIDs, "server-ids", nil, "服务器 ID 列表;留空表示清除全部关联")
_ = command.MarkFlagRequired("domain-id")
return command
}
type serverFlags struct {
name, address, username, privateKeyRef, fingerprint string
certificatePath, privateKeyPath, chainPath, webServer string
validationCommand, reloadCommand []string
port int
}
func (f *serverFlags) bind(command *cobra.Command) {
command.Flags().StringVar(&f.name, "name", "", "服务器名称")
command.Flags().StringVar(&f.address, "address", "", "服务器地址")
command.Flags().IntVar(&f.port, "port", 22, "SSH 端口")
command.Flags().StringVar(&f.username, "username", "", "SSH 用户")
command.Flags().StringVar(&f.privateKeyRef, "private-key-ref", "", "SSH 私钥的敏感信息引用")
command.Flags().StringVar(&f.fingerprint, "host-key-fingerprint", "", "SSH 主机 SHA256 指纹")
command.Flags().StringVar(&f.certificatePath, "certificate-path", deployment.DefaultCertificatePath, "远程证书文件路径模板")
command.Flags().StringVar(&f.privateKeyPath, "private-key-path", deployment.DefaultPrivateKeyPath, "远程私钥文件路径模板")
command.Flags().StringVar(&f.chainPath, "chain-path", deployment.DefaultCertificateChainPath, "远程完整证书链路径模板")
command.Flags().StringVar(&f.webServer, "web-server", string(deployment.WebServerNginx), "Web 服务类型:nginx、caddy、apache、custom")
command.Flags().StringSliceVar(&f.validationCommand, "validation-command", nil, "自定义配置校验命令及参数")
command.Flags().StringSliceVar(&f.reloadCommand, "reload-command", nil, "自定义 reload 命令及参数")
for _, name := range []string{"name", "address", "username", "private-key-ref", "host-key-fingerprint"} {
_ = command.MarkFlagRequired(name)
}
}
func (f serverFlags) server() deployment.Server {
return deployment.Server{
Name: f.name, Address: f.address, Port: f.port, Username: f.username,
PrivateKeyRef: f.privateKeyRef, HostKeyFingerprint: f.fingerprint,
CertificatePath: f.certificatePath, PrivateKeyPath: f.privateKeyPath,
CertificateChainPath: f.chainPath, WebServer: deployment.WebServer(f.webServer),
ValidationCommand: normalizeCommand(f.validationCommand), ReloadCommand: normalizeCommand(f.reloadCommand),
}
}
func normalizeCommand(values []string) []string {
command := make([]string, 0, len(values))
for _, value := range values {
if value = strings.TrimSpace(value); value != "" {
command = append(command, value)
}
}
return command
}
func openApplication(cmd *cobra.Command) (*app.App, error) {
cfg, err := loadConfig()
if err != nil {
return nil, err
}
return app.New(cmd.Context(), cfg)
}