This commit is contained in:
2025-05-23 09:33:05 +04:00
parent a16ac8f627
commit 79d66e4b6b
34 changed files with 603 additions and 608 deletions

View File

@@ -0,0 +1,178 @@
package postgresql
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"github.com/freeflowuniverse/heroagent/pkg/system/builders/postgresql/dependencies"
"github.com/freeflowuniverse/heroagent/pkg/system/builders/postgresql/gosp"
"github.com/freeflowuniverse/heroagent/pkg/system/builders/postgresql/postgres"
"github.com/freeflowuniverse/heroagent/pkg/system/builders/postgresql/verification"
)
// Constants for PostgreSQL installation
const (
DefaultInstallPrefix = "/opt/postgresql"
)
// Builder represents a PostgreSQL builder
type Builder struct {
InstallPrefix string
PostgresBuilder *postgres.PostgresBuilder
GoSPBuilder *gosp.GoSPBuilder
DependencyManager *dependencies.DependencyManager
Verifier *verification.Verifier
}
// NewBuilder creates a new PostgreSQL builder with default values
func NewBuilder() *Builder {
installPrefix := DefaultInstallPrefix
return &Builder{
InstallPrefix: installPrefix,
PostgresBuilder: postgres.NewPostgresBuilder().WithInstallPrefix(installPrefix),
GoSPBuilder: gosp.NewGoSPBuilder(installPrefix),
DependencyManager: dependencies.NewDependencyManager("bison", "flex", "libreadline-dev"),
Verifier: verification.NewVerifier(installPrefix),
}
}
// WithInstallPrefix sets the installation prefix
func (b *Builder) WithInstallPrefix(prefix string) *Builder {
b.InstallPrefix = prefix
b.PostgresBuilder.WithInstallPrefix(prefix)
b.GoSPBuilder = gosp.NewGoSPBuilder(prefix)
return b
}
// WithPostgresURL sets the PostgreSQL download URL
// RunPostgresInScreen starts PostgreSQL in a screen session
func (b *Builder) RunPostgresInScreen() error {
return b.PostgresBuilder.RunPostgresInScreen()
}
// CheckPostgresUser checks if PostgreSQL can be run as postgres user
func (b *Builder) CheckPostgresUser() error {
return b.PostgresBuilder.CheckPostgresUser()
}
func (b *Builder) WithPostgresURL(url string) *Builder {
b.PostgresBuilder.WithPostgresURL(url)
return b
}
// WithDependencies sets the dependencies to install
func (b *Builder) WithDependencies(deps ...string) *Builder {
b.DependencyManager.WithDependencies(deps...)
return b
}
// Build builds PostgreSQL
func (b *Builder) Build() error {
fmt.Println("=== Starting PostgreSQL Build ===")
// Install dependencies
fmt.Println("Installing dependencies...")
if err := b.DependencyManager.Install(); err != nil {
return fmt.Errorf("failed to install dependencies: %w", err)
}
// Build PostgreSQL
if err := b.PostgresBuilder.Build(); err != nil {
return fmt.Errorf("failed to build PostgreSQL: %w", err)
}
// Ensure Go is installed first to get its path
goInstaller := postgres.NewGoInstaller()
goPath, err := goInstaller.InstallGo()
if err != nil {
return fmt.Errorf("failed to ensure Go is installed: %w", err)
}
fmt.Printf("Using Go executable from: %s\n", goPath)
// Pass the Go path explicitly to the GoSPBuilder
b.GoSPBuilder.WithGoPath(goPath)
// For the Go stored procedure, we'll create and execute a shell script directly
// to ensure all environment variables are properly set
fmt.Println("Building Go stored procedure via shell script...")
tempDir, err := os.MkdirTemp("", "gosp-build-")
if err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tempDir)
// Create the Go source file in the temp directory
libPath := filepath.Join(tempDir, "gosp.go")
libSrc := `
package main
import "C"
import "fmt"
//export helloworld
func helloworld() {
fmt.Println("Hello from Go stored procedure!")
}
func main() {}
`
if err := os.WriteFile(libPath, []byte(libSrc), 0644); err != nil {
return fmt.Errorf("failed to write Go source file: %w", err)
}
// Create a shell script to build the Go stored procedure
buildScript := filepath.Join(tempDir, "build.sh")
buildScriptContent := fmt.Sprintf(`#!/bin/sh
set -e
# Set environment variables
export GOROOT=/usr/local/go
export GOPATH=/root/go
export PATH=/usr/local/go/bin:$PATH
echo "Current directory: $(pwd)"
echo "Go source file: %s"
echo "Output file: %s/lib/libgosp.so"
# Create output directory
mkdir -p %s/lib
# Run the build command
echo "Running: go build -buildmode=c-shared -o %s/lib/libgosp.so %s"
go build -buildmode=c-shared -o %s/lib/libgosp.so %s
echo "Go stored procedure built successfully!"
`,
libPath, b.InstallPrefix, b.InstallPrefix, b.InstallPrefix, libPath, b.InstallPrefix, libPath)
if err := os.WriteFile(buildScript, []byte(buildScriptContent), 0755); err != nil {
return fmt.Errorf("failed to write build script: %w", err)
}
// Execute the build script
cmd := exec.Command("/bin/sh", buildScript)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
fmt.Println("Executing build script:", buildScript)
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to run build script: %w", err)
}
// Verify the installation
fmt.Println("Verifying installation...")
success, err := b.Verifier.Verify()
if err != nil {
fmt.Printf("Warning: Verification had issues: %v\n", err)
}
if success {
fmt.Println("✅ Done! PostgreSQL installed and verified in:", b.InstallPrefix)
} else {
fmt.Println("⚠️ Done with warnings! PostgreSQL installed in:", b.InstallPrefix)
}
return nil
}

View File

@@ -0,0 +1,25 @@
#!/bin/bash
set -e
# Change to the script's directory to ensure relative paths work
cd "$(dirname "$0")"
echo "Building PostgreSQL Builder for Linux on AMD64..."
# Create build directory if it doesn't exist
mkdir -p build
# Build the PostgreSQL builder
echo "Building PostgreSQL builder..."
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-ldflags="-s -w" \
-trimpath \
-o build/postgresql_builder \
../cmd/main.go
# Set executable permissions
chmod +x build/postgresql_builder
# Output binary info
echo "Build complete!"
ls -lh build/

View File

@@ -0,0 +1,27 @@
package main
import (
"fmt"
"os"
"github.com/freeflowuniverse/heroagent/pkg/system/builders/postgresql"
)
func main() {
// Create a new PostgreSQL builder with default settings
builder := postgresql.NewBuilder()
// Build PostgreSQL
if err := builder.Build(); err != nil {
fmt.Fprintf(os.Stderr, "Error building PostgreSQL: %v\n", err)
os.Exit(1) // Ensure we exit with non-zero status on error
}
// Run PostgreSQL in screen
if err := builder.PostgresBuilder.RunPostgresInScreen(); err != nil {
fmt.Fprintf(os.Stderr, "Error running PostgreSQL in screen: %v\n", err)
os.Exit(1) // Ensure we exit with non-zero status on error
}
fmt.Println("PostgreSQL build completed successfully!")
}

View File

