修改为 docker like 模式,新增 start create change stop ps 等常见操作

This commit is contained in:
keac
2026-01-07 21:23:39 +08:00
parent b706fe88db
commit ce5c99d134
13 changed files with 668 additions and 244 deletions

135
cmd/actions.go Normal file
View File

@@ -0,0 +1,135 @@
package cmd
import (
"fmt"
redc "red-cloud/mod"
"red-cloud/mod/gologger"
"github.com/spf13/cobra"
)
// helper: 通用的执行器
func runAction(actionType string, caseID string) {
// 1. 解析项目
pro, err := redc.ProjectParse(redc.Project, redc.U) // 注意:这里可能需要处理 global U 或者从配置读取
if err != nil {
gologger.Fatal().Msgf("项目解析失败: %s", err)
}
// 2. 查找 Case
c, err := pro.GetCase(caseID)
if err != nil {
gologger.Error().Msgf("操作失败: 找不到 ID 为「%s」的场景\n错误: %s", caseID, err)
return
}
redc.RedcLog(fmt.Sprintf("Action %s on %s", actionType, caseID))
// 3. 执行动作
var actionErr error
switch actionType {
case "stop":
actionErr = c.Stop()
case "start":
actionErr = c.TfApply()
case "kill":
actionErr = c.Kill()
case "change":
actionErr = c.Change()
case "status":
actionErr = c.Status()
case "rm":
if actionErr = c.Remove(); actionErr == nil {
actionErr = pro.HandleCase(c)
}
}
if actionErr != nil {
gologger.Error().Msgf("执行 %s 失败: %v", actionType, actionErr)
} else {
if err := pro.SaveProject(); err != nil {
gologger.Error().Msgf("项目状态保存失败!%s", err.Error())
return
}
gologger.Info().Msgf("✅ %s 操作执行成功: %s", actionType, caseID)
}
}
// 定义各个命令
var stopCmd = &cobra.Command{
Use: "stop [id]",
Short: "停止指定场景",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
runAction("stop", args[0])
},
}
var statusCmd = &cobra.Command{
Use: "status [id]",
Short: "查看场景状态",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
runAction("status", args[0])
},
}
var changeCmd = &cobra.Command{
Use: "change [id]",
Short: "更改场景",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
runAction("change", args[0])
},
}
var startCmd = &cobra.Command{
Use: "start [id]",
Short: "启动场景",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
runAction("start", args[0])
},
}
var killCmd = &cobra.Command{
Use: "kill [id]",
Short: "销毁指定场景",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
runAction("kill", args[0])
},
}
var rmCmd = &cobra.Command{
Use: "rm [id]",
Short: "删除场景 case",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
runAction("rm", args[0])
},
}
var listCmd = &cobra.Command{
Use: "ps",
Short: "列出当前所有场景",
Run: func(cmd *cobra.Command, args []string) {
pro, err := redc.ProjectParse(redc.Project, redc.U)
if err != nil {
gologger.Fatal().Msgf("项目解析失败: %s", err)
}
pro.CaseList()
},
}
// 注册命令
func init() {
rootCmd.AddCommand(stopCmd)
rootCmd.AddCommand(killCmd)
rootCmd.AddCommand(listCmd)
rootCmd.AddCommand(statusCmd)
rootCmd.AddCommand(changeCmd)
rootCmd.AddCommand(startCmd)
rootCmd.AddCommand(rmCmd)
//listCmd.Flags().BoolVarP(&redc.ShowAll, "all", "a", false, "查看所有 case")
}

68
cmd/create.go Normal file
View File

@@ -0,0 +1,68 @@
package cmd
import (
redc "red-cloud/mod"
"red-cloud/mod/gologger"
"github.com/spf13/cobra"
)
var (
userName string
projectName string
)
var runCmd = &cobra.Command{
Use: "run [template_name]",
Short: "创建并立即启动一个场景",
Example: "redc run ecs",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
templateName := args[0]
c := createLogic(templateName)
if err := c.TfApply(); err != nil {
gologger.Error().Msgf("场景启动失败!%s", err.Error())
}
},
}
var createCmd = &cobra.Command{
Use: "create [template_name]",
Short: "创建一个新的基础设施场景",
Example: "redc create ecs -u team1 -n operation_alpha",
Args: cobra.ExactArgs(1), // 强制要求输入一个模板名,例如 pte
Run: func(cmd *cobra.Command, args []string) {
templateName := args[0]
createLogic(templateName)
},
}
func createLogic(templateName string) *redc.Case {
// 别名处理
if templateName == "pte" {
templateName = "pte_arm"
}
// 解析 Project (这里需要确保 Config 已经在 root.go 加载了)
pro, err := redc.ProjectParse(redc.Project, userName) // 注意:这里使用了 flag 传入的 userName
if err != nil {
gologger.Fatal().Msgf("项目解析失败: %s", err)
}
// 创建 Case
c, err := pro.CaseCreate(templateName, userName, projectName)
if err != nil {
gologger.Error().Msgf("❌「%s」场景创建失败: %v", templateName, err)
return nil
}
gologger.Info().Msgf("✅「%s」场景创建完成", templateName)
return c
}
func init() {
rootCmd.AddCommand(createCmd)
rootCmd.AddCommand(runCmd)
createCmd.Flags().StringVarP(&userName, "user", "u", "system", "指定用户/操作员")
createCmd.Flags().StringVarP(&projectName, "name", "n", "", "指定项目/任务名称")
}

78
cmd/init.go Normal file
View File

@@ -0,0 +1,78 @@
package cmd
import (
"os"
redc "red-cloud/mod"
"red-cloud/mod/gologger"
"red-cloud/utils"
"github.com/spf13/cobra"
)
var initCmd = &cobra.Command{
Use: "init",
Short: "初始化环境和模板",
Run: func(cmd *cobra.Command, args []string) {
redc.RedcLog("执行初始化")
gologger.Info().Msg("初始化中...")
const templateDir = "redc-templates"
// 清理旧目录
os.RemoveAll(templateDir)
// 释放资源
if err := utils.ReleaseDir(templateDir); err != nil {
gologger.Fatal().Msgf("释放模板资源失败: %s", err)
}
// 遍历初始化
_, dirs := utils.GetFilesAndDirs("./" + templateDir)
for _, v := range dirs {
if err := redc.TfInit(v); err != nil {
gologger.Error().Msgf("❌「%s」场景初始化失败: %s", v, err)
} else {
gologger.Info().Msgf("✅「%s」场景初始化完成", v)
}
}
},
}
// ---------------------------------------------------------
// 5. Completion 命令 (自动生成补全脚本)
// ---------------------------------------------------------
var completionCmd = &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "生成命令补全脚本",
Long: `要在当前 Shell 中加载补全,请运行以下命令:
Bash:
$ source <(redc completion bash)
Zsh:
# 如果开启了 oh-my-zsh通常可以直接运行:
$ source <(redc completion zsh)
# 如果没有生效,可能需要手动配置 fpath (详细参考官方文档)
`,
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
Run: func(cmd *cobra.Command, args []string) {
switch args[0] {
case "bash":
cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
}
},
}
func init() {
rootCmd.AddCommand(completionCmd)
rootCmd.AddCommand(initCmd)
}

80
cmd/root.go Normal file
View File

@@ -0,0 +1,80 @@
package cmd
import (
"fmt"
"os"
redc "red-cloud/mod"
"red-cloud/mod/gologger"
"github.com/projectdiscovery/gologger/levels"
"github.com/spf13/cobra"
)
var (
cfgFile string
showVer bool
)
const BannerArt = `
██████╗ ███████╗ ██████╗ ██████╗
██╔══██╗ ██╔════╝ ██╔══██╗ ██╔════╝
██████╔╝ █████╗ ██║ ██║ ██║
██╔══██╗ ██╔══╝ ██║ ██║ ██║
██║ ██║ ███████╗ ██████╔╝ ╚██████╗
╚═╝ ╚═╝ ╚══════╝ ╚═════╝ ╚═════╝
`
// rootCmd
var rootCmd = &cobra.Command{
Use: "redc",
Short: "Red Cloud - 红队基础设施自动化工具",
Long: BannerArt + "\nRed Cloud 是一个用于快速部署和管理红队云基础设施的工具。",
// PersistentPreRun 在任何子命令执行前都会运行
PersistentPreRun: func(cmd *cobra.Command, args []string) {
// 如果是查版本,就不加载配置
if showVer {
return
}
// 统一加载配置
if err := redc.LoadConfig(cfgFile); err != nil {
gologger.Fatal().Msgf("配置文件加载失败: %s", err.Error())
}
if redc.Debug {
gologger.DefaultLogger.SetMaxLevel(levels.LevelDebug)
gologger.Debug().Msgf("当前已开始 DEBUG 模式!")
}
},
Run: func(cmd *cobra.Command, args []string) {
if showVer {
gologger.Print().Msgf("%s\nVersion: %s\n", BannerArt, redc.Version)
return
}
// 如果没参数也没flag打印帮助
cmd.Help()
},
}
// Execute 是 main.go 调用的入口
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func init() {
// 定义本地 Flag (只在 root 下有效)
rootCmd.Flags().BoolVarP(&showVer, "version", "v", false, "显示版本信息")
// 定义全局 Flag
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "./config.yaml", "配置文件路径")
// -u / --user
rootCmd.PersistentFlags().StringVarP(&redc.U, "user", "u", "system", "操作者")
// -p / --project
rootCmd.PersistentFlags().StringVarP(&redc.Project, "project", "p", "default", "项目名称")
// --debug
rootCmd.PersistentFlags().BoolVar(&redc.Debug, "debug", false, "开启调试模式")
}

21
go.mod
View File

@@ -3,12 +3,12 @@ module red-cloud
go 1.24.0
require (
github.com/gen2brain/beeep v0.0.0-20240516210008-9c006672e7f4
github.com/hashicorp/go-version v1.7.0
github.com/hashicorp/hc-install v0.9.2
github.com/hashicorp/terraform-exec v0.24.0
github.com/olekukonko/tablewriter v1.1.2
github.com/projectdiscovery/gologger v1.1.66
github.com/satori/go.uuid v1.2.0
github.com/spf13/cobra v1.10.2
golang.org/x/crypto v0.45.0
golang.org/x/text v0.31.0
gopkg.in/ini.v1 v1.67.0
@@ -23,30 +23,39 @@ require (
github.com/bodgit/plumbing v1.3.0 // indirect
github.com/bodgit/sevenzip v1.6.0 // indirect
github.com/bodgit/windows v1.0.1 // indirect
github.com/clipperhouse/displaywidth v0.6.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/djherbis/times v1.6.0 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/terraform-json v0.27.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/logrusorgru/aurora/v4 v4.0.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mholt/archives v0.1.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/nwaples/rardecode/v2 v2.2.0 // indirect
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.1.3 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/projectdiscovery/utils v0.8.0 // indirect
github.com/sorairolake/lzip-go v0.3.5 // indirect
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/therootcompany/xz v1.0.1 // indirect
github.com/ulikunitz/xz v0.5.14 // indirect
github.com/zclconf/go-cty v1.16.4 // indirect

37
go.sum
View File

@@ -40,8 +40,15 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s=
github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -59,8 +66,6 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/gen2brain/beeep v0.0.0-20240516210008-9c006672e7f4 h1:ygs9POGDQpQGLJPlq4+0LBUmMBNox1N4JSpw+OETcvI=
github.com/gen2brain/beeep v0.0.0-20240516210008-9c006672e7f4/go.mod h1:0W7dI87PvXJ1Sjs0QPvWXKcQmNERY77e8l7GFhZB/s4=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
@@ -69,10 +74,6 @@ github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi
github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -128,6 +129,8 @@ github.com/hashicorp/terraform-exec v0.24.0/go.mod h1:lluc/rDYfAhYdslLJQg3J0oDqo
github.com/hashicorp/terraform-json v0.27.1 h1:zWhEracxJW6lcjt/JvximOYyc12pS/gaKSy/wzzE7nY=
github.com/hashicorp/terraform-json v0.27.1/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -153,8 +156,11 @@ github.com/logrusorgru/aurora/v4 v4.0.0 h1:sRjfPpun/63iADiSvGGjgA1cAYegEWMPCJdUp
github.com/logrusorgru/aurora/v4 v4.0.0/go.mod h1:lP0iIa2nrnT/qoFXcOZSrZQpJ1o6n2CUf/hyHi2Q4ZQ=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mholt/archives v0.1.0 h1:FacgJyrjiuyomTuNA92X5GyRBRZjE43Y/lrzKIlF35Q=
github.com/mholt/archives v0.1.0/go.mod h1:j/Ire/jm42GN7h90F5kzj6hf6ZFzEH66de+hmjEKu+I=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -162,10 +168,16 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A=
github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.1.3 h1:sV2jrhQGq5B3W0nENUISCR6azIPf7UBUpVq0x/y70Fg=
github.com/olekukonko/ll v0.1.3/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=
github.com/olekukonko/tablewriter v1.1.2 h1:L2kI1Y5tZBct/O/TyZK1zIE9GlBj/TVs+AY5tZDCDSc=
github.com/olekukonko/tablewriter v1.1.2/go.mod h1:z7SYPugVqGVavWoA2sGsFIoOVNmEHxUAAMrhXONtfkg=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
@@ -179,6 +191,7 @@ github.com/projectdiscovery/utils v0.8.0 h1:8d79OCs5xGDNXdKxMUKMY/lgQSUWJMYB1B2S
github.com/projectdiscovery/utils v0.8.0/go.mod h1:CU6tjtyTRxBrnNek+GPJplw4IIHcXNZNKO09kWgqTdg=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
@@ -188,6 +201,10 @@ github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnB
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/sorairolake/lzip-go v0.3.5 h1:ms5Xri9o1JBIWvOFAorYtUNik6HI3HgBTkISiqu0Cwg=
github.com/sorairolake/lzip-go v0.3.5/go.mod h1:N0KYq5iWrMXI0ZEXKXaS9hCyOjZUQdBDEIbXfoUwbdk=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -198,8 +215,6 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk=
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o=
github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=
github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
@@ -216,6 +231,7 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -302,6 +318,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=

126
main.go
View File

@@ -1,131 +1,9 @@
package main
import (
_ "embed"
"flag"
"os"
redc "red-cloud/mod"
"red-cloud/mod/gologger"
"red-cloud/utils"
"red-cloud/cmd"
)
const banner = `
██████╗ ███████╗ ██████╗ ██████╗
██╔══██╗ ██╔════╝ ██╔══██╗ ██╔════╝
██████╔╝ █████╗ ██║ ██║ ██║
██╔══██╗ ██╔══╝ ██║ ██║ ██║
██║ ██║ ███████╗ ██████╔╝ ╚██████╗
╚═╝ ╚═╝ ╚══════╝ ╚═════╝ ╚═════╝
`
func Banner() {
gologger.Print().Msgf("%s\nVersion: %s\n\n", banner, redc.Version)
}
func main() {
flag.Parse()
// -version 显示版本号
if redc.V {
Banner()
os.Exit(0)
}
// 解析配置文件
if err := redc.LoadConfig("./config.yaml"); err != nil {
gologger.Fatal().Msgf("配置文件加载失败! %s", err.Error())
}
// -init 初始化
if redc.Init {
redc.RedcLog("进行初始化")
gologger.Info().Msgf("初始化中")
// 先删除文件夹
err := os.RemoveAll("redc-templates")
if err != nil {
gologger.Error().Msgf("初始化过程中删除模板文件夹失败: %s", err)
}
// 释放 templates 资源
utils.ReleaseDir("redc-templates")
// 遍历 redc-templates 文件夹,不包括子目录
_, dirs := utils.GetFilesAndDirs("./redc-templates")
for _, v := range dirs {
err = redc.TfInit(v)
if err != nil {
gologger.Error().Msgf("「%s」场景初始化失败\n %s", v, err)
} else {
gologger.Info().Msgf("✅「%s」场景初始化任务完成", v)
}
}
return
}
// 解析项目名称
pro, err := redc.ProjectParse(redc.Project, redc.U)
if err != nil {
gologger.Fatal().Msgf("项目解析失败: %s", err)
}
// list 操作查看项目里所有 case
if redc.List {
pro.CaseList()
}
// start 操作,去调用 case 创建方法
if redc.Start != "" {
redc.RedcLog("start " + redc.Start)
if redc.Start == "pte" {
redc.Start = "pte_arm"
}
err = pro.CaseCreate(redc.Start, redc.U, redc.Name)
if err != nil {
gologger.Error().Msgf("「%s」场景创建失败\n %s", redc.Start, err)
return
}
return
}
var targetID string // 用来存用户输入的那个 ID
// 先看用户用了哪个 flag把 ID 拿出来
switch {
case redc.Stop != "":
targetID = redc.Stop
case redc.Kill != "":
targetID = redc.Kill
case redc.Change != "":
targetID = redc.Change
case redc.Status != "":
targetID = redc.Status
default:
// 如果都不是,说明没输命令,直接结束
return
}
// 根据 ID 查找 Case 对象 (只查一次)
c, err := pro.GetCaseByUid(targetID)
if err != nil {
gologger.Error().Msgf("操作失败: 找不到 ID 为「%s」的场景\n错误: %s", targetID, err)
return
}
redc.RedcLog("Action on " + targetID)
var actionErr error // 接收执行错误
switch {
case redc.Stop != "":
// Stop 有特殊逻辑,需要额外处理
actionErr = c.Stop()
if actionErr == nil {
actionErr = pro.HandleCase(c)
}
case redc.Kill != "":
actionErr = c.Kill()
case redc.Change != "":
actionErr = c.Change()
case redc.Status != "":
actionErr = c.Status()
}
// 统一报错打印
if actionErr != nil {
gologger.Error().Msgf("执行失败: %v", actionErr)
}
cmd.Execute()
}

View File

@@ -1,6 +1,7 @@
package mod
import (
"encoding/hex"
"fmt"
"math/rand"
"os"
@@ -8,8 +9,16 @@ import (
"red-cloud/mod/gologger"
"red-cloud/utils"
"time"
)
uuid "github.com/satori/go.uuid"
type CaseState string
const (
StateRunning CaseState = "running"
StateStopped CaseState = "stopped"
StateError CaseState = "error"
StateCreated CaseState = "created"
StatePending CaseState = "pending"
)
func RandomName() string {
@@ -33,32 +42,71 @@ func RandomName() string {
return fmt.Sprint(lastName[rand.Intn(lastNameLen-1)]) + first
}
func (p *RedcProject) CaseCreate(CaseName string, User string, Name string) error {
// GenerateCaseID 生成 ID (64字符 hex string)
// 本质是 32 字节 (256 bit) 的随机数
func GenerateCaseID() string {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
// 极端情况下随机数生成失败,回退到时间戳+简单的随机,或者直接 panic
return fmt.Sprintf("%d", time.Now().UnixNano())
}
return hex.EncodeToString(b)
}
// CaseScene 场景参数判断
func CaseScene(t string) ([]string, error) {
var par []string
switch t {
case "cs-49", "c2-new", "snowc2":
par = RVar(
fmt.Sprintf("node_count=%d", Node),
fmt.Sprintf("domain=%s", Domain),
)
case "aws-proxy", "aliyun-proxy", "asm":
par = RVar(fmt.Sprintf("node_count=%d", Node))
case "dnslog", "xraydnslog", "interactsh":
if Domain == "360.com" {
return par, fmt.Errorf("创建 dnslog 时,域名不可为默认值")
}
par = RVar(fmt.Sprintf("domain=%s", Domain))
case "pss5", "frp", "frp-loki", "nps":
par = []string{fmt.Sprintf("base64_command=%s", Base64Command)}
case "asm-node":
par = RVar(
fmt.Sprintf("node_count=%d", Node),
fmt.Sprintf("domain2=%s", Domain2),
fmt.Sprintf("doamin=%s", Domain),
)
}
return par, nil
}
func (p *RedcProject) CaseCreate(CaseName string, User string, Name string) (*Case, error) {
// 创建新的 case 目录,这里不需要检测是否存在,因为名称是采用nanoID
gologger.Info().Msgf("正在创建场景 「%s」", CaseName)
uid := uuid.NewV4()
uid := GenerateCaseID()
// 从模版文件夹复制模版
tpPath := filepath.Join("redc-templates", CaseName)
casePath := filepath.Join(p.ProjectPath, uid.String())
casePath := filepath.Join(p.ProjectPath, uid)
// 复制 tf文件
gologger.Debug().Msgf("复制模版中 %s", uid.String())
gologger.Debug().Msgf("复制模版中 %s", uid)
if err := utils.Dir(tpPath, casePath); err != nil {
return fmt.Errorf("复制模版出错!\n%v", err)
return nil, fmt.Errorf("复制模版出错!\n%v", err)
}
// 在次 init,防止万一
if err := TfInit2(casePath); err != nil {
gologger.Error().Msgf("二次初始化失败!%s", err.Error())
return err
return nil, err
}
// 初始化结构参数
par, err := CaseScene(CaseName)
if err != nil {
gologger.Error().Msgf("场景参数校验失败!%s", err.Error())
return err
return nil, err
}
// 初始化实例名称
@@ -69,29 +117,30 @@ func (p *RedcProject) CaseCreate(CaseName string, User string, Name string) erro
// 初始化实例
c := &Case{
CreateTime: time.Now().Format("2006-01-02 15:04:05"),
Id: uid.String(),
Id: uid,
Name: Name,
Operator: User,
Path: casePath,
Type: CaseName,
Parameter: par,
State: StateCreated,
}
// 构建场景
if err := c.TfApply(); err != nil {
if err := c.TfPlan(); err != nil {
gologger.Error().Msgf("场景创建失败!%s", err.Error())
return err
return nil, err
}
gologger.Info().Msgf("场景创建成功!%s\n关闭命令: ./redc -stop %s", uid.String(), uid.String())
gologger.Info().Msgf("场景创建成功!%s", uid)
// 确认场景创建无误后,才会写入到配置文件中
err = p.AddCase(c)
err = p.SaveProject()
if err != nil {
gologger.Error().Msgf("项目配置保存失败!")
return err
return nil, err
}
RedcLog("创建成功 " + p.ProjectPath + uid.String() + " " + CaseName)
return nil
RedcLog("创建成功 " + p.ProjectPath + uid + " " + CaseName)
return c, nil
}
func (c *Case) TfApply() error {
@@ -100,33 +149,55 @@ func (c *Case) TfApply() error {
if err != nil {
return err
}
c.StateTime = time.Now().Format("2006-01-02 15:04:05")
c.State = StateRunning
return nil
}
func (c *Case) TfPlan() error {
var err error
err = TfPlan(c.Path, c.Parameter...)
if err != nil {
return err
}
c.StateTime = time.Now().Format("2006-01-02 15:04:05")
c.State = StateCreated
return nil
}
func (c *Case) TfDestroy() error {
err := TfDestroy(c.Path, c.Parameter)
if err != nil {
gologger.Error().Msgf("场景销毁失败!%s", err.Error())
return err
}
c.StateTime = time.Now().Format("2006-01-02 15:04:05")
c.State = StateStopped
return nil
}
func (c *Case) Remove() error {
if c.State == StateRunning {
return fmt.Errorf("场景正在运行中,请先停止场景后删除!")
}
c.StateTime = time.Now().Format("2006-01-02 15:04:05")
err := os.RemoveAll(c.Path)
if err != nil {
return fmt.Errorf("删除场景文件失败!%s", err.Error())
}
return nil
}
// Stop 停止场景
func (c *Case) Stop() error {
err := c.TfDestroy()
if err != nil {
return err
}
// 成功销毁场景后,删除 case 文件夹
err = os.RemoveAll(c.Path)
if err != nil {
fmt.Println(err)
os.Exit(3)
}
return nil
}
// Kill 强制销毁场景
func (c *Case) Kill() error {
// 在次 init,防止万一
dirs := utils.ChechDirMain(c.Path)
@@ -144,6 +215,7 @@ func (c *Case) Kill() error {
return nil
}
// Change 重建场景
func (c *Case) Change() error {
// 销毁场景,不删除项目
if err := c.TfDestroy(); err != nil {
@@ -153,17 +225,6 @@ func (c *Case) Change() error {
if err := c.TfApply(); err != nil {
return err
}
//if cfg.Section(UUID).Key("Type").String() == "cs-49" || cfg.Section(UUID).Key("Type").String() == "c2-new" || cfg.Section(UUID).Key("Type").String() == "snowc2" {
// C2Change(ProjectPath + "/" + UUID)
//} else if cfg.Section(UUID).Key("Type").String() == "aliyun-proxy" {
// AliyunProxyChange(ProjectPath + "/" + UUID)
//} else if cfg.Section(UUID).Key("Type").String() == "asm" {
// AsmChange(ProjectPath + "/" + UUID)
//} else {
// fmt.Printf("不适用与当前场景")
// os.Exit(3)
//}
return nil
}
@@ -171,3 +232,32 @@ func (c *Case) Status() error {
TfStatus(c.Path)
return nil
}
// humanDuration 计算时间差并返回 Docker 风格的字符串
// 例如: "Up 2 hours", "Up 5 minutes"
func humanDuration(t time.Time) string {
duration := time.Since(t)
seconds := int(duration.Seconds())
switch {
case seconds < 60:
return fmt.Sprintf("%d seconds", seconds)
case seconds < 3600:
return fmt.Sprintf("%d minutes", seconds/60)
case seconds < 86400:
return fmt.Sprintf("%d hours", seconds/3600)
default:
return fmt.Sprintf("%d days", seconds/86400)
}
}
// parseTime 将字符串时间转为 time.Time
func parseTime(timeStr string) time.Time {
// 对应你代码中的 time.Now().Format("2006-01-02 15:04:05")
layout := "2006-01-02 15:04:05"
t, err := time.ParseInLocation(layout, timeStr, time.Local)
if err != nil {
return time.Now() // 解析失败则返回当前时间,避免 panic
}
return t
}

View File

@@ -22,6 +22,7 @@ var (
Version = "v1.0.0(2025/12/04)"
C2Port string
C2Pass string
ShowAll bool
)
func init() {

View File

@@ -6,6 +6,7 @@ import (
"os"
"path/filepath"
"red-cloud/mod/gologger"
"strings"
"text/tabwriter"
"time"
)
@@ -22,42 +23,16 @@ type RedcProject struct {
// Case 项目信息
type Case struct {
// Id uuid
Id string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Operator string `json:"operator"`
Path string `json:"path"`
Node int `json:"node"`
CreateTime string `json:"create_time"`
Parameter []string `json:"parameter"`
}
// CaseScene 场景参数判断
func CaseScene(t string) ([]string, error) {
var par []string
switch t {
case "cs-49", "c2-new", "snowc2":
par = RVar(
fmt.Sprintf("node_count=%d", Node),
fmt.Sprintf("domain=%s", Domain),
)
case "aws-proxy", "aliyun-proxy", "asm":
par = RVar(fmt.Sprintf("node_count=%d", Node))
case "dnslog", "xraydnslog", "interactsh":
if Domain == "360.com" {
return par, fmt.Errorf("创建 dnslog 时,域名不可为默认值")
}
par = RVar(fmt.Sprintf("domain=%s", Domain))
case "pss5", "frp", "frp-loki", "nps":
par = []string{fmt.Sprintf("base64_command=%s", Base64Command)}
case "asm-node":
par = RVar(
fmt.Sprintf("node_count=%d", Node),
fmt.Sprintf("domain2=%s", Domain2),
fmt.Sprintf("doamin=%s", Domain),
)
}
return par, nil
Id string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Operator string `json:"operator"`
Path string `json:"path"`
Node int `json:"node"`
CreateTime string `json:"create_time"`
StateTime string `json:"state_time"`
Parameter []string `json:"parameter"`
State CaseState `json:"state"`
}
// NewProjectConfig 创建项目配置文件
@@ -125,14 +100,42 @@ func ProjectByName(name string) (*RedcProject, error) {
return &project, nil
}
// GetCaseByUid 从项目中匹配 case
func (p *RedcProject) GetCaseByUid(uid string) (*Case, error) {
for i, caseInfo := range p.Case {
if caseInfo.Id == uid {
return &p.Case[i], nil
// GetCase 支持通过 ID(精确/模糊) 或 Name(精确) 查找 Case
// 逻辑参考 Docker: 优先精确匹配,其次 ID 前缀匹配。如果 ID 前缀匹配到多个,则报错歧义。
func (p *RedcProject) GetCase(identifier string) (*Case, error) {
var candidates []*Case
// 遍历所有 Case
for i := range p.Case {
// 使用指针引用,避免大结构体复制,且允许返回原始切片中的地址
c := &p.Case[i]
// 1. 第一优先级:精确匹配 (ID 或 Name)
// 如果输入的字符串完全等于 ID 或 Name直接认定为目标
if c.Id == identifier || c.Name == identifier {
return c, nil
}
// 2. 第二优先级ID 前缀模糊匹配 (Docker 风格)
// 只有当 identifier 是 ID 的前缀时才算 (例如输入 "abc" 匹配 "abcde")
// 注意:通常不对 Name 做前缀匹配,防止误操作,这里只针对 ID
if strings.HasPrefix(c.Id, identifier) {
candidates = append(candidates, c)
}
}
return nil, fmt.Errorf("项目 %s ,未找到uid为 %s 的case", p.ProjectName, uid)
// 3. 处理匹配结果
if len(candidates) == 0 {
return nil, fmt.Errorf("在项目 %s 中未找到 ID 或名称为 '%s' 的场景", p.ProjectName, identifier)
}
if len(candidates) == 1 {
return candidates[0], nil
}
// 4. 歧义处理 (匹配到多个 ID 前缀)
// 例如输入 "a1", 既匹配了 "a1b2..." 也匹配了 "a1c3..."
return nil, fmt.Errorf("输入 '%s' 存在歧义,匹配到 %d 个场景 (请提供更完整的 ID)", identifier, len(candidates))
}
// HandleCase 删除指定uid的case
@@ -166,25 +169,68 @@ func (p *RedcProject) AddCase(c *Case) error {
return nil
}
// CaseList 输出项目进程
func (p *RedcProject) CaseList() {
// 使用 tabwriter 创建表格输出
w := tabwriter.NewWriter(os.Stdout, 15, 0, 1, ' ', tabwriter.AlignRight)
fmt.Fprintln(w, "UUID\tType\tName\tOperator\tCreateTime\t")
// minwidth=0: 最小单元格宽度
// tabwidth=8: tab 字符宽度
// padding=3: 列之间至少保留 3 个空格(比原来的 1 个更清晰)
// padchar=' ': 填充符
// flags=0: 默认左对齐 (Docker 风格),去掉 AlignRight
w := tabwriter.NewWriter(os.Stdout, 0, 8, 3, ' ', 0)
// 优化2: 表头全大写,符合 CLI 惯例
// 并在每一列后明确加上 \t 进行分割
fmt.Fprintln(w, "Case ID\tTYPE\tNAME\tOPERATOR\tCREATED\tSTATUS")
// 遍历项目中的所有 Case
for _, c := range p.Case {
// 鉴权:只显示当前用户或 system 用户的 Case
if c.Operator == U || U == "system" {
// 从 Case 结构中获取 Type从 Name 字段获取类型信息,或者需要额外的 Type 字段)
caseType := c.Name // 假设 Name 包含类型信息,或者需要在 Case 结构中添加 Type 字段
fmt.Fprintln(w, c.Id, "\t", caseType, "\t", c.Name, "\t", c.Operator, "\t", c.CreateTime)
// ID过长显示前12位
displayID := c.Id
if len(c.Id) > 12 {
displayID = c.Id[:12]
}
createTime := parseTime(c.StateTime) // 解析字符串时间
var displayStatus string
// 3. 这里使用 c.State 和新的常量
switch c.State {
case StateRunning:
displayStatus = fmt.Sprintf("Up %s", humanDuration(createTime))
case StateStopped:
displayStatus = fmt.Sprintf("Exited (0) %s ago", humanDurationShort(createTime))
case StateError:
displayStatus = "Error"
case StateCreated:
displayStatus = "Created"
// 如果之前的旧数据没有 State 字段,可能需要一个默认兜底
case "":
displayStatus = "Unknown"
default:
displayStatus = string(c.State)
}
// 使用 Fprintf 配合 \t 格式化输出
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
displayID,
c.Type,
c.Name,
c.Operator,
c.CreateTime,
displayStatus,
)
}
// 刷新缓冲区,确保输出
if err := w.Flush(); err != nil {
gologger.Fatal().Msgf("表格输出失败:/%s", err.Error())
// 假设 gologger 是你的日志库
// gologger.Fatal().Msgf("表格输出失败: %s", err.Error())
fmt.Fprintf(os.Stderr, "表格输出失败: %s\n", err.Error())
}
}
// SaveProject 将修改后的项目配置写回 JSON 文件
@@ -212,3 +258,17 @@ func (p *RedcProject) SaveProject() error {
return nil
}
// 简单的时长计算,返回 "2 hours", "5 minutes" 等
func humanDurationShort(t time.Time) string {
d := time.Since(t)
if d.Seconds() < 60 {
return fmt.Sprintf("%.0f seconds", d.Seconds())
} else if d.Minutes() < 60 {
return fmt.Sprintf("%.0f minutes", d.Minutes())
} else if d.Hours() < 24 {
return fmt.Sprintf("%.0f hours", d.Hours())
} else {
return fmt.Sprintf("%.0f days", d.Hours()/24)
}
}

View File

@@ -46,7 +46,9 @@ func NewTerraformExecutor(workingDir string) (*TerraformExecutor, error) {
return nil, fmt.Errorf("failed to create terraform executor: %w", err)
}
// Set stdout and stderr to os defaults for visibility
tf.SetStdout(os.Stdout)
if Debug {
tf.SetStdout(os.Stdout)
}
tf.SetStderr(os.Stderr)
return &TerraformExecutor{

View File

@@ -60,29 +60,35 @@ func TfInit2(Path string) error {
func RVar(s ...string) []string {
return s
}
func TfPlan(Path string, opts ...string) error {
ctx, cancel := createContextWithTimeout()
defer cancel()
fmt.Printf("Planing terraform in %s\n", Path)
te, err := NewTerraformExecutor(Path)
if err != nil {
return fmt.Errorf("场景创建失败: %w", err)
}
err = te.Plan(ctx, ToPlan(opts)...)
if err != nil {
gologger.Error().Msgf("场景创建失败: %v", err)
return err
}
return nil
}
func TfApply(Path string, opts ...string) error {
ctx, cancel := createContextWithTimeout()
defer cancel()
fmt.Printf("Applying terraform in %s\n", Path)
te, err := NewTerraformExecutor(Path)
if err != nil {
return fmt.Errorf("场景创建失败,terraform未找到或配置错误: %w", err)
return fmt.Errorf("场景启动失败,terraform未找到或配置错误: %w", err)
}
err = te.Apply(ctx, ToApply(opts)...)
if err != nil {
gologger.Error().Msgf("场景创建失败,正在尝试第二次创建: %v", err)
err = te.Destroy(ctx)
if err != nil {
gologger.Error().Msgf("场景删除失败!: %v", err)
return err
}
// Retry apply
err2 := te.Apply(ctx, ToApply(opts)...)
if err2 != nil {
return fmt.Errorf("场景创建第二次失败!请手动排查问题,path路径: %s : %w", Path, err2)
}
gologger.Error().Msgf("场景启动失败: %v", err)
return err
}
return nil
}
@@ -97,7 +103,6 @@ func TfStatus(Path string) {
if err != nil {
gologger.Error().Msgf("场景状态查询失败,terraform未找到或配置错误: %v", err)
}
err = te.Show(ctx)
if err != nil {
gologger.Error().Msgf("场景状态查询失败!请手动排查问题,path路径: %s,%v", Path, err)

View File

@@ -108,7 +108,7 @@ func GetFilesAndDirs(dirPth string) (files []string, dirs []string) {
}
// ReleaseDir 释放文件夹
func ReleaseDir(path string) {
func ReleaseDir(path string) error {
dirs, _ := local.ReadDir(path)
for _, entry := range dirs {
if entry.IsDir() {
@@ -116,7 +116,7 @@ func ReleaseDir(path string) {
err := os.MkdirAll(path+"/"+entry.Name(), os.ModePerm)
if err != nil {
fmt.Println(err)
return
return err
}
ReleaseDir(path + "/" + entry.Name())
} else {
@@ -126,11 +126,12 @@ func ReleaseDir(path string) {
_, err := io.Copy(out, in)
if err != nil {
fmt.Println(err)
return
return err
}
in.Close()
}
}
return nil
}
// ChechDirMain 递归