Browse Source

Add setup script

Owen Schwartz 6 months ago
parent
commit
37790c850a

+ 0 - 1
.gitignore

@@ -26,6 +26,5 @@ migrations
 package-lock.json
 package-lock.json
 tsconfig.tsbuildinfo
 tsconfig.tsbuildinfo
 config/
 config/
-config.yml
 dist
 dist
 .dist
 .dist

+ 54 - 0
docker-compose.example.yml

@@ -0,0 +1,54 @@
+version: "3.7"
+
+services:
+  pangolin:
+    image: fossorial/pangolin
+    container_name: pangolin
+    restart: unless-stopped
+    ports:
+      - 3001:3001
+      - 3000:3000
+    volumes:
+      - ./config:/app/config
+    healthcheck:
+      test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
+      interval: "3s"
+      timeout: "3s"
+      retries: 5
+
+  gerbil:
+    image: fossorial/gerbil
+    container_name: gerbil
+    restart: unless-stopped
+    depends_on:
+      pangolin:
+        condition: service_healthy
+    command:
+      - --reachableAt=http://gerbil:3003
+      - --generateAndSaveKeyTo=/var/config/key
+      - --remoteConfig=http://pangolin:3001/api/v1/gerbil/get-config
+      - --reportBandwidthTo=http://pangolin:3001/api/v1/gerbil/receive-bandwidth
+    volumes:
+      - ./config/:/var/config
+    cap_add:
+      - NET_ADMIN
+      - SYS_MODULE
+    ports:
+      - 51820:51820/udp
+      - 8080:8080 # Port for traefik because of the network_mode
+      - 443:443 # Port for traefik because of the network_mode
+      - 80:80 # Port for traefik because of the network_mode
+
+  traefik:
+    image: traefik:v3.1
+    container_name: traefik
+    restart: unless-stopped
+    network_mode: service:gerbil # Ports appear on the gerbil service
+    depends_on:
+      pangolin:
+        condition: service_healthy
+    command:
+      - --configFile=/etc/traefik/traefik_config.yml
+    volumes:
+      - ./traefik:/etc/traefik:ro # Volume to store the Traefik configuration
+      - ./letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates

+ 1 - 0
install/.gitignore

@@ -0,0 +1 @@
+installer

+ 8 - 0
install/Makefile

@@ -0,0 +1,8 @@
+
+all: build
+
+build: 
+	CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o installer
+
+clean:
+	rm installer

+ 48 - 0
install/fs/config.yml

@@ -0,0 +1,48 @@
+app:
+    base_url: https://{{.Domain}}
+    log_level: info
+    save_logs: false
+
+server:
+    external_port: 3000
+    internal_port: 3001
+    next_port: 3002
+    internal_hostname: pangolin
+    secure_cookies: false
+    session_cookie_name: session
+    resource_session_cookie_name: resource_session
+
+traefik:
+    cert_resolver: letsencrypt
+    http_entrypoint: web
+    https_entrypoint: websecure
+    prefer_wildcard_cert: false
+
+gerbil:
+    start_port: 51820
+    base_endpoint: {{.Domain}}
+    use_subdomain: false
+    block_size: 16
+    subnet_group: 10.0.0.0/8
+
+rate_limits:
+    global:
+        window_minutes: 1
+        max_requests: 100
+{{if .EnableEmail}}
+email:
+    smtp_host: {{.EmailSMTPHost}}
+    smtp_port: {{.EmailSMTPPort}}
+    smtp_user: {{.EmailSMTPUser}}
+    smtp_pass: {{.EmailSMTPPass}}
+    no_reply: {{.EmailNoReply}}
+{{end}}
+users:
+    server_admin:
+        email: {{.AdminUserEmail}}
+        password: {{.AdminUserPassword}}
+
+flags:
+    require_email_verification: {{.EnableEmail}}
+    disable_signup_without_invite: {{.DisableSignupWithoutInvite}}
+    disable_user_create_org: {{.DisableUserCreateOrg}}

+ 51 - 0
install/fs/docker-compose.yml

@@ -0,0 +1,51 @@
+services:
+  pangolin:
+    image: fossorial/pangolin
+    container_name: pangolin
+    restart: unless-stopped
+    ports:
+      - 3001:3001
+      - 3000:3000
+    volumes:
+      - ./config:/app/config
+    healthcheck:
+      test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
+      interval: "3s"
+      timeout: "3s"
+      retries: 5
+
+  gerbil:
+    image: fossorial/gerbil
+    container_name: gerbil
+    restart: unless-stopped
+    depends_on:
+      pangolin:
+        condition: service_healthy
+    command:
+      - --reachableAt=http://gerbil:3003
+      - --generateAndSaveKeyTo=/var/config/key
+      - --remoteConfig=http://pangolin:3001/api/v1/gerbil/get-config
+      - --reportBandwidthTo=http://pangolin:3001/api/v1/gerbil/receive-bandwidth
+    volumes:
+      - ./config/:/var/config
+    cap_add:
+      - NET_ADMIN
+      - SYS_MODULE
+    ports:
+      - 51820:51820/udp
+      - 443:443 # Port for traefik because of the network_mode
+      - 80:80 # Port for traefik because of the network_mode
+
+  traefik:
+    image: traefik:v3.1
+    container_name: traefik
+    restart: unless-stopped
+    network_mode: service:gerbil # Ports appear on the gerbil service
+    depends_on:
+      pangolin:
+        condition: service_healthy
+    command:
+      - --configFile=/etc/traefik/traefik_config.yml
+    volumes:
+      - ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
+      - ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates

+ 54 - 0
install/fs/traefik/dynamic_config.yml

@@ -0,0 +1,54 @@
+http:
+  middlewares:
+    redirect-to-https:
+      redirectScheme:
+        scheme: https
+        permanent: true
+
+  routers:
+    # HTTP to HTTPS redirect router
+    main-app-router-redirect:
+      rule: "Host(`{{.Domain}}`)"
+      service: next-service
+      entryPoints:
+        - web
+      middlewares:
+        - redirect-to-https
+
+    # Next.js router (handles everything except API and WebSocket paths)
+    next-router:
+      rule: "Host(`{{.Domain}}`) && !PathPrefix(`/api/v1`)"
+      service: next-service
+      entryPoints:
+        - websecure
+      tls:
+        certResolver: letsencrypt
+
+    # API router (handles /api/v1 paths)
+    api-router:
+      rule: "Host(`{{.Domain}}`) && PathPrefix(`/api/v1`)"
+      service: api-service
+      entryPoints:
+        - websecure
+      tls:
+        certResolver: letsencrypt
+
+    # WebSocket router
+    ws-router:
+      rule: "Host(`{{.Domain}}`)"
+      service: api-service
+      entryPoints:
+        - websecure
+      tls:
+        certResolver: letsencrypt
+
+  services:
+    next-service:
+      loadBalancer:
+        servers:
+          - url: "http://pangolin:3002"  # Next.js server
+
+    api-service:
+      loadBalancer:
+        servers:
+          - url: "http://pangolin:3000"  # API/WebSocket server

+ 41 - 0
install/fs/traefik/traefik_config.yml

@@ -0,0 +1,41 @@
+api:
+  insecure: true
+  dashboard: true
+
+providers:
+  http:
+    endpoint: "http://pangolin:3001/api/v1/traefik-config"
+    pollInterval: "5s"
+  file:
+    filename: "/etc/traefik/dynamic_config.yml"
+
+experimental:
+  plugins:
+    badger:
+      moduleName: "github.com/fosrl/badger"
+      version: "v1.0.0-beta.1"
+
+log:
+  level: "INFO"
+  format: "common"
+
+certificatesResolvers:
+  letsencrypt:
+    acme:
+      httpChallenge:
+        entryPoint: web
+      email: "{{.LetsEncryptEmail}}"
+      storage: "/letsencrypt/acme.json"
+      caServer: "https://acme-v02.api.letsencrypt.org/directory"
+
+entryPoints:
+  web:
+    address: ":80"
+  websecure:
+    address: ":443"
+    http:
+      tls:
+        certResolver: "letsencrypt"
+
+serversTransport:
+  insecureSkipVerify: true

+ 3 - 0
install/go.mod

@@ -0,0 +1,3 @@
+module installer
+
+go 1.23.0

+ 0 - 0
install/go.sum


+ 296 - 0
install/main.go

