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: # To see all available options, please visit the docs:
# https://docs.digpangolin.com/self-host/advanced/config-file # 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: app:
dashboard_url: "https://{{.DashboardDomain}}" dashboard_url: "https://{{.DashboardDomain}}"
log_level: "info" log_level: "info"
@ -17,11 +26,6 @@ server:
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"] methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
allowed_headers: ["X-CSRF-Token", "Content-Type"] allowed_headers: ["X-CSRF-Token", "Content-Type"]
credentials: false credentials: false
gerbil:
start_port: 51820
base_endpoint: "{{.DashboardDomain}}"
{{if .EnableEmail}} {{if .EnableEmail}}
email: email:
smtp_host: "{{.EmailSMTPHost}}" smtp_host: "{{.EmailSMTPHost}}"
@ -30,15 +34,9 @@ email:
smtp_pass: "{{.EmailSMTPPass}}" smtp_pass: "{{.EmailSMTPPass}}"
no_reply: "{{.EmailNoReply}}" no_reply: "{{.EmailNoReply}}"
{{end}} {{end}}
flags: flags:
require_email_verification: {{.EnableEmail}} require_email_verification: {{.EnableEmail}}
disable_signup_without_invite: true disable_signup_without_invite: true
disable_user_create_org: false disable_user_create_org: false
allow_raw_resources: true allow_raw_resources: true
{{if and .HybridMode .HybridId .HybridSecret}}
hybrid:
id: "{{.HybridId}}"
secret: "{{.HybridSecret}}"
{{end}} {{end}}

View file

@ -6,6 +6,8 @@ services:
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- ./config:/app/config - ./config:/app/config
- pangolin-data:/var/certificates
- pangolin-data:/var/dynamic
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"] test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
interval: "10s" interval: "10s"
@ -31,8 +33,8 @@ services:
ports: ports:
- 51820:51820/udp - 51820:51820/udp
- 21820:21820/udp - 21820:21820/udp
- 443:443 # Port for traefik because of the network_mode - 443:{{if .HybridMode}}8443{{else}}443{{end}}
- 80:80 # Port for traefik because of the network_mode - 80:80
{{end}} {{end}}
traefik: traefik:
image: docker.io/traefik:v3.5 image: docker.io/traefik:v3.5
@ -54,9 +56,15 @@ services:
- ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration - ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates - ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
- ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs - ./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: networks:
default: default:
driver: bridge driver: bridge
name: pangolin name: pangolin
{{if .EnableIPv6}} enable_ipv6: true{{end}} {{if .EnableIPv6}} enable_ipv6: true{{end}}
volumes:
pangolin-data:

View file

@ -3,12 +3,17 @@ api:
dashboard: true dashboard: true
providers: providers:
{{if not .HybridMode}}
http: http:
endpoint: "http://pangolin:3001/api/v1/traefik-config" endpoint: "http://pangolin:3001/api/v1/traefik-config"
pollInterval: "5s" pollInterval: "5s"
file: file:
filename: "/etc/traefik/dynamic_config.yml" filename: "/etc/traefik/dynamic_config.yml"
{{else}}
file:
directory: "/var/dynamic"
watch: true
{{end}}
experimental: experimental:
plugins: plugins:
badger: badger:
@ -22,7 +27,7 @@ log:
maxBackups: 3 maxBackups: 3
maxAge: 3 maxAge: 3
compress: true compress: true
{{if not .HybridMode}}
certificatesResolvers: certificatesResolvers:
letsencrypt: letsencrypt:
acme: acme:
@ -31,7 +36,7 @@ certificatesResolvers:
email: "{{.LetsEncryptEmail}}" email: "{{.LetsEncryptEmail}}"
storage: "/letsencrypt/acme.json" storage: "/letsencrypt/acme.json"
caServer: "https://acme-v02.api.letsencrypt.org/directory" caServer: "https://acme-v02.api.letsencrypt.org/directory"
{{end}}
entryPoints: entryPoints:
web: web:
address: ":80" address: ":80"
@ -40,9 +45,12 @@ entryPoints:
transport: transport:
respondingTimeouts: respondingTimeouts:
readTimeout: "30m" readTimeout: "30m"
http: {{if not .HybridMode}} http:
tls: tls:
certResolver: "letsencrypt" certResolver: "letsencrypt"{{end}}
serversTransport: serversTransport:
insecureSkipVerify: true insecureSkipVerify: true
ping:
entryPoint: "web"

View file

@ -65,12 +65,9 @@ func main() {
fmt.Println("Welcome to the Pangolin installer!") fmt.Println("Welcome to the Pangolin installer!")
fmt.Println("This installer will help you set up Pangolin on your server.") fmt.Println("This installer will help you set up Pangolin on your server.")
fmt.Println("") fmt.Println("\nPlease make sure you have the following prerequisites:")
fmt.Println("Please 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("- Open TCP ports 80 and 443 and UDP ports 51820 and 21820 on your VPS and firewall.")
fmt.Println("") fmt.Println("\nLets get started!")
fmt.Println("Lets get started!")
fmt.Println("")
if os.Geteuid() == 0 { // WE NEED TO BE SUDO TO CHECK THIS if os.Geteuid() == 0 { // WE NEED TO BE SUDO TO CHECK THIS
for _, p := range []int{80, 443} { for _, p := range []int{80, 443} {
@ -102,6 +99,12 @@ func main() {
moveFile("config/docker-compose.yml", "docker-compose.yml") moveFile("config/docker-compose.yml", "docker-compose.yml")
fmt.Println("\nConfiguration files created successfully!")
fmt.Println("\n=== Starting installation ===")
if readBool(reader, "Would you like to install and start the containers?", true) {
config.InstallationContainerType = podmanOrDocker(reader) config.InstallationContainerType = podmanOrDocker(reader)
if !isDockerInstalled() && runtime.GOOS == "linux" && config.InstallationContainerType == Docker { if !isDockerInstalled() && runtime.GOOS == "linux" && config.InstallationContainerType == Docker {
@ -131,11 +134,6 @@ func main() {
} }
} }
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 { if err := pullContainers(config.InstallationContainerType); err != nil {
fmt.Println("Error: ", err) fmt.Println("Error: ", err)
return return
@ -146,7 +144,7 @@ func main() {
return return
} }
} }
}
} else { } else {
fmt.Println("Looks like you already installed, so I am going to do the setup...") fmt.Println("Looks like you already installed, so I am going to do the setup...")
@ -171,15 +169,16 @@ func main() {
config = collectUserInput(reader) config = collectUserInput(reader)
} }
} }
}
// Check if Pangolin is already installed with hybrid section // Check if Pangolin is already installed with hybrid section
if checkIsPangolinInstalledWithHybrid() { // if checkIsPangolinInstalledWithHybrid() {
fmt.Println("\n=== Convert to Self-Host Node ===") // 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) { // if readBool(reader, "Do you want to convert this Pangolin instance into a managed self-host node?", true) {
fmt.Println("hello world") // fmt.Println("hello world")
return // return
} // }
// }
} }
if !checkIsCrowdsecInstalledInCompose() { if !checkIsCrowdsecInstalledInCompose() {
@ -217,6 +216,7 @@ func main() {
} }
} }
if !config.HybridMode {
// Setup Token Section // Setup Token Section
fmt.Println("\n=== Setup Token ===") fmt.Println("\n=== Setup Token ===")
@ -233,10 +233,14 @@ func main() {
if !containersStarted { if !containersStarted {
showSetupTokenInstructions(config.InstallationContainerType, config.DashboardDomain) showSetupTokenInstructions(config.InstallationContainerType, config.DashboardDomain)
} }
}
fmt.Println("Installation complete!") fmt.Println("\nInstallation complete!")
if !config.HybridMode {
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain) fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
} }
}
func podmanOrDocker(reader *bufio.Reader) SupportedContainer { func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
inputContainer := readString(reader, "Would you like to run Pangolin as Docker or Podman containers?", "docker") inputContainer := readString(reader, "Would you like to run Pangolin as Docker or Podman containers?", "docker")
@ -310,7 +314,17 @@ func collectUserInput(reader *bufio.Reader) Config {
// Basic configuration // Basic configuration
fmt.Println("\n=== 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 { if config.HybridMode {
alreadyHaveCreds := readBool(reader, "Do you already have credentials from the dashboard?", false) alreadyHaveCreds := readBool(reader, "Do you already have credentials from the dashboard?", false)
@ -318,10 +332,14 @@ func collectUserInput(reader *bufio.Reader) Config {
if alreadyHaveCreds { if alreadyHaveCreds {
config.HybridId = readString(reader, "Enter your hybrid ID", "") config.HybridId = readString(reader, "Enter your hybrid ID", "")
config.HybridSecret = readString(reader, "Enter your hybrid secret", "") 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)", "") config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
// Set default dashboard domain after base domain is collected // 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.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "") 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 // Email configuration
fmt.Println("\n=== Email Configuration ===") fmt.Println("\n=== Email Configuration ===")
config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false) 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", "") config.EmailNoReply = readString(reader, "Enter no-reply email address", "")
} }
// Validate required fields // Validate required fields
if config.BaseDomain == "" { if config.BaseDomain == "" {
fmt.Println("Error: Domain name is required") fmt.Println("Error: Domain name is required")
os.Exit(1) os.Exit(1)
} }
if config.DashboardDomain == "" {
fmt.Println("Error: Dashboard Domain name is required")
os.Exit(1)
}
if config.LetsEncryptEmail == "" { if config.LetsEncryptEmail == "" {
fmt.Println("Error: Let's Encrypt email is required") fmt.Println("Error: Let's Encrypt email is required")
os.Exit(1) 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 return config
} }
@ -393,6 +413,11 @@ func createConfigFiles(config Config) error {
return nil return nil
} }
// the hybrid does not need the dynamic config
if config.HybridMode && strings.Contains(path, "dynamic_config.yml") {
return nil
}
// skip .DS_Store // skip .DS_Store
if strings.Contains(path, ".DS_Store") { if strings.Contains(path, ".DS_Store") {
return nil return nil
@ -443,6 +468,7 @@ func createConfigFiles(config Config) error {
return nil return nil
} }
func copyFile(src, dst string) error { func copyFile(src, dst string) error {
source, err := os.Open(src) source, err := os.Open(src)
if err != nil { if err != nil {