Add podman support to the installer.

This commit is contained in:
Wayne Yao 2025-07-08 23:14:42 +08:00
parent 607b168b56
commit e83e8c2ee4
3 changed files with 183 additions and 61 deletions

View file

@ -13,7 +13,7 @@ import (
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)
}
@ -72,12 +72,12 @@ func installCrowdsec(config Config) error {
os.Exit(1)
}
if err := startContainers(); err != nil {
if err := startContainers(config.InstallationContainerType); err != nil {
return fmt.Errorf("failed to start containers: %v", err)
}
// get API key
apiKey, err := GetCrowdSecAPIKey()
apiKey, err := GetCrowdSecAPIKey(config.InstallationContainerType)
if err != nil {
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)
}
if err := restartContainer("traefik"); err != nil {
if err := restartContainer("traefik", config.InstallationContainerType); err != nil {
return fmt.Errorf("failed to restart containers: %v", err)
}
@ -110,9 +110,9 @@ func checkIsCrowdsecInstalledInCompose() bool {
return bytes.Contains(content, []byte("crowdsec:"))
}
func GetCrowdSecAPIKey() (string, error) {
func GetCrowdSecAPIKey(containerType SupportedContainer) (string, error) {
// 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)
}

View file

@ -1,3 +1,4 @@
docker
example.com
pangolin.example.com
admin@example.com

View file

@ -7,17 +7,17 @@ import (
"fmt"
"io"
"io/fs"
"math/rand"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
"strconv"
"strings"
"syscall"
"text/template"
"time"
"math/rand"
"strconv"
"golang.org/x/term"
)
@ -33,6 +33,7 @@ func loadVersions(config *Config) {
var configFiles embed.FS
type Config struct {
InstallationContainerType SupportedContainer
PangolinVersion string
GerbilVersion string
BadgerVersion string
@ -51,9 +52,59 @@ type Config struct {
Secret string
}
type SupportedContainer string
const (
Docker SupportedContainer = "docker"
Podman = "podman"
)
func main() {
reader := bufio.NewReader(os.Stdin)
inputContainer := readString(reader, "Would you like to run pangolin as docker or podman container?", "docker")
chosenContainer := Docker
if strings.EqualFold(inputContainer, "docker") {
chosenContainer = Docker
} else if strings.EqualFold(inputContainer, "podman") {
chosenContainer = Podman
} else {
fmt.Printf("Unrecognized container type: %s. Valid options are 'docker' or 'podman'.\n", inputContainer)
os.Exit(1)
}
if chosenContainer == Podman {
if !isPodmanInstalled() {
fmt.Println("Podman or podman-compose is not installed. Please install both manually. Automated installation will be available in a later release.")
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 {
@ -68,8 +119,13 @@ func main() {
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)
}
var config Config
config.InstallationContainerType = chosenContainer
// check if there is already a config file
if _, err := os.Stat("config/config.yml"); err != nil {
@ -86,7 +142,7 @@ func main() {
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) {
installDocker()
// try to start docker service but ignore errors
@ -115,14 +171,15 @@ func main() {
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 err := pullContainers(); err != nil {
if err := pullContainers(chosenContainer); err != nil {
fmt.Println("Error: ", err)
return
}
if err := startContainers(); err != nil {
if err := startContainers(chosenContainer); err != nil {
fmt.Println("Error: ", err)
return
}
@ -137,6 +194,8 @@ func main() {
// check if crowdsec is installed
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.")
// 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 config.DashboardDomain == "" {
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.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
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", "")
}
@ -456,7 +515,15 @@ func startDockerService() error {
}
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 {
return false
}
@ -533,46 +600,92 @@ func executeDockerComposeCommandWithArgs(args ...string) error {
}
// pullContainers pulls the containers using the appropriate command.
func pullContainers() error {
func pullContainers(containerType SupportedContainer) error {
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)
}
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.
func startContainers() error {
func startContainers(containerType SupportedContainer) error {
fmt.Println("Starting containers...")
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
}
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.
func stopContainers() error {
func stopContainers(containerType SupportedContainer) error {
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)
}
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.
func restartContainer(container string) error {
func restartContainer(container string, containerType SupportedContainer) error {
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)
}
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 {
@ -600,13 +713,13 @@ func moveFile(src, dst string) error {
return os.Remove(src)
}
func waitForContainer(containerName string) error {
func waitForContainer(containerName string, containerType SupportedContainer) error {
maxAttempts := 30
retryInterval := time.Second * 2
for attempt := 0; attempt < maxAttempts; attempt++ {
// 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
cmd.Stdout = &out
@ -641,3 +754,11 @@ func generateRandomSecretKey() string {
}
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()
}