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/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 +} 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/configs/config.go b/go/configs/config.go new file mode 100644 index 0000000..a1d6c84 --- /dev/null +++ b/go/configs/config.go @@ -0,0 +1,153 @@ +package configs + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "ocl/portainer-devtool/utils" + "os" +) + +const ( + ConfigFileName string = ".devtool" +) + +var ( + ErrConfigNotInitialized error = errors.New("Config file is not initialized") +) + +type Config struct { + // 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 stored + VolumePath string + // Credentials for UI login + LoginCredential LoginCredential + // key is repository name, for example, "repository-ee" + 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 { + 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") + } +} + +// 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{} + 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 = 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 + // } + + 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 *os.File) (*Config, error) { + config := &Config{} + + 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 { + 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/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/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..51c02ce --- /dev/null +++ b/go/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "log" + "ocl/portainer-devtool/configs" + "ocl/portainer-devtool/tasks" +) + +func main() { + config, err := configs.GetConfig() + if err != nil { + log.Fatalln(err) + } + + config.Summarize() + + // Init tasks + taskItems := []tasks.Tasker{ + tasks.NewGenerateJwtTokenTask(config), + tasks.NewCurlLookupTask(), + tasks.NewCodeSecurityScanTask(), + tasks.NewListDevToolCommandTask(config), + } + + tasks.ListCommandMenu(taskItems, "Which repository of action do you want operate:") +} 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..eed87e7 --- /dev/null +++ b/go/repositories/ee.go @@ -0,0 +1,71 @@ +package repositories + +import ( + "fmt" + "ocl/portainer-devtool/commands" + "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 + 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 + } + + 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`) +} 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/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" +} 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 new file mode 100644 index 0000000..17c8f93 --- /dev/null +++ b/go/tasks/jwt_token_gen.go @@ -0,0 +1,57 @@ +package tasks + +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, + } +} + +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 +} + +func (task *GenerateJwtTokenTask) String() string { + return "Generate JWT Token" +} diff --git a/go/tasks/list_dev_tool_cmd.go b/go/tasks/list_dev_tool_cmd.go new file mode 100644 index 0000000..7cbdfe0 --- /dev/null +++ b/go/tasks/list_dev_tool_cmd.go @@ -0,0 +1,31 @@ +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), + subtasks.NewListRepositorySubTask(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_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 new file mode 100644 index 0000000..5eb6cf4 --- /dev/null +++ b/go/tasks/subtasks/list_volume.go @@ -0,0 +1,51 @@ +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)) + + 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() { + if utils.MatchPathLength(task.Config.VolumePath, path, 1) { + volumeList = append(volumeList, d.Name()) + } + } + + return nil + }) + + utils.SuccessPrint(strings.Join(volumeList, "\n")) + + return nil +} + +func (task *ListVolumeSubTask) String() string { + return "List Volumes" +} diff --git a/go/tasks/tasker.go b/go/tasks/tasker.go new file mode 100644 index 0000000..ce5be30 --- /dev/null +++ b/go/tasks/tasker.go @@ -0,0 +1,54 @@ +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 { + taskItems = append(taskItems, NewExitTask()) + 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) + } + } +} 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 +} 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 +} diff --git a/go/utils/print.go b/go/utils/print.go new file mode 100644 index 0000000..ef65601 --- /dev/null +++ b/go/utils/print.go @@ -0,0 +1,87 @@ +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 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 + fmt.Println() + fmt.Println(colorYellow, message, colorReset) +} + +func PrintMenu(question string, taskNames []string) { + if question != "" { + InputPrint(fmt.Sprintf("[%s]", question)) + } + + // adding \n before setting colorful output can + // remove the first space in the colorful output + menuContent := "\n" + + for i, name := range taskNames { + menuContent += fmt.Sprintf("%d. %s\n", i+1, name) + } + + fmt.Println(colorCyan, menuContent, colorReset) +} + +func MenuPrint(question, menu string) { + if question != "" { + InputPrint(fmt.Sprintf("[%s]", question)) + } + + // 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 + // ` + + fmt.Println(colorCyan, menu, colorReset) +} diff --git a/go/utils/prompt.go b/go/utils/prompt.go new file mode 100644 index 0000000..111e505 --- /dev/null +++ b/go/utils/prompt.go @@ -0,0 +1,39 @@ +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 PromptConfirm(question string) bool { + ret := Prompt(fmt.Sprintf("%s (y/n)?", question)) + if ret == "y" || ret == "yes" { + return true + } + + return false +} + +func SelectMenuItem(listMenu func()) string { + listMenu() + + var option string + fmt.Scanf("%s", &option) + return option +} + +func Prompt(question string) string { + fmt.Printf("%s %s :%s", colorYellow, question, colorReset) + var ret string + fmt.Scanf("%s", &ret) + return ret +} diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..6a7e9dd --- /dev/null +++ b/run.sh @@ -0,0 +1,183 @@ +#!/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 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: ' + OPTIONS=( + 'PortainerEE' + 'PortainerCE' + 'Build Portainer EE/CE Backend' + 'Generate Portainer JWT Token' + 'Run Before Commit [Portainer EE/CE]' + 'Get Portainer CE API Reference' + 'Look Up Curl Commands' + '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 + ;; + 'Look Up Curl Commands') + look_up_curl_commands + ;; + '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