@@ -0,0 +1,296 @@
+package main
+
+import (
+	"bufio"
+	"embed"
+	"fmt"
+	"io/fs"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"runtime"
+	"strings"
+	"text/template"
+)
+
+//go:embed fs/*
+var configFiles embed.FS
+
+type Config struct {
+	Domain                     string `yaml:"domain"`
+	LetsEncryptEmail           string `yaml:"letsEncryptEmail"`
+	AdminUserEmail             string `yaml:"adminUserEmail"`
+	AdminUserPassword          string `yaml:"adminUserPassword"`
+	DisableSignupWithoutInvite bool   `yaml:"disableSignupWithoutInvite"`
+	DisableUserCreateOrg       bool   `yaml:"disableUserCreateOrg"`
+	EnableEmail                bool   `yaml:"enableEmail"`
+	EmailSMTPHost              string `yaml:"emailSMTPHost"`
+	EmailSMTPPort              int    `yaml:"emailSMTPPort"`
+	EmailSMTPUser              string `yaml:"emailSMTPUser"`
+	EmailSMTPPass              string `yaml:"emailSMTPPass"`
+	EmailNoReply               string `yaml:"emailNoReply"`
+}
+
+func main() {
+	reader := bufio.NewReader(os.Stdin)
+
+	config := collectUserInput(reader)
+	createConfigFiles(config)
+
+	if !isDockerInstalled() && runtime.GOOS == "linux" {
+		if shouldInstallDocker() {
+			// ask user if they want to install docker
+			if readBool(reader, "Would you like to install Docker?", true) {
+				installDocker()
+			}
+		}
+	}
+
+	if isDockerInstalled() {
+		if readBool(reader, "Would you like to install and start the containers?", true) {
+			pullAndStartContainers()
+		}
+	}
+
+	fmt.Println("Installation complete!")
+}
+
+func readString(reader *bufio.Reader, prompt string, defaultValue string) string {
+	if defaultValue != "" {
+		fmt.Printf("%s (default: %s): ", prompt, defaultValue)
+	} else {
+		fmt.Print(prompt + ": ")
+	}
+	input, _ := reader.ReadString('\n')
+	input = strings.TrimSpace(input)
+	if input == "" {
+		return defaultValue
+	}
+	return input
+}
+
+func readBool(reader *bufio.Reader, prompt string, defaultValue bool) bool {
+	defaultStr := "no"
+	if defaultValue {
+		defaultStr = "yes"
+	}
+	input := readString(reader, prompt+" (yes/no)", defaultStr)
+	return strings.ToLower(input) == "yes"
+}
+
+func readInt(reader *bufio.Reader, prompt string, defaultValue int) int {
+	input := readString(reader, prompt, fmt.Sprintf("%d", defaultValue))
+	if input == "" {
+		return defaultValue
+	}
+	value := defaultValue
+	fmt.Sscanf(input, "%d", &value)
+	return value
+}
+
+func collectUserInput(reader *bufio.Reader) Config {
+	config := Config{}
+
+	// Basic configuration
+	fmt.Println("\n=== Basic Configuration ===")
+	config.Domain = readString(reader, "Enter your domain name", "")
+	config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
+
+	// Admin user configuration
+	fmt.Println("\n=== Admin User Configuration ===")
+	config.AdminUserEmail = readString(reader, "Enter admin user email", "admin@"+config.Domain)
+	config.AdminUserPassword = readString(reader, "Enter admin user password", "")
+
+	// Security settings
+	fmt.Println("\n=== Security Settings ===")
+	config.DisableSignupWithoutInvite = readBool(reader, "Disable signup without invite", true)
+	config.DisableUserCreateOrg = readBool(reader, "Disable users from creating organizations", false)
+
+	// Email configuration
+	fmt.Println("\n=== Email Configuration ===")
+	config.EnableEmail = readBool(reader, "Enable email functionality", false)
+
+	if config.EnableEmail {
+		config.EmailSMTPHost = readString(reader, "Enter SMTP host: ", "")
+		config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587): ", 587)
+		config.EmailSMTPUser = readString(reader, "Enter SMTP username: ", "")
+		config.EmailSMTPPass = readString(reader, "Enter SMTP password: ", "")
+		config.EmailNoReply = readString(reader, "Enter no-reply email address: ", "")
+	}
+
+	// Validate required fields
+	if config.Domain == "" {
+		fmt.Println("Error: Domain name is required")
+		os.Exit(1)
+	}
+	if config.LetsEncryptEmail == "" {
+		fmt.Println("Error: Let's Encrypt email is required")
+		os.Exit(1)
+	}
+	if config.AdminUserEmail == "" || config.AdminUserPassword == "" {
+		fmt.Println("Error: Admin user email and password are required")
+		os.Exit(1)
+	}
+
+	return config
+}
+
+func createConfigFiles(config Config) error {
+	os.MkdirAll("config", 0755)
+	os.MkdirAll("config/letsencrypt", 0755)
+	os.MkdirAll("config/db", 0755)
+	os.MkdirAll("config/logs", 0755)
+
+	// Walk through all embedded files
+	err := fs.WalkDir(configFiles, "fs", func(path string, d fs.DirEntry, err error) error {
+		if err != nil {
+			return err
+		}
+
+		// Skip the root fs directory itself
+		if path == "fs" {
+			return nil
+		}
+
+		// Get the relative path by removing the "fs/" prefix
+		relPath := strings.TrimPrefix(path, "fs/")
+
+		// Create the full output path under "config/"
+		outPath := filepath.Join("config", relPath)
+
+		if d.IsDir() {
+			// Create directory
+			if err := os.MkdirAll(outPath, 0755); err != nil {
+				return fmt.Errorf("failed to create directory %s: %v", outPath, err)
+			}
+			return nil
+		}
+
+		// Read the template file
+		content, err := configFiles.ReadFile(path)
+		if err != nil {
+			return fmt.Errorf("failed to read %s: %v", path, err)
+		}
+
+		// Parse template
+		tmpl, err := template.New(d.Name()).Parse(string(content))
+		if err != nil {
+			return fmt.Errorf("failed to parse template %s: %v", path, err)
+		}
+
+		// Ensure parent directory exists
+		if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil {
+			return fmt.Errorf("failed to create parent directory for %s: %v", outPath, err)
+		}
+
+		// Create output file
+		outFile, err := os.Create(outPath)
+		if err != nil {
+			return fmt.Errorf("failed to create %s: %v", outPath, err)
+		}
+		defer outFile.Close()
+
+		// Execute template
+		if err := tmpl.Execute(outFile, config); err != nil {
+			return fmt.Errorf("failed to execute template %s: %v", path, err)
+		}
+
+		return nil
+	})
+
+	if err != nil {
+		return fmt.Errorf("error walking config files: %v", err)
+	}
+
+	// move the docker-compose.yml file to the root directory
+	os.Rename("config/docker-compose.yml", "docker-compose.yml")
+
+	return nil
+}
+
+func shouldInstallDocker() bool {
+	reader := bufio.NewReader(os.Stdin)
+	fmt.Print("Would you like to install Docker? (yes/no): ")
+	response, _ := reader.ReadString('\n')
+	return strings.ToLower(strings.TrimSpace(response)) == "yes"
+}
+
+func installDocker() error {
+	// Detect Linux distribution
+	cmd := exec.Command("cat", "/etc/os-release")
+	output, err := cmd.Output()
+	if err != nil {
+		return fmt.Errorf("failed to detect Linux distribution: %v", err)
+	}
+
+	osRelease := string(output)
+	var installCmd *exec.Cmd
+
+	switch {
+	case strings.Contains(osRelease, "ID=ubuntu") || strings.Contains(osRelease, "ID=debian"):
+		installCmd = exec.Command("bash", "-c", `
+            apt-get update && 
+            apt-get install -y apt-transport-https ca-certificates curl software-properties-common &&
+            curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
+            echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
+            apt-get update &&
+            apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
+        `)
+	case strings.Contains(osRelease, "ID=fedora"):
+		installCmd = exec.Command("bash", "-c", `
+            dnf -y install dnf-plugins-core &&
+            dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo &&
+            dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
+        `)
+	default:
+		return fmt.Errorf("unsupported Linux distribution")
+	}
+
+	installCmd.Stdout = os.Stdout
+	installCmd.Stderr = os.Stderr
+	return installCmd.Run()
+}
+
+func isDockerInstalled() bool {
+	cmd := exec.Command("docker", "--version")
+	if err := cmd.Run(); err != nil {
+		return false
+	}
+	return true
+}
+
+func pullAndStartContainers() error {
+	containers := []string{
+		"traefik:v3.1",
+		"fossorial/pangolin:latest",
+		"fossorial/gerbil:latest",
+	}
+
+	for _, container := range containers {
+		fmt.Printf("Pulling %s...\n", container)
+		cmd := exec.Command("docker", "pull", container)
+		cmd.Stdout = os.Stdout
+		cmd.Stderr = os.Stderr
+		if err := cmd.Run(); err != nil {
+			return fmt.Errorf("failed to pull %s: %v", container, err)
+		}
+	}
+
+	fmt.Println("Starting containers...")
+
+	// First try docker compose (new style)
+	cmd := exec.Command("docker", "compose", "-f", "docker-compose.yml", "up", "-d")
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	err := cmd.Run()
+
+	// If docker compose fails, try docker-compose (legacy style)
+	if err != nil {
+		cmd = exec.Command("docker-compose", "-f", "docker-compose.yml", "up", "-d")
+		cmd.Stdout = os.Stdout
+		cmd.Stderr = os.Stderr
+		err = cmd.Run()
+	}
+
+	return err
+}