492 lines
14 KiB
Go
492 lines
14 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"embed"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"syscall"
|
|
"text/template"
|
|
"unicode"
|
|
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
// DO NOT EDIT THIS FUNCTION; IT MATCHED BY REGEX IN CICD
|
|
func loadVersions(config *Config) {
|
|
config.PangolinVersion = "replaceme"
|
|
config.GerbilVersion = "replaceme"
|
|
config.BadgerVersion = "replaceme"
|
|
}
|
|
|
|
//go:embed fs/*
|
|
var configFiles embed.FS
|
|
|
|
type Config struct {
|
|
PangolinVersion string
|
|
GerbilVersion string
|
|
BadgerVersion string
|
|
BaseDomain string
|
|
DashboardDomain string
|
|
LetsEncryptEmail string
|
|
AdminUserEmail string
|
|
AdminUserPassword string
|
|
DisableSignupWithoutInvite bool
|
|
DisableUserCreateOrg bool
|
|
EnableEmail bool
|
|
EmailSMTPHost string
|
|
EmailSMTPPort int
|
|
EmailSMTPUser string
|
|
EmailSMTPPass string
|
|
EmailNoReply string
|
|
InstallGerbil bool
|
|
}
|
|
|
|
func main() {
|
|
reader := bufio.NewReader(os.Stdin)
|
|
|
|
// check if the user is root
|
|
if os.Geteuid() != 0 {
|
|
fmt.Println("This script must be run as root")
|
|
os.Exit(1)
|
|
}
|
|
|
|
// check if there is already a config file
|
|
if _, err := os.Stat("config/config.yml"); err != nil {
|
|
config := collectUserInput(reader)
|
|
|
|
loadVersions(&config)
|
|
|
|
if err := createConfigFiles(config); err != nil {
|
|
fmt.Printf("Error creating config files: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if !isDockerInstalled() && runtime.GOOS == "linux" {
|
|
if readBool(reader, "Docker is not installed. Would you like to install it?", true) {
|
|
installDocker()
|
|
}
|
|
}
|
|
} else {
|
|
fmt.Println("Config file already exists... skipping configuration")
|
|
}
|
|
|
|
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 readPassword(prompt string) string {
|
|
fmt.Print(prompt + ": ")
|
|
|
|
// Read password without echo
|
|
password, err := term.ReadPassword(int(syscall.Stdin))
|
|
fmt.Println() // Add a newline since ReadPassword doesn't add one
|
|
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
input := strings.TrimSpace(string(password))
|
|
if input == "" {
|
|
return readPassword(prompt)
|
|
}
|
|
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.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
|
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", "pangolin."+config.BaseDomain)
|
|
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
|
|
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunned connections", true)
|
|
|
|
// Admin user configuration
|
|
fmt.Println("\n=== Admin User Configuration ===")
|
|
config.AdminUserEmail = readString(reader, "Enter admin user email", "admin@"+config.BaseDomain)
|
|
for {
|
|
pass1 := readPassword("Create admin user password")
|
|
pass2 := readPassword("Confirm admin user password")
|
|
|
|
if pass1 != pass2 {
|
|
fmt.Println("Passwords do not match")
|
|
} else {
|
|
config.AdminUserPassword = pass1
|
|
if valid, message := validatePassword(config.AdminUserPassword); valid {
|
|
break
|
|
} else {
|
|
fmt.Println("Invalid password:", message)
|
|
fmt.Println("Password requirements:")
|
|
fmt.Println("- At least one uppercase English letter")
|
|
fmt.Println("- At least one lowercase English letter")
|
|
fmt.Println("- At least one digit")
|
|
fmt.Println("- At least one special character")
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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.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)
|
|
}
|
|
if config.AdminUserEmail == "" || config.AdminUserPassword == "" {
|
|
fmt.Println("Error: Admin user email and password are required")
|
|
os.Exit(1)
|
|
}
|
|
|
|
return config
|
|
}
|
|
|
|
func validatePassword(password string) (bool, string) {
|
|
if len(password) == 0 {
|
|
return false, "Password cannot be empty"
|
|
}
|
|
|
|
var (
|
|
hasUpper bool
|
|
hasLower bool
|
|
hasDigit bool
|
|
hasSpecial bool
|
|
)
|
|
|
|
for _, char := range password {
|
|
switch {
|
|
case unicode.IsUpper(char):
|
|
hasUpper = true
|
|
case unicode.IsLower(char):
|
|
hasLower = true
|
|
case unicode.IsDigit(char):
|
|
hasDigit = true
|
|
case unicode.IsPunct(char) || unicode.IsSymbol(char):
|
|
hasSpecial = true
|
|
}
|
|
}
|
|
|
|
var missing []string
|
|
if !hasUpper {
|
|
missing = append(missing, "an uppercase letter")
|
|
}
|
|
if !hasLower {
|
|
missing = append(missing, "a lowercase letter")
|
|
}
|
|
if !hasDigit {
|
|
missing = append(missing, "a digit")
|
|
}
|
|
if !hasSpecial {
|
|
missing = append(missing, "a special character")
|
|
}
|
|
|
|
if len(missing) > 0 {
|
|
return false, fmt.Sprintf("Password must contain %s", strings.Join(missing, ", "))
|
|
}
|
|
|
|
return true, ""
|
|
}
|
|
|
|
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/")
|
|
|
|
// skip .DS_Store
|
|
if strings.Contains(relPath, ".DS_Store") {
|
|
return nil
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// get the current directory
|
|
dir, err := os.Getwd()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get current directory: %v", err)
|
|
}
|
|
|
|
sourcePath := filepath.Join(dir, "config/docker-compose.yml")
|
|
destPath := filepath.Join(dir, "docker-compose.yml")
|
|
|
|
// Check if source file exists
|
|
if _, err := os.Stat(sourcePath); err != nil {
|
|
return fmt.Errorf("source docker-compose.yml not found: %v", err)
|
|
}
|
|
|
|
// Try to move the file
|
|
err = os.Rename(sourcePath, destPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to move docker-compose.yml from %s to %s: %v",
|
|
sourcePath, destPath, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
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)
|
|
|
|
// Detect system architecture
|
|
archCmd := exec.Command("uname", "-m")
|
|
archOutput, err := archCmd.Output()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to detect system architecture: %v", err)
|
|
}
|
|
arch := strings.TrimSpace(string(archOutput))
|
|
|
|
// Map architecture to Docker's architecture naming
|
|
var dockerArch string
|
|
switch arch {
|
|
case "x86_64":
|
|
dockerArch = "amd64"
|
|
case "aarch64":
|
|
dockerArch = "arm64"
|
|
default:
|
|
return fmt.Errorf("unsupported architecture: %s", arch)
|
|
}
|
|
|
|
var installCmd *exec.Cmd
|
|
switch {
|
|
case strings.Contains(osRelease, "ID=ubuntu"):
|
|
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
|
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=%s 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
|
|
`, dockerArch))
|
|
case strings.Contains(osRelease, "ID=debian"):
|
|
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
|
apt-get update &&
|
|
apt-get install -y apt-transport-https ca-certificates curl software-properties-common &&
|
|
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
|
|
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(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
|
|
`, dockerArch))
|
|
case strings.Contains(osRelease, "ID=fedora"):
|
|
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
|
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
|
|
`))
|
|
case strings.Contains(osRelease, "ID=opensuse") || strings.Contains(osRelease, "ID=\"opensuse-"):
|
|
installCmd = exec.Command("bash", "-c", `
|
|
zypper install -y docker docker-compose &&
|
|
systemctl enable docker
|
|
`)
|
|
case strings.Contains(osRelease, "ID=rhel") || strings.Contains(osRelease, "ID=\"rhel"):
|
|
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
|
dnf remove -y runc &&
|
|
dnf -y install yum-utils &&
|
|
dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo &&
|
|
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin &&
|
|
systemctl enable docker
|
|
`))
|
|
case strings.Contains(osRelease, "ID=amzn"):
|
|
installCmd = exec.Command("bash", "-c", `
|
|
yum update -y &&
|
|
yum install -y docker &&
|
|
systemctl enable docker &&
|
|
usermod -a -G docker ec2-user
|
|
`)
|
|
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 getCommandString(useNewStyle bool) string {
|
|
if useNewStyle {
|
|
return "'docker compose'"
|
|
}
|
|
return "'docker-compose'"
|
|
}
|
|
|
|
func pullAndStartContainers() error {
|
|
fmt.Println("Starting containers...")
|
|
|
|
// Check which docker compose command is available
|
|
var useNewStyle bool
|
|
checkCmd := exec.Command("docker", "compose", "version")
|
|
if err := checkCmd.Run(); err == nil {
|
|
useNewStyle = true
|
|
} else {
|
|
// Check if docker-compose (old style) is available
|
|
checkCmd = exec.Command("docker-compose", "version")
|
|
if err := checkCmd.Run(); err != nil {
|
|
return fmt.Errorf("neither 'docker compose' nor 'docker-compose' command is available: %v", err)
|
|
}
|
|
}
|
|
|
|
// Helper function to execute docker compose commands
|
|
executeCommand := func(args ...string) error {
|
|
var cmd *exec.Cmd
|
|
if useNewStyle {
|
|
cmd = exec.Command("docker", append([]string{"compose"}, args...)...)
|
|
} else {
|
|
cmd = exec.Command("docker-compose", args...)
|
|
}
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
return cmd.Run()
|
|
}
|
|
|
|
// Pull containers
|
|
fmt.Printf("Using %s command to pull containers...\n", getCommandString(useNewStyle))
|
|
if err := executeCommand("-f", "docker-compose.yml", "pull"); err != nil {
|
|
return fmt.Errorf("failed to pull containers: %v", err)
|
|
}
|
|
|
|
// Start containers
|
|
fmt.Printf("Using %s command to start containers...\n", getCommandString(useNewStyle))
|
|
if err := executeCommand("-f", "docker-compose.yml", "up", "-d"); err != nil {
|
|
return fmt.Errorf("failed to start containers: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|