mirror of
https://github.com/fosrl/pangolin.git
synced 2025-07-27 14:15:50 +02:00
Add podman support to the installer.
This commit is contained in:
parent
607b168b56
commit
e83e8c2ee4
3 changed files with 183 additions and 61 deletions
|
@ -13,7 +13,7 @@ import (
|
||||||
|
|
||||||
func installCrowdsec(config Config) error {
|
func installCrowdsec(config Config) error {
|
||||||
|
|
||||||
if err := stopContainers(); err != nil {
|
if err := stopContainers(config.InstallationContainerType); err != nil {
|
||||||
return fmt.Errorf("failed to stop containers: %v", err)
|
return fmt.Errorf("failed to stop containers: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,12 +72,12 @@ func installCrowdsec(config Config) error {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := startContainers(); err != nil {
|
if err := startContainers(config.InstallationContainerType); err != nil {
|
||||||
return fmt.Errorf("failed to start containers: %v", err)
|
return fmt.Errorf("failed to start containers: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// get API key
|
// get API key
|
||||||
apiKey, err := GetCrowdSecAPIKey()
|
apiKey, err := GetCrowdSecAPIKey(config.InstallationContainerType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get API key: %v", err)
|
return fmt.Errorf("failed to get API key: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -87,7 +87,7 @@ func installCrowdsec(config Config) error {
|
||||||
return fmt.Errorf("failed to replace bouncer key: %v", err)
|
return fmt.Errorf("failed to replace bouncer key: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := restartContainer("traefik"); err != nil {
|
if err := restartContainer("traefik", config.InstallationContainerType); err != nil {
|
||||||
return fmt.Errorf("failed to restart containers: %v", err)
|
return fmt.Errorf("failed to restart containers: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,9 +110,9 @@ func checkIsCrowdsecInstalledInCompose() bool {
|
||||||
return bytes.Contains(content, []byte("crowdsec:"))
|
return bytes.Contains(content, []byte("crowdsec:"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetCrowdSecAPIKey() (string, error) {
|
func GetCrowdSecAPIKey(containerType SupportedContainer) (string, error) {
|
||||||
// First, ensure the container is running
|
// First, ensure the container is running
|
||||||
if err := waitForContainer("crowdsec"); err != nil {
|
if err := waitForContainer("crowdsec", containerType); err != nil {
|
||||||
return "", fmt.Errorf("waiting for container: %w", err)
|
return "", fmt.Errorf("waiting for container: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
docker
|
||||||
example.com
|
example.com
|
||||||
pangolin.example.com
|
pangolin.example.com
|
||||||
admin@example.com
|
admin@example.com
|
||||||
|
|
231
install/main.go
231
install/main.go
|
@ -7,17 +7,17 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/user"
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
"math/rand"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
@ -33,43 +33,99 @@ func loadVersions(config *Config) {
|
||||||
var configFiles embed.FS
|
var configFiles embed.FS
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
PangolinVersion string
|
InstallationContainerType SupportedContainer
|
||||||
GerbilVersion string
|
PangolinVersion string
|
||||||
BadgerVersion string
|
GerbilVersion string
|
||||||
BaseDomain string
|
BadgerVersion string
|
||||||
DashboardDomain string
|
BaseDomain string
|
||||||
LetsEncryptEmail string
|
DashboardDomain string
|
||||||
EnableEmail bool
|
LetsEncryptEmail string
|
||||||
EmailSMTPHost string
|
EnableEmail bool
|
||||||
EmailSMTPPort int
|
EmailSMTPHost string
|
||||||
EmailSMTPUser string
|
EmailSMTPPort int
|
||||||
EmailSMTPPass string
|
EmailSMTPUser string
|
||||||
EmailNoReply string
|
EmailSMTPPass string
|
||||||
InstallGerbil bool
|
EmailNoReply string
|
||||||
TraefikBouncerKey string
|
InstallGerbil bool
|
||||||
DoCrowdsecInstall bool
|
TraefikBouncerKey string
|
||||||
Secret string
|
DoCrowdsecInstall bool
|
||||||
|
Secret string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SupportedContainer string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Docker SupportedContainer = "docker"
|
||||||
|
Podman = "podman"
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
inputContainer := readString(reader, "Would you like to run pangolin as docker or podman container?", "docker")
|
||||||
|
|
||||||
// check if docker is not installed and the user is root
|
chosenContainer := Docker
|
||||||
if !isDockerInstalled() {
|
if strings.EqualFold(inputContainer, "docker") {
|
||||||
if os.Geteuid() != 0 {
|
chosenContainer = Docker
|
||||||
fmt.Println("Docker is not installed. Please install Docker manually or run this installer as root.")
|
} else if strings.EqualFold(inputContainer, "podman") {
|
||||||
os.Exit(1)
|
chosenContainer = Podman
|
||||||
}
|
} else {
|
||||||
|
fmt.Printf("Unrecognized container type: %s. Valid options are 'docker' or 'podman'.\n", inputContainer)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the user is in the docker group (linux only)
|
if chosenContainer == Podman {
|
||||||
if !isUserInDockerGroup() {
|
if !isPodmanInstalled() {
|
||||||
fmt.Println("You are not in the docker group.")
|
fmt.Println("Podman or podman-compose is not installed. Please install both manually. Automated installation will be available in a later release.")
|
||||||
fmt.Println("The installer will not be able to run docker commands without running it as root.")
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := exec.Command("bash", "-c", "cat /etc/sysctl.conf | grep 'net.ipv4.ip_unprivileged_port_start='"); err == nil {
|
||||||
|
fmt.Println("Would you like to configure ports >= 80 as unprivileged ports? This enables podman containers to listen on low-range ports.")
|
||||||
|
fmt.Println("Pangolin will experience startup issues if this is not configured, because it needs to listen on port 80/443 by default.")
|
||||||
|
approved := readBool(reader, "The installer is about to execute \"echo 'net.ipv4.ip_unprivileged_port_start=80' >> /etc/sysctl.conf && sysctl -p\". Approve?", true)
|
||||||
|
if approved {
|
||||||
|
if os.Geteuid() != 0 {
|
||||||
|
fmt.Println("You need to run the installer as root for such a configuration.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Podman containers are not able to listen on privileged ports. The official recommendation is to
|
||||||
|
// container low-range ports as unprivileged ports.
|
||||||
|
// Linux only.
|
||||||
|
|
||||||
|
if err := run("bash", "-c", "echo 'net.ipv4.ip_unprivileged_port_start=80' >> /etc/sysctl.conf && sysctl -p"); err != nil {
|
||||||
|
fmt.Sprintf("failed to configure unprivileged ports: %v.\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println("You need to configure port forwarding or adjust the listening ports before running pangolin.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println("Unprivileged ports have been configured.")
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if chosenContainer == Docker {
|
||||||
|
// check if docker is not installed and the user is root
|
||||||
|
if !isDockerInstalled() {
|
||||||
|
if os.Geteuid() != 0 {
|
||||||
|
fmt.Println("Docker is not installed. Please install Docker manually or run this installer as root.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the user is in the docker group (linux only)
|
||||||
|
if !isUserInDockerGroup() {
|
||||||
|
fmt.Println("You are not in the docker group.")
|
||||||
|
fmt.Println("The installer will not be able to run docker commands without running it as root.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This shouldn't happen unless there's a third container runtime.
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
var config Config
|
var config Config
|
||||||
|
config.InstallationContainerType = chosenContainer
|
||||||
|
|
||||||
// check if there is already a config file
|
// check if there is already a config file
|
||||||
if _, err := os.Stat("config/config.yml"); err != nil {
|
if _, err := os.Stat("config/config.yml"); err != nil {
|
||||||
|
@ -86,7 +142,7 @@ func main() {
|
||||||
|
|
||||||
moveFile("config/docker-compose.yml", "docker-compose.yml")
|
moveFile("config/docker-compose.yml", "docker-compose.yml")
|
||||||
|
|
||||||
if !isDockerInstalled() && runtime.GOOS == "linux" {
|
if !isDockerInstalled() && runtime.GOOS == "linux" && chosenContainer == Docker {
|
||||||
if readBool(reader, "Docker is not installed. Would you like to install it?", true) {
|
if readBool(reader, "Docker is not installed. Would you like to install it?", true) {
|
||||||
installDocker()
|
installDocker()
|
||||||
// try to start docker service but ignore errors
|
// try to start docker service but ignore errors
|
||||||
|
@ -115,14 +171,15 @@ func main() {
|
||||||
|
|
||||||
fmt.Println("\n=== Starting installation ===")
|
fmt.Println("\n=== Starting installation ===")
|
||||||
|
|
||||||
if isDockerInstalled() {
|
if (isDockerInstalled() && chosenContainer == Docker) ||
|
||||||
|
(isPodmanInstalled() && chosenContainer == Podman) {
|
||||||
if readBool(reader, "Would you like to install and start the containers?", true) {
|
if readBool(reader, "Would you like to install and start the containers?", true) {
|
||||||
if err := pullContainers(); err != nil {
|
if err := pullContainers(chosenContainer); err != nil {
|
||||||
fmt.Println("Error: ", err)
|
fmt.Println("Error: ", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := startContainers(); err != nil {
|
if err := startContainers(chosenContainer); err != nil {
|
||||||
fmt.Println("Error: ", err)
|
fmt.Println("Error: ", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -137,6 +194,8 @@ func main() {
|
||||||
// check if crowdsec is installed
|
// check if crowdsec is installed
|
||||||
if readBool(reader, "Would you like to install CrowdSec?", false) {
|
if readBool(reader, "Would you like to install CrowdSec?", false) {
|
||||||
fmt.Println("This installer constitutes a minimal viable CrowdSec deployment. CrowdSec will add extra complexity to your Pangolin installation and may not work to the best of its abilities out of the box. Users are expected to implement configuration adjustments on their own to achieve the best security posture. Consult the CrowdSec documentation for detailed configuration instructions.")
|
fmt.Println("This installer constitutes a minimal viable CrowdSec deployment. CrowdSec will add extra complexity to your Pangolin installation and may not work to the best of its abilities out of the box. Users are expected to implement configuration adjustments on their own to achieve the best security posture. Consult the CrowdSec documentation for detailed configuration instructions.")
|
||||||
|
|
||||||
|
// BUG: crowdsec installation will be skipped if the user chooses to install on the first installation.
|
||||||
if readBool(reader, "Are you willing to manage CrowdSec?", false) {
|
if readBool(reader, "Are you willing to manage CrowdSec?", false) {
|
||||||
if config.DashboardDomain == "" {
|
if config.DashboardDomain == "" {
|
||||||
traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml", "config/traefik/dynamic_config.yml")
|
traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml", "config/traefik/dynamic_config.yml")
|
||||||
|
@ -240,7 +299,7 @@ func collectUserInput(reader *bufio.Reader) Config {
|
||||||
config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
|
config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
|
||||||
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
|
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
|
||||||
config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
|
config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
|
||||||
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "")
|
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword?
|
||||||
config.EmailNoReply = readString(reader, "Enter no-reply email address", "")
|
config.EmailNoReply = readString(reader, "Enter no-reply email address", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -456,7 +515,15 @@ func startDockerService() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func isDockerInstalled() bool {
|
func isDockerInstalled() bool {
|
||||||
cmd := exec.Command("docker", "--version")
|
return isContainerInstalled("docker")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPodmanInstalled() bool {
|
||||||
|
return isContainerInstalled("podman") && isContainerInstalled("podman-compose")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isContainerInstalled(container string) bool {
|
||||||
|
cmd := exec.Command(container, "--version")
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -527,52 +594,98 @@ func executeDockerComposeCommandWithArgs(args ...string) error {
|
||||||
cmd = exec.Command("docker-compose", args...)
|
cmd = exec.Command("docker-compose", args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
return cmd.Run()
|
return cmd.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
// pullContainers pulls the containers using the appropriate command.
|
// pullContainers pulls the containers using the appropriate command.
|
||||||
func pullContainers() error {
|
func pullContainers(containerType SupportedContainer) error {
|
||||||
fmt.Println("Pulling the container images...")
|
fmt.Println("Pulling the container images...")
|
||||||
|
if containerType == Podman {
|
||||||
|
if err := run("podman-compose", "-f", "docker-compose.yml", "pull"); err != nil {
|
||||||
|
return fmt.Errorf("failed to pull the containers: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "pull", "--policy", "always"); err != nil {
|
return nil
|
||||||
return fmt.Errorf("failed to pull the containers: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
if containerType == Docker {
|
||||||
|
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "pull", "--policy", "always"); err != nil {
|
||||||
|
return fmt.Errorf("failed to pull the containers: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("Unsupported container type: %s", containerType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// startContainers starts the containers using the appropriate command.
|
// startContainers starts the containers using the appropriate command.
|
||||||
func startContainers() error {
|
func startContainers(containerType SupportedContainer) error {
|
||||||
fmt.Println("Starting containers...")
|
fmt.Println("Starting containers...")
|
||||||
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "up", "-d", "--force-recreate"); err != nil {
|
|
||||||
return fmt.Errorf("failed to start containers: %v", err)
|
if containerType == Podman {
|
||||||
|
if err := run("podman-compose", "-f", "docker-compose.yml", "up", "-d", "--force-recreate"); err != nil {
|
||||||
|
return fmt.Errorf("failed start containers: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
if containerType == Docker {
|
||||||
|
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "up", "-d", "--force-recreate"); err != nil {
|
||||||
|
return fmt.Errorf("failed to start containers: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("Unsupported container type: %s", containerType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// stopContainers stops the containers using the appropriate command.
|
// stopContainers stops the containers using the appropriate command.
|
||||||
func stopContainers() error {
|
func stopContainers(containerType SupportedContainer) error {
|
||||||
fmt.Println("Stopping containers...")
|
fmt.Println("Stopping containers...")
|
||||||
|
if containerType == Podman {
|
||||||
|
if err := run("podman-compose", "-f", "docker-compose.yml", "down"); err != nil {
|
||||||
|
return fmt.Errorf("failed to stop containers: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "down"); err != nil {
|
return nil
|
||||||
return fmt.Errorf("failed to stop containers: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
if containerType == Docker {
|
||||||
|
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "down"); err != nil {
|
||||||
|
return fmt.Errorf("failed to stop containers: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("Unsupported container type: %s", containerType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// restartContainer restarts a specific container using the appropriate command.
|
// restartContainer restarts a specific container using the appropriate command.
|
||||||
func restartContainer(container string) error {
|
func restartContainer(container string, containerType SupportedContainer) error {
|
||||||
fmt.Println("Restarting containers...")
|
fmt.Println("Restarting containers...")
|
||||||
|
if containerType == Podman {
|
||||||
|
if err := run("podman-compose", "-f", "docker-compose.yml", "restart"); err != nil {
|
||||||
|
return fmt.Errorf("failed to stop the container \"%s\": %v", container, err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "restart", container); err != nil {
|
return nil
|
||||||
return fmt.Errorf("failed to stop the container \"%s\": %v", container, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
if containerType == Docker {
|
||||||
|
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "restart", container); err != nil {
|
||||||
|
return fmt.Errorf("failed to stop the container \"%s\": %v", container, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("Unsupported container type: %s", containerType)
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyFile(src, dst string) error {
|
func copyFile(src, dst string) error {
|
||||||
|
@ -600,13 +713,13 @@ func moveFile(src, dst string) error {
|
||||||
return os.Remove(src)
|
return os.Remove(src)
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitForContainer(containerName string) error {
|
func waitForContainer(containerName string, containerType SupportedContainer) error {
|
||||||
maxAttempts := 30
|
maxAttempts := 30
|
||||||
retryInterval := time.Second * 2
|
retryInterval := time.Second * 2
|
||||||
|
|
||||||
for attempt := 0; attempt < maxAttempts; attempt++ {
|
for attempt := 0; attempt < maxAttempts; attempt++ {
|
||||||
// Check if container is running
|
// Check if container is running
|
||||||
cmd := exec.Command("docker", "container", "inspect", "-f", "{{.State.Running}}", containerName)
|
cmd := exec.Command(string(containerType), "container", "inspect", "-f", "{{.State.Running}}", containerName)
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
cmd.Stdout = &out
|
cmd.Stdout = &out
|
||||||
|
|
||||||
|
@ -641,3 +754,11 @@ func generateRandomSecretKey() string {
|
||||||
}
|
}
|
||||||
return string(b)
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run external commands with stdio/stderr attached.
|
||||||
|
func run(name string, args ...string) error {
|
||||||
|
cmd := exec.Command(name, args...)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue