mirror of
https://github.com/wgpsec/redc.git
synced 2026-01-24 12:43:19 +08:00
修改为 docker like 模式,新增 start create change stop ps 等常见操作
This commit is contained in:
135
cmd/actions.go
Normal file
135
cmd/actions.go
Normal 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
68
cmd/create.go
Normal 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
78
cmd/init.go
Normal 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
80
cmd/root.go
Normal 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
21
go.mod
@@ -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
37
go.sum
@@ -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
126
main.go
@@ -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()
|
||||
}
|
||||
|
||||
158
mod/case.go
158
mod/case.go
@@ -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
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ var (
|
||||
Version = "v1.0.0(2025/12/04)"
|
||||
C2Port string
|
||||
C2Pass string
|
||||
ShowAll bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
166
mod/project.go
166
mod/project.go
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
31
mod/tf.go
31
mod/tf.go
@@ -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)
|
||||
|
||||
@@ -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 递归
|
||||
|
||||
Reference in New Issue
Block a user