...
This commit is contained in:
50
pkg/system/builders/postgresql/postgres/download.go
Normal file
50
pkg/system/builders/postgresql/postgres/download.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
// DownloadPostgres downloads the PostgreSQL source code if it doesn't already exist
|
||||
func (b *PostgresBuilder) DownloadPostgres() error {
|
||||
// Check if the file already exists
|
||||
if _, err := os.Stat(b.PostgresTar); err == nil {
|
||||
fmt.Printf("PostgreSQL source already downloaded at %s, skipping download\n", b.PostgresTar)
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Println("Downloading PostgreSQL source...")
|
||||
return downloadFile(b.PostgresURL, b.PostgresTar)
|
||||
}
|
||||
|
||||
// downloadFile downloads a file from url to destination path
|
||||
func downloadFile(url, dst string) error {
|
||||
// Create the file
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create file %s: %w", dst, err)
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// Get the data
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download from %s: %w", url, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check server response
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("bad status: %s when downloading %s", resp.Status, url)
|
||||
}
|
||||
|
||||
// Write the body to file
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write to file %s: %w", dst, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
100
pkg/system/builders/postgresql/postgres/fs.go
Normal file
100
pkg/system/builders/postgresql/postgres/fs.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// moveContents moves all contents from src directory to dst directory
|
||||
func moveContents(src, dst string) error {
|
||||
entries, err := os.ReadDir(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
srcPath := filepath.Join(src, entry.Name())
|
||||
dstPath := filepath.Join(dst, entry.Name())
|
||||
|
||||
// Handle existing destination
|
||||
if _, err := os.Stat(dstPath); err == nil {
|
||||
// If it exists, remove it first
|
||||
if err := os.RemoveAll(dstPath); err != nil {
|
||||
return fmt.Errorf("failed to remove existing path %s: %w", dstPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Move the file or directory
|
||||
if err := os.Rename(srcPath, dstPath); err != nil {
|
||||
// If rename fails (possibly due to cross-device link), try copy and delete
|
||||
if strings.Contains(err.Error(), "cross-device link") {
|
||||
if entry.IsDir() {
|
||||
if err := copyDir(srcPath, dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := copyFile(srcPath, dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
os.RemoveAll(srcPath)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// copyFile copies a file from src to dst
|
||||
func copyFile(src, dst string) error {
|
||||
srcFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
dstFile, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
_, err = dstFile.ReadFrom(srcFile)
|
||||
return err
|
||||
}
|
||||
|
||||
// copyDir copies a directory recursively
|
||||
func copyDir(src, dst string) error {
|
||||
srcInfo, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(dst, srcInfo.Mode()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
srcPath := filepath.Join(src, entry.Name())
|
||||
dstPath := filepath.Join(dst, entry.Name())
|
||||
|
||||
if entry.IsDir() {
|
||||
if err := copyDir(srcPath, dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := copyFile(srcPath, dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
178
pkg/system/builders/postgresql/postgres/goinstall.go
Normal file
178
pkg/system/builders/postgresql/postgres/goinstall.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/archiver/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultGoVersion is the default Go version to install
|
||||
DefaultGoVersion = "1.22.2"
|
||||
)
|
||||
|
||||
// GoInstaller handles Go installation checks and installation
|
||||
type GoInstaller struct {
|
||||
Version string
|
||||
}
|
||||
|
||||
// NewGoInstaller creates a new Go installer with the default version
|
||||
func NewGoInstaller() *GoInstaller {
|
||||
return &GoInstaller{
|
||||
Version: DefaultGoVersion,
|
||||
}
|
||||
}
|
||||
|
||||
// WithVersion sets the Go version to install
|
||||
func (g *GoInstaller) WithVersion(version string) *GoInstaller {
|
||||
g.Version = version
|
||||
return g
|
||||
}
|
||||
|
||||
// IsGoInstalled checks if Go is installed and available
|
||||
func (g *GoInstaller) IsGoInstalled() bool {
|
||||
// Check if go command is available
|
||||
cmd := exec.Command("go", "version")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// GetGoVersion gets the installed Go version
|
||||
func (g *GoInstaller) GetGoVersion() (string, error) {
|
||||
cmd := exec.Command("go", "version")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get Go version: %w", err)
|
||||
}
|
||||
|
||||
// Parse go version output (format: "go version go1.x.x ...")
|
||||
version := strings.TrimSpace(string(output))
|
||||
parts := strings.Split(version, " ")
|
||||
if len(parts) < 3 {
|
||||
return "", fmt.Errorf("unexpected go version output format: %s", version)
|
||||
}
|
||||
|
||||
// Return just the version number without the "go" prefix
|
||||
return strings.TrimPrefix(parts[2], "go"), nil
|
||||
}
|
||||
|
||||
// InstallGo installs Go if it's not already installed and returns the path to the Go executable
|
||||
func (g *GoInstaller) InstallGo() (string, error) {
|
||||
// First check if Go is available in PATH
|
||||
if path, err := exec.LookPath("go"); err == nil {
|
||||
// Test if it works
|
||||
cmd := exec.Command(path, "version")
|
||||
if output, err := cmd.Output(); err == nil {
|
||||
fmt.Printf("Found working Go in PATH: %s, version: %s\n", path, strings.TrimSpace(string(output)))
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
// Default Go installation location
|
||||
var installDir string = "/usr/local"
|
||||
var goExePath string = filepath.Join(installDir, "go", "bin", "go")
|
||||
|
||||
// Check if Go is already installed by checking the binary directly
|
||||
if _, err := os.Stat(goExePath); err == nil {
|
||||
version, err := g.GetGoVersion()
|
||||
if err == nil {
|
||||
fmt.Printf("Go is already installed (version %s), skipping installation\n", version)
|
||||
return goExePath, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Also check if Go is available in PATH as a fallback
|
||||
if g.IsGoInstalled() {
|
||||
path, err := exec.LookPath("go")
|
||||
if err == nil {
|
||||
version, err := g.GetGoVersion()
|
||||
if err == nil {
|
||||
fmt.Printf("Go is already installed (version %s) at %s, skipping installation\n", version, path)
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("Installing Go version %s...\n", g.Version)
|
||||
|
||||
// Determine architecture and OS
|
||||
goOS := runtime.GOOS
|
||||
goArch := runtime.GOARCH
|
||||
|
||||
// Construct download URL
|
||||
downloadURL := fmt.Sprintf("https://golang.org/dl/go%s.%s-%s.tar.gz", g.Version, goOS, goArch)
|
||||
|
||||
// Create a temporary directory for download
|
||||
tempDir, err := os.MkdirTemp("", "go-install-")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create temporary directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Download Go tarball
|
||||
tarballPath := filepath.Join(tempDir, "go.tar.gz")
|
||||
if err := downloadFile(downloadURL, tarballPath); err != nil {
|
||||
return "", fmt.Errorf("failed to download Go: %w", err)
|
||||
}
|
||||
|
||||
// Install directory - typically /usr/local for Linux/macOS
|
||||
|
||||
// Check if existing Go installation exists and remove it
|
||||
existingGoDir := filepath.Join(installDir, "go")
|
||||
if _, err := os.Stat(existingGoDir); err == nil {
|
||||
fmt.Printf("Removing existing Go installation at %s\n", existingGoDir)
|
||||
if err := os.RemoveAll(existingGoDir); err != nil {
|
||||
return "", fmt.Errorf("failed to remove existing Go installation: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Extract tarball to install directory
|
||||
fmt.Printf("Extracting Go to %s\n", installDir)
|
||||
err = extractTarGz(tarballPath, installDir)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to extract Go tarball: %w", err)
|
||||
}
|
||||
|
||||
// Verify installation
|
||||
var goExePathVerify = filepath.Join(installDir, "go", "bin", "go") // Use = instead of := to avoid variable shadowing
|
||||
|
||||
// Check if the Go binary exists
|
||||
var statErr error
|
||||
_, statErr = os.Stat(goExePathVerify)
|
||||
if statErr != nil {
|
||||
return "", fmt.Errorf("Go installation failed - go executable not found at %s", goExePathVerify)
|
||||
}
|
||||
|
||||
// Set up environment variables
|
||||
fmt.Println("Setting up Go environment variables...")
|
||||
|
||||
// Update PATH in /etc/profile
|
||||
profilePath := "/etc/profile"
|
||||
profileContent, err := os.ReadFile(profilePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read profile: %w", err)
|
||||
}
|
||||
|
||||
// Add Go bin to PATH if not already there
|
||||
goBinPath := filepath.Join(installDir, "go", "bin")
|
||||
if !strings.Contains(string(profileContent), goBinPath) {
|
||||
newContent := string(profileContent) + fmt.Sprintf("\n# Added by PostgreSQL builder\nexport PATH=$PATH:%s\n", goBinPath)
|
||||
if err := os.WriteFile(profilePath, []byte(newContent), 0644); err != nil {
|
||||
return "", fmt.Errorf("failed to update profile: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("✅ Go %s installed successfully!\n", g.Version)
|
||||
return goExePath, nil
|
||||
}
|
||||
|
||||
// Helper function to extract tarball
|
||||
func extractTarGz(src, dst string) error {
|
||||
return archiver.Unarchive(src, dst)
|
||||
}
|
505
pkg/system/builders/postgresql/postgres/postgres.go
Normal file
505
pkg/system/builders/postgresql/postgres/postgres.go
Normal file
@@ -0,0 +1,505 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Constants for PostgreSQL installation
|
||||
const (
|
||||
DefaultPostgresURL = "https://github.com/postgres/postgres/archive/refs/tags/REL_17_4.tar.gz"
|
||||
DefaultPostgresTar = "postgres.tar.gz"
|
||||
DefaultInstallPrefix = "/opt/postgresql"
|
||||
DefaultPatchFile = "src/backend/postmaster/postmaster.c"
|
||||
BuildMarkerFile = ".build_complete"
|
||||
// Set ForceReset to true to force a complete rebuild
|
||||
ForceReset = true
|
||||
)
|
||||
|
||||
// PostgresBuilder represents a PostgreSQL builder
|
||||
type PostgresBuilder struct {
|
||||
PostgresURL string
|
||||
PostgresTar string
|
||||
InstallPrefix string
|
||||
PatchFile string
|
||||
BuildMarker string
|
||||
}
|
||||
|
||||
// NewPostgresBuilder creates a new PostgreSQL builder with default values
|
||||
func NewPostgresBuilder() *PostgresBuilder {
|
||||
return &PostgresBuilder{
|
||||
PostgresURL: DefaultPostgresURL,
|
||||
PostgresTar: DefaultPostgresTar,
|
||||
InstallPrefix: DefaultInstallPrefix,
|
||||
PatchFile: DefaultPatchFile,
|
||||
BuildMarker: filepath.Join(DefaultInstallPrefix, BuildMarkerFile),
|
||||
}
|
||||
}
|
||||
|
||||
// WithPostgresURL sets the PostgreSQL download URL
|
||||
func (b *PostgresBuilder) WithPostgresURL(url string) *PostgresBuilder {
|
||||
b.PostgresURL = url
|
||||
return b
|
||||
}
|
||||
|
||||
// WithInstallPrefix sets the installation prefix
|
||||
func (b *PostgresBuilder) WithInstallPrefix(prefix string) *PostgresBuilder {
|
||||
b.InstallPrefix = prefix
|
||||
return b
|
||||
}
|
||||
|
||||
// run executes a command with the given arguments
|
||||
func (b *PostgresBuilder) run(cmd string, args ...string) error {
|
||||
fmt.Println("Running:", cmd, strings.Join(args, " "))
|
||||
c := exec.Command(cmd, args...)
|
||||
c.Stdout = os.Stdout
|
||||
c.Stderr = os.Stderr
|
||||
return c.Run()
|
||||
}
|
||||
|
||||
// PatchPostmasterC patches the postmaster.c file to allow running as root
|
||||
func (b *PostgresBuilder) PatchPostmasterC(baseDir string) error {
|
||||
fmt.Println("Patching postmaster.c to allow root...")
|
||||
|
||||
// Look for the postmaster.c file in the expected location
|
||||
file := filepath.Join(baseDir, b.PatchFile)
|
||||
|
||||
// If the file doesn't exist, try to find it
|
||||
if _, err := os.Stat(file); os.IsNotExist(err) {
|
||||
fmt.Println("File not found in the expected location, searching for it...")
|
||||
|
||||
// Search for postmaster.c
|
||||
var postmasterPath string
|
||||
err := filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.Name() == "postmaster.c" {
|
||||
postmasterPath = path
|
||||
return filepath.SkipAll
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to search for postmaster.c: %w", err)
|
||||
}
|
||||
|
||||
if postmasterPath == "" {
|
||||
return fmt.Errorf("could not find postmaster.c in the extracted directory")
|
||||
}
|
||||
|
||||
fmt.Printf("Found postmaster.c at: %s\n", postmasterPath)
|
||||
file = postmasterPath
|
||||
}
|
||||
|
||||
// Read the file
|
||||
input, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
|
||||
// Patch the file
|
||||
modified := strings.Replace(string(input),
|
||||
"geteuid() == 0",
|
||||
"false",
|
||||
1)
|
||||
|
||||
if err := os.WriteFile(file, []byte(modified), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write to file: %w", err)
|
||||
}
|
||||
|
||||
// Verify that the patch was applied
|
||||
updatedContent, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read file after patching: %w", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(string(updatedContent), "patched to allow root") {
|
||||
return fmt.Errorf("patching postmaster.c failed: verification check failed")
|
||||
}
|
||||
|
||||
fmt.Println("✅ Successfully patched postmaster.c")
|
||||
return nil
|
||||
}
|
||||
|
||||
// PatchInitdbC patches the initdb.c file to allow running as root
|
||||
func (b *PostgresBuilder) PatchInitdbC(baseDir string) error {
|
||||
fmt.Println("Patching initdb.c to allow root...")
|
||||
|
||||
// Search for initdb.c
|
||||
var initdbPath string
|
||||
err := filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.Name() == "initdb.c" {
|
||||
initdbPath = path
|
||||
return filepath.SkipAll
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to search for initdb.c: %w", err)
|
||||
}
|
||||
|
||||
if initdbPath == "" {
|
||||
return fmt.Errorf("could not find initdb.c in the extracted directory")
|
||||
}
|
||||
|
||||
fmt.Printf("Found initdb.c at: %s\n", initdbPath)
|
||||
|
||||
// Read the file
|
||||
input, err := os.ReadFile(initdbPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read initdb.c: %w", err)
|
||||
}
|
||||
// Patch the file to bypass root user check
|
||||
// This modifies the condition that checks if the user is root
|
||||
modified := strings.Replace(string(input),
|
||||
"geteuid() == 0", // Common pattern to check for root
|
||||
"false",
|
||||
-1) // Replace all occurrences
|
||||
|
||||
// Also look for any alternate ways the check might be implemented
|
||||
modified = strings.Replace(modified,
|
||||
"pg_euid == 0", // Alternative check pattern
|
||||
"false",
|
||||
-1) // Replace all occurrences
|
||||
|
||||
if err := os.WriteFile(initdbPath, []byte(modified), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write to initdb.c: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("✅ Successfully patched initdb.c")
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuildPostgres builds PostgreSQL
|
||||
func (b *PostgresBuilder) BuildPostgres(sourceDir string) error {
|
||||
fmt.Println("Building PostgreSQL...")
|
||||
currentDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current directory: %w", err)
|
||||
}
|
||||
defer os.Chdir(currentDir)
|
||||
|
||||
if err := os.Chdir(sourceDir); err != nil {
|
||||
return fmt.Errorf("failed to change directory: %w", err)
|
||||
}
|
||||
|
||||
// Add --without-icu to disable ICU dependency
|
||||
if err := b.run("/usr/bin/bash", "configure", "--prefix="+b.InstallPrefix, "--without-icu"); err != nil {
|
||||
return fmt.Errorf("failed to configure PostgreSQL: %w", err)
|
||||
}
|
||||
|
||||
if err := b.run("make", "-j4"); err != nil {
|
||||
return fmt.Errorf("failed to build PostgreSQL: %w", err)
|
||||
}
|
||||
|
||||
if err := b.run("make", "install"); err != nil {
|
||||
return fmt.Errorf("failed to install PostgreSQL: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanInstall cleans the installation directory
|
||||
func (b *PostgresBuilder) CleanInstall() error {
|
||||
fmt.Println("Cleaning install dir...")
|
||||
keepDirs := []string{"bin", "lib", "share"}
|
||||
entries, err := os.ReadDir(b.InstallPrefix)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read install directory: %w", err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
keep := false
|
||||
for _, d := range keepDirs {
|
||||
if entry.Name() == d {
|
||||
keep = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !keep {
|
||||
if err := os.RemoveAll(filepath.Join(b.InstallPrefix, entry.Name())); err != nil {
|
||||
return fmt.Errorf("failed to remove directory: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckRequirements checks if the current environment meets the requirements
|
||||
func (b *PostgresBuilder) CheckRequirements() error {
|
||||
// Check if running as root
|
||||
if os.Geteuid() != 0 {
|
||||
return fmt.Errorf("this PostgreSQL builder must be run as root")
|
||||
}
|
||||
|
||||
// Check if we can bypass OS checks with environment variable
|
||||
if os.Getenv("POSTGRES_BUILDER_FORCE") == "1" {
|
||||
fmt.Println("✅ Environment check bypassed due to POSTGRES_BUILDER_FORCE=1")
|
||||
return nil
|
||||
}
|
||||
|
||||
// // Check if running on Ubuntu
|
||||
// isUbuntu, err := b.isUbuntu()
|
||||
// if err != nil {
|
||||
// fmt.Printf("⚠️ Warning determining OS: %v\n", err)
|
||||
// fmt.Println("⚠️ Will proceed anyway, but you might encounter issues.")
|
||||
// fmt.Println("⚠️ Set POSTGRES_BUILDER_FORCE=1 to bypass this check in the future.")
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// if !isUbuntu {
|
||||
// // Debug information for troubleshooting OS detection
|
||||
// fmt.Println("⚠️ OS detection failed. Debug information:")
|
||||
// exec.Command("cat", "/etc/os-release").Run()
|
||||
// exec.Command("uname", "-a").Run()
|
||||
|
||||
// fmt.Println("⚠️ Set POSTGRES_BUILDER_FORCE=1 to bypass this check.")
|
||||
// return fmt.Errorf("this PostgreSQL builder only works on Ubuntu")
|
||||
// }
|
||||
|
||||
fmt.Println("✅ Environment check passed: running as root on Ubuntu")
|
||||
return nil
|
||||
}
|
||||
|
||||
// isUbuntu checks if the current OS is Ubuntu
|
||||
func (b *PostgresBuilder) isUbuntu() (bool, error) {
|
||||
// First try lsb_release as it's more reliable
|
||||
lsbCmd := exec.Command("lsb_release", "-a")
|
||||
lsbOut, err := lsbCmd.CombinedOutput()
|
||||
if err == nil && strings.Contains(strings.ToLower(string(lsbOut)), "ubuntu") {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// As a fallback, check /etc/os-release
|
||||
osReleaseBytes, err := os.ReadFile("/etc/os-release")
|
||||
if err != nil {
|
||||
// If /etc/os-release doesn't exist, check for /etc/lsb-release
|
||||
lsbReleaseBytes, lsbErr := os.ReadFile("/etc/lsb-release")
|
||||
if lsbErr == nil && strings.Contains(strings.ToLower(string(lsbReleaseBytes)), "ubuntu") {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("could not determine if OS is Ubuntu: %w", err)
|
||||
}
|
||||
|
||||
// Check multiple ways Ubuntu might be identified
|
||||
osRelease := strings.ToLower(string(osReleaseBytes))
|
||||
return strings.Contains(osRelease, "ubuntu") ||
|
||||
strings.Contains(osRelease, "id=ubuntu") ||
|
||||
strings.Contains(osRelease, "id_like=ubuntu"), nil
|
||||
}
|
||||
|
||||
// Build builds PostgreSQL
|
||||
func (b *PostgresBuilder) Build() error {
|
||||
// Check requirements first
|
||||
if err := b.CheckRequirements(); err != nil {
|
||||
fmt.Printf("⚠️ Requirements check failed: %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if reset is forced
|
||||
if ForceReset {
|
||||
fmt.Println("Force reset enabled, removing existing installation...")
|
||||
if err := os.RemoveAll(b.InstallPrefix); err != nil {
|
||||
return fmt.Errorf("failed to remove installation directory: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if PostgreSQL is already installed and build is complete
|
||||
binPath := filepath.Join(b.InstallPrefix, "bin", "postgres")
|
||||
if _, err := os.Stat(binPath); err == nil {
|
||||
// Check for build marker
|
||||
if _, err := os.Stat(b.BuildMarker); err == nil {
|
||||
fmt.Printf("✅ PostgreSQL already installed at %s with build marker, skipping build\n", b.InstallPrefix)
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("PostgreSQL installation found at %s but no build marker, will verify\n", b.InstallPrefix)
|
||||
}
|
||||
|
||||
// Check if install directory exists but is incomplete/corrupt
|
||||
if _, err := os.Stat(b.InstallPrefix); err == nil {
|
||||
fmt.Printf("Found incomplete installation at %s, removing it to start fresh\n", b.InstallPrefix)
|
||||
if err := os.RemoveAll(b.InstallPrefix); err != nil {
|
||||
return fmt.Errorf("failed to clean incomplete installation: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Download PostgreSQL source
|
||||
if err := b.DownloadPostgres(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Extract the source code
|
||||
srcDir, err := b.ExtractTarGz()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Patch to allow running as root
|
||||
if err := b.PatchPostmasterC(srcDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Patch initdb.c to allow running as root
|
||||
if err := b.PatchInitdbC(srcDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Build PostgreSQL
|
||||
if err := b.BuildPostgres(srcDir); err != nil {
|
||||
// Clean up on build failure
|
||||
fmt.Printf("Build failed, cleaning up installation directory %s\n", b.InstallPrefix)
|
||||
cleanErr := os.RemoveAll(b.InstallPrefix)
|
||||
if cleanErr != nil {
|
||||
fmt.Printf("Warning: Failed to clean up installation directory: %v\n", cleanErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Final cleanup
|
||||
if err := b.CleanInstall(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create build marker file
|
||||
f, err := os.Create(b.BuildMarker)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create build marker: %w", err)
|
||||
}
|
||||
f.Close()
|
||||
|
||||
fmt.Println("✅ Done! PostgreSQL installed in:", b.InstallPrefix)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunPostgresInScreen starts PostgreSQL in a screen session
|
||||
func (b *PostgresBuilder) RunPostgresInScreen() error {
|
||||
fmt.Println("Starting PostgreSQL in screen...")
|
||||
|
||||
// Check if screen is installed
|
||||
if _, err := exec.LookPath("screen"); err != nil {
|
||||
return fmt.Errorf("screen is not installed: %w", err)
|
||||
}
|
||||
|
||||
// Create data directory if it doesn't exist
|
||||
dataDir := filepath.Join(b.InstallPrefix, "data")
|
||||
initdbPath := filepath.Join(b.InstallPrefix, "bin", "initdb")
|
||||
postgresPath := filepath.Join(b.InstallPrefix, "bin", "postgres")
|
||||
psqlPath := filepath.Join(b.InstallPrefix, "bin", "psql")
|
||||
|
||||
// Check if data directory exists
|
||||
if _, err := os.Stat(dataDir); os.IsNotExist(err) {
|
||||
fmt.Println("Initializing database directory...")
|
||||
|
||||
// Initialize database
|
||||
cmd := exec.Command(initdbPath, "-D", dataDir)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to initialize database: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if screen session already exists
|
||||
checkCmd := exec.Command("screen", "-list")
|
||||
output, err := checkCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check screen sessions: %w", err)
|
||||
}
|
||||
|
||||
// Kill existing session if it exists
|
||||
if strings.Contains(string(output), "postgresql") {
|
||||
fmt.Println("PostgreSQL screen session already exists, killing it...")
|
||||
killCmd := exec.Command("screen", "-X", "-S", "postgresql", "quit")
|
||||
killCmd.Run() // Ignore errors if the session doesn't exist
|
||||
}
|
||||
|
||||
// Start PostgreSQL in a new screen session
|
||||
cmd := exec.Command("screen", "-dmS", "postgresql", "-L", "-Logfile",
|
||||
filepath.Join(b.InstallPrefix, "postgres_screen.log"),
|
||||
postgresPath, "-D", dataDir)
|
||||
|
||||
fmt.Println("Running command:", cmd.String())
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to start PostgreSQL in screen: %w", err)
|
||||
}
|
||||
|
||||
// Wait for PostgreSQL to start
|
||||
fmt.Println("Waiting for PostgreSQL to start...")
|
||||
for i := 0; i < 10; i++ {
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// Try to connect to PostgreSQL
|
||||
testCmd := exec.Command(psqlPath, "-c", "SELECT 1;")
|
||||
out, err := testCmd.CombinedOutput()
|
||||
|
||||
if err == nil && bytes.Contains(out, []byte("1")) {
|
||||
fmt.Println("✅ PostgreSQL is running and accepting connections")
|
||||
break
|
||||
}
|
||||
|
||||
if i == 9 {
|
||||
return fmt.Errorf("failed to connect to PostgreSQL after 10 seconds")
|
||||
}
|
||||
}
|
||||
|
||||
// Test user creation
|
||||
fmt.Println("Testing user creation...")
|
||||
userCmd := exec.Command(psqlPath, "-c", "CREATE USER test_user WITH PASSWORD 'password';")
|
||||
userOut, userErr := userCmd.CombinedOutput()
|
||||
|
||||
if userErr != nil {
|
||||
return fmt.Errorf("failed to create test user: %s: %w", string(userOut), userErr)
|
||||
}
|
||||
|
||||
// Check if we can log screen output
|
||||
logCmd := exec.Command("screen", "-S", "postgresql", "-X", "hardcopy",
|
||||
filepath.Join(b.InstallPrefix, "screen_hardcopy.log"))
|
||||
if err := logCmd.Run(); err != nil {
|
||||
fmt.Printf("Warning: Failed to capture screen log: %v\n", err)
|
||||
}
|
||||
|
||||
fmt.Println("✅ PostgreSQL is running in screen session 'postgresql'")
|
||||
fmt.Println(" - Log file: ", filepath.Join(b.InstallPrefix, "postgres_screen.log"))
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckPostgresUser checks if PostgreSQL can be run as postgres user
|
||||
func (b *PostgresBuilder) CheckPostgresUser() error {
|
||||
// Try to get postgres user information
|
||||
cmd := exec.Command("id", "postgres")
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("⚠️ postgres user does not exist, consider creating it")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("Found postgres user: %s\n", strings.TrimSpace(string(output)))
|
||||
|
||||
// Try to run a command as postgres user
|
||||
sudoCmd := exec.Command("sudo", "-u", "postgres", "echo", "Running as postgres user")
|
||||
sudoOutput, sudoErr := sudoCmd.CombinedOutput()
|
||||
|
||||
if sudoErr != nil {
|
||||
fmt.Printf("⚠️ Cannot run commands as postgres user: %v\n", sudoErr)
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully ran command as postgres user: %s\n",
|
||||
strings.TrimSpace(string(sudoOutput)))
|
||||
return nil
|
||||
}
|
88
pkg/system/builders/postgresql/postgres/tar.go
Normal file
88
pkg/system/builders/postgresql/postgres/tar.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mholt/archiver/v3"
|
||||
)
|
||||
|
||||
// ExtractTarGz extracts the tar.gz file and returns the top directory
|
||||
func (b *PostgresBuilder) ExtractTarGz() (string, error) {
|
||||
// Get the current working directory
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get working directory: %w", err)
|
||||
}
|
||||
|
||||
// Check if sources are already extracted
|
||||
srcDir := filepath.Join(cwd, "src")
|
||||
if _, err := os.Stat(srcDir); err == nil {
|
||||
fmt.Println("PostgreSQL source already extracted, skipping extraction")
|
||||
return cwd, nil
|
||||
}
|
||||
|
||||
fmt.Println("Extracting...")
|
||||
fmt.Println("Current working directory:", cwd)
|
||||
|
||||
// Check if the archive exists
|
||||
if _, err := os.Stat(b.PostgresTar); os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("archive file %s does not exist", b.PostgresTar)
|
||||
}
|
||||
fmt.Println("Archive exists at:", b.PostgresTar)
|
||||
|
||||
// Create a temporary directory to extract to
|
||||
tempDir, err := os.MkdirTemp("", "postgres-extract-")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
fmt.Println("Created temp directory:", tempDir)
|
||||
defer os.RemoveAll(tempDir) // Clean up temp dir when function returns
|
||||
|
||||
// Extract the archive using archiver
|
||||
fmt.Println("Extracting archive to:", tempDir)
|
||||
err = archiver.Unarchive(b.PostgresTar, tempDir)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to extract archive: %w", err)
|
||||
}
|
||||
|
||||
// Find the top-level directory
|
||||
entries, err := os.ReadDir(tempDir)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read temp directory: %w", err)
|
||||
}
|
||||
|
||||
if len(entries) == 0 {
|
||||
return "", fmt.Errorf("no files found in extracted archive")
|
||||
}
|
||||
|
||||
// In most cases, a properly packaged tarball will extract to a single top directory
|
||||
topDir := entries[0].Name()
|
||||
topDirPath := filepath.Join(tempDir, topDir)
|
||||
fmt.Println("Top directory path:", topDirPath)
|
||||
|
||||
// Verify the top directory exists
|
||||
if info, err := os.Stat(topDirPath); err != nil {
|
||||
return "", fmt.Errorf("top directory not found: %w", err)
|
||||
} else if !info.IsDir() {
|
||||
return "", fmt.Errorf("top path is not a directory: %s", topDirPath)
|
||||
}
|
||||
|
||||
// Create absolute path for the destination
|
||||
dstDir, err := filepath.Abs(".")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get absolute path: %w", err)
|
||||
}
|
||||
fmt.Println("Destination directory (absolute):", dstDir)
|
||||
|
||||
// Move the contents to the current directory
|
||||
fmt.Println("Moving contents from:", topDirPath, "to:", dstDir)
|
||||
err = moveContents(topDirPath, dstDir)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to move contents from temp directory: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Extraction complete")
|
||||
return dstDir, nil
|
||||
}
|
Reference in New Issue
Block a user