Merge pull request #2491 from rodneyosodo/fix/install

fix(install): add error handling, code cleanups, and YAML type refactor
This commit is contained in:
Owen Schwartz
2026-02-25 10:41:03 -08:00
committed by GitHub
5 changed files with 91 additions and 65 deletions

View File

@@ -118,19 +118,19 @@ func copyDockerService(sourceFile, destFile, serviceName string) error {
} }
// Parse source Docker Compose YAML // Parse source Docker Compose YAML
var sourceCompose map[string]interface{} var sourceCompose map[string]any
if err := yaml.Unmarshal(sourceData, &sourceCompose); err != nil { if err := yaml.Unmarshal(sourceData, &sourceCompose); err != nil {
return fmt.Errorf("error parsing source Docker Compose file: %w", err) return fmt.Errorf("error parsing source Docker Compose file: %w", err)
} }
// Parse destination Docker Compose YAML // Parse destination Docker Compose YAML
var destCompose map[string]interface{} var destCompose map[string]any
if err := yaml.Unmarshal(destData, &destCompose); err != nil { if err := yaml.Unmarshal(destData, &destCompose); err != nil {
return fmt.Errorf("error parsing destination Docker Compose file: %w", err) return fmt.Errorf("error parsing destination Docker Compose file: %w", err)
} }
// Get services section from source // Get services section from source
sourceServices, ok := sourceCompose["services"].(map[string]interface{}) sourceServices, ok := sourceCompose["services"].(map[string]any)
if !ok { if !ok {
return fmt.Errorf("services section not found in source file or has invalid format") return fmt.Errorf("services section not found in source file or has invalid format")
} }
@@ -142,10 +142,10 @@ func copyDockerService(sourceFile, destFile, serviceName string) error {
} }
// Get or create services section in destination // Get or create services section in destination
destServices, ok := destCompose["services"].(map[string]interface{}) destServices, ok := destCompose["services"].(map[string]any)
if !ok { if !ok {
// If services section doesn't exist, create it // If services section doesn't exist, create it
destServices = make(map[string]interface{}) destServices = make(map[string]any)
destCompose["services"] = destServices destCompose["services"] = destServices
} }
@@ -187,13 +187,12 @@ func backupConfig() error {
return nil return nil
} }
func MarshalYAMLWithIndent(data interface{}, indent int) ([]byte, error) { func MarshalYAMLWithIndent(data any, indent int) ([]byte, error) {
buffer := new(bytes.Buffer) buffer := new(bytes.Buffer)
encoder := yaml.NewEncoder(buffer) encoder := yaml.NewEncoder(buffer)
encoder.SetIndent(indent) encoder.SetIndent(indent)
err := encoder.Encode(data) if err := encoder.Encode(data); err != nil {
if err != nil {
return nil, err return nil, err
} }
@@ -209,7 +208,7 @@ func replaceInFile(filepath, oldStr, newStr string) error {
} }
// Replace the string // Replace the string
newContent := strings.Replace(string(content), oldStr, newStr, -1) newContent := strings.ReplaceAll(string(content), oldStr, newStr)
// Write the modified content back to the file // Write the modified content back to the file
err = os.WriteFile(filepath, []byte(newContent), 0644) err = os.WriteFile(filepath, []byte(newContent), 0644)
@@ -228,28 +227,28 @@ func CheckAndAddTraefikLogVolume(composePath string) error {
} }
// Parse YAML into a generic map // Parse YAML into a generic map
var compose map[string]interface{} var compose map[string]any
if err := yaml.Unmarshal(data, &compose); err != nil { if err := yaml.Unmarshal(data, &compose); err != nil {
return fmt.Errorf("error parsing compose file: %w", err) return fmt.Errorf("error parsing compose file: %w", err)
} }
// Get services section // Get services section
services, ok := compose["services"].(map[string]interface{}) services, ok := compose["services"].(map[string]any)
if !ok { if !ok {
return fmt.Errorf("services section not found or invalid") return fmt.Errorf("services section not found or invalid")
} }
// Get traefik service // Get traefik service
traefik, ok := services["traefik"].(map[string]interface{}) traefik, ok := services["traefik"].(map[string]any)
if !ok { if !ok {
return fmt.Errorf("traefik service not found or invalid") return fmt.Errorf("traefik service not found or invalid")
} }
// Check volumes // Check volumes
logVolume := "./config/traefik/logs:/var/log/traefik" logVolume := "./config/traefik/logs:/var/log/traefik"
var volumes []interface{} var volumes []any
if existingVolumes, ok := traefik["volumes"].([]interface{}); ok { if existingVolumes, ok := traefik["volumes"].([]any); ok {
// Check if volume already exists // Check if volume already exists
for _, v := range existingVolumes { for _, v := range existingVolumes {
if v.(string) == logVolume { if v.(string) == logVolume {
@@ -295,13 +294,13 @@ func MergeYAML(baseFile, overlayFile string) error {
} }
// Parse base YAML into a map // Parse base YAML into a map
var baseMap map[string]interface{} var baseMap map[string]any
if err := yaml.Unmarshal(baseContent, &baseMap); err != nil { if err := yaml.Unmarshal(baseContent, &baseMap); err != nil {
return fmt.Errorf("error parsing base YAML: %v", err) return fmt.Errorf("error parsing base YAML: %v", err)
} }
// Parse overlay YAML into a map // Parse overlay YAML into a map
var overlayMap map[string]interface{} var overlayMap map[string]any
if err := yaml.Unmarshal(overlayContent, &overlayMap); err != nil { if err := yaml.Unmarshal(overlayContent, &overlayMap); err != nil {
return fmt.Errorf("error parsing overlay YAML: %v", err) return fmt.Errorf("error parsing overlay YAML: %v", err)
} }
@@ -324,8 +323,8 @@ func MergeYAML(baseFile, overlayFile string) error {
} }
// mergeMap recursively merges two maps // mergeMap recursively merges two maps
func mergeMap(base, overlay map[string]interface{}) map[string]interface{} { func mergeMap(base, overlay map[string]any) map[string]any {
result := make(map[string]interface{}) result := make(map[string]any)
// Copy all key-values from base map // Copy all key-values from base map
for k, v := range base { for k, v := range base {
@@ -336,8 +335,8 @@ func mergeMap(base, overlay map[string]interface{}) map[string]interface{} {
for k, v := range overlay { for k, v := range overlay {
// If both maps have the same key and both values are maps, merge recursively // If both maps have the same key and both values are maps, merge recursively
if baseVal, ok := base[k]; ok { if baseVal, ok := base[k]; ok {
if baseMap, isBaseMap := baseVal.(map[string]interface{}); isBaseMap { if baseMap, isBaseMap := baseVal.(map[string]any); isBaseMap {
if overlayMap, isOverlayMap := v.(map[string]interface{}); isOverlayMap { if overlayMap, isOverlayMap := v.(map[string]any); isOverlayMap {
result[k] = mergeMap(baseMap, overlayMap) result[k] = mergeMap(baseMap, overlayMap)
continue continue
} }

View File

@@ -144,12 +144,13 @@ func installDocker() error {
} }
func startDockerService() error { func startDockerService() error {
if runtime.GOOS == "linux" { switch runtime.GOOS {
case "linux":
cmd := exec.Command("systemctl", "enable", "--now", "docker") cmd := exec.Command("systemctl", "enable", "--now", "docker")
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
return cmd.Run() return cmd.Run()
} else if runtime.GOOS == "darwin" { case "darwin":
// On macOS, Docker is usually started via the Docker Desktop application // On macOS, Docker is usually started via the Docker Desktop application
fmt.Println("Please start Docker Desktop manually on macOS.") fmt.Println("Please start Docker Desktop manually on macOS.")
return nil return nil
@@ -302,7 +303,7 @@ func pullContainers(containerType SupportedContainer) error {
return nil return nil
} }
return fmt.Errorf("Unsupported container type: %s", containerType) return fmt.Errorf("unsupported container type: %s", containerType)
} }
// startContainers starts the containers using the appropriate command. // startContainers starts the containers using the appropriate command.
@@ -325,7 +326,7 @@ func startContainers(containerType SupportedContainer) error {
return nil return nil
} }
return fmt.Errorf("Unsupported container type: %s", containerType) return fmt.Errorf("unsupported container type: %s", containerType)
} }
// stopContainers stops the containers using the appropriate command. // stopContainers stops the containers using the appropriate command.
@@ -347,7 +348,7 @@ func stopContainers(containerType SupportedContainer) error {
return nil return nil
} }
return fmt.Errorf("Unsupported container type: %s", containerType) return fmt.Errorf("unsupported container type: %s", containerType)
} }
// restartContainer restarts a specific container using the appropriate command. // restartContainer restarts a specific container using the appropriate command.
@@ -369,5 +370,5 @@ func restartContainer(container string, containerType SupportedContainer) error
return nil return nil
} }
return fmt.Errorf("Unsupported container type: %s", containerType) return fmt.Errorf("unsupported container type: %s", containerType)
} }

View File

@@ -27,9 +27,18 @@ func installCrowdsec(config Config) error {
os.Exit(1) os.Exit(1)
} }
os.MkdirAll("config/crowdsec/db", 0755) if err := os.MkdirAll("config/crowdsec/db", 0755); err != nil {
os.MkdirAll("config/crowdsec/acquis.d", 0755) fmt.Printf("Error creating config files: %v\n", err)
os.MkdirAll("config/traefik/logs", 0755) os.Exit(1)
}
if err := os.MkdirAll("config/crowdsec/acquis.d", 0755); err != nil {
fmt.Printf("Error creating config files: %v\n", err)
os.Exit(1)
}
if err := os.MkdirAll("config/traefik/logs", 0755); err != nil {
fmt.Printf("Error creating config files: %v\n", err)
os.Exit(1)
}
if err := copyDockerService("config/crowdsec/docker-compose.yml", "docker-compose.yml", "crowdsec"); err != nil { if err := copyDockerService("config/crowdsec/docker-compose.yml", "docker-compose.yml", "crowdsec"); err != nil {
fmt.Printf("Error copying docker service: %v\n", err) fmt.Printf("Error copying docker service: %v\n", err)
@@ -153,34 +162,34 @@ func CheckAndAddCrowdsecDependency(composePath string) error {
} }
// Parse YAML into a generic map // Parse YAML into a generic map
var compose map[string]interface{} var compose map[string]any
if err := yaml.Unmarshal(data, &compose); err != nil { if err := yaml.Unmarshal(data, &compose); err != nil {
return fmt.Errorf("error parsing compose file: %w", err) return fmt.Errorf("error parsing compose file: %w", err)
} }
// Get services section // Get services section
services, ok := compose["services"].(map[string]interface{}) services, ok := compose["services"].(map[string]any)
if !ok { if !ok {
return fmt.Errorf("services section not found or invalid") return fmt.Errorf("services section not found or invalid")
} }
// Get traefik service // Get traefik service
traefik, ok := services["traefik"].(map[string]interface{}) traefik, ok := services["traefik"].(map[string]any)
if !ok { if !ok {
return fmt.Errorf("traefik service not found or invalid") return fmt.Errorf("traefik service not found or invalid")
} }
// Get dependencies // Get dependencies
dependsOn, ok := traefik["depends_on"].(map[string]interface{}) dependsOn, ok := traefik["depends_on"].(map[string]any)
if ok { if ok {
// Append the new block for crowdsec // Append the new block for crowdsec
dependsOn["crowdsec"] = map[string]interface{}{ dependsOn["crowdsec"] = map[string]any{
"condition": "service_healthy", "condition": "service_healthy",
} }
} else { } else {
// No dependencies exist, create it // No dependencies exist, create it
traefik["depends_on"] = map[string]interface{}{ traefik["depends_on"] = map[string]any{
"crowdsec": map[string]interface{}{ "crowdsec": map[string]any{
"condition": "service_healthy", "condition": "service_healthy",
}, },
} }

View File

@@ -57,11 +57,12 @@ func readBool(reader *bufio.Reader, prompt string, defaultValue bool) bool {
for { for {
input := readString(reader, prompt+" (yes/no)", defaultStr) input := readString(reader, prompt+" (yes/no)", defaultStr)
lower := strings.ToLower(input) lower := strings.ToLower(input)
if lower == "yes" { switch lower {
case "yes":
return true return true
} else if lower == "no" { case "no":
return false return false
} else { default:
fmt.Println("Please enter 'yes' or 'no'.") fmt.Println("Please enter 'yes' or 'no'.")
} }
} }
@@ -71,11 +72,12 @@ func readBoolNoDefault(reader *bufio.Reader, prompt string) bool {
for { for {
input := readStringNoDefault(reader, prompt+" (yes/no)") input := readStringNoDefault(reader, prompt+" (yes/no)")
lower := strings.ToLower(input) lower := strings.ToLower(input)
if lower == "yes" { switch lower {
case "yes":
return true return true
} else if lower == "no" { case "no":
return false return false
} else { default:
fmt.Println("Please enter 'yes' or 'no'.") fmt.Println("Please enter 'yes' or 'no'.")
} }
} }

View File

@@ -2,12 +2,12 @@ package main
import ( import (
"bufio" "bufio"
"crypto/rand"
"embed" "embed"
"encoding/base64"
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
"crypto/rand"
"encoding/base64"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
@@ -102,7 +102,10 @@ func main() {
os.Exit(1) os.Exit(1)
} }
moveFile("config/docker-compose.yml", "docker-compose.yml") if err := moveFile("config/docker-compose.yml", "docker-compose.yml"); err != nil {
fmt.Printf("Error moving docker-compose.yml: %v\n", err)
os.Exit(1)
}
fmt.Println("\nConfiguration files created successfully!") fmt.Println("\nConfiguration files created successfully!")
@@ -123,7 +126,11 @@ func main() {
if !isDockerInstalled() && runtime.GOOS == "linux" && config.InstallationContainerType == Docker { if !isDockerInstalled() && runtime.GOOS == "linux" && config.InstallationContainerType == Docker {
if readBool(reader, "Docker is not installed. Would you like to install it?", true) { if readBool(reader, "Docker is not installed. Would you like to install it?", true) {
installDocker() if err := installDocker(); err != nil {
fmt.Printf("Error installing Docker: %v\n", err)
return
}
// try to start docker service but ignore errors // try to start docker service but ignore errors
if err := startDockerService(); err != nil { if err := startDockerService(); err != nil {
fmt.Println("Error starting Docker service:", err) fmt.Println("Error starting Docker service:", err)
@@ -132,7 +139,7 @@ func main() {
} }
// wait 10 seconds for docker to start checking if docker is running every 2 seconds // wait 10 seconds for docker to start checking if docker is running every 2 seconds
fmt.Println("Waiting for Docker to start...") fmt.Println("Waiting for Docker to start...")
for i := 0; i < 5; i++ { for range 5 {
if isDockerRunning() { if isDockerRunning() {
fmt.Println("Docker is running!") fmt.Println("Docker is running!")
break break
@@ -290,7 +297,8 @@ func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
os.Exit(1) os.Exit(1)
} }
if chosenContainer == Podman { switch chosenContainer {
case Podman:
if !isPodmanInstalled() { if !isPodmanInstalled() {
fmt.Println("Podman or podman-compose is not installed. Please install both manually. Automated installation will be available in a later release.") fmt.Println("Podman or podman-compose is not installed. Please install both manually. Automated installation will be available in a later release.")
os.Exit(1) os.Exit(1)
@@ -311,7 +319,7 @@ func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
// Linux only. // Linux only.
if err := run("bash", "-c", "echo 'net.ipv4.ip_unprivileged_port_start=80' > /etc/sysctl.d/99-podman.conf && sysctl --system"); err != nil { if err := run("bash", "-c", "echo 'net.ipv4.ip_unprivileged_port_start=80' > /etc/sysctl.d/99-podman.conf && sysctl --system"); err != nil {
fmt.Printf("Error configuring unprivileged ports: %v\n", err) fmt.Printf("Error configuring unprivileged ports: %v\n", err)
os.Exit(1) os.Exit(1)
} }
} else { } else {
@@ -321,7 +329,7 @@ func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
fmt.Println("Unprivileged ports have been configured.") fmt.Println("Unprivileged ports have been configured.")
} }
} else if chosenContainer == Docker { case Docker:
// check if docker is not installed and the user is root // check if docker is not installed and the user is root
if !isDockerInstalled() { if !isDockerInstalled() {
if os.Geteuid() != 0 { if os.Geteuid() != 0 {
@@ -336,7 +344,7 @@ func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
fmt.Println("The installer will not be able to run docker commands without running it as root.") fmt.Println("The installer will not be able to run docker commands without running it as root.")
os.Exit(1) os.Exit(1)
} }
} else { default:
// This shouldn't happen unless there's a third container runtime. // This shouldn't happen unless there's a third container runtime.
os.Exit(1) os.Exit(1)
} }
@@ -405,10 +413,18 @@ func collectUserInput(reader *bufio.Reader) Config {
} }
func createConfigFiles(config Config) error { func createConfigFiles(config Config) error {
os.MkdirAll("config", 0755) if err := os.MkdirAll("config", 0755); err != nil {
os.MkdirAll("config/letsencrypt", 0755) return fmt.Errorf("failed to create config directory: %v", err)
os.MkdirAll("config/db", 0755) }
os.MkdirAll("config/logs", 0755) if err := os.MkdirAll("config/letsencrypt", 0755); err != nil {
return fmt.Errorf("failed to create letsencrypt directory: %v", err)
}
if err := os.MkdirAll("config/db", 0755); err != nil {
return fmt.Errorf("failed to create db directory: %v", err)
}
if err := os.MkdirAll("config/logs", 0755); err != nil {
return fmt.Errorf("failed to create logs directory: %v", err)
}
// Walk through all embedded files // Walk through all embedded files
err := fs.WalkDir(configFiles, "config", func(path string, d fs.DirEntry, err error) error { err := fs.WalkDir(configFiles, "config", func(path string, d fs.DirEntry, err error) error {
@@ -562,22 +578,24 @@ func showSetupTokenInstructions(containerType SupportedContainer, dashboardDomai
fmt.Println("To get your setup token, you need to:") fmt.Println("To get your setup token, you need to:")
fmt.Println("") fmt.Println("")
fmt.Println("1. Start the containers") fmt.Println("1. Start the containers")
if containerType == Docker { switch containerType {
case Docker:
fmt.Println(" docker compose up -d") fmt.Println(" docker compose up -d")
} else if containerType == Podman { case Podman:
fmt.Println(" podman-compose up -d") fmt.Println(" podman-compose up -d")
} else {
} }
fmt.Println("") fmt.Println("")
fmt.Println("2. Wait for the Pangolin container to start and generate the token") fmt.Println("2. Wait for the Pangolin container to start and generate the token")
fmt.Println("") fmt.Println("")
fmt.Println("3. Check the container logs for the setup token") fmt.Println("3. Check the container logs for the setup token")
if containerType == Docker { switch containerType {
case Docker:
fmt.Println(" docker logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'") fmt.Println(" docker logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'")
} else if containerType == Podman { case Podman:
fmt.Println(" podman logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'") fmt.Println(" podman logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'")
} else {
} }
fmt.Println("") fmt.Println("")
fmt.Println("4. Look for output like") fmt.Println("4. Look for output like")
fmt.Println(" === SETUP TOKEN GENERATED ===") fmt.Println(" === SETUP TOKEN GENERATED ===")
@@ -639,10 +657,7 @@ func checkPortsAvailable(port int) error {
addr := fmt.Sprintf(":%d", port) addr := fmt.Sprintf(":%d", port)
ln, err := net.Listen("tcp", addr) ln, err := net.Listen("tcp", addr)
if err != nil { if err != nil {
return fmt.Errorf( return fmt.Errorf("ERROR: port %d is occupied or cannot be bound: %w", port, err)
"ERROR: port %d is occupied or cannot be bound: %w\n\n",
port, err,
)
} }
if closeErr := ln.Close(); closeErr != nil { if closeErr := ln.Close(); closeErr != nil {
fmt.Fprintf(os.Stderr, fmt.Fprintf(os.Stderr,