From 8273554a1cb32a5597efb9d715186e38dfff2207 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 20 Aug 2025 12:40:21 -0700 Subject: [PATCH] Hybrid install mode done? --- install/config/config.yml | 20 +-- install/config/docker-compose.yml | 12 +- install/config/traefik/traefik_config.yml | 18 +- install/main.go | 190 ++++++++++++---------- 4 files changed, 140 insertions(+), 100 deletions(-) diff --git a/install/config/config.yml b/install/config/config.yml index 2fd9ee68..0dde388a 100644 --- a/install/config/config.yml +++ b/install/config/config.yml @@ -1,6 +1,15 @@ # To see all available options, please visit the docs: # https://docs.digpangolin.com/self-host/advanced/config-file +gerbil: + start_port: 51820 + base_endpoint: "{{.DashboardDomain}}" +{{if .HybridMode}} +hybrid: + id: "{{.HybridId}}" + secret: "{{.HybridSecret}}" + +{{else}} app: dashboard_url: "https://{{.DashboardDomain}}" log_level: "info" @@ -17,11 +26,6 @@ server: methods: ["GET", "POST", "PUT", "DELETE", "PATCH"] allowed_headers: ["X-CSRF-Token", "Content-Type"] credentials: false - -gerbil: - start_port: 51820 - base_endpoint: "{{.DashboardDomain}}" - {{if .EnableEmail}} email: smtp_host: "{{.EmailSMTPHost}}" @@ -30,15 +34,9 @@ email: smtp_pass: "{{.EmailSMTPPass}}" no_reply: "{{.EmailNoReply}}" {{end}} - flags: require_email_verification: {{.EnableEmail}} disable_signup_without_invite: true disable_user_create_org: false allow_raw_resources: true - -{{if and .HybridMode .HybridId .HybridSecret}} -hybrid: - id: "{{.HybridId}}" - secret: "{{.HybridSecret}}" {{end}} \ No newline at end of file diff --git a/install/config/docker-compose.yml b/install/config/docker-compose.yml index 44af4199..97b30317 100644 --- a/install/config/docker-compose.yml +++ b/install/config/docker-compose.yml @@ -6,6 +6,8 @@ services: restart: unless-stopped volumes: - ./config:/app/config + - pangolin-data:/var/certificates + - pangolin-data:/var/dynamic healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"] interval: "10s" @@ -31,8 +33,8 @@ services: ports: - 51820:51820/udp - 21820:21820/udp - - 443:443 # Port for traefik because of the network_mode - - 80:80 # Port for traefik because of the network_mode + - 443:{{if .HybridMode}}8443{{else}}443{{end}} + - 80:80 {{end}} traefik: image: docker.io/traefik:v3.5 @@ -54,9 +56,15 @@ services: - ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration - ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates - ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs + # Shared volume for certificates and dynamic config in file mode + - pangolin-data:/var/certificates:ro + - pangolin-data:/var/dynamic:ro networks: default: driver: bridge name: pangolin {{if .EnableIPv6}} enable_ipv6: true{{end}} + +volumes: + pangolin-data: diff --git a/install/config/traefik/traefik_config.yml b/install/config/traefik/traefik_config.yml index 842786fa..dd0ba1b2 100644 --- a/install/config/traefik/traefik_config.yml +++ b/install/config/traefik/traefik_config.yml @@ -3,12 +3,17 @@ api: dashboard: true providers: +{{if not .HybridMode}} http: endpoint: "http://pangolin:3001/api/v1/traefik-config" pollInterval: "5s" file: filename: "/etc/traefik/dynamic_config.yml" - +{{else}} + file: + directory: "/var/dynamic" + watch: true +{{end}} experimental: plugins: badger: @@ -22,7 +27,7 @@ log: maxBackups: 3 maxAge: 3 compress: true - +{{if not .HybridMode}} certificatesResolvers: letsencrypt: acme: @@ -31,7 +36,7 @@ certificatesResolvers: email: "{{.LetsEncryptEmail}}" storage: "/letsencrypt/acme.json" caServer: "https://acme-v02.api.letsencrypt.org/directory" - +{{end}} entryPoints: web: address: ":80" @@ -40,9 +45,12 @@ entryPoints: transport: respondingTimeouts: readTimeout: "30m" - http: +{{if not .HybridMode}} http: tls: - certResolver: "letsencrypt" + certResolver: "letsencrypt"{{end}} serversTransport: insecureSkipVerify: true + +ping: + entryPoint: "web" \ No newline at end of file diff --git a/install/main.go b/install/main.go index 8b5284f7..1dd0b37c 100644 --- a/install/main.go +++ b/install/main.go @@ -65,12 +65,9 @@ func main() { fmt.Println("Welcome to the Pangolin installer!") fmt.Println("This installer will help you set up Pangolin on your server.") - fmt.Println("") - fmt.Println("Please make sure you have the following prerequisites:") + fmt.Println("\nPlease make sure you have the following prerequisites:") fmt.Println("- Open TCP ports 80 and 443 and UDP ports 51820 and 21820 on your VPS and firewall.") - fmt.Println("") - fmt.Println("Lets get started!") - fmt.Println("") + fmt.Println("\nLets get started!") if os.Geteuid() == 0 { // WE NEED TO BE SUDO TO CHECK THIS for _, p := range []int{80, 443} { @@ -102,51 +99,52 @@ func main() { moveFile("config/docker-compose.yml", "docker-compose.yml") - config.InstallationContainerType = podmanOrDocker(reader) - - if !isDockerInstalled() && runtime.GOOS == "linux" && config.InstallationContainerType == Docker { - if readBool(reader, "Docker is not installed. Would you like to install it?", true) { - installDocker() - // try to start docker service but ignore errors - if err := startDockerService(); err != nil { - fmt.Println("Error starting Docker service:", err) - } else { - fmt.Println("Docker service started successfully!") - } - // wait 10 seconds for docker to start checking if docker is running every 2 seconds - fmt.Println("Waiting for Docker to start...") - for i := 0; i < 5; i++ { - if isDockerRunning() { - fmt.Println("Docker is running!") - break - } - fmt.Println("Docker is not running yet, waiting...") - time.Sleep(2 * time.Second) - } - if !isDockerRunning() { - fmt.Println("Docker is still not running after 10 seconds. Please check the installation.") - os.Exit(1) - } - fmt.Println("Docker installed successfully!") - } - } + fmt.Println("\nConfiguration files created successfully!") fmt.Println("\n=== Starting installation ===") - if (isDockerInstalled() && config.InstallationContainerType == Docker) || - (isPodmanInstalled() && config.InstallationContainerType == Podman) { - if readBool(reader, "Would you like to install and start the containers?", true) { - if err := pullContainers(config.InstallationContainerType); err != nil { - fmt.Println("Error: ", err) - return - } + if readBool(reader, "Would you like to install and start the containers?", true) { - if err := startContainers(config.InstallationContainerType); err != nil { - fmt.Println("Error: ", err) - return + config.InstallationContainerType = podmanOrDocker(reader) + + if !isDockerInstalled() && runtime.GOOS == "linux" && config.InstallationContainerType == Docker { + if readBool(reader, "Docker is not installed. Would you like to install it?", true) { + installDocker() + // try to start docker service but ignore errors + if err := startDockerService(); err != nil { + fmt.Println("Error starting Docker service:", err) + } else { + fmt.Println("Docker service started successfully!") + } + // wait 10 seconds for docker to start checking if docker is running every 2 seconds + fmt.Println("Waiting for Docker to start...") + for i := 0; i < 5; i++ { + if isDockerRunning() { + fmt.Println("Docker is running!") + break + } + fmt.Println("Docker is not running yet, waiting...") + time.Sleep(2 * time.Second) + } + if !isDockerRunning() { + fmt.Println("Docker is still not running after 10 seconds. Please check the installation.") + os.Exit(1) + } + fmt.Println("Docker installed successfully!") } } + + if err := pullContainers(config.InstallationContainerType); err != nil { + fmt.Println("Error: ", err) + return + } + + if err := startContainers(config.InstallationContainerType); err != nil { + fmt.Println("Error: ", err) + return + } } + } else { fmt.Println("Looks like you already installed, so I am going to do the setup...") @@ -171,15 +169,16 @@ func main() { config = collectUserInput(reader) } } - } - // Check if Pangolin is already installed with hybrid section - if checkIsPangolinInstalledWithHybrid() { - fmt.Println("\n=== Convert to Self-Host Node ===") - if readBool(reader, "Do you want to convert this Pangolin instance into a manage self-host node?", true) { - fmt.Println("hello world") - return - } + // Check if Pangolin is already installed with hybrid section + // if checkIsPangolinInstalledWithHybrid() { + // fmt.Println("\n=== Convert to Self-Host Node ===") + // if readBool(reader, "Do you want to convert this Pangolin instance into a managed self-host node?", true) { + // fmt.Println("hello world") + // return + // } + // } + } if !checkIsCrowdsecInstalledInCompose() { @@ -217,25 +216,30 @@ func main() { } } - // Setup Token Section - fmt.Println("\n=== Setup Token ===") + if !config.HybridMode { + // Setup Token Section + fmt.Println("\n=== Setup Token ===") - // Check if containers were started during this installation - containersStarted := false - if (isDockerInstalled() && config.InstallationContainerType == Docker) || - (isPodmanInstalled() && config.InstallationContainerType == Podman) { - // Try to fetch and display the token if containers are running - containersStarted = true - printSetupToken(config.InstallationContainerType, config.DashboardDomain) + // Check if containers were started during this installation + containersStarted := false + if (isDockerInstalled() && config.InstallationContainerType == Docker) || + (isPodmanInstalled() && config.InstallationContainerType == Podman) { + // Try to fetch and display the token if containers are running + containersStarted = true + printSetupToken(config.InstallationContainerType, config.DashboardDomain) + } + + // If containers weren't started or token wasn't found, show instructions + if !containersStarted { + showSetupTokenInstructions(config.InstallationContainerType, config.DashboardDomain) + } } - // If containers weren't started or token wasn't found, show instructions - if !containersStarted { - showSetupTokenInstructions(config.InstallationContainerType, config.DashboardDomain) - } + fmt.Println("\nInstallation complete!") - fmt.Println("Installation complete!") - fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain) + if !config.HybridMode { + fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain) + } } func podmanOrDocker(reader *bufio.Reader) SupportedContainer { @@ -310,18 +314,32 @@ func collectUserInput(reader *bufio.Reader) Config { // Basic configuration fmt.Println("\n=== Basic Configuration ===") - config.HybridMode = readBoolNoDefault(reader, "Do you want to use hybrid mode?") + for { + response := readString(reader, "Do you want to install Pangolin as a cloud-managed self-hosted node? (yes/no)", "") + if strings.EqualFold(response, "yes") || strings.EqualFold(response, "y") { + config.HybridMode = true + break + } else if strings.EqualFold(response, "no") || strings.EqualFold(response, "n") { + config.HybridMode = false + break + } + fmt.Println("Please answer 'yes' or 'no'") + } if config.HybridMode { alreadyHaveCreds := readBool(reader, "Do you already have credentials from the dashboard?", false) - + if alreadyHaveCreds { config.HybridId = readString(reader, "Enter your hybrid ID", "") config.HybridSecret = readString(reader, "Enter your hybrid secret", "") + } else { + // Just print instructions for right now + fmt.Println("Please visit https://pangolin.fossorial.io, create a self hosted node, and return with the credentials.") } - } - if !config.HybridMode { + config.DashboardDomain = readString(reader, "The public addressable IP address for this node or a domain pointing to it", "") + config.InstallGerbil = true + } else { config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "") // Set default dashboard domain after base domain is collected @@ -331,16 +349,12 @@ func collectUserInput(reader *bufio.Reader) Config { } config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain) config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "") - } + config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true) - config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true) - config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true) - - if !config.HybridMode { // Email configuration fmt.Println("\n=== Email Configuration ===") config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false) - + if config.EnableEmail { config.EmailSMTPHost = readString(reader, "Enter SMTP host", "") config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587) @@ -348,23 +362,29 @@ func collectUserInput(reader *bufio.Reader) Config { config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword? config.EmailNoReply = readString(reader, "Enter no-reply email address", "") } - - + // Validate required fields if config.BaseDomain == "" { fmt.Println("Error: Domain name is required") os.Exit(1) } - if config.DashboardDomain == "" { - fmt.Println("Error: Dashboard Domain name is required") - os.Exit(1) - } if config.LetsEncryptEmail == "" { fmt.Println("Error: Let's Encrypt email is required") os.Exit(1) } } + // Advanced configuration + + fmt.Println("\n=== Advanced Configuration ===") + + config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true) + + if config.DashboardDomain == "" { + fmt.Println("Error: Dashboard Domain name is required") + os.Exit(1) + } + return config } @@ -393,6 +413,11 @@ func createConfigFiles(config Config) error { return nil } + // the hybrid does not need the dynamic config + if config.HybridMode && strings.Contains(path, "dynamic_config.yml") { + return nil + } + // skip .DS_Store if strings.Contains(path, ".DS_Store") { return nil @@ -443,6 +468,7 @@ func createConfigFiles(config Config) error { return nil } + func copyFile(src, dst string) error { source, err := os.Open(src) if err != nil {