@@ -0,0 +1,93 @@
#!/bin/bash
set -e
export SERVER="65.109.18.183"
LOG_FILE="postgresql_deployment_$(date +%Y%m%d_%H%M%S).log"
cd "$(dirname "$0")"
# Configure logging
log() {
local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
echo "[$timestamp] $1" | tee -a "$LOG_FILE"
}
log "=== Starting PostgreSQL Builder Deployment ==="
log "Log file: $LOG_FILE"
# Check if SERVER environment variable is set
if [ -z "$SERVER" ]; then
log "Error: SERVER environment variable is not set."
log "Please set it to the IPv4 or IPv6 address of the target server."
log "Example: export SERVER=192.168.1.100"
exit 1
fi
# Validate if SERVER is a valid IP address (IPv4 or IPv6)
if ! [[ "$SERVER" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] && \
! [[ "$SERVER" =~ ^[0-9a-fA-F:]+$ ]]; then
log "Error: SERVER must be a valid IPv4 or IPv6 address."
exit 1
fi
log "Using server: $SERVER"
# Build the PostgreSQL builder binary
log "Building PostgreSQL builder binary..."
./build.sh | tee -a "$LOG_FILE"
# Check if binary exists
if [ ! -f "build/postgresql_builder" ]; then
log "Error: PostgreSQL builder binary not found after build."
exit 1
fi
log "Binary size:"
ls -lh build/ | tee -a "$LOG_FILE"
# Create deployment directory on server
log "Creating deployment directory on server..."
ssh "root@$SERVER" "mkdir -p ~/postgresql_builder" 2>&1 | tee -a "$LOG_FILE"
# Transfer the binary to the server
log "Transferring PostgreSQL builder binary to server..."
rsync -avz --progress build/postgresql_builder "root@$SERVER:~/postgresql_builder/" 2>&1 | tee -a "$LOG_FILE"
# Run the PostgreSQL builder on the server
log "Running PostgreSQL builder on server..."
ssh -t "root@$SERVER" "cd ~/postgresql_builder && ./postgresql_builder" 2>&1 | tee -a "$LOG_FILE"
BUILD_EXIT_CODE=${PIPESTATUS[0]}
# If there was an error, make it very clear
if [ $BUILD_EXIT_CODE -ne 0 ]; then
log "⚠️ PostgreSQL builder failed with exit code: $BUILD_EXIT_CODE"
fi
# Check for errors in exit code
if [ $BUILD_EXIT_CODE -eq 0 ]; then
log "✅ SUCCESS: PostgreSQL builder completed successfully!"
log "----------------------------------------------------------------"
# Note: Verification is now handled by the builder itself
# Check for build logs or error messages
log "Checking for build logs on server..."
BUILD_LOGS=$(ssh "root@$SERVER" "cd ~/postgresql_builder && ls -la *.log 2>/dev/null || echo 'No log files found'" 2>&1)
log "Build log files:"
echo "$BUILD_LOGS" | tee -a "$LOG_FILE"
log "----------------------------------------------------------------"
log "🎉 PostgreSQL Builder deployment COMPLETED"
log "================================================================"
else
log "❌ ERROR: PostgreSQL builder failed to run properly on the server."
# Get more detailed error information
# log "Checking for error logs on server..."
# ssh "root@$SERVER" "cd ~/postgresql_builder && ls -la" 2>&1 | tee -a "$LOG_FILE"
exit 1
fi
log "=== Deployment Completed ==="

View File

@@ -0,0 +1,55 @@
package dependencies
import (
"fmt"
"os/exec"
"strings"
)
// DependencyManager handles the installation of dependencies
type DependencyManager struct {
Dependencies []string
}
// NewDependencyManager creates a new dependency manager
func NewDependencyManager(dependencies ...string) *DependencyManager {
return &DependencyManager{
Dependencies: dependencies,
}
}
// WithDependencies sets the dependencies to install
func (d *DependencyManager) WithDependencies(dependencies ...string) *DependencyManager {
d.Dependencies = dependencies
return d
}
// Install installs the dependencies
func (d *DependencyManager) Install() error {
if len(d.Dependencies) == 0 {
fmt.Println("No dependencies to install")
return nil
}
fmt.Printf("Installing dependencies: %s\n", strings.Join(d.Dependencies, ", "))
// Update package lists
updateCmd := exec.Command("apt-get", "update")
updateCmd.Stdout = nil
updateCmd.Stderr = nil
if err := updateCmd.Run(); err != nil {
return fmt.Errorf("failed to update package lists: %w", err)
}
// Install dependencies
args := append([]string{"install", "-y"}, d.Dependencies...)
installCmd := exec.Command("apt-get", args...)
installCmd.Stdout = nil
installCmd.Stderr = nil
if err := installCmd.Run(); err != nil {
return fmt.Errorf("failed to install dependencies: %w", err)
}
fmt.Println("✅ Dependencies installed successfully")
return nil
}

View File

@@ -0,0 +1,172 @@
package gosp
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"github.com/freeflowuniverse/heroagent/pkg/system/builders/postgresql/postgres"
)
// Constants for Go stored procedure
const (
DefaultGoSharedLibDir = "go_sp"
)
// GoSPBuilder represents a Go stored procedure builder
type GoSPBuilder struct {
GoSharedLibDir string
InstallPrefix string
GoPath string // Path to Go executable
}
// NewGoSPBuilder creates a new Go stored procedure builder
func NewGoSPBuilder(installPrefix string) *GoSPBuilder {
return &GoSPBuilder{
GoSharedLibDir: DefaultGoSharedLibDir,
InstallPrefix: installPrefix,
}
}
// WithGoSharedLibDir sets the Go shared library directory
func (b *GoSPBuilder) WithGoSharedLibDir(dir string) *GoSPBuilder {
b.GoSharedLibDir = dir
return b
}
// WithGoPath sets the path to the Go executable
func (b *GoSPBuilder) WithGoPath(path string) *GoSPBuilder {
b.GoPath = path
return b
}
// run executes a command with the given arguments and environment variables
func (b *GoSPBuilder) run(cmd string, args ...string) error {
fmt.Println("Running:", cmd, args)
c := exec.Command(cmd, args...)
// Set environment variables
c.Env = append(os.Environ(),
"GOROOT=/usr/local/go",
"GOPATH=/root/go",
"PATH=/usr/local/go/bin:" + os.Getenv("PATH"))
c.Stdout = os.Stdout
c.Stderr = os.Stderr
return c.Run()
}
// Build builds a Go stored procedure
func (b *GoSPBuilder) Build() error {
fmt.Println("Building Go stored procedure...")
// Use the explicitly provided Go path if available
var goExePath string
if b.GoPath != "" {
goExePath = b.GoPath
fmt.Printf("Using explicitly provided Go executable: %s\n", goExePath)
} else {
// Fallback to ensuring Go is installed via the installer
goInstaller := postgres.NewGoInstaller()
var err error
goExePath, err = goInstaller.InstallGo()
if err != nil {
return fmt.Errorf("failed to ensure Go is installed: %w", err)
}
fmt.Printf("Using detected Go executable from: %s\n", goExePath)
}
if err := os.MkdirAll(b.GoSharedLibDir, 0755); err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
libPath := filepath.Join(b.GoSharedLibDir, "gosp.go")
libSrc := `
package main
import "C"
import "fmt"
//export helloworld
func helloworld() {
fmt.Println("Hello from Go stored procedure!")
}
func main() {}
`
if err := os.WriteFile(libPath, []byte(libSrc), 0644); err != nil {
return fmt.Errorf("failed to write to file: %w", err)
}
// Use the full path to Go rather than relying on PATH
fmt.Println("Running Go build with full path:", goExePath)
// Show debug information
fmt.Println("Environment variables that will be set:")
fmt.Println(" GOROOT=/usr/local/go")
fmt.Println(" GOPATH=/root/go")
fmt.Println(" PATH=/usr/local/go/bin:" + os.Getenv("PATH"))
// Verify that the Go executable exists before using it
if _, err := os.Stat(goExePath); err != nil {
return fmt.Errorf("Go executable not found at %s: %w", goExePath, err)
}
// Create the output directory if it doesn't exist
outputDir := filepath.Join(b.InstallPrefix, "lib")
if err := os.MkdirAll(outputDir, 0755); err != nil {
return fmt.Errorf("failed to create output directory %s: %w", outputDir, err)
}
// Prepare output path
outputPath := filepath.Join(outputDir, "libgosp.so")
// Instead of relying on environment variables, create a wrapper shell script
// that sets all required environment variables and then calls the Go executable
tempDir, err := os.MkdirTemp("", "go-build-")
if err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tempDir) // Clean up when done
goRoot := filepath.Dir(filepath.Dir(goExePath)) // /usr/local/go
wrapperScript := filepath.Join(tempDir, "go-wrapper.sh")
wrapperContent := fmt.Sprintf(`#!/bin/sh
# Go wrapper script created by GoSPBuilder
export GOROOT=%s
export GOPATH=/root/go
export PATH=%s:$PATH
echo "=== Go environment variables ==="
echo "GOROOT=$GOROOT"
echo "GOPATH=$GOPATH"
echo "PATH=$PATH"
echo "=== Running Go command ==="
echo "%s $@"
exec %s "$@"
`,
goRoot,
filepath.Dir(goExePath),
goExePath,
goExePath)
// Write the wrapper script
if err := os.WriteFile(wrapperScript, []byte(wrapperContent), 0755); err != nil {
return fmt.Errorf("failed to write wrapper script: %w", err)
}
fmt.Printf("Created wrapper script at %s\n", wrapperScript)
// Use the wrapper script to build the Go shared library
cmd := exec.Command(wrapperScript, "build", "-buildmode=c-shared", "-o", outputPath, libPath)
cmd.Dir = filepath.Dir(libPath) // Set working directory to where the source file is
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
fmt.Printf("Executing Go build via wrapper script\n")
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to build Go stored procedure: %w", err)
}
fmt.Println("✅ Go stored procedure built successfully!")
return nil
}

