From 77cd6aac14fc7aa6f4e67c24a0649ba06981d212 Mon Sep 17 00:00:00 2001 From: oscar Date: Sun, 21 Aug 2022 17:03:53 +1200 Subject: [PATCH 01/25] go(utils): support to print message with color --- go/utils/print.go | 65 ++++++++++++++++++++++++++++++++++++++++++++++ go/utils/prompt.go | 22 ++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 go/utils/print.go create mode 100644 go/utils/prompt.go diff --git a/go/utils/print.go b/go/utils/print.go new file mode 100644 index 0000000..7e24429 --- /dev/null +++ b/go/utils/print.go @@ -0,0 +1,65 @@ +package utils + +import "fmt" + +const ( + colorReset string = "\033[0m" + + colorRed string = "\033[31m" + colorGreen string = "\033[32m" + colorYellow string = "\033[33m" + colorBlue string = "\033[34m" + colorPurple string = "\033[35m" + colorCyan string = "\033[36m" + colorWhite string = "\033[37m" +) + +func PrintOutput(message string, output []byte) { + if message != "" { + HighlightPrint(message) + } + fmt.Println(string(output)) +} + +func HighlightPrint(message string) { + fmt.Println() + fmt.Println(colorBlue, message, colorReset) +} + +func SuccessPrint(message string) { + fmt.Println() + fmt.Println(colorGreen, message, colorReset) +} + +func ErrorPrint(message string) { + fmt.Println() + fmt.Println(colorRed, message, colorReset) +} + +func InputPrint(message string) { + fmt.Println() + fmt.Println(colorYellow, message, colorReset) +} + +func MenuPrint() { + InputPrint("Which repository or action do you want to operate:") + + // menu := ` + // 1. Build Portainer EE/CE All + // 2. Build Portainer EE/CE Frontend + // 3. Build Portainer EE/CE Backend + // 4. Generate Portainer EE/CE JWT + // 5. Run Before Commit [Portainer EE/CE] + // 6. Get Portainer CE API Reference + // 7. Run Before Commit [k8s] + // 8. Build Portainer Agent + // 9. Cleanup Temporary Volume + // ` + + menu := `1. Portainer EE Repository + 2. Portainer CE Repository + 3. Portainer Agent Repository + 4. Others` + + fmt.Println(colorCyan, menu, colorReset) +} diff --git a/go/utils/prompt.go b/go/utils/prompt.go new file mode 100644 index 0000000..5d7465d --- /dev/null +++ b/go/utils/prompt.go @@ -0,0 +1,22 @@ +package utils + +import ( + "fmt" + "strings" +) + +func PromptContinue() bool { + ret := strings.ToLower(prompt("Continue (y/n)")) + if ret == "y" || ret == "yes" { + return true + } + + return false +} + +func prompt(question string) string { + fmt.Printf("%s %s :%s", colorYellow, question, colorReset) + var ret string + fmt.Scanf("%s", &ret) + return ret +} From 10544e1c2d881a57435e1c28d4c657c14864d03d Mon Sep 17 00:00:00 2001 From: oscar Date: Sun, 21 Aug 2022 17:04:37 +1200 Subject: [PATCH 02/25] go(commands): add commands to list git branch --- go/commands/git.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 go/commands/git.go diff --git a/go/commands/git.go b/go/commands/git.go new file mode 100644 index 0000000..c4b8e5b --- /dev/null +++ b/go/commands/git.go @@ -0,0 +1,18 @@ +package commands + +import ( + "ocl/portainer-devtool/utils" + "os/exec" +) + +func ListBranches(workdir string) error { + cmd := exec.Command("git", "branch") + cmd.Dir = workdir + out, err := cmd.Output() + if err != nil { + return err + } + + utils.PrintOutput("Your current checkout branch", out) + return nil +} From 8028a50cc3f71ec9cf5195460fad903eb5143ade Mon Sep 17 00:00:00 2001 From: oscar Date: Sun, 21 Aug 2022 17:05:29 +1200 Subject: [PATCH 03/25] go(action): add the tool skeleton --- go/go.mod | 3 +++ go/main.go | 40 +++++++++++++++++++++++++++++++++++++ go/repositories/actioner.go | 5 +++++ go/repositories/ee.go | 37 ++++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+) create mode 100644 go/go.mod create mode 100644 go/main.go create mode 100644 go/repositories/actioner.go create mode 100644 go/repositories/ee.go diff --git a/go/go.mod b/go/go.mod new file mode 100644 index 0000000..075a3b6 --- /dev/null +++ b/go/go.mod @@ -0,0 +1,3 @@ +module ocl/portainer-devtool + +go 1.18 diff --git a/go/main.go b/go/main.go new file mode 100644 index 0000000..b919258 --- /dev/null +++ b/go/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + "log" + "ocl/portainer-devtool/repositories" + "ocl/portainer-devtool/utils" +) + +const ( + MENU_OPTION_EE_REPO int = iota + 1 + MENU_OPTION_CE_REPO + MENU_OPTION_AGENT_REPO + MENU_OPTION_OTHERS +) + +func main() { + + utils.MenuPrint() + + var option int + _, err := fmt.Scanf("%d", &option) + if err != nil { + log.Fatal(err) + } + + var action repositories.Actioner + switch option { + case MENU_OPTION_EE_REPO: + action = repositories.NewPortainerEERepository() + case MENU_OPTION_CE_REPO: + + case MENU_OPTION_AGENT_REPO: + + case MENU_OPTION_OTHERS: + + } + + log.Fatal(action.Execute()) +} diff --git a/go/repositories/actioner.go b/go/repositories/actioner.go new file mode 100644 index 0000000..3cbd209 --- /dev/null +++ b/go/repositories/actioner.go @@ -0,0 +1,5 @@ +package repositories + +type Actioner interface { + Execute() error +} diff --git a/go/repositories/ee.go b/go/repositories/ee.go new file mode 100644 index 0000000..d83d015 --- /dev/null +++ b/go/repositories/ee.go @@ -0,0 +1,37 @@ +package repositories + +import ( + "fmt" + "ocl/portainer-devtool/commands" + "ocl/portainer-devtool/utils" +) + +type PortainerEE struct { + WorkDir string + FrontendDir string + BackendDir string +} + +func NewPortainerEERepository() *PortainerEE { + repo := &PortainerEE{ + WorkDir: "/home/oscarzhou/source/github.com/portainer/portainer-ee", + } + + utils.HighlightPrint("Your portainer EE repository work directory is ") + fmt.Println(repo.WorkDir) + + return repo +} + +func (repo *PortainerEE) Execute() error { + err := commands.ListBranches(repo.WorkDir) + if err != nil { + return err + } + + if !utils.PromptContinue() { + return nil + } + + return nil +} From 4a5b954ad6c11245e48d04c57ee7ebd7bb75c9f2 Mon Sep 17 00:00:00 2001 From: oscar Date: Sun, 21 Aug 2022 21:22:41 +1200 Subject: [PATCH 04/25] go(utils): add command util for printing output with stdout pipe --- go/commands/yarn.go | 15 +++++++++++++++ go/utils/command.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 go/commands/yarn.go create mode 100644 go/utils/command.go diff --git a/go/commands/yarn.go b/go/commands/yarn.go new file mode 100644 index 0000000..e6dd9f6 --- /dev/null +++ b/go/commands/yarn.go @@ -0,0 +1,15 @@ +package commands + +import ( + "ocl/portainer-devtool/utils" +) + +// RunClient starts the portainer client +func RunPortainerClient(workdir string) error { + err := utils.RunCommandWithStdoutPipe(workdir, "yarn") + if err != nil { + return err + } + + return utils.RunCommandWithStdoutPipe(workdir, "yarn", "start:client") +} diff --git a/go/utils/command.go b/go/utils/command.go new file mode 100644 index 0000000..82cd379 --- /dev/null +++ b/go/utils/command.go @@ -0,0 +1,45 @@ +package utils + +import ( + "bufio" + "fmt" + "os/exec" +) + +func RunCommandWithStdoutPipe(workdir, progName string, args ...string) error { + cmd := exec.Command(progName, args...) + cmd.Dir = workdir + out, err := cmd.StdoutPipe() + if err != nil { + return err + } + + scanner := bufio.NewScanner(out) + go func() { + counter := 0 + for scanner.Scan() { + if counter > 10 { + // Swallow the output + if counter%50 == 0 { + fmt.Printf("output %d lines in total.\n", counter) + } + counter++ + continue + } + PrintOutput("", scanner.Bytes()) + counter++ + } + }() + + err = cmd.Start() + if err != nil { + return err + } + + err = cmd.Wait() + if err != nil { + return err + } + + return nil +} From 5a6cd4ac16680c268c1c63a764e793f05aa6c35b Mon Sep 17 00:00:00 2001 From: oscar Date: Sun, 21 Aug 2022 21:33:02 +1200 Subject: [PATCH 05/25] go(utils): add promptMenu util function --- go/utils/print.go | 11 ++++------- go/utils/prompt.go | 8 ++++++++ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/go/utils/print.go b/go/utils/print.go index 7e24429..7354135 100644 --- a/go/utils/print.go +++ b/go/utils/print.go @@ -41,8 +41,10 @@ func InputPrint(message string) { fmt.Println(colorYellow, message, colorReset) } -func MenuPrint() { - InputPrint("Which repository or action do you want to operate:") +func MenuPrint(question, menu string) { + if question != "" { + InputPrint(fmt.Sprintf("[%s]", question)) + } // menu := ` // 1. Build Portainer EE/CE All @@ -56,10 +58,5 @@ func MenuPrint() { // 9. Cleanup Temporary Volume // ` - menu := `1. Portainer EE Repository - 2. Portainer CE Repository - 3. Portainer Agent Repository - 4. Others` - fmt.Println(colorCyan, menu, colorReset) } diff --git a/go/utils/prompt.go b/go/utils/prompt.go index 5d7465d..8fd6eed 100644 --- a/go/utils/prompt.go +++ b/go/utils/prompt.go @@ -14,6 +14,14 @@ func PromptContinue() bool { return false } +func PromptMenu(listMenu func()) int { + listMenu() + + var option int + fmt.Scanf("%d", &option) + return option +} + func prompt(question string) string { fmt.Printf("%s %s :%s", colorYellow, question, colorReset) var ret string From b24dcfa9d92726a84438c12037a5c91c26922b9f Mon Sep 17 00:00:00 2001 From: oscar Date: Sun, 21 Aug 2022 21:34:10 +1200 Subject: [PATCH 06/25] go(repositories): add sub menu for ee repository --- go/main.go | 51 +++++++++++++++++++++++++++---------------- go/repositories/ee.go | 34 +++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 19 deletions(-) diff --git a/go/main.go b/go/main.go index b919258..c6e9b43 100644 --- a/go/main.go +++ b/go/main.go @@ -1,10 +1,10 @@ package main import ( - "fmt" "log" "ocl/portainer-devtool/repositories" "ocl/portainer-devtool/utils" + "os" ) const ( @@ -12,29 +12,42 @@ const ( MENU_OPTION_CE_REPO MENU_OPTION_AGENT_REPO MENU_OPTION_OTHERS + MENU_OPTION_QUIT ) func main() { - utils.MenuPrint() + for { - var option int - _, err := fmt.Scanf("%d", &option) - if err != nil { - log.Fatal(err) + printMainMenu := func() { + utils.MenuPrint("Which repository or action do you want to operate:", ` + 1. Portainer EE Repository + 2. Portainer CE Repository + 3. Portainer Agent Repository + 4. Others + 5. Quit`) + } + + option := utils.PromptMenu(printMainMenu) + + var action repositories.Actioner + switch option { + case MENU_OPTION_EE_REPO: + action = repositories.NewPortainerEERepository() + case MENU_OPTION_CE_REPO: + + case MENU_OPTION_AGENT_REPO: + + case MENU_OPTION_OTHERS: + + case MENU_OPTION_QUIT: + os.Exit(0) + } + + err := action.Execute() + if err != nil { + log.Fatalln(err) + } } - var action repositories.Actioner - switch option { - case MENU_OPTION_EE_REPO: - action = repositories.NewPortainerEERepository() - case MENU_OPTION_CE_REPO: - - case MENU_OPTION_AGENT_REPO: - - case MENU_OPTION_OTHERS: - - } - - log.Fatal(action.Execute()) } diff --git a/go/repositories/ee.go b/go/repositories/ee.go index d83d015..eed87e7 100644 --- a/go/repositories/ee.go +++ b/go/repositories/ee.go @@ -6,6 +6,18 @@ import ( "ocl/portainer-devtool/utils" ) +const ( + ACTION_EE_BUILD_ALL int = iota + 1 + ACTION_EE_RUN_FRONTEND + ACTION_EE_RUN_BACKEND + ACTION_EE_VALIDATE_ALL + ACTION_EE_VALIDATE_FRONTEND + ACTION_EE_VALIDATE_BACKEND + ACTION_EE_RUN_UNIT_TEST_ALL + ACTION_EE_RUN_UNIT_TEST_FRONTEND + ACTION_EE_RUN_UNIT_TEST_BACKEND +) + type PortainerEE struct { WorkDir string FrontendDir string @@ -33,5 +45,27 @@ func (repo *PortainerEE) Execute() error { return nil } + option := utils.PromptMenu(repo.listSubMenu) + switch option { + case ACTION_EE_BUILD_ALL: + + case ACTION_EE_RUN_FRONTEND: + commands.RunPortainerClient(repo.WorkDir) + + } + return nil } + +func (repo *PortainerEE) listSubMenu() { + utils.MenuPrint("Do you want?", ` + 1. Build both front-end and backend + 2. Run front-end only + 3. Run backend only + 4. Validate both fornt-end and backend before commit + 5. Validate front-end only before commit + 6. Validate backend only before commit + 7. Run unit tests for both front-end and backend + 8. Run unit tests for front-end only + 9. Run unit tests for backend only`) +} From f3c725b792e735e94b5225cd3be244eb5306e7a5 Mon Sep 17 00:00:00 2001 From: oscar Date: Wed, 24 Aug 2022 14:01:07 +1200 Subject: [PATCH 07/25] create a new shell with repository proned option --- run.sh | 168 ++++++++++++++++++++++++++++++++++++++++++++++++ utils/common.sh | 11 ++++ 2 files changed, 179 insertions(+) create mode 100755 run.sh diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..9c7c1f0 --- /dev/null +++ b/run.sh @@ -0,0 +1,168 @@ +#!/bin/bash + +set -eu + +source ./utils/common.sh + +WORKDIR=/home/oscarzhou/source/github.com/portainer +GLOBAL_VOLUME=/home/oscarzhou/volumes +TRUE=0; +FALSE=1; +REPO_DIR= +REPO_VOLUME= + +function debug_portainer_client() { + print_highlight "[debug portainer client]" + yarn + yarn start:client +} + +function generate_portainer_jwt_token() { + print_highlight "[generate portainer jwt token]" + + read -p "Username(admin):" username + if [ -z "$username" ]; then + username="admin"; + fi + + read -p "Password(****):" password + read -p "Address(http://127.0.0.1:9000):" address + if [ -z "$address" ]; then + address="http://127.0.0.1:9000"; + fi + + payload="{\"username\":\"${username}\",\"password\":\"${password}\"}" + curl -d ${payload} -H 'Content-Type: application/json' "${address}/api/auth" +} + +function list_portainer_ee_menu() { + print_highlight "Your current working directory is ${WORKDIR}/portainer-ee" + if ! prompt_continue; then + exit; + fi + + REPO_DIR=${WORKDIR}/portainer-ee + print_highlight "Your current volume is ${VOLUME}/portainer-ee-data" + if ! prompt_continue; then + exit; + fi + + REPO_VOLUME=${VOLUME}/portainer-ee-data + + PS3='Please select the action: ' + OPTIONS=( + 'Debug Client' + 'Lint Client' + 'Run Unit Test for Client' + 'Before Commit' + 'Build Client' + 'Build Server' + 'Run Unit Test for Server' + 'Get Portainer CE API Reference' + 'Quit' + ) + + select opt in "${OPTIONS[@]}" + do + case $opt in + 'Debug Client') + debug_portainer_client + ;; + 'PortainerCE') + build_portainer_frontend + ;; + 'Build Portainer EE/CE Backend') + build_portainer_backend + ;; + 'Generate Portainer EE/CE JWT') + generate_portainer_jwt + ;; + 'Run Before Commit [Portainer EE/CE]') + run_before_commit + ;; + 'Get Portainer CE API Reference') + get_portainer_ce_api_reference + ;; + 'Quit') + break + ;; + esac + done +} + +function code_security_scan_summary() { + echo " + 1. Scan client with snyk: $(print_highlight "snyk test") + 2. Scan server with snyk: $(print_highlight "cd api && snyk test") + 3. If snyk is not authenticated: $(print_highlight "snyk auth") + 4. Specify the severity threshold: $(print_highlight "snyk test --severity-threshold=") + 5. Other commands with snyk: $(print_highlight "snyk --help") + " + + echo " + Steps to scan portainer image with Trivy: + 1. Build the local image: $(print_highlight "docker build -t oscarzhou/portainer:dev-ee -f build/linux/Dockfile .") + 2. Scan with trivy: $(print_highlight 'docker run --rm -v "/var/run/docker.sock":"/var/run/docker.sock" aquasec/trivy:latest image oscarzhou/portainer:dev-ee') + 3. Other commands with trivy: $(print_highlight 'docker run --rm -v "/var/run/docker.sock":"/var/run/docker.sock" aquasec/trivy:latest --help') + " +} + + +function menu() { + PS3='Please select the action/repository: ' + OPTIONS=( + 'PortainerEE' + 'PortainerCE' + 'Build Portainer EE/CE Backend' + 'Generate Portainer JWT Token' + 'Run Before Commit [Portainer EE/CE]' + 'Get Portainer CE API Reference' + 'Run Before Commit [k8s]' + 'Code Security Scan' + 'Cleanup Temporary Volume' + 'Quit' + ) + + select opt in "${OPTIONS[@]}" + do + case $opt in + 'PortainerEE') + list_portainer_ee_menu + ;; + 'PortainerCE') + build_portainer_frontend + ;; + 'Build Portainer EE/CE Backend') + build_portainer_backend + ;; + 'Generate Portainer JWT Token') + generate_portainer_jwt + ;; + 'Run Before Commit [Portainer EE/CE]') + run_before_commit + ;; + 'Get Portainer CE API Reference') + get_portainer_ce_api_reference + ;; + 'Run Before Commit [k8s]') + run_before_commit_k8s + ;; + 'Code Security Scan') + code_security_scan_summary + ;; + 'Cleanup Temporary Volume') + cleanup_temporary_volume + ;; + 'Quit') + break + ;; + esac + done +} + +# check if the function exists (bash specific) +if [ "$#" -eq 0 ]; then + menu +else + "$@" +fi diff --git a/utils/common.sh b/utils/common.sh index b736d58..8147e63 100644 --- a/utils/common.sh +++ b/utils/common.sh @@ -1,5 +1,7 @@ #!/bin/bash +TRUE=0; +FALSE=1; ERROR_COLOR='\033[0;31m'; HIGHLIGHT_COLOR='\033[0;32m'; @@ -17,4 +19,13 @@ function print_error() { function input() { read -p "$(echo -e ${INPUT_COLOR}$1 ${NO_COLOR})" $2 +} + +function prompt_continue() { + read -p "Continue N/(Y)?" is_continue + if [[ "${is_continue}" == "N" || "${is_continue}" == "n" ]]; then + return $FALSE; + fi + + return $TRUE; } \ No newline at end of file From 3f10f63c282ff3fb20b3dd256c0be5e18b0438a8 Mon Sep 17 00:00:00 2001 From: oscar Date: Wed, 24 Aug 2022 15:53:27 +1200 Subject: [PATCH 08/25] bash: add curl command lookup --- run.sh | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/run.sh b/run.sh index 9c7c1f0..6a7e9dd 100755 --- a/run.sh +++ b/run.sh @@ -107,6 +107,21 @@ function code_security_scan_summary() { " } +function look_up_curl_commands() { + input "1.POST 2.GET 3.PUT 4.DELETE :" option + if [[ "${option}" == "1" ]]; then + echo "$(print_highlight "curl -d '{\"repository\":\"https://github.com/portainer/portainer-ee\",\"username\":\"oscarzhou\", \"password\":\"your PAT\"}' -H 'Content-Type: application/json' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGUiOjEsInNjb3BlIjoiZGVmYXVsdCIsImZvcmNlQ2hhbmdlUGFzc3dvcmQiOmZhbHNlLCJleHAiOjE2NjAwMzQ2MjUsImlhdCI6MTY2MDAwNTgyNX0.S0UbPO4POD9kbuWOmvO9WR6LY6v424bpGw46rlEkNs0' http://127.0.0.1:9000/api/gitops/repo/refs")" + elif [[ "${option}" == "2" ]]; then + echo "$(print_highlight "curl -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGUiOjEsInNjb3BlIjoiZGVmYXVsdCIsImZvcmNlQ2hhbmdlUGFzc3dvcmQiOmZhbHNlLCJleHAiOjE2NTUxMTg2ODUsImlhdCI6MTY1NTA4OTg4NX0.mJSZomeiEpRlz36MxSsLFWpUbA0BHRXWYijsZAo1NWc' http://127.0.0.1:9000/api/users/1/gitcredentials")" + elif [[ "${option}" == "3" ]]; then + echo "$(print_highlight "curl -X PUT http://127.0.0.1:9000/api/users/1/gitcredentials/11 -d '{"name":"test-credential-11","username":"cred11", "password":"cred11"}' -H 'Content-Type: application/json' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGUiOjEsInNjb3BlIjoiZGVmYXVsdCIsImZvcmNlQ2hhbmdlUGFzc3dvcmQiOmZhbHNlLCJleHAiOjE2NTcwODQ5MzUsImlhdCI6MTY1NzA1NjEzNX0.kUhkhhSt4WH33Q3hYzLwsYDv1a9a2ygCi6p8MkKMbwc'")" + elif [[ "${option}" == "4" ]]; then + echo "$(print_highlight "curl -X DELETE http://192.168.1.109:9000/api/users/1/gitcredentials/1 -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGUiOjEsInNjb3BlIjoiZGVmYXVsdCIsImZvcmNlQ2hhbmdlUGFzc3dvcmQiOmZhbHNlLCJleHAiOjE2NTQ3NTc1NzYsImlhdCI6MTY1NDcyODc3Nn0.GlxGmL6XTTH29Ns8aRnX5qp1qBfDVF2zaPzuSmG7qUs'")" + else + print_error "Invalid option" + fi +} + function menu() { PS3='Please select the action/repository: ' @@ -117,7 +132,7 @@ function menu() { 'Generate Portainer JWT Token' 'Run Before Commit [Portainer EE/CE]' 'Get Portainer CE API Reference' - 'Run Before Commit [k8s]' + 'Look Up Curl Commands' 'Code Security Scan' 'Cleanup Temporary Volume' 'Quit' @@ -144,8 +159,8 @@ function menu() { 'Get Portainer CE API Reference') get_portainer_ce_api_reference ;; - 'Run Before Commit [k8s]') - run_before_commit_k8s + 'Look Up Curl Commands') + look_up_curl_commands ;; 'Code Security Scan') code_security_scan_summary From 3bb03bad871f6f4e35b9dab76b9bb4a141f2cc5f Mon Sep 17 00:00:00 2001 From: oscar Date: Sat, 17 Sep 2022 21:00:56 +1200 Subject: [PATCH 09/25] go(utils): add helper function to get config file handler --- go/utils/config.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 go/utils/config.go diff --git a/go/utils/config.go b/go/utils/config.go new file mode 100644 index 0000000..54db0c9 --- /dev/null +++ b/go/utils/config.go @@ -0,0 +1,24 @@ +package utils + +import ( + "errors" + "fmt" + "os" +) + +func GetConfigFile(name string) (*os.File, error) { + _, err := os.Stat(name) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + //create file + file, err := os.Create(name) + if err != nil { + return nil, fmt.Errorf("fail to create config file: %w", err) + } + return file, err + } else { + return nil, err + } + } + return os.OpenFile(name, os.O_RDWR, 0644) +} From 2ab17147fb6c58187c4e02948a4687cc6a25a647 Mon Sep 17 00:00:00 2001 From: oscar Date: Wed, 21 Sep 2022 17:00:10 +1200 Subject: [PATCH 10/25] config: initialize and read config file --- go/configs/config.go | 147 +++++++++++++++++++++++++++++++++++++++++++ go/utils/prompt.go | 15 ++++- 2 files changed, 159 insertions(+), 3 deletions(-) create mode 100644 go/configs/config.go diff --git a/go/configs/config.go b/go/configs/config.go new file mode 100644 index 0000000..6534be0 --- /dev/null +++ b/go/configs/config.go @@ -0,0 +1,147 @@ +package configs + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "os" +) + +const ( + ConfigFileName string = ".devtool" +) + +var ( + ErrConfigNotInitialized error = errors.New("Config file is not initialized") +) + +type Config struct { + // ProjectPath is where all git repositories will be downloaded to + ProjectPath string + // VolumePath is where all the persisitant data will be saved to + VolumePath string + // + LoginCredential LoginCredential + // + RepositoryConfig map[string]RepositoryConfig +} + +// LoginCredential stores the user credential for API request +type LoginCredential struct { + Username string + Password string + Address string +} + +type RepositoryConfig struct { + Name string + URL string + Directory string + Private bool + GitUsername string + GitPassword string +} + +func GetConfig() (*Config, error) { + file, err := getConfigFile(ConfigFileName) + if err != nil { + if err == ErrConfigNotInitialized { + config, err := initializeConfig(file) + if err != nil { + return config, err + } + } + } + + return getConfig(file) +} + +func initializeConfig(w io.WriteCloser) (*Config, error) { + config := &Config{} + fmt.Printf("Set the project path: ") + fmt.Scanf("%s", &(config.ProjectPath)) + + fmt.Printf("Set the volume path: ") + fmt.Scanf("%s", &(config.VolumePath)) + + var loginCredential LoginCredential + fmt.Printf("Set login credential username(admin): ") + fmt.Scanf("%s", &(loginCredential.Username)) + if loginCredential.Username == "" { + loginCredential.Username = "admin" + } + + for { + fmt.Printf("Set login credential password(******): ") + fmt.Scanf("%s", &(loginCredential.Password)) + if loginCredential.Password != "" { + break + } + + fmt.Println("Login credential password must be provided") + } + + fmt.Printf("Set login address(127.0.0.1): ") + fmt.Scanf("%s", &(loginCredential.Address)) + if loginCredential.Address == "" { + loginCredential.Address = "http://127.0.0.1:9000/api/auth" + } else { + loginCredential.Address = fmt.Sprintf("http://%s:9000/api/auth", loginCredential.Address) + } + + config.LoginCredential = loginCredential + + // able to configure multiple project + // if utils.PromptConfirm("Do you want to configure the repository now?") { + // // configure repository + // } + + bytes, err := json.Marshal(config) + if err != nil { + return nil, err + } + + _, err = w.Write(bytes) + if err != nil { + return nil, err + } + return config, nil +} + +func getConfig(f io.Reader) (*Config, error) { + config := &Config{} + bytes := make([]byte, 0) + _, err := f.Read(bytes) + if err != nil { + return nil, err + } + + err = json.Unmarshal(bytes, &config) + if err != nil { + return nil, err + } + return config, nil +} + +// getConfigFile get the config file handler +func getConfigFile(name string) (*os.File, error) { + _, err := os.Stat(name) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + //create file + file, err := os.Create(name) + if err != nil { + return nil, fmt.Errorf("fail to create config file: %w", err) + } + + // first set up the git project path and volume path + // git credential + + return file, err + } else { + return nil, err + } + } + return os.OpenFile(name, os.O_RDWR, 0644) +} diff --git a/go/utils/prompt.go b/go/utils/prompt.go index 8fd6eed..827881e 100644 --- a/go/utils/prompt.go +++ b/go/utils/prompt.go @@ -14,11 +14,20 @@ func PromptContinue() bool { return false } -func PromptMenu(listMenu func()) int { +func PromptConfirm(question string) bool { + ret := fmt.Sprintf("%s (y/n)?", question) + if ret == "y" || ret == "yes" { + return true + } + + return false +} + +func PromptMenu(listMenu func()) string { listMenu() - var option int - fmt.Scanf("%d", &option) + var option string + fmt.Scanf("%s", &option) return option } From 8463725b9c88e62c9cedaed16167818e3654ab78 Mon Sep 17 00:00:00 2001 From: oscar Date: Wed, 21 Sep 2022 17:42:47 +1200 Subject: [PATCH 11/25] task: add tasks skeleton --- go/main.go | 58 ++++++++++++++++----------------------- go/tasks/build_all.go | 16 +++++++++++ go/tasks/build_backend.go | 15 ++++++++++ go/tasks/jwt_token_gen.go | 21 ++++++++++++++ go/tasks/tasker.go | 6 ++++ 5 files changed, 81 insertions(+), 35 deletions(-) create mode 100644 go/tasks/build_all.go create mode 100644 go/tasks/build_backend.go create mode 100644 go/tasks/jwt_token_gen.go create mode 100644 go/tasks/tasker.go diff --git a/go/main.go b/go/main.go index c6e9b43..45c851a 100644 --- a/go/main.go +++ b/go/main.go @@ -2,52 +2,40 @@ package main import ( "log" - "ocl/portainer-devtool/repositories" + "ocl/portainer-devtool/configs" + "ocl/portainer-devtool/tasks" "ocl/portainer-devtool/utils" - "os" -) - -const ( - MENU_OPTION_EE_REPO int = iota + 1 - MENU_OPTION_CE_REPO - MENU_OPTION_AGENT_REPO - MENU_OPTION_OTHERS - MENU_OPTION_QUIT ) func main() { + config, err := configs.GetConfig() + if err != nil { + log.Fatalln(err) + } + + // Init tasks + + taskItems := []tasks.Tasker{ + tasks.NewGenerateJwtTokenTask(config), + } + for { printMainMenu := func() { - utils.MenuPrint("Which repository or action do you want to operate:", ` - 1. Portainer EE Repository - 2. Portainer CE Repository - 3. Portainer Agent Repository - 4. Others - 5. Quit`) + + utils.PrintMenu("Which repository of action do you want operate:", taskItems) + + // utils.MenuPrint("Which repository or action do you want to operate:", ` + // 1. Portainer EE Repository + // 2. Portainer CE Repository + // 3. Portainer Agent Repository + // 4. Others + // 5. Quit`) } - option := utils.PromptMenu(printMainMenu) + utils.PromptMenu(printMainMenu) - var action repositories.Actioner - switch option { - case MENU_OPTION_EE_REPO: - action = repositories.NewPortainerEERepository() - case MENU_OPTION_CE_REPO: - - case MENU_OPTION_AGENT_REPO: - - case MENU_OPTION_OTHERS: - - case MENU_OPTION_QUIT: - os.Exit(0) - } - - err := action.Execute() - if err != nil { - log.Fatalln(err) - } } } diff --git a/go/tasks/build_all.go b/go/tasks/build_all.go new file mode 100644 index 0000000..4f4208e --- /dev/null +++ b/go/tasks/build_all.go @@ -0,0 +1,16 @@ +package tasks + +import "ocl/portainer-devtool/configs" + +type BuildAllTask struct { + Config *configs.Config +} + +func (task *BuildAllTask) Execute() error { + + return nil +} + +func (task *BuildAllTask) String() string { + return "Build all" +} diff --git a/go/tasks/build_backend.go b/go/tasks/build_backend.go new file mode 100644 index 0000000..6644eb1 --- /dev/null +++ b/go/tasks/build_backend.go @@ -0,0 +1,15 @@ +package tasks + +import "ocl/portainer-devtool/configs" + +type BuildBackendOnlyTask struct { + Config *configs.Config +} + +func (task *BuildBackendOnlyTask) Execute() error { + return nil +} + +func (task *BuildBackendOnlyTask) String() string { + return "Build backend only" +} diff --git a/go/tasks/jwt_token_gen.go b/go/tasks/jwt_token_gen.go new file mode 100644 index 0000000..393764f --- /dev/null +++ b/go/tasks/jwt_token_gen.go @@ -0,0 +1,21 @@ +package tasks + +import "ocl/portainer-devtool/configs" + +type GenerateJwtTokenTask struct { + Config *configs.Config +} + +func NewGenerateJwtTokenTask(cfg *configs.Config) *GenerateJwtTokenTask { + return &GenerateJwtTokenTask{ + Config: cfg, + } +} + +func (task *GenerateJwtTokenTask) Execute() error { + return nil +} + +func (task *GenerateJwtTokenTask) String() string { + return "Generate JWT token" +} diff --git a/go/tasks/tasker.go b/go/tasks/tasker.go new file mode 100644 index 0000000..bc922dd --- /dev/null +++ b/go/tasks/tasker.go @@ -0,0 +1,6 @@ +package tasks + +type Tasker interface { + Execute() error + String() string +} From a115970e7e39f1bf1f514ab51a374c40f2073eaf Mon Sep 17 00:00:00 2001 From: oscar Date: Wed, 21 Sep 2022 17:43:21 +1200 Subject: [PATCH 12/25] utils: remove the config.go file --- go/utils/config.go | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 go/utils/config.go diff --git a/go/utils/config.go b/go/utils/config.go deleted file mode 100644 index 54db0c9..0000000 --- a/go/utils/config.go +++ /dev/null @@ -1,24 +0,0 @@ -package utils - -import ( - "errors" - "fmt" - "os" -) - -func GetConfigFile(name string) (*os.File, error) { - _, err := os.Stat(name) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - //create file - file, err := os.Create(name) - if err != nil { - return nil, fmt.Errorf("fail to create config file: %w", err) - } - return file, err - } else { - return nil, err - } - } - return os.OpenFile(name, os.O_RDWR, 0644) -} From 9c56ec0d8549068f805dd7da62e506a96fbd8b1c Mon Sep 17 00:00:00 2001 From: oscar Date: Wed, 21 Sep 2022 17:43:56 +1200 Subject: [PATCH 13/25] utils: refactor PrintMenu with tasker array --- go/utils/print.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/go/utils/print.go b/go/utils/print.go index 7354135..d095382 100644 --- a/go/utils/print.go +++ b/go/utils/print.go @@ -1,6 +1,9 @@ package utils -import "fmt" +import ( + "fmt" + "ocl/portainer-devtool/tasks" +) const ( colorReset string = "\033[0m" @@ -41,6 +44,20 @@ func InputPrint(message string) { fmt.Println(colorYellow, message, colorReset) } +func PrintMenu(question string, tasks []tasks.Tasker) { + if question != "" { + InputPrint(fmt.Sprintf("[%s]", question)) + } + + menuContent := "" + + for i, task := range tasks { + menuContent += fmt.Sprintf("%d. %s\n", i+1, task.String()) + } + + fmt.Println(colorCyan, menuContent, colorReset) +} + func MenuPrint(question, menu string) { if question != "" { InputPrint(fmt.Sprintf("[%s]", question)) From f25a6f7806320282de331580e3a8c8c54803a8be Mon Sep 17 00:00:00 2001 From: oscar Date: Thu, 22 Sep 2022 09:24:10 +1200 Subject: [PATCH 14/25] fix: allocate the correct size for byte array to read info from existing files --- go/configs/config.go | 39 ++++++++++++++++++++++++++++++++------- go/main.go | 2 ++ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/go/configs/config.go b/go/configs/config.go index 6534be0..95a18fc 100644 --- a/go/configs/config.go +++ b/go/configs/config.go @@ -47,16 +47,31 @@ func GetConfig() (*Config, error) { file, err := getConfigFile(ConfigFileName) if err != nil { if err == ErrConfigNotInitialized { - config, err := initializeConfig(file) - if err != nil { - return config, err - } + return initializeConfig(file) } + return nil, err } + defer file.Close() + return getConfig(file) } +func (config *Config) Summarize() { + fmt.Printf("The project path is %s\nThe volume path is %s\n", config.ProjectPath, config.VolumePath) + if config.LoginCredential.Username != "" && config.LoginCredential.Password != "" { + fmt.Printf("Login credential [%s] is configured\n", config.LoginCredential.Username) + } + + if len(config.RepositoryConfig) > 0 { + for name := range config.RepositoryConfig { + fmt.Printf("Repository [%s] is added\n", name) + } + } else { + fmt.Println("No repository is added") + } +} + func initializeConfig(w io.WriteCloser) (*Config, error) { config := &Config{} fmt.Printf("Set the project path: ") @@ -109,13 +124,23 @@ func initializeConfig(w io.WriteCloser) (*Config, error) { return config, nil } -func getConfig(f io.Reader) (*Config, error) { +func getConfig(f *os.File) (*Config, error) { config := &Config{} - bytes := make([]byte, 0) - _, err := f.Read(bytes) + + info, err := f.Stat() if err != nil { return nil, err } + bytes := make([]byte, info.Size()) + n, err := f.Read(bytes) + if err != nil { + return nil, err + } + + if n == 0 { + // The file exists, but it's empty file, so we need to initalize + return initializeConfig(f) + } err = json.Unmarshal(bytes, &config) if err != nil { diff --git a/go/main.go b/go/main.go index 45c851a..9389030 100644 --- a/go/main.go +++ b/go/main.go @@ -14,6 +14,8 @@ func main() { log.Fatalln(err) } + config.Summarize() + // Init tasks taskItems := []tasks.Tasker{ From 423c0b5a375f4d2113b827adb62013db89623034 Mon Sep 17 00:00:00 2001 From: oscar Date: Thu, 22 Sep 2022 13:56:37 +1200 Subject: [PATCH 15/25] task: support api token request --- go/tasks/jwt_token_gen.go | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/go/tasks/jwt_token_gen.go b/go/tasks/jwt_token_gen.go index 393764f..c1ffecb 100644 --- a/go/tasks/jwt_token_gen.go +++ b/go/tasks/jwt_token_gen.go @@ -1,11 +1,22 @@ package tasks -import "ocl/portainer-devtool/configs" +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "ocl/portainer-devtool/configs" +) type GenerateJwtTokenTask struct { Config *configs.Config } +type GenerateJwtTokenResponse struct { + JWT string `json:"jwt"` +} + func NewGenerateJwtTokenTask(cfg *configs.Config) *GenerateJwtTokenTask { return &GenerateJwtTokenTask{ Config: cfg, @@ -13,6 +24,31 @@ func NewGenerateJwtTokenTask(cfg *configs.Config) *GenerateJwtTokenTask { } func (task *GenerateJwtTokenTask) Execute() error { + postBody, _ := json.Marshal(map[string]string{ + "username": task.Config.LoginCredential.Username, + "password": task.Config.LoginCredential.Password, + }) + + responseBody := bytes.NewBuffer(postBody) + resp, err := http.Post(task.Config.LoginCredential.Address, "application/json", responseBody) + if err != nil { + return fmt.Errorf("http requset error: %s", err.Error()) + } + defer resp.Body.Close() + + //Read the response body + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to parse the response body: %s", err.Error()) + } + + var ret GenerateJwtTokenResponse + err = json.Unmarshal(body, &ret) + if err != nil { + return err + } + + fmt.Printf("jwt token is:\n%s\n", ret.JWT) return nil } From d50bf2422670ea7da87ca121e3e709f451913a7a Mon Sep 17 00:00:00 2001 From: oscar Date: Thu, 22 Sep 2022 13:58:21 +1200 Subject: [PATCH 16/25] task: add the task selection skeleton --- go/main.go | 18 +++++++++++++++++- go/utils/prompt.go | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/go/main.go b/go/main.go index 9389030..932f70f 100644 --- a/go/main.go +++ b/go/main.go @@ -5,6 +5,7 @@ import ( "ocl/portainer-devtool/configs" "ocl/portainer-devtool/tasks" "ocl/portainer-devtool/utils" + "strconv" ) func main() { @@ -36,8 +37,23 @@ func main() { // 5. Quit`) } - utils.PromptMenu(printMainMenu) + option := utils.SelectMenuItem(printMainMenu) + index, err := strconv.Atoi(option) + if err != nil { + log.Printf("please type the option number\n") + continue + } + + if index < 1 || index > len(taskItems) { + log.Printf("no such option %s, please select again\n", option) + continue + } + + err = taskItems[index-1].Execute() + if err != nil { + log.Fatalln(err) + } } } diff --git a/go/utils/prompt.go b/go/utils/prompt.go index 827881e..9c7fabb 100644 --- a/go/utils/prompt.go +++ b/go/utils/prompt.go @@ -23,7 +23,7 @@ func PromptConfirm(question string) bool { return false } -func PromptMenu(listMenu func()) string { +func SelectMenuItem(listMenu func()) string { listMenu() var option string From 290e872c15e17c4e530460e5147c98b825413972 Mon Sep 17 00:00:00 2001 From: oscarzhou Date: Mon, 26 Dec 2022 00:53:51 +1300 Subject: [PATCH 17/25] task: add curl lookup task --- go/main.go | 9 +++++++- go/tasks/curl_lookup.go | 48 +++++++++++++++++++++++++++++++++++++++ go/tasks/exit.go | 18 +++++++++++++++ go/tasks/jwt_token_gen.go | 2 +- go/utils/print.go | 13 +++++++---- 5 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 go/tasks/curl_lookup.go create mode 100644 go/tasks/exit.go diff --git a/go/main.go b/go/main.go index 932f70f..f70a3f5 100644 --- a/go/main.go +++ b/go/main.go @@ -21,13 +21,20 @@ func main() { taskItems := []tasks.Tasker{ tasks.NewGenerateJwtTokenTask(config), + tasks.NewCurlLookupTask(), + + tasks.NewExitTask(), } for { printMainMenu := func() { + taskNames := []string{} + for _, task := range taskItems { + taskNames = append(taskNames, task.String()) + } - utils.PrintMenu("Which repository of action do you want operate:", taskItems) + utils.PrintMenu("Which repository of action do you want operate:", taskNames) // utils.MenuPrint("Which repository or action do you want to operate:", ` // 1. Portainer EE Repository diff --git a/go/tasks/curl_lookup.go b/go/tasks/curl_lookup.go new file mode 100644 index 0000000..be80959 --- /dev/null +++ b/go/tasks/curl_lookup.go @@ -0,0 +1,48 @@ +package tasks + +import ( + "fmt" + "ocl/portainer-devtool/utils" +) + +type CurlLookupTask struct { +} + +func NewCurlLookupTask() *CurlLookupTask { + return &CurlLookupTask{} +} + +func (task *CurlLookupTask) Execute() error { + var option string + utils.InputPrint("1.POST 2.GET 3.PUT 4.DELETE: ") + fmt.Scanf("%s", &option) + switch option { + case "1", "POST", "post": + utils.HighlightPrint("POST Command:") + utils.SuccessPrint("curl -d '{\"repository\":\"https://github.com/portainer/portainer-ee\",\"username\":\"oscarzhou\", \"password\":\"your PAT\"}' -H 'Content-Type: application/json' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGUiOjEsInNjb3BlIjoiZGVmYXVsdCIsImZvcmNlQ2hhbmdlUGFzc3dvcmQiOmZhbHNlLCJleHAiOjE2NjAwMzQ2MjUsImlhdCI6MTY2MDAwNTgyNX0.S0UbPO4POD9kbuWOmvO9WR6LY6v424bpGw46rlEkNs0' http://127.0.0.1:9000/api/gitops/repo/refs") + break + + case "2", "GET", "get": + utils.HighlightPrint("GET Command:") + utils.SuccessPrint("curl -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGUiOjEsInNjb3BlIjoiZGVmYXVsdCIsImZvcmNlQ2hhbmdlUGFzc3dvcmQiOmZhbHNlLCJleHAiOjE2NTUxMTg2ODUsImlhdCI6MTY1NTA4OTg4NX0.mJSZomeiEpRlz36MxSsLFWpUbA0BHRXWYijsZAo1NWc' http://127.0.0.1:9000/api/users/1/gitcredentials") + break + + case "3", "PUT", "put": + utils.HighlightPrint("PUT Command:") + utils.SuccessPrint(`curl -X PUT http://127.0.0.1:9000/api/users/1/gitcredentials/11 -d '{"name":"test-credential-11","username":"cred11", "password":"cred11"}' -H 'Content-Type: application/json' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGUiOjEsInNjb3BlIjoiZGVmYXVsdCIsImZvcmNlQ2hhbmdlUGFzc3dvcmQiOmZhbHNlLCJleHAiOjE2NTcwODQ5MzUsImlhdCI6MTY1NzA1NjEzNX0.kUhkhhSt4WH33Q3hYzLwsYDv1a9a2ygCi6p8MkKMbwc'`) + break + + case "4", "DELETE", "delete": + utils.HighlightPrint("DELETE Command:") + utils.SuccessPrint(`curl -X DELETE http://192.168.1.109:9000/api/users/1/gitcredentials/1 -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGUiOjEsInNjb3BlIjoiZGVmYXVsdCIsImZvcmNlQ2hhbmdlUGFzc3dvcmQiOmZhbHNlLCJleHAiOjE2NTQ3NTc1NzYsImlhdCI6MTY1NDcyODc3Nn0.GlxGmL6XTTH29Ns8aRnX5qp1qBfDVF2zaPzuSmG7qUs'`) + break + + default: + return fmt.Errorf("No option %v\n", option) + } + return nil +} + +func (task *CurlLookupTask) String() string { + return "Lookup Curl Commands" +} diff --git a/go/tasks/exit.go b/go/tasks/exit.go new file mode 100644 index 0000000..2067151 --- /dev/null +++ b/go/tasks/exit.go @@ -0,0 +1,18 @@ +package tasks + +import "errors" + +type ExitTask struct { +} + +func NewExitTask() *ExitTask { + return &ExitTask{} +} + +func (task *ExitTask) Execute() error { + return errors.New("exit") +} + +func (task *ExitTask) String() string { + return "Exit" +} diff --git a/go/tasks/jwt_token_gen.go b/go/tasks/jwt_token_gen.go index c1ffecb..17c8f93 100644 --- a/go/tasks/jwt_token_gen.go +++ b/go/tasks/jwt_token_gen.go @@ -53,5 +53,5 @@ func (task *GenerateJwtTokenTask) Execute() error { } func (task *GenerateJwtTokenTask) String() string { - return "Generate JWT token" + return "Generate JWT Token" } diff --git a/go/utils/print.go b/go/utils/print.go index d095382..0144aee 100644 --- a/go/utils/print.go +++ b/go/utils/print.go @@ -2,7 +2,6 @@ package utils import ( "fmt" - "ocl/portainer-devtool/tasks" ) const ( @@ -40,19 +39,23 @@ func ErrorPrint(message string) { } func InputPrint(message string) { + // adding \n before setting colorful output can + // remove the first space in the colorful output fmt.Println() fmt.Println(colorYellow, message, colorReset) } -func PrintMenu(question string, tasks []tasks.Tasker) { +func PrintMenu(question string, taskNames []string) { if question != "" { InputPrint(fmt.Sprintf("[%s]", question)) } - menuContent := "" + // adding \n before setting colorful output can + // remove the first space in the colorful output + menuContent := "\n" - for i, task := range tasks { - menuContent += fmt.Sprintf("%d. %s\n", i+1, task.String()) + for i, name := range taskNames { + menuContent += fmt.Sprintf("%d. %s\n", i+1, name) } fmt.Println(colorCyan, menuContent, colorReset) From d8431550f3f7abe57945428b3d73c622b131e169 Mon Sep 17 00:00:00 2001 From: oscarzhou Date: Mon, 26 Dec 2022 01:10:16 +1300 Subject: [PATCH 18/25] task: add code security scan option --- go/main.go | 1 + go/tasks/code_security_scan.go | 35 ++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 go/tasks/code_security_scan.go diff --git a/go/main.go b/go/main.go index f70a3f5..16c38c2 100644 --- a/go/main.go +++ b/go/main.go @@ -22,6 +22,7 @@ func main() { taskItems := []tasks.Tasker{ tasks.NewGenerateJwtTokenTask(config), tasks.NewCurlLookupTask(), + tasks.NewCodeSecurityScanTask(), tasks.NewExitTask(), } diff --git a/go/tasks/code_security_scan.go b/go/tasks/code_security_scan.go new file mode 100644 index 0000000..63b6411 --- /dev/null +++ b/go/tasks/code_security_scan.go @@ -0,0 +1,35 @@ +package tasks + +import ( + "ocl/portainer-devtool/utils" +) + +type CodeSecurityScanTask struct { +} + +func NewCodeSecurityScanTask() *CodeSecurityScanTask { + return &CodeSecurityScanTask{} +} + +func (task *CodeSecurityScanTask) Execute() error { + utils.SuccessPrint(` + 1. Scan client with snyk: "snyk test" + 2. Scan server with snyk: "cd api && snyk test" + 3. If snyk is not authenticated: "snyk auth" + 4. Specify the severity threshold: "snyk test --severity-threshold=" + 5. Other commands with snyk: "snyk --help" + `) + + utils.SuccessPrint(` + Steps to scan portainer image with Trivy: + 1. Build the local image: "docker build -t oscarzhou/portainer:dev-ee -f build/linux/Dockfile ." + 2. Scan with trivy: 'docker run --rm -v "/var/run/docker.sock":"/var/run/docker.sock" aquasec/trivy:latest image oscarzhou/portainer:dev-ee' + 3. Other commands with trivy: 'docker run --rm -v "/var/run/docker.sock":"/var/run/docker.sock" aquasec/trivy:latest --help' + `) + + return nil +} + +func (task *CodeSecurityScanTask) String() string { + return "Code Security Scan" +} From c32698b510b5cfb985eefad6310abd3cac73ea61 Mon Sep 17 00:00:00 2001 From: oscarzhou Date: Mon, 26 Dec 2022 01:10:59 +1300 Subject: [PATCH 19/25] config: add comments --- go/configs/config.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/go/configs/config.go b/go/configs/config.go index 95a18fc..2a66fb7 100644 --- a/go/configs/config.go +++ b/go/configs/config.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "os" + "path" ) const ( @@ -17,13 +18,13 @@ var ( ) type Config struct { - // ProjectPath is where all git repositories will be downloaded to + // ProjectPath is the location on your host where all dev relevant folders will be stored to ProjectPath string - // VolumePath is where all the persisitant data will be saved to + // VolumePath is where all the persisitant data will be stored VolumePath string - // + // Credentials for UI login LoginCredential LoginCredential - // + // key is repository name, for example, "repository-ee" RepositoryConfig map[string]RepositoryConfig } @@ -72,13 +73,16 @@ func (config *Config) Summarize() { } } +// initializeConfig will set up the mandatory dev information for the first time. +// such as devtool path, login credential +// The configuration also can be updated later func initializeConfig(w io.WriteCloser) (*Config, error) { config := &Config{} - fmt.Printf("Set the project path: ") + fmt.Printf("Initialize devtool path:\n (Project path will store volumes and repositories)") fmt.Scanf("%s", &(config.ProjectPath)) - fmt.Printf("Set the volume path: ") - fmt.Scanf("%s", &(config.VolumePath)) + // generate volume path automatically + config.VolumePath = path.Join(config.ProjectPath, "volumes") var loginCredential LoginCredential fmt.Printf("Set login credential username(admin): ") From 747c774c985878301229c495160b10e8e7fc8eeb Mon Sep 17 00:00:00 2001 From: oscarzhou Date: Mon, 26 Dec 2022 01:56:02 +1300 Subject: [PATCH 20/25] task: extract the function for listing command menu --- go/main.go | 43 ++---------------------------------------- go/tasks/tasker.go | 47 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 41 deletions(-) diff --git a/go/main.go b/go/main.go index 16c38c2..ec094d9 100644 --- a/go/main.go +++ b/go/main.go @@ -4,12 +4,9 @@ import ( "log" "ocl/portainer-devtool/configs" "ocl/portainer-devtool/tasks" - "ocl/portainer-devtool/utils" - "strconv" ) func main() { - config, err := configs.GetConfig() if err != nil { log.Fatalln(err) @@ -18,50 +15,14 @@ func main() { config.Summarize() // Init tasks - taskItems := []tasks.Tasker{ tasks.NewGenerateJwtTokenTask(config), tasks.NewCurlLookupTask(), tasks.NewCodeSecurityScanTask(), + tasks.NewListDevToolCommandTask(config), tasks.NewExitTask(), } - for { - - printMainMenu := func() { - taskNames := []string{} - for _, task := range taskItems { - taskNames = append(taskNames, task.String()) - } - - utils.PrintMenu("Which repository of action do you want operate:", taskNames) - - // utils.MenuPrint("Which repository or action do you want to operate:", ` - // 1. Portainer EE Repository - // 2. Portainer CE Repository - // 3. Portainer Agent Repository - // 4. Others - // 5. Quit`) - } - - option := utils.SelectMenuItem(printMainMenu) - - index, err := strconv.Atoi(option) - if err != nil { - log.Printf("please type the option number\n") - continue - } - - if index < 1 || index > len(taskItems) { - log.Printf("no such option %s, please select again\n", option) - continue - } - - err = taskItems[index-1].Execute() - if err != nil { - log.Fatalln(err) - } - } - + tasks.ListCommandMenu(taskItems, "Which repository of action do you want operate:") } diff --git a/go/tasks/tasker.go b/go/tasks/tasker.go index bc922dd..154f696 100644 --- a/go/tasks/tasker.go +++ b/go/tasks/tasker.go @@ -1,6 +1,53 @@ package tasks +import ( + "fmt" + "ocl/portainer-devtool/utils" + "os" + "strconv" +) + type Tasker interface { Execute() error String() string } + +// ListCommandMenu iterates task items to display them // on the screen as the menu options +func ListCommandMenu(taskItems []Tasker, menuDesp string) error { + for { + printMainMenu := func() { + taskNames := []string{} + for _, task := range taskItems { + taskNames = append(taskNames, task.String()) + } + + utils.PrintMenu(menuDesp, taskNames) + + // utils.MenuPrint("Which repository or action do you want to operate:", ` + // 1. Portainer EE Repository + // 2. Portainer CE Repository + // 3. Portainer Agent Repository + // 4. Others + // 5. Quit`) + } + + option := utils.SelectMenuItem(printMainMenu) + + index, err := strconv.Atoi(option) + if err != nil { + utils.ErrorPrint("please type the option number\n") + continue + } + + if index < 1 || index > len(taskItems) { + utils.ErrorPrint(fmt.Sprintf("no such option %s, please select again\n", option)) + continue + } + + err = taskItems[index-1].Execute() + if err != nil { + utils.ErrorPrint(err.Error()) + os.Exit(1) + } + } +} From a0e852feaba3898f5ce0baa7fe0a8103afa0f818 Mon Sep 17 00:00:00 2001 From: oscarzhou Date: Mon, 26 Dec 2022 15:28:24 +1300 Subject: [PATCH 21/25] subtask: allow to list volumes --- go/tasks/list_dev_tool_cmd.go | 30 +++++++++++++++++ go/tasks/subtasks/list_volume.go | 58 ++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 go/tasks/list_dev_tool_cmd.go create mode 100644 go/tasks/subtasks/list_volume.go diff --git a/go/tasks/list_dev_tool_cmd.go b/go/tasks/list_dev_tool_cmd.go new file mode 100644 index 0000000..170b3f4 --- /dev/null +++ b/go/tasks/list_dev_tool_cmd.go @@ -0,0 +1,30 @@ +package tasks + +import ( + "ocl/portainer-devtool/configs" + "ocl/portainer-devtool/tasks/subtasks" +) + +type ListDevToolCommandTask struct { + Config *configs.Config +} + +func NewListDevToolCommandTask(cfg *configs.Config) *ListDevToolCommandTask { + return &ListDevToolCommandTask{ + Config: cfg, + } +} + +func (task *ListDevToolCommandTask) Execute() error { + subTaskItems := []Tasker{ + subtasks.NewListVolumeSubTask(task.Config), + } + + ListCommandMenu(subTaskItems, "Which management commands do you want to choose:") + + return nil +} + +func (task *ListDevToolCommandTask) String() string { + return "List Dev Tool Commands" +} diff --git a/go/tasks/subtasks/list_volume.go b/go/tasks/subtasks/list_volume.go new file mode 100644 index 0000000..887972a --- /dev/null +++ b/go/tasks/subtasks/list_volume.go @@ -0,0 +1,58 @@ +package subtasks + +import ( + "fmt" + "io/fs" + "ocl/portainer-devtool/configs" + "ocl/portainer-devtool/utils" + "path/filepath" + "strings" +) + +type ListVolumeSubTask struct { + Config *configs.Config +} + +func NewListVolumeSubTask(cfg *configs.Config) *ListVolumeSubTask { + return &ListVolumeSubTask{ + Config: cfg, + } +} + +func (task *ListVolumeSubTask) Execute() error { + utils.HighlightPrint(fmt.Sprintf("Volume path: %s", task.Config.VolumePath)) + + volumeLength := lenPath(task.Config.VolumePath) + + volumeList := []string{" "} + filepath.WalkDir(task.Config.VolumePath, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if path == task.Config.VolumePath { + return nil + } + + if d.IsDir() { + dirLength := lenPath(path) + if volumeLength+1 == dirLength { + volumeList = append(volumeList, d.Name()) + } + } + + return nil + }) + + utils.SuccessPrint(strings.Join(volumeList, "\n")) + + return nil +} + +func (task *ListVolumeSubTask) String() string { + return "List Volume" +} + +func lenPath(path string) int { + return len(strings.Split(path, string(filepath.Separator))) +} From d3056fc0c93292c516b60cfc7294862831ebbb45 Mon Sep 17 00:00:00 2001 From: oscarzhou Date: Mon, 26 Dec 2022 20:54:17 +1300 Subject: [PATCH 22/25] task: add exit task as the default task --- go/main.go | 2 -- go/tasks/tasker.go | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/go/main.go b/go/main.go index ec094d9..51c02ce 100644 --- a/go/main.go +++ b/go/main.go @@ -20,8 +20,6 @@ func main() { tasks.NewCurlLookupTask(), tasks.NewCodeSecurityScanTask(), tasks.NewListDevToolCommandTask(config), - - tasks.NewExitTask(), } tasks.ListCommandMenu(taskItems, "Which repository of action do you want operate:") diff --git a/go/tasks/tasker.go b/go/tasks/tasker.go index 154f696..ce5be30 100644 --- a/go/tasks/tasker.go +++ b/go/tasks/tasker.go @@ -14,6 +14,7 @@ type Tasker interface { // ListCommandMenu iterates task items to display them // on the screen as the menu options func ListCommandMenu(taskItems []Tasker, menuDesp string) error { + taskItems = append(taskItems, NewExitTask()) for { printMainMenu := func() { taskNames := []string{} From 7f2afd1bdbfc5cc7b7d27f2b0c00eee917911cc1 Mon Sep 17 00:00:00 2001 From: oscarzhou Date: Mon, 26 Dec 2022 20:55:06 +1300 Subject: [PATCH 23/25] subtask: add list repository task --- go/.gitignore | 1 + go/tasks/list_dev_tool_cmd.go | 1 + go/tasks/subtasks/list_repository.go | 34 ++++++++++++++++++++++++++++ go/tasks/subtasks/list_volume.go | 2 +- 4 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 go/.gitignore create mode 100644 go/tasks/subtasks/list_repository.go diff --git a/go/.gitignore b/go/.gitignore new file mode 100644 index 0000000..32bcdf1 --- /dev/null +++ b/go/.gitignore @@ -0,0 +1 @@ +/test \ No newline at end of file diff --git a/go/tasks/list_dev_tool_cmd.go b/go/tasks/list_dev_tool_cmd.go index 170b3f4..7cbdfe0 100644 --- a/go/tasks/list_dev_tool_cmd.go +++ b/go/tasks/list_dev_tool_cmd.go @@ -18,6 +18,7 @@ func NewListDevToolCommandTask(cfg *configs.Config) *ListDevToolCommandTask { func (task *ListDevToolCommandTask) Execute() error { subTaskItems := []Tasker{ subtasks.NewListVolumeSubTask(task.Config), + subtasks.NewListRepositorySubTask(task.Config), } ListCommandMenu(subTaskItems, "Which management commands do you want to choose:") diff --git a/go/tasks/subtasks/list_repository.go b/go/tasks/subtasks/list_repository.go new file mode 100644 index 0000000..1582ee7 --- /dev/null +++ b/go/tasks/subtasks/list_repository.go @@ -0,0 +1,34 @@ +package subtasks + +import ( + "ocl/portainer-devtool/configs" + "ocl/portainer-devtool/utils" + "strings" +) + +type ListRepositorySubTask struct { + Config *configs.Config +} + +func NewListRepositorySubTask(cfg *configs.Config) *ListRepositorySubTask { + return &ListRepositorySubTask{ + Config: cfg, + } +} + +func (task *ListRepositorySubTask) Execute() error { + utils.InputPrint("Which repository?") + + repositoryList := []string{" "} + for _, repo := range task.Config.RepositoryConfig { + repositoryList = append(repositoryList, repo.Name) + } + + utils.SuccessPrint(strings.Join(repositoryList, "\n")) + + return nil +} + +func (task *ListRepositorySubTask) String() string { + return "List Repositories" +} diff --git a/go/tasks/subtasks/list_volume.go b/go/tasks/subtasks/list_volume.go index 887972a..d0854e2 100644 --- a/go/tasks/subtasks/list_volume.go +++ b/go/tasks/subtasks/list_volume.go @@ -50,7 +50,7 @@ func (task *ListVolumeSubTask) Execute() error { } func (task *ListVolumeSubTask) String() string { - return "List Volume" + return "List Volumes" } func lenPath(path string) int { From 889b7a4ca2bfca13c07fd5b157e2164c699e6e04 Mon Sep 17 00:00:00 2001 From: oscarzhou Date: Wed, 28 Dec 2022 14:38:14 +1300 Subject: [PATCH 24/25] utils: add common function to match the directory layer --- go/tasks/subtasks/list_volume.go | 9 +-------- go/utils/path.go | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 8 deletions(-) create mode 100644 go/utils/path.go diff --git a/go/tasks/subtasks/list_volume.go b/go/tasks/subtasks/list_volume.go index d0854e2..5eb6cf4 100644 --- a/go/tasks/subtasks/list_volume.go +++ b/go/tasks/subtasks/list_volume.go @@ -22,8 +22,6 @@ func NewListVolumeSubTask(cfg *configs.Config) *ListVolumeSubTask { func (task *ListVolumeSubTask) Execute() error { utils.HighlightPrint(fmt.Sprintf("Volume path: %s", task.Config.VolumePath)) - volumeLength := lenPath(task.Config.VolumePath) - volumeList := []string{" "} filepath.WalkDir(task.Config.VolumePath, func(path string, d fs.DirEntry, err error) error { if err != nil { @@ -35,8 +33,7 @@ func (task *ListVolumeSubTask) Execute() error { } if d.IsDir() { - dirLength := lenPath(path) - if volumeLength+1 == dirLength { + if utils.MatchPathLength(task.Config.VolumePath, path, 1) { volumeList = append(volumeList, d.Name()) } } @@ -52,7 +49,3 @@ func (task *ListVolumeSubTask) Execute() error { func (task *ListVolumeSubTask) String() string { return "List Volumes" } - -func lenPath(path string) int { - return len(strings.Split(path, string(filepath.Separator))) -} diff --git a/go/utils/path.go b/go/utils/path.go new file mode 100644 index 0000000..6f73b19 --- /dev/null +++ b/go/utils/path.go @@ -0,0 +1,18 @@ +package utils + +import ( + "path/filepath" + "strings" +) + +// MatchPathLength matches the length of target path separated by path separator +// to the length of base path separated by path separator plus offset +func MatchPathLength(basePath, targetPath string, offset int) bool { + basePathLength := len(strings.Split(basePath, string(filepath.Separator))) + targetPathLength := len(strings.Split(targetPath, string(filepath.Separator))) + if basePathLength+offset == targetPathLength { + return true + } + + return false +} From f4f3164978d2c72885291d3e063966cb251603d4 Mon Sep 17 00:00:00 2001 From: oscarzhou Date: Wed, 28 Dec 2022 14:39:18 +1300 Subject: [PATCH 25/25] config: refactor data initialization process --- go/configs/config.go | 39 ++++++----------------------- go/configs/credential.go | 32 ++++++++++++++++++++++++ go/configs/repository.go | 54 ++++++++++++++++++++++++++++++++++++++++ go/utils/print.go | 5 ++++ go/utils/prompt.go | 6 ++--- 5 files changed, 102 insertions(+), 34 deletions(-) create mode 100644 go/configs/credential.go create mode 100644 go/configs/repository.go diff --git a/go/configs/config.go b/go/configs/config.go index 2a66fb7..a1d6c84 100644 --- a/go/configs/config.go +++ b/go/configs/config.go @@ -5,8 +5,8 @@ import ( "errors" "fmt" "io" + "ocl/portainer-devtool/utils" "os" - "path" ) const ( @@ -78,39 +78,16 @@ func (config *Config) Summarize() { // The configuration also can be updated later func initializeConfig(w io.WriteCloser) (*Config, error) { config := &Config{} - fmt.Printf("Initialize devtool path:\n (Project path will store volumes and repositories)") - fmt.Scanf("%s", &(config.ProjectPath)) + config.ProjectPath = utils.Prompt("Specify Git Project Root Path") + + // analyze all the repositories in the project root path + // add the parsed information to RepositoryConfig + config.configureRepositories() // generate volume path automatically - config.VolumePath = path.Join(config.ProjectPath, "volumes") - - var loginCredential LoginCredential - fmt.Printf("Set login credential username(admin): ") - fmt.Scanf("%s", &(loginCredential.Username)) - if loginCredential.Username == "" { - loginCredential.Username = "admin" - } - - for { - fmt.Printf("Set login credential password(******): ") - fmt.Scanf("%s", &(loginCredential.Password)) - if loginCredential.Password != "" { - break - } - - fmt.Println("Login credential password must be provided") - } - - fmt.Printf("Set login address(127.0.0.1): ") - fmt.Scanf("%s", &(loginCredential.Address)) - if loginCredential.Address == "" { - loginCredential.Address = "http://127.0.0.1:9000/api/auth" - } else { - loginCredential.Address = fmt.Sprintf("http://%s:9000/api/auth", loginCredential.Address) - } - - config.LoginCredential = loginCredential + config.VolumePath = utils.Prompt("Specify Volume Path") + config.configureLoginCredential() // able to configure multiple project // if utils.PromptConfirm("Do you want to configure the repository now?") { // // configure repository diff --git a/go/configs/credential.go b/go/configs/credential.go new file mode 100644 index 0000000..e0d1219 --- /dev/null +++ b/go/configs/credential.go @@ -0,0 +1,32 @@ +package configs + +import ( + "fmt" + "ocl/portainer-devtool/utils" +) + +func (config *Config) configureLoginCredential() { + var loginCredential LoginCredential + loginCredential.Username = utils.Prompt("Set Login Credential Username(admin)") + if loginCredential.Username == "" { + loginCredential.Username = "admin" + } + + for { + loginCredential.Password = utils.Prompt("Set Login Credential Password(*****)") + if loginCredential.Password != "" { + break + } + + utils.WarnPrint("Login Credential Password must be provided") + } + + loginCredential.Address = utils.Prompt("Set Login Address(127.0.0.1)") + if loginCredential.Address == "" { + loginCredential.Address = "http://127.0.0.1:9000/api/auth" + } else { + loginCredential.Address = fmt.Sprintf("http://%s:9000/api/auth", loginCredential.Address) + } + + config.LoginCredential = loginCredential +} diff --git a/go/configs/repository.go b/go/configs/repository.go new file mode 100644 index 0000000..a9ec14a --- /dev/null +++ b/go/configs/repository.go @@ -0,0 +1,54 @@ +package configs + +import ( + "fmt" + "io/fs" + "log" + "ocl/portainer-devtool/utils" + "path/filepath" +) + +func (config *Config) configureRepositories() { + if config.RepositoryConfig == nil { + config.RepositoryConfig = make(map[string]RepositoryConfig) + } + for { + if !utils.PromptConfirm("Set up new repository") { + break + } + + repoConfig := RepositoryConfig{} + repoConfig.Name = utils.Prompt("Name") + repoConfig.URL = utils.Prompt("URL") + repoConfig.Directory = utils.Prompt("Directory") + config.RepositoryConfig[repoConfig.Name] = repoConfig + } + + utils.HighlightPrint("Configure repositories completed") +} + +func (config *Config) generateRepositoriesBasedOnProjectPath(projectPath string) error { + + filepath.WalkDir(projectPath, func(path string, d fs.DirEntry, err error) error { + if err != nil { + log.Printf("fail to walk in the project path %s, error: %v\n", projectPath, err) + return err + } + + if utils.MatchPathLength(projectPath, path, 1) { + fmt.Println(path) + + // posLastSeparator := strings.LastIndex(path, string(filepath.Separator)) + // repoName := path[posLastSeparator+1:] + + // repoConfig := RepositoryConfig{ + // Name: repoName, + // // URL: + // } + // config.RepositoryConfig[repoName] = + } + + return nil + }) + return nil +} diff --git a/go/utils/print.go b/go/utils/print.go index 0144aee..ef65601 100644 --- a/go/utils/print.go +++ b/go/utils/print.go @@ -38,6 +38,11 @@ func ErrorPrint(message string) { fmt.Println(colorRed, message, colorReset) } +func WarnPrint(message string) { + fmt.Println() + fmt.Println(colorPurple, message, colorReset) +} + func InputPrint(message string) { // adding \n before setting colorful output can // remove the first space in the colorful output diff --git a/go/utils/prompt.go b/go/utils/prompt.go index 9c7fabb..111e505 100644 --- a/go/utils/prompt.go +++ b/go/utils/prompt.go @@ -6,7 +6,7 @@ import ( ) func PromptContinue() bool { - ret := strings.ToLower(prompt("Continue (y/n)")) + ret := strings.ToLower(Prompt("Continue (y/n)")) if ret == "y" || ret == "yes" { return true } @@ -15,7 +15,7 @@ func PromptContinue() bool { } func PromptConfirm(question string) bool { - ret := fmt.Sprintf("%s (y/n)?", question) + ret := Prompt(fmt.Sprintf("%s (y/n)?", question)) if ret == "y" || ret == "yes" { return true } @@ -31,7 +31,7 @@ func SelectMenuItem(listMenu func()) string { return option } -func prompt(question string) string { +func Prompt(question string) string { fmt.Printf("%s %s :%s", colorYellow, question, colorReset) var ret string fmt.Scanf("%s", &ret)