Hybrid install mode done?

This commit is contained in:
Owen 2025-08-20 12:40:21 -07:00
parent ad8ab63fd5
commit 8273554a1c
No known key found for this signature in database
GPG key ID: 8271FDFFD9E0CCBD
4 changed files with 140 additions and 100 deletions

View file

@ -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}}

View file

@ -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:

View file

@ -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"

View file

@ -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,7 +314,17 @@ 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)
@ -318,10 +332,14 @@ func collectUserInput(reader *bufio.Reader) Config {
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,12 +349,8 @@ 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)
@ -349,22 +363,28 @@ func collectUserInput(reader *bufio.Reader) Config {
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 {