View 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
}

View 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
}

View 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)
}

View 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
}

View 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
}

View File

@@ -0,0 +1,103 @@
package verification
import (
"fmt"
"os/exec"
)
// Verifier handles the verification of PostgreSQL installation
type Verifier struct {
InstallPrefix string
}
// NewVerifier creates a new verifier
func NewVerifier(installPrefix string) *Verifier {
return &Verifier{
InstallPrefix: installPrefix,
}
}
// VerifyPostgres verifies the PostgreSQL installation
func (v *Verifier) VerifyPostgres() (bool, error) {
fmt.Println("Verifying PostgreSQL installation...")
// Check for PostgreSQL binary
postgresPath := fmt.Sprintf("%s/bin/postgres", v.InstallPrefix)
fmt.Printf("Checking for PostgreSQL binary at %s\n", postgresPath)
checkCmd := exec.Command("ls", "-la", postgresPath)
output, err := checkCmd.CombinedOutput()
if err != nil {
fmt.Printf("❌ WARNING: PostgreSQL binary not found at expected location: %s\n", postgresPath)
fmt.Println("This may indicate that the build process failed or installed to a different location.")
// Search for PostgreSQL binary in other locations
fmt.Println("Searching for PostgreSQL binary in other locations...")
findCmd := exec.Command("find", "/", "-name", "postgres", "-type", "f")
findOutput, _ := findCmd.CombinedOutput()
fmt.Printf("Search results:\n%s\n", string(findOutput))
return false, fmt.Errorf("PostgreSQL binary not found at expected location")
}
fmt.Printf("✅ PostgreSQL binary found at expected location:\n%s\n", string(output))
return true, nil
}
// VerifyGoSP verifies the Go stored procedure installation
func (v *Verifier) VerifyGoSP() (bool, error) {
fmt.Println("Verifying Go stored procedure installation...")
// Check for Go stored procedure
gospPath := fmt.Sprintf("%s/lib/libgosp.so", v.InstallPrefix)
fmt.Printf("Checking for Go stored procedure at %s\n", gospPath)
checkCmd := exec.Command("ls", "-la", gospPath)
output, err := checkCmd.CombinedOutput()
if err != nil {
fmt.Printf("❌ WARNING: Go stored procedure library not found at expected location: %s\n", gospPath)
// Search for Go stored procedure in other locations
fmt.Println("Searching for Go stored procedure in other locations...")
findCmd := exec.Command("find", "/", "-name", "libgosp.so", "-type", "f")
findOutput, _ := findCmd.CombinedOutput()
fmt.Printf("Search results:\n%s\n", string(findOutput))
return false, fmt.Errorf("Go stored procedure library not found at expected location")
}
fmt.Printf("✅ Go stored procedure library found at expected location:\n%s\n", string(output))
return true, nil
}
// Verify verifies the entire PostgreSQL installation
func (v *Verifier) Verify() (bool, error) {
fmt.Println("=== Verifying PostgreSQL Installation ===")
// Verify PostgreSQL
postgresOk, postgresErr := v.VerifyPostgres()
// Verify Go stored procedure
gospOk, gospErr := v.VerifyGoSP()
// Overall verification result
success := postgresOk && gospOk
if success {
fmt.Println("✅ All components verified successfully!")
} else {
fmt.Println("⚠️ Some components could not be verified.")
if postgresErr != nil {
fmt.Printf("PostgreSQL verification error: %v\n", postgresErr)
}
if gospErr != nil {
fmt.Printf("Go stored procedure verification error: %v\n", gospErr)
}
}
return success, nil
}