main.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. // @Author: F.Michel
  2. // @github: https://github.com/phpdevcommunity
  3. package main
  4. import (
  5. "bufio"
  6. "crypto/rand"
  7. "fmt"
  8. "io"
  9. "log"
  10. "net"
  11. "os"
  12. "os/exec"
  13. "path/filepath"
  14. "strconv"
  15. "strings"
  16. "time"
  17. "github.com/pterm/pterm"
  18. "github.com/urfave/cli/v2"
  19. )
  20. var (
  21. workflows []Workflow
  22. dockerComposeCommand = ""
  23. workflowDepth = 0
  24. )
  25. func main() {
  26. currentDir, _ := os.Getwd()
  27. wfFiles, _ := filepath.Glob(filepath.Join(currentDir, "*.wf"))
  28. if len(wfFiles) == 0 {
  29. pterm.Warning.Println("No workflow files found")
  30. }
  31. workflows = []Workflow{}
  32. for _, wfFile := range wfFiles {
  33. for _, wf := range ParseContentToWorkFlowStruct(wfFile) {
  34. if len(wf.Lines) > 0 {
  35. workflows = append(workflows, wf)
  36. }
  37. }
  38. }
  39. Commands := []*cli.Command{}
  40. values := InitDefaultVariables()
  41. for _, wf := range workflows {
  42. Commands = append(Commands, &cli.Command{
  43. Name: wf.Name,
  44. Usage: wf.Comment,
  45. Action: func(c *cli.Context) error {
  46. executeWorkflow(wf, values)
  47. return nil
  48. },
  49. })
  50. }
  51. if len(os.Args) <= 1 {
  52. pterm.DefaultBigText.WithLetters(
  53. pterm.NewLettersFromStringWithStyle("W", pterm.NewStyle(pterm.FgCyan)),
  54. pterm.NewLettersFromStringWithStyle("F", pterm.NewStyle(pterm.FgBlue)),
  55. ).Render()
  56. }
  57. app := &cli.App{Commands: Commands}
  58. if err := app.Run(os.Args); err != nil {
  59. log.Fatal(err)
  60. }
  61. }
  62. func executeWorkflow(wf Workflow, values map[string]string) {
  63. fmt.Println()
  64. start := time.Now()
  65. depth := workflowDepth
  66. workflowDepth++
  67. defer func() { workflowDepth-- }()
  68. indent := ""
  69. if depth > 0 {
  70. indent = strings.Repeat(" ", depth)
  71. }
  72. if depth == 0 {
  73. pterm.DefaultHeader.
  74. WithFullWidth().
  75. WithBackgroundStyle(pterm.NewStyle(pterm.BgCyan)).
  76. WithMargin(1).
  77. Printfln("🚀 WORKFLOW: %s", strings.ToUpper(wf.Name))
  78. } else {
  79. pterm.FgYellow.Printfln("%s╭── 📦 Sub-Workflow: %s", indent, wf.Name)
  80. }
  81. for _, line := range wf.Lines {
  82. executeLine(line, values)
  83. }
  84. fmt.Println()
  85. elapsed := time.Since(start).Round(time.Millisecond)
  86. if depth == 0 {
  87. pterm.Success.Printfln("✨ Workflow '%s' completed in %s.", wf.Name, elapsed)
  88. fmt.Println()
  89. } else {
  90. pterm.FgYellow.Printfln("%s╰── ✔ Completed in %s", indent, elapsed)
  91. }
  92. }
  93. func FileExists(filename string) bool {
  94. _, err := os.Stat(filename)
  95. return err == nil || !os.IsNotExist(err)
  96. }
  97. func Touch(filename string) {
  98. fd, err := os.OpenFile(filename, os.O_RDONLY|os.O_CREATE, 0666)
  99. if err != nil {
  100. fatal("Failed to touch file %s: %v", filename, err)
  101. }
  102. fd.Close()
  103. }
  104. func Copy(source string, dest string) {
  105. fd1, err := os.Open(source)
  106. if err != nil {
  107. fatal("Failed to open source file %s: %v", source, err)
  108. }
  109. defer fd1.Close()
  110. fd2, err := os.OpenFile(dest, os.O_WRONLY|os.O_CREATE, 0644)
  111. if err != nil {
  112. fatal("Failed to create destination file %s: %v", dest, err)
  113. }
  114. defer fd2.Close()
  115. if _, err := io.Copy(fd2, fd1); err != nil {
  116. fatal("Failed to copy %s to %s: %v", source, dest, err)
  117. }
  118. }
  119. func executeLine(lineOriginal string, values map[string]string) {
  120. if lineOriginal == "" || strings.HasPrefix(lineOriginal, "#") {
  121. return
  122. }
  123. lineOriginal = ResolveVariables(values, lineOriginal)
  124. actionOriginal := strings.Split(lineOriginal, " ")[0]
  125. action := strings.ToLower(actionOriginal)
  126. line := strings.Replace(lineOriginal, actionOriginal, action, 1)
  127. switch action {
  128. case "set":
  129. line = strings.TrimPrefix(line, "set ")
  130. parts := strings.Split(line, "=")
  131. if len(parts) < 2 || parts[0] == "" || parts[1] == "" {
  132. fatal("Invalid set command")
  133. }
  134. values[parts[0]] = parts[1]
  135. printLine(pterm.Info.Sprintf("Variable %s set to %s", parts[0], values[parts[0]]))
  136. case "run":
  137. run(strings.TrimPrefix(line, "run "), true)
  138. case "echo":
  139. msg := strings.TrimSpace(strings.TrimPrefix(line, "echo "))
  140. if strings.HasPrefix(msg, "\"") && strings.HasSuffix(msg, "\"") {
  141. msg = msg[1 : len(msg)-1]
  142. }
  143. printLine(pterm.DefaultBasicText.Sprint(" "+msg))
  144. case "exit":
  145. os.Exit(0)
  146. case "touch":
  147. fileToCreate := strings.TrimSpace(strings.TrimPrefix(line, "touch "))
  148. if fileToCreate == "" {
  149. fatal("Invalid touch command")
  150. }
  151. if FileExists(fileToCreate) {
  152. printLine(pterm.Info.Sprintf("%s already exists, skipping...", fileToCreate))
  153. return
  154. }
  155. Touch(fileToCreate)
  156. printLine(pterm.Success.Sprintf("%s created", fileToCreate))
  157. case "copy", "cp":
  158. line = strings.TrimPrefix(line, "copy ")
  159. parts := strings.Fields(line)
  160. if len(parts) < 2 {
  161. fatal("Invalid copy command")
  162. }
  163. fileOrFolder, destination := parts[0], parts[1]
  164. if !FileExists(fileOrFolder) {
  165. fatal("%s not found", fileOrFolder)
  166. }
  167. if FileExists(destination) {
  168. printLine(pterm.Info.Sprintf("%s already exists, skipping...", destination))
  169. return
  170. }
  171. Copy(fileOrFolder, destination)
  172. printLine(pterm.Success.Sprintf("%s copied to %s", fileOrFolder, destination))
  173. case "mkdir":
  174. parts := strings.Fields(line)
  175. if len(parts) < 2 {
  176. fatal("Invalid mkdir command")
  177. }
  178. folderName := parts[1]
  179. if FileExists(folderName) {
  180. printLine(pterm.Info.Sprintf("Folder %s already exists, skipping...", folderName))
  181. return
  182. }
  183. if err := os.MkdirAll(folderName, os.ModePerm); err != nil {
  184. fatal(err.Error())
  185. }
  186. printLine(pterm.Success.Sprintf("Folder '%s' created", folderName))
  187. case "set_permissions":
  188. args := strings.Fields(line)
  189. if len(args) < 3 {
  190. fatal("Invalid set_permissions command: %s", line)
  191. }
  192. folderName := args[1]
  193. permissions, err := strconv.ParseUint(args[2], 8, 32)
  194. if err != nil {
  195. fatal("Invalid permissions: %s", args[2])
  196. }
  197. fi, err := os.Stat(folderName)
  198. if err != nil {
  199. fatal("Failed to access '%s': %s", folderName, err)
  200. }
  201. err = filepath.Walk(folderName, func(path string, info os.FileInfo, walkErr error) error {
  202. if walkErr != nil {
  203. return walkErr
  204. }
  205. return os.Chmod(path, os.FileMode(permissions))
  206. })
  207. if err != nil {
  208. fatal("Failed to set permissions on '%s': %s", folderName, err)
  209. }
  210. if fi.IsDir() {
  211. printLine(pterm.Info.Sprintf("Permissions 0%o applied recursively to '%s'", permissions, folderName))
  212. } else {
  213. printLine(pterm.Info.Sprintf("Permissions 0%o applied to '%s'", permissions, folderName))
  214. }
  215. case "sync_time":
  216. printLine(pterm.Info.Sprintf("System time: %s", time.Now().Format("2006-01-02 15:04:05 MST")))
  217. case "docker_compose":
  218. args := strings.Fields(line)
  219. if len(args) < 2 {
  220. fatal("Invalid execute command: %s", line)
  221. }
  222. run(fmt.Sprintf("%s %s", GetDockerComposeCommand(), strings.Join(args[1:], " ")), true)
  223. case "wf":
  224. args := strings.Fields(line)
  225. if len(args) < 2 {
  226. fatal("Invalid wf command: %s", line)
  227. }
  228. wfName := args[1]
  229. for _, wf := range workflows {
  230. if wf.Name == wfName {
  231. executeWorkflow(wf, values)
  232. break
  233. }
  234. }
  235. case "notify_success", "notify_error", "notify_warning", "notify_info":
  236. notifies := map[string]*pterm.PrefixPrinter{
  237. "notify_success": &pterm.Success,
  238. "notify_error": &pterm.Error,
  239. "notify_warning": &pterm.Warning,
  240. "notify_info": &pterm.Info,
  241. }
  242. printer := notifies[action]
  243. msg := strings.TrimSpace(strings.TrimPrefix(line, action))
  244. if strings.HasPrefix(msg, "\"") && strings.HasSuffix(msg, "\"") {
  245. msg = msg[1 : len(msg)-1]
  246. }
  247. printLine(printer.Sprint(msg))
  248. case "notify":
  249. msg := strings.TrimSpace(strings.TrimPrefix(line, "notify"))
  250. if strings.HasPrefix(msg, "\"") && strings.HasSuffix(msg, "\"") {
  251. msg = msg[1 : len(msg)-1]
  252. }
  253. printLine(pterm.DefaultBasicText.Sprint(" " + msg))
  254. default:
  255. fatal("Unknown command or invalid syntax : " + line)
  256. }
  257. }
  258. func run(line string, printCommand bool) {
  259. line = strings.TrimSpace(line)
  260. if line == "" {
  261. return
  262. }
  263. var start time.Time
  264. indent := ""
  265. if workflowDepth > 1 {
  266. indent = strings.Repeat(" ", workflowDepth-1)
  267. }
  268. if printCommand {
  269. fmt.Println()
  270. pterm.FgCyan.Printfln("%s╭─ ▶ %s", indent, line)
  271. start = time.Now()
  272. }
  273. args := strings.Fields(line)
  274. cmd := exec.Command(args[0], args[1:]...)
  275. cmd.Env = os.Environ()
  276. if printCommand {
  277. prefixStr := pterm.FgCyan.Sprint(indent + "│ ")
  278. cmd.Stdout = NewPrefixWriter(prefixStr, os.Stdout)
  279. cmd.Stderr = NewPrefixWriter(prefixStr, os.Stderr)
  280. } else {
  281. cmd.Stdout = os.Stdout
  282. cmd.Stderr = os.Stderr
  283. }
  284. err := cmd.Run()
  285. if printCommand {
  286. elapsed := time.Since(start).Round(time.Millisecond)
  287. if err != nil {
  288. pterm.FgRed.Printfln("%s╰─ ✘ Failed in %s", indent, elapsed)
  289. } else {
  290. pterm.FgGreen.Printfln("%s╰─ ✔ %s", indent, elapsed)
  291. }
  292. }
  293. if err != nil {
  294. fmt.Println()
  295. pterm.Error.Printfln("Command failed '%s': %v", line, err)
  296. os.Exit(1)
  297. }
  298. }
  299. func ParseContentToWorkFlowStruct(filename string) map[string]Workflow {
  300. file, err := os.Open(filename)
  301. if err != nil {
  302. panic(err)
  303. }
  304. defer file.Close()
  305. workflowsMap := map[string]Workflow{}
  306. scanner := bufio.NewScanner(file)
  307. currentWorkflow := Workflow{Name: "main"}
  308. workflowsMap[currentWorkflow.Name] = currentWorkflow
  309. for scanner.Scan() {
  310. line := strings.TrimSpace(scanner.Text())
  311. if line == "" || strings.HasPrefix(line, "#") {
  312. continue
  313. }
  314. if strings.HasPrefix(line, "[") {
  315. end := strings.Index(line, "]")
  316. if end == -1 {
  317. continue
  318. }
  319. wfName := line[1:end]
  320. comment := ""
  321. after := strings.TrimSpace(line[end+1:])
  322. if strings.HasPrefix(after, "#") {
  323. comment = strings.TrimSpace(strings.TrimPrefix(after, "#"))
  324. }
  325. currentWorkflow = Workflow{Name: wfName, Comment: comment}
  326. workflowsMap[wfName] = currentWorkflow
  327. continue
  328. }
  329. currentWorkflow.Lines = append(currentWorkflow.Lines, line)
  330. workflowsMap[currentWorkflow.Name] = currentWorkflow
  331. }
  332. return workflowsMap
  333. }
  334. func InitDefaultVariables() map[string]string {
  335. return map[string]string{
  336. "IP_LOCAL": GetLocalIP(),
  337. "CURRENT_PATH": getCurrentDir(),
  338. }
  339. }
  340. func getCurrentDir() string {
  341. dir, _ := os.Getwd()
  342. return dir
  343. }
  344. func GetLocalIP() string {
  345. addrs, err := net.InterfaceAddrs()
  346. if err != nil {
  347. return ""
  348. }
  349. for _, addr := range addrs {
  350. if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() && ipNet.IP.To4() != nil {
  351. return ipNet.IP.String()
  352. }
  353. }
  354. return ""
  355. }
  356. func TokenGenerator(size int) string {
  357. b := make([]byte, size)
  358. rand.Read(b)
  359. return fmt.Sprintf("%x", b)
  360. }
  361. func ResolveVariables(values map[string]string, line string) string {
  362. for key, value := range values {
  363. line = strings.ReplaceAll(line, "${"+key+"}", value)
  364. }
  365. return strings.ReplaceAll(line, "${GENERATE_SECRET}", TokenGenerator(32))
  366. }
  367. func GetDockerComposeCommand() string {
  368. if dockerComposeCommand == "" {
  369. if err := exec.Command("docker", "compose", "--version").Run(); err != nil {
  370. dockerComposeCommand = "docker-compose"
  371. } else {
  372. dockerComposeCommand = "docker compose"
  373. }
  374. }
  375. return dockerComposeCommand
  376. }
  377. type Workflow struct {
  378. Name string
  379. Comment string
  380. Lines []string
  381. }
  382. type PrefixWriter struct {
  383. Prefix []byte
  384. Writer io.Writer
  385. isBOL bool
  386. }
  387. func NewPrefixWriter(prefix string, writer io.Writer) *PrefixWriter {
  388. return &PrefixWriter{
  389. Prefix: []byte(prefix),
  390. Writer: writer,
  391. isBOL: true,
  392. }
  393. }
  394. func (pw *PrefixWriter) Write(p []byte) (n int, err error) {
  395. for _, b := range p {
  396. if pw.isBOL {
  397. if _, err = pw.Writer.Write(pw.Prefix); err != nil {
  398. return n, err
  399. }
  400. pw.isBOL = false
  401. }
  402. if _, err = pw.Writer.Write([]byte{b}); err != nil {
  403. return n, err
  404. }
  405. n++
  406. if b == '\n' {
  407. pw.isBOL = true
  408. }
  409. }
  410. return n, nil
  411. }
  412. func printLine(text string) {
  413. if workflowDepth > 1 {
  414. fmt.Print(strings.Repeat(" ", workflowDepth-1))
  415. }
  416. fmt.Println(text)
  417. }
  418. func fatal(format string, a ...interface{}) {
  419. printLine(pterm.Error.Sprintf(format, a...))
  420. os.Exit(1)
  421. }