diff --git a/cmd/debug.go b/cmd/debug.go new file mode 100644 index 0000000..6dabec3 --- /dev/null +++ b/cmd/debug.go @@ -0,0 +1,187 @@ +package cmd + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "os" + "os/exec" + "path" + "runtime" + "strings" + "syscall" + + "github.com/aquasecurity/table" + "github.com/fatih/color" + "github.com/spf13/cobra" + "github.com/steveiliop56/runtipi-cli-go/internal/env" + "github.com/steveiliop56/runtipi-cli-go/internal/system" + "github.com/steveiliop56/runtipi-cli-go/internal/utils" +) + +func init() { + debugCmd.Flags().BoolVar(&showLogs, "logs", false, "Show last 15 lines of all container logs.") + rootCmd.AddCommand(debugCmd) +} + +func GetEnvSafe(key string) (string) { + val, err := env.GetEnvValue(key) + if err != nil { + return "Error" + } + if val == "" { + return "Not set" + } + return val +} + +func GetEnvSafeRedact(key string) (string) { + val, err := env.GetEnvValue(key) + if err != nil { + return "Error" + } + if val == "" { + return "Not set" + } + return "" +} + +var debugCmd = &cobra.Command{ + Use: "debug", + Short: "Debug runtipi", + Long: "Use this command to debug your runtipi instance (useful for issues)", + Run: func(cmd *cobra.Command, args []string) { + // Print warning + fmt.Println("\n⚠️ Make sure you have started tipi before running this command") + + // Containers + containers := []string{"runtipi", "runtipi-db", "runtipi-redis", "runtipi-reverse-proxy"} + + // Colors + green := color.New(color.FgGreen).SprintFunc() + red := color.New(color.FgRed).SprintFunc() + blue := color.New(color.FgBlue).SprintFunc() + + // Root folder + rootFolder, rootFolderErr := os.Getwd() + + if rootFolderErr != nil { + utils.PrintError("Failed to get root folder") + fmt.Printf("Error: %s\n", rootFolderErr) + os.Exit(1) + } + + // System Info + fmt.Println("\n--- " + blue("System Info") + " ---") + operatingSystem := runtime.GOOS + kernel, kernelErr := exec.Command("uname", "-r").Output() + if kernelErr != nil { + utils.PrintError("Failed to run uname command") + fmt.Printf("Error: %s\n", kernelErr) + os.Exit(1) + } + sysSchema := syscall.Sysinfo_t{} + sysErr := syscall.Sysinfo(&sysSchema) + if sysErr != nil { + utils.PrintError("Failed to get total memory") + fmt.Printf("Error: %s\n", sysErr) + os.Exit(1) + } + totalMemory := uint64(sysSchema.Totalram) * uint64(sysSchema.Unit) + totalMemoryGB := fmt.Sprintf("%.2f", float32(totalMemory)/(1<<30)) + arch := system.GetArch() + sysInfoTable := table.New(os.Stdout) + sysInfoTable.AddRow("OS", operatingSystem) + sysInfoTable.AddRow("OS Version", string(kernel[:])) + sysInfoTable.AddRow("Memory (GB)", totalMemoryGB) + sysInfoTable.AddRow("Architecture", arch) + sysInfoTable.Render() + + // User config + fmt.Println("\n--- " + blue("User Config") + " ---") + userConfigTable := table.New(os.Stdout) + if _, err := os.Stat(path.Join(rootFolder, "user-config", "tipi-compose.yml")); errors.Is(err, os.ErrNotExist) { + userConfigTable.AddRow("Custom tipi docker compose", "false") + } else { + userConfigTable.AddRow("Custom tipi docker compose", "true") + } + userConfigTable.Render() + + // Settings + fmt.Println("\n--- " + blue("Settings") + " ---") + settings, settingsErr := os.ReadFile(path.Join(rootFolder, "state", "settings.json")) + if settingsErr != nil { + utils.PrintError("Failed to read settings file") + fmt.Printf("Error: %s\n", settingsErr) + os.Exit(1) + } + var prettyJson bytes.Buffer + prettifyErr := json.Indent(&prettyJson, settings, "", "\t") + if prettifyErr != nil { + utils.PrintError("Failed to prettify json") + fmt.Printf("Error: %s\n", prettifyErr) + os.Exit(1) + } + fmt.Println(prettyJson.String()) + + // Env + fmt.Println("\n--- " + blue("Environment") + " ---") + envTable := table.New(os.Stdout) + envTable.AddRow("POSTGRES_PASSWORD", GetEnvSafeRedact("POSTGRES_PASSWORD")) + envTable.AddRow("REDIS_PASSWORD", GetEnvSafeRedact("REDIS_PASSWORD")) + envTable.AddRow("APPS_REPO_ID", GetEnvSafe("APPS_REPO_ID")) + envTable.AddRow("APPS_REPO_URL", GetEnvSafe("APPS_REPO_URL")) + envTable.AddRow("TIPI_VERSION", GetEnvSafe("TIPI_VERSION")) + envTable.AddRow("INTERNAL_IP", GetEnvSafe("INTERNAL_IP")) + envTable.AddRow("ARCHITECTURE", GetEnvSafe("ARCHITECTURE")) + envTable.AddRow("JWT_SECRET", GetEnvSafeRedact("JWT_SECRET")) + envTable.AddRow("ROOT_FOLDER_HOST", GetEnvSafe("ROOT_FOLDER_HOST")) + envTable.AddRow("RUNTIPI_APP_DATA_PATH", GetEnvSafe("RUNTIPI_APP_DATA_PATH")) + envTable.AddRow("NGINX_PORT", GetEnvSafe("NGINX_PORT")) + envTable.AddRow("NGINX_PORT_SSL", GetEnvSafe("NGINX_PORT_SSL")) + envTable.AddRow("DOMAIN", GetEnvSafeRedact("DOMAIN")) + envTable.AddRow("POSTGRES_HOST", GetEnvSafe("POSTGRES_HOST")) + envTable.AddRow("POSTGRES_DBNAME", GetEnvSafe("POSTGRES_DBNAME")) + envTable.AddRow("POSTGRES_USERNAME", GetEnvSafe("POSTGRES_USERNAME")) + envTable.AddRow("POSTGRES_PORT", GetEnvSafe("POSTGRES_PORT")) + envTable.AddRow("REDIS_HOST", GetEnvSafe("REDIS_HOST")) + envTable.AddRow("DEMO_MODE", GetEnvSafe("DEMO_MODE")) + envTable.AddRow("LOCAL_DOMAIN", GetEnvSafe("LOCAL_DOMAIN")) + envTable.Render() + + // Containers + fmt.Println("\n--- " + blue("Container Status") + " ---") + containerTable := table.New(os.Stdout) + for _, container := range containers { + status, err := exec.Command("docker", "ps", "-a", "--filter", "name=" + container, "--format", "{{.Status}}").Output() + if err != nil { + containerTable.AddRow(container, red("down")) + } else { + if strings.Contains(strings.ToLower(string(status)), "up") { + containerTable.AddRow(container, green("up")) + } else { + containerTable.AddRow(container, red("down")) + } + } + } + containerTable.Render() + fmt.Println("\n^ If a container is not 'Up', you can run the command `docker logs ` to see the logs of that container.") + + // Logs + if showLogs { + fmt.Println("\n--- " + blue("Container Logs") + " ---") + for _, container := range containers { + logs, err := exec.Command("docker", "logs", "-n", "15", container).Output() + if err != nil { + utils.PrintError("Failed to get container logs") + fmt.Printf("Error: %s\n", err) + os.Exit(1) + } + fmt.Println("\n" + green(container)) + fmt.Printf("\n%s", logs) + } + fmt.Println("^ Make sure to remove any personal information from the logs.") + } + }, +} diff --git a/cmd/reset-password.go b/cmd/reset-password.go index 9754620..4277668 100644 --- a/cmd/reset-password.go +++ b/cmd/reset-password.go @@ -30,7 +30,7 @@ var resetPasswordCmd = &cobra.Command{ spinner.Fail("Failed to get root folder") spinner.Stop() fmt.Printf("Error: %s\n", osErr) - return; + os.Exit(1) } time := time.Now().Unix() @@ -40,7 +40,7 @@ var resetPasswordCmd = &cobra.Command{ spinner.Fail("Failed to create password change request") spinner.Stop() fmt.Printf("Error: %s\n", writeErr) - return; + os.Exit(1) } internalIp, _ := env.GetEnvValue("INTERNAL_IP") diff --git a/cmd/root.go b/cmd/root.go index 8caebb6..fecbf71 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -9,6 +9,7 @@ import ( var noPermissions bool var envFile string +var showLogs bool var rootCmd = &cobra.Command{ Use: "runtipi-cli-go", diff --git a/cmd/tipifetch.go b/cmd/tipifetch.go index d21bdc1..1441dae 100644 --- a/cmd/tipifetch.go +++ b/cmd/tipifetch.go @@ -6,9 +6,9 @@ import ( "os/exec" "path" - "github.com/fatih/color" "github.com/spf13/cobra" "github.com/steveiliop56/runtipi-cli-go/internal/constants" + "github.com/steveiliop56/runtipi-cli-go/internal/utils" ) func init() { @@ -25,11 +25,9 @@ var tipiFetchCmd = &cobra.Command{ // Write temp ascii file if err := os.WriteFile(asciiPath, []byte(constants.Neofetch), 0644); err != nil { - color.Set(color.FgRed) - fmt.Print("✗ ") - color.Unset() - fmt.Printf("Failed write neofetch ascii art, error: %s\n", err) - return + utils.PrintError("Failed to write neofetch ascii") + fmt.Printf("Error: %s\n", err) + os.Exit(1) } // Run the neofetch command @@ -37,20 +35,16 @@ var tipiFetchCmd = &cobra.Command{ // Check for errors if err != nil { - color.Set(color.FgRed) - fmt.Print("✗ ") - color.Unset() - fmt.Printf("Failed to run neofetch command, error: %s\n", err) - return + utils.PrintError("Failed to run neofetch command") + fmt.Printf("Error: %s\n", err) + os.Exit(1) } // Delete temp file if err := os.Remove(asciiPath); err != nil { - color.Set(color.FgRed) - fmt.Print("✗ ") - color.Unset() - fmt.Printf("Failed to remove temp neofetch ascii art, error: %s\n", err) - return + utils.PrintError("Failed to remove neofetch ascii") + fmt.Printf("Error: %s\n", err) + os.Exit(1) } // Print output diff --git a/go.mod b/go.mod index 20c76a1..054bda7 100644 --- a/go.mod +++ b/go.mod @@ -11,13 +11,14 @@ require ( ) require ( + github.com/aquasecurity/table v1.8.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/rivo/uniseg v0.2.0 // indirect diff --git a/go.sum b/go.sum index e5d0dbe..d0ceb7e 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/Delta456/box-cli-maker v1.3.2 h1:p3LJ67tYrkWOqd3Xju1rYWxx7cRcfHL42poOxdmOLi8= github.com/Delta456/box-cli-maker v1.3.2/go.mod h1:MsanLNTlPCfUdYyhfPY4Aond1NbPGnRxJiAWFcDBIsA= +github.com/aquasecurity/table v1.8.0 h1:9ntpSwrUfjrM6/YviArlx/ZBGd6ix8W+MtojQcM7tv0= +github.com/aquasecurity/table v1.8.0/go.mod h1:eqOmvjjB7AhXFgFqpJUEE/ietg7RrMSJZXyTN8E/wZw= github.com/briandowns/spinner v1.23.1 h1:t5fDPmScwUjozhDj4FA46p5acZWIPXYE30qW2Ptu650= github.com/briandowns/spinner v1.23.1/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -40,8 +42,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= -github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= diff --git a/internal/commands/commands.go b/internal/commands/commands.go index 37c8f31..e51eed3 100644 --- a/internal/commands/commands.go +++ b/internal/commands/commands.go @@ -22,11 +22,11 @@ func Start(envFile string, noPermissions bool) { if dockerErr.Error() == "docker-error" { spinner.Fail("Docker is not installed or user has not the right permissions. See https://docs.docker.com/engine/install/ for more information") spinner.Stop() - return + os.Exit(1) } else if dockerErr.Error() == "compose-error" { spinner.Fail("Docker compose plugin is not installed. See https://docs.docker.com/compose/install/linux/ for more information") spinner.Stop() - return + os.Exit(1) } } spinner.Succeed("User permissions are ok") @@ -38,7 +38,7 @@ func Start(envFile string, noPermissions bool) { spinner.Fail("Failed to copy system files") spinner.Stop() fmt.Printf("Error: %s\n", fileCopyErr) - return + os.Exit(1) } spinner.Succeed("Copied system files") @@ -49,7 +49,7 @@ func Start(envFile string, noPermissions bool) { spinner.Fail("Failed to generate env file") spinner.Stop() fmt.Printf("Error: %s\n", envErr) - return + os.Exit(1) } spinner.Succeed("Env file generated") @@ -61,7 +61,7 @@ func Start(envFile string, noPermissions bool) { spinner.Fail("Failed to chmod files") spinner.Stop() fmt.Printf("Error: %s\n", filePermErr) - return + os.Exit(1) } spinner.Succeed("File permissions ok") } @@ -75,7 +75,7 @@ func Start(envFile string, noPermissions bool) { spinner.Fail("Failed to get root folder") spinner.Stop() fmt.Printf("Error: %s\n", rootFolderErr) - return + os.Exit(1) } _, pullError := exec.Command("docker", "compose", "--env-file", path.Join(rootFolder, ".env"), "pull").Output() @@ -84,7 +84,7 @@ func Start(envFile string, noPermissions bool) { spinner.Fail("Failed to pull images") spinner.Stop() fmt.Printf("Error: %s\n", pullError) - return + os.Exit(1) } spinner.Succeed("Images pulled") @@ -116,7 +116,7 @@ func Start(envFile string, noPermissions bool) { spinner.Fail("Failed to start containers") spinner.Stop() fmt.Printf("Error: %s\n", upErr) - return + os.Exit(1) } spinner.Succeed("Containers started") @@ -143,7 +143,7 @@ func Stop() { spinner.Fail("Error in stopping containers") spinner.Stop() fmt.Printf("Error: %s\n", err) - return + os.Exit(1) } containersToRm := []string{"runtipi-reverse-proxy", "runtipi-db", "runtipi-redis", "runtipi", "tipi-db", "tipi-redis", "tipi-reverse-proxy", "tipi-docker-proxy", "tipi-dashboard", "tipi-worker"} diff --git a/internal/spinner/spinner.go b/internal/spinner/spinner.go index cbcd2f7..1db42a8 100644 --- a/internal/spinner/spinner.go +++ b/internal/spinner/spinner.go @@ -1,11 +1,10 @@ package spinner import ( - "fmt" "time" "github.com/briandowns/spinner" - "github.com/fatih/color" + "github.com/steveiliop56/runtipi-cli-go/internal/utils" ) var s = spinner.New([]string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}, 50*time.Millisecond) @@ -25,18 +24,12 @@ func SetMessage(message string) { func Succeed(message string) { s.Stop() - color.Set(color.FgGreen) - fmt.Print("✓ ") - color.Unset() - fmt.Println(message) + utils.PrintSuccess(message) s.Start() } func Fail(message string) { s.Stop() - color.Set(color.FgRed) - fmt.Print("✗ ") - color.Unset() - fmt.Println(message) + utils.PrintError(message) s.Start() } \ No newline at end of file diff --git a/internal/utils/utils.go b/internal/utils/utils.go new file mode 100644 index 0000000..da72161 --- /dev/null +++ b/internal/utils/utils.go @@ -0,0 +1,21 @@ +package utils + +import ( + "fmt" + + "github.com/fatih/color" +) + +func PrintError(message string) { + color.Set(color.FgRed) + fmt.Print("✗ ") + color.Unset() + fmt.Println(message) +} + +func PrintSuccess(message string) { + color.Set(color.FgGreen) + fmt.Print("✓ ") + color.Unset() + fmt.Println(message) +} \ No newline at end of file