...
This commit is contained in:
528
pkg/builders/hetznerinstall/builder.go
Normal file
528
pkg/builders/hetznerinstall/builder.go
Normal file
@@ -0,0 +1,528 @@
|
||||
package hetznerinstall
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Struct to parse lsblk JSON output
|
||||
type lsblkOutput struct {
|
||||
BlockDevices []lsblkDevice `json:"blockdevices"`
|
||||
}
|
||||
|
||||
type lsblkDevice struct {
|
||||
Name string `json:"name"`
|
||||
Rota bool `json:"rota"` // Rotational device (false for SSD/NVMe)
|
||||
Type string `json:"type"` // disk, part, lvm, etc.
|
||||
}
|
||||
|
||||
const installImageConfigPath = "/root/.installimage" // Standard path in Rescue System
|
||||
|
||||
// DefaultImage is the default OS image to install.
|
||||
const DefaultImage = "Ubuntu-2404"
|
||||
|
||||
// Partition represents a partition definition in the installimage config.
|
||||
type Partition struct {
|
||||
MountPoint string // e.g., "/", "/boot", "swap"
|
||||
FileSystem string // e.g., "ext4", "swap"
|
||||
Size string // e.g., "512M", "all", "8G"
|
||||
}
|
||||
|
||||
// HetznerInstallBuilder configures and runs the Hetzner installimage process.
|
||||
type HetznerInstallBuilder struct {
|
||||
// Drives are now auto-detected
|
||||
Hostname string // Target hostname
|
||||
Image string // OS Image name, e.g., "Ubuntu-2404"
|
||||
Partitions []Partition // Partition layout
|
||||
Swraid bool // Enable software RAID
|
||||
SwraidLevel int // RAID level (0, 1, 5, 6, 10)
|
||||
ClearPart bool // Wipe disks before partitioning
|
||||
// Add PostInstallScript path later if needed
|
||||
detectedDrives []string // Stores drives detected by detectSSDDevicePaths
|
||||
}
|
||||
|
||||
// NewBuilder creates a new HetznerInstallBuilder with default settings.
|
||||
func NewBuilder() *HetznerInstallBuilder {
|
||||
return &HetznerInstallBuilder{
|
||||
Image: DefaultImage,
|
||||
ClearPart: true, // Default to wiping disks
|
||||
Swraid: false,
|
||||
SwraidLevel: 0,
|
||||
Partitions: []Partition{ // Default simple layout
|
||||
{MountPoint: "/boot", FileSystem: "ext4", Size: "512M"},
|
||||
{MountPoint: "/", FileSystem: "ext4", Size: "all"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// WithHostname sets the target hostname.
|
||||
func (b *HetznerInstallBuilder) WithHostname(hostname string) *HetznerInstallBuilder {
|
||||
b.Hostname = hostname
|
||||
return b
|
||||
}
|
||||
|
||||
// WithImage sets the OS image to install.
|
||||
func (b *HetznerInstallBuilder) WithImage(image string) *HetznerInstallBuilder {
|
||||
b.Image = image
|
||||
return b
|
||||
}
|
||||
|
||||
// WithPartitions sets the partition layout. Replaces the default.
|
||||
func (b *HetznerInstallBuilder) WithPartitions(partitions ...Partition) *HetznerInstallBuilder {
|
||||
if len(partitions) > 0 {
|
||||
b.Partitions = partitions
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// WithSoftwareRAID enables and configures software RAID.
|
||||
func (b *HetznerInstallBuilder) WithSoftwareRAID(enable bool, level int) *HetznerInstallBuilder {
|
||||
b.Swraid = enable
|
||||
if enable {
|
||||
b.SwraidLevel = level
|
||||
} else {
|
||||
b.SwraidLevel = 0 // Ensure level is 0 if RAID is disabled
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// WithClearPart enables or disables wiping disks.
|
||||
func (b *HetznerInstallBuilder) WithClearPart(clear bool) *HetznerInstallBuilder {
|
||||
b.ClearPart = clear
|
||||
return b
|
||||
}
|
||||
|
||||
// Validate checks if the builder configuration is valid *before* running install.
|
||||
// Note: Drive validation happens in RunInstall after auto-detection.
|
||||
func (b *HetznerInstallBuilder) Validate() error {
|
||||
if b.Hostname == "" {
|
||||
return fmt.Errorf("hostname must be specified using WithHostname()")
|
||||
}
|
||||
if b.Image == "" {
|
||||
return fmt.Errorf("OS image must be specified using WithImage()")
|
||||
}
|
||||
if len(b.Partitions) == 0 {
|
||||
return fmt.Errorf("at least one partition must be specified using WithPartitions()")
|
||||
}
|
||||
// Add more validation as needed (e.g., valid RAID levels, partition sizes)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateConfig generates the content for the installimage config file.
|
||||
func (b *HetznerInstallBuilder) GenerateConfig() (string, error) {
|
||||
if err := b.Validate(); err != nil {
|
||||
return "", fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
|
||||
// Use detectedDrives for the template
|
||||
if len(b.detectedDrives) == 0 {
|
||||
// This should ideally be caught earlier in RunInstall, but double-check
|
||||
return "", fmt.Errorf("internal error: GenerateConfig called with no detected drives")
|
||||
}
|
||||
|
||||
tmplData := struct {
|
||||
*HetznerInstallBuilder // Embed original builder fields
|
||||
Drives []string // Override Drives field for the template
|
||||
}{
|
||||
HetznerInstallBuilder: b,
|
||||
Drives: b.detectedDrives,
|
||||
}
|
||||
|
||||
tmpl := `{{range $i, $drive := .Drives}}DRIVE{{add $i 1}} {{$drive}}
|
||||
{{end}}
|
||||
SWRAID {{if .Swraid}}1{{else}}0{{end}}
|
||||
SWRAIDLEVEL {{.SwraidLevel}}
|
||||
|
||||
HOSTNAME {{.Hostname}}
|
||||
BOOTLOADER grub
|
||||
IMAGE {{.Image}}
|
||||
|
||||
{{range .Partitions}}PART {{.MountPoint}} {{.FileSystem}} {{.Size}}
|
||||
{{end}}
|
||||
# Wipe disks
|
||||
CLEARPART {{if .ClearPart}}yes{{else}}no{{end}}
|
||||
`
|
||||
// Using text/template requires a function map for simple arithmetic like add
|
||||
funcMap := template.FuncMap{
|
||||
"add": func(a, b int) int {
|
||||
return a + b
|
||||
},
|
||||
}
|
||||
|
||||
t, err := template.New("installimageConfig").Funcs(funcMap).Parse(tmpl)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse config template: %w", err)
|
||||
}
|
||||
|
||||
var configContent bytes.Buffer
|
||||
// Execute template with the overridden Drives data
|
||||
if err := t.Execute(&configContent, tmplData); err != nil {
|
||||
return "", fmt.Errorf("failed to execute config template: %w", err)
|
||||
}
|
||||
|
||||
return configContent.String(), nil
|
||||
}
|
||||
|
||||
// detectSSDDevicePaths finds non-rotational block devices (SSDs, NVMe).
|
||||
// Assumes lsblk is available and supports JSON output.
|
||||
func detectSSDDevicePaths() ([]string, error) {
|
||||
fmt.Println("Attempting to detect SSD/NVMe devices using lsblk...")
|
||||
cmd := exec.Command("lsblk", "-J", "-o", "NAME,ROTA,TYPE")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute lsblk: %w. Output: %s", err, string(output))
|
||||
}
|
||||
|
||||
var data lsblkOutput
|
||||
if err := json.Unmarshal(output, &data); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse lsblk JSON output: %w", err)
|
||||
}
|
||||
|
||||
var ssdPaths []string
|
||||
for _, device := range data.BlockDevices {
|
||||
// We only care about top-level disks, not partitions
|
||||
if device.Type == "disk" && !device.Rota {
|
||||
fullPath := "/dev/" + device.Name
|
||||
fmt.Printf("Detected potential SSD/NVMe device: %s\n", fullPath)
|
||||
ssdPaths = append(ssdPaths, fullPath)
|
||||
}
|
||||
}
|
||||
|
||||
if len(ssdPaths) == 0 {
|
||||
fmt.Println("Warning: No SSD/NVMe devices detected via lsblk.")
|
||||
// Don't return an error here, let RunInstall decide if it's fatal
|
||||
} else {
|
||||
fmt.Printf("Detected SSD/NVMe devices: %v\n", ssdPaths)
|
||||
}
|
||||
|
||||
return ssdPaths, nil
|
||||
}
|
||||
|
||||
// findAndStopRaidArrays attempts to find and stop all active RAID arrays.
|
||||
// Uses multiple methods to ensure arrays are properly stopped.
|
||||
func findAndStopRaidArrays() error {
|
||||
fmt.Println("--- Attempting to find and stop active RAID arrays ---")
|
||||
var overallErr error
|
||||
|
||||
// Method 1: Use lsblk to find md devices
|
||||
fmt.Println("Method 1: Finding md devices using lsblk...")
|
||||
cmdLsblk := exec.Command("lsblk", "-J", "-o", "NAME,TYPE")
|
||||
output, err := cmdLsblk.Output()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: Failed to execute lsblk to find md devices: %v. Trying alternative methods.\n", err)
|
||||
} else {
|
||||
var data lsblkOutput
|
||||
if err := json.Unmarshal(output, &data); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: Failed to parse lsblk JSON for md devices: %v. Trying alternative methods.\n", err)
|
||||
} else {
|
||||
for _, device := range data.BlockDevices {
|
||||
// Check for various RAID types lsblk might report
|
||||
isRaid := strings.HasPrefix(device.Type, "raid") || device.Type == "md"
|
||||
if strings.HasPrefix(device.Name, "md") && isRaid {
|
||||
mdPath := "/dev/" + device.Name
|
||||
fmt.Printf("Attempting to stop md device: %s\n", mdPath)
|
||||
// Try executing via bash -c
|
||||
stopCmdStr := fmt.Sprintf("mdadm --stop %s", mdPath)
|
||||
cmdStop := exec.Command("bash", "-c", stopCmdStr)
|
||||
stopOutput, stopErr := cmdStop.CombinedOutput() // Capture both stdout and stderr
|
||||
if stopErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: Failed to stop %s: %v. Output: %s\n", mdPath, stopErr, string(stopOutput))
|
||||
if overallErr == nil {
|
||||
overallErr = fmt.Errorf("failed to stop some md devices")
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("Stopped %s successfully.\n", mdPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Method 2: Use /proc/mdstat to find arrays
|
||||
fmt.Println("Method 2: Finding md devices using /proc/mdstat...")
|
||||
cmdCat := exec.Command("cat", "/proc/mdstat")
|
||||
mdstatOutput, mdstatErr := cmdCat.Output()
|
||||
if mdstatErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: Failed to read /proc/mdstat: %v\n", mdstatErr)
|
||||
} else {
|
||||
// Parse mdstat output to find active arrays
|
||||
// Example line: md0 : active raid1 sda1[0] sdb1[1]
|
||||
lines := strings.Split(string(mdstatOutput), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "active") {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) >= 1 && strings.HasPrefix(parts[0], "md") {
|
||||
mdPath := "/dev/" + parts[0]
|
||||
fmt.Printf("Found active array in mdstat: %s\n", mdPath)
|
||||
stopCmd := exec.Command("mdadm", "--stop", mdPath)
|
||||
stopOutput, stopErr := stopCmd.CombinedOutput()
|
||||
if stopErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: Failed to stop %s: %v. Output: %s\n", mdPath, stopErr, string(stopOutput))
|
||||
} else {
|
||||
fmt.Printf("Stopped %s successfully.\n", mdPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Method 3: Brute force attempt to stop common md devices
|
||||
fmt.Println("Method 3: Attempting to stop common md devices...")
|
||||
commonMdPaths := []string{"/dev/md0", "/dev/md1", "/dev/md2", "/dev/md3", "/dev/md127"}
|
||||
for _, mdPath := range commonMdPaths {
|
||||
fmt.Printf("Attempting to stop %s (brute force)...\n", mdPath)
|
||||
stopCmd := exec.Command("mdadm", "--stop", mdPath)
|
||||
stopOutput, _ := stopCmd.CombinedOutput() // Ignore errors, just try
|
||||
fmt.Printf("Output: %s\n", string(stopOutput))
|
||||
}
|
||||
|
||||
// Sync to ensure changes are written
|
||||
syncCmd := exec.Command("sync")
|
||||
syncCmd.Run() // Ignore errors
|
||||
|
||||
fmt.Println("--- Finished attempting to stop RAID arrays ---")
|
||||
return overallErr
|
||||
}
|
||||
|
||||
// zeroSuperblocks attempts to zero mdadm superblocks on all given devices.
|
||||
func zeroSuperblocks(physicalDevices []string) error {
|
||||
fmt.Println("--- Zeroing mdadm superblocks on physical devices ---")
|
||||
var overallErr error
|
||||
|
||||
for _, devicePath := range physicalDevices {
|
||||
fmt.Printf("Executing: mdadm --zero-superblock %s\n", devicePath)
|
||||
// Try executing via bash -c
|
||||
zeroCmdStr := fmt.Sprintf("mdadm --zero-superblock %s", devicePath)
|
||||
cmdZero := exec.Command("bash", "-c", zeroCmdStr)
|
||||
zeroOutput, zeroErr := cmdZero.CombinedOutput() // Capture both stdout and stderr
|
||||
if zeroErr != nil {
|
||||
// Log error but continue
|
||||
fmt.Fprintf(os.Stderr, "Warning: Failed to zero superblock on %s: %v. Output: %s\n", devicePath, zeroErr, string(zeroOutput))
|
||||
if overallErr == nil {
|
||||
overallErr = fmt.Errorf("failed to zero superblock on some devices")
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("Zeroed superblock on %s successfully.\n", devicePath)
|
||||
}
|
||||
}
|
||||
|
||||
// Sync to ensure changes are written
|
||||
syncCmd := exec.Command("sync")
|
||||
syncCmd.Run() // Ignore errors
|
||||
|
||||
fmt.Println("--- Finished zeroing superblocks ---")
|
||||
return overallErr
|
||||
}
|
||||
|
||||
// overwriteDiskStart uses dd to zero out the beginning of a disk.
|
||||
// EXTREMELY DANGEROUS. Use only when absolutely necessary to destroy metadata.
|
||||
func overwriteDiskStart(devicePath string) error {
|
||||
fmt.Printf("☢️☢️ EXTREME WARNING: Overwriting start of disk %s with zeros using dd!\n", devicePath)
|
||||
// Write 10MB of zeros. Should be enough to kill most metadata (MBR, GPT, RAID superblocks)
|
||||
// bs=1M count=10
|
||||
ddCmdStr := fmt.Sprintf("dd if=/dev/zero of=%s bs=1M count=10 oflag=direct", devicePath)
|
||||
fmt.Printf("Executing: %s\n", ddCmdStr)
|
||||
|
||||
cmdDD := exec.Command("bash", "-c", ddCmdStr)
|
||||
ddOutput, ddErr := cmdDD.CombinedOutput()
|
||||
if ddErr != nil {
|
||||
// Log error but consider it potentially non-fatal if subsequent wipefs works
|
||||
fmt.Fprintf(os.Stderr, "Warning: dd command on %s failed: %v. Output: %s\n", devicePath, ddErr, string(ddOutput))
|
||||
// Return the error so the caller knows something went wrong
|
||||
return fmt.Errorf("dd command failed on %s: %w", devicePath, ddErr)
|
||||
}
|
||||
|
||||
fmt.Printf("✅ Successfully overwrote start of %s with zeros.\n", devicePath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// wipeDevice erases partition table signatures from a given device path.
|
||||
// USE WITH EXTREME CAUTION.
|
||||
func wipeDevice(devicePath string) error {
|
||||
fmt.Printf("⚠️ WARNING: Preparing to wipe partition signatures from device %s\n", devicePath)
|
||||
fmt.Printf("Executing: wipefs --all --force %s\n", devicePath)
|
||||
|
||||
cmd := exec.Command("wipefs", "--all", "--force", devicePath)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to wipe device %s: %w", devicePath, err)
|
||||
}
|
||||
|
||||
fmt.Printf("✅ Successfully wiped partition signatures from %s\n", devicePath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeInstallImage attempts to execute the installimage command using multiple methods.
|
||||
// Returns the first successful execution or the last error.
|
||||
func executeInstallImage(configPath string) error {
|
||||
fmt.Println("--- Attempting to execute installimage using multiple methods ---")
|
||||
|
||||
// Define all the methods we'll try
|
||||
methods := []struct {
|
||||
name string
|
||||
cmdArgs []string
|
||||
}{
|
||||
{
|
||||
name: "Method 1: Interactive bash shell",
|
||||
cmdArgs: []string{"bash", "-i", "-c", fmt.Sprintf("installimage -a -c %s", configPath)},
|
||||
},
|
||||
{
|
||||
name: "Method 2: Login bash shell",
|
||||
cmdArgs: []string{"bash", "-l", "-c", fmt.Sprintf("installimage -a -c %s", configPath)},
|
||||
},
|
||||
{
|
||||
name: "Method 3: Source profile first",
|
||||
cmdArgs: []string{"bash", "-c", fmt.Sprintf("source /etc/profile && installimage -a -c %s", configPath)},
|
||||
},
|
||||
{
|
||||
name: "Method 4: Try absolute path /usr/sbin/installimage",
|
||||
cmdArgs: []string{"/usr/sbin/installimage", "-a", "-c", configPath},
|
||||
},
|
||||
{
|
||||
name: "Method 5: Try absolute path /root/bin/installimage",
|
||||
cmdArgs: []string{"/root/bin/installimage", "-a", "-c", configPath},
|
||||
},
|
||||
{
|
||||
name: "Method 6: Try absolute path /bin/installimage",
|
||||
cmdArgs: []string{"/bin/installimage", "-a", "-c", configPath},
|
||||
},
|
||||
{
|
||||
name: "Method 7: Try absolute path /sbin/installimage",
|
||||
cmdArgs: []string{"/sbin/installimage", "-a", "-c", configPath},
|
||||
},
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for _, method := range methods {
|
||||
fmt.Printf("Trying %s\n", method.name)
|
||||
fmt.Printf("Executing: %s\n", strings.Join(method.cmdArgs, " "))
|
||||
|
||||
cmd := exec.Command(method.cmdArgs[0], method.cmdArgs[1:]...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
err := cmd.Run()
|
||||
if err == nil {
|
||||
fmt.Printf("✅ Success with %s\n", method.name)
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("❌ Failed with %s: %v\n", method.name, err)
|
||||
lastErr = err
|
||||
|
||||
// Short pause between attempts
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
fmt.Println("--- All installimage execution methods failed ---")
|
||||
return fmt.Errorf("all installimage execution methods failed, last error: %w", lastErr)
|
||||
}
|
||||
|
||||
// RunInstall detects drives if needed, wipes them, generates config, and executes installimage.
|
||||
// Assumes it's running within the Hetzner Rescue System.
|
||||
func (b *HetznerInstallBuilder) RunInstall() error {
|
||||
// 1. Auto-Detect Drives
|
||||
fmt.Println("Attempting auto-detection of SSD/NVMe drives...")
|
||||
detected, err := detectSSDDevicePaths()
|
||||
if err != nil {
|
||||
// Make detection failure fatal if we rely solely on it
|
||||
return fmt.Errorf("failed to auto-detect SSD devices: %w. Cannot proceed without target drives.", err)
|
||||
}
|
||||
if len(detected) == 0 {
|
||||
return fmt.Errorf("auto-detection did not find any suitable SSD/NVMe drives. Cannot proceed.")
|
||||
}
|
||||
b.detectedDrives = detected // Store detected drives
|
||||
fmt.Printf("Using auto-detected drives for installation: %v\n", b.detectedDrives)
|
||||
|
||||
// 2. Validate other parameters (Hostname, Image, Partitions)
|
||||
if err := b.Validate(); err != nil {
|
||||
return fmt.Errorf("pre-install validation failed: %w", err)
|
||||
}
|
||||
|
||||
// 3. Find and stop all RAID arrays (using multiple methods)
|
||||
if err := findAndStopRaidArrays(); err != nil {
|
||||
// Log the warning but proceed, as zeroing might partially succeed
|
||||
fmt.Fprintf(os.Stderr, "Warning during RAID array stopping: %v. Proceeding with disk cleaning...\n", err)
|
||||
}
|
||||
|
||||
// 4. Zero superblocks on all detected drives
|
||||
if err := zeroSuperblocks(b.detectedDrives); err != nil {
|
||||
// Log the warning but proceed to dd/wipefs, as zeroing might partially succeed
|
||||
fmt.Fprintf(os.Stderr, "Warning during superblock zeroing: %v. Proceeding with dd/wipefs...\n", err)
|
||||
}
|
||||
|
||||
// 5. Overwrite start of disks using dd (Forceful metadata destruction)
|
||||
fmt.Println("--- Preparing to Overwrite Disk Starts (dd) ---")
|
||||
var ddFailed bool
|
||||
for _, drivePath := range b.detectedDrives {
|
||||
if err := overwriteDiskStart(drivePath); err != nil {
|
||||
// Log the error, mark as failed, but continue to try wipefs
|
||||
fmt.Fprintf(os.Stderr, "ERROR during dd on %s: %v. Will still attempt wipefs.\n", drivePath, err)
|
||||
ddFailed = true // If dd fails, we rely heavily on wipefs
|
||||
}
|
||||
}
|
||||
fmt.Println("--- Finished Overwriting Disk Starts (dd) ---")
|
||||
// Sync filesystem buffers to disk
|
||||
fmt.Println("Syncing after dd...")
|
||||
syncCmdDD := exec.Command("sync")
|
||||
if syncErr := syncCmdDD.Run(); syncErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: sync after dd failed: %v\n", syncErr)
|
||||
}
|
||||
|
||||
// 6. Wipe Target Drives (Partition Signatures) using wipefs (as a fallback/cleanup)
|
||||
fmt.Println("--- Preparing to Wipe Target Devices (wipefs) ---")
|
||||
for _, drivePath := range b.detectedDrives { // Use detectedDrives
|
||||
if err := wipeDevice(drivePath); err != nil {
|
||||
// If dd also failed, this wipefs failure is critical. Otherwise, maybe okay.
|
||||
if ddFailed {
|
||||
return fmt.Errorf("CRITICAL: dd failed AND wipefs failed on %s: %w. Aborting installation.", drivePath, err)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "Warning: wipefs failed on %s after dd succeeded: %v. Proceeding cautiously.\n", drivePath, err)
|
||||
// Allow proceeding if dd succeeded, but log prominently.
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Println("--- Finished Wiping Target Devices (wipefs) ---")
|
||||
// Sync filesystem buffers to disk again
|
||||
fmt.Println("Syncing after wipefs...")
|
||||
syncCmdWipe := exec.Command("sync")
|
||||
if syncErr := syncCmdWipe.Run(); syncErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: sync after wipefs failed: %v\n", syncErr)
|
||||
}
|
||||
|
||||
// 7. Generate installimage Config (using detectedDrives)
|
||||
fmt.Println("Generating installimage configuration...")
|
||||
configContent, err := b.GenerateConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate config: %w", err)
|
||||
}
|
||||
|
||||
// 8. Write Config File
|
||||
fmt.Printf("Writing configuration to %s...\n", installImageConfigPath)
|
||||
fmt.Printf("--- Config Content ---\n%s\n----------------------\n", configContent) // Log the config
|
||||
err = os.WriteFile(installImageConfigPath, []byte(configContent), 0600) // Secure permissions
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write config file %s: %w", installImageConfigPath, err)
|
||||
}
|
||||
fmt.Printf("Successfully wrote configuration to %s\n", installImageConfigPath)
|
||||
|
||||
// 9. Execute installimage using multiple methods
|
||||
err = executeInstallImage(installImageConfigPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("installimage execution failed: %w", err)
|
||||
}
|
||||
|
||||
// If installimage succeeds, it usually triggers a reboot.
|
||||
// This part of the code might not be reached in a typical successful run.
|
||||
fmt.Println("installimage command finished. System should reboot shortly if successful.")
|
||||
return nil
|
||||
}
|
25
pkg/builders/hetznerinstall/cmd/build.sh
Executable file
25
pkg/builders/hetznerinstall/cmd/build.sh
Executable 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 Hetzner Installer for Linux on AMD64..."
|
||||
|
||||
# Create build directory if it doesn't exist
|
||||
mkdir -p build
|
||||
|
||||
# Build the Hetzner installer binary
|
||||
echo "Building Hetzner installer..."
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
|
||||
-ldflags="-s -w" \
|
||||
-trimpath \
|
||||
-o build/hetzner_installer \
|
||||
main.go # Reference main.go in the current directory
|
||||
|
||||
# Set executable permissions
|
||||
chmod +x build/hetzner_installer
|
||||
|
||||
# Output binary info
|
||||
echo "Build complete!"
|
||||
ls -lh build/
|
53
pkg/builders/hetznerinstall/cmd/main.go
Normal file
53
pkg/builders/hetznerinstall/cmd/main.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/freeflowuniverse/heroagent/pkg/system/builders/hetznerinstall"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Define command-line flags
|
||||
hostname := flag.String("hostname", "", "Target hostname for the server (required)")
|
||||
image := flag.String("image", hetznerinstall.DefaultImage, "OS image to install (e.g., Ubuntu-2404)")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
// Validate required flags
|
||||
if *hostname == "" {
|
||||
fmt.Fprintln(os.Stderr, "Error: -hostname flag is required.")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
// Drives are now always auto-detected by the builder
|
||||
|
||||
// Create a new HetznerInstall builder
|
||||
builder := hetznerinstall.NewBuilder().
|
||||
WithHostname(*hostname).
|
||||
WithImage(*image)
|
||||
|
||||
// Example: Add custom partitions (optional, overrides default)
|
||||
// builder.WithPartitions(
|
||||
// hetznerinstall.Partition{MountPoint: "/boot", FileSystem: "ext4", Size: "1G"},
|
||||
// hetznerinstall.Partition{MountPoint: "swap", FileSystem: "swap", Size: "4G"},
|
||||
// hetznerinstall.Partition{MountPoint: "/", FileSystem: "ext4", Size: "all"},
|
||||
// )
|
||||
|
||||
// Example: Enable Software RAID 1 (optional)
|
||||
// builder.WithSoftwareRAID(true, 1)
|
||||
|
||||
// Run the Hetzner installation process
|
||||
// The builder will handle drive detection/validation internally if drives were not set
|
||||
fmt.Printf("Starting Hetzner installation for hostname %s using image %s...\n",
|
||||
*hostname, *image)
|
||||
if err := builder.RunInstall(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error during Hetzner installation: %v\n", err)
|
||||
os.Exit(1) // Ensure we exit with non-zero status on error
|
||||
}
|
||||
|
||||
// Note: If RunInstall succeeds, the system typically reboots,
|
||||
// so this message might not always be seen.
|
||||
fmt.Println("Hetzner installation process initiated successfully!")
|
||||
}
|
134
pkg/builders/hetznerinstall/cmd/run.sh
Executable file
134
pkg/builders/hetznerinstall/cmd/run.sh
Executable file
@@ -0,0 +1,134 @@
|
||||
#!/bin/bash
|
||||
set -e # Exit immediately if a command exits with a non-zero status.
|
||||
|
||||
# --- Configuration ---
|
||||
# Required Environment Variables:
|
||||
# SERVER: IPv4 or IPv6 address of the target Hetzner server (already in Rescue Mode).
|
||||
# HOSTNAME: The desired hostname for the installed system.
|
||||
# Drives are now always auto-detected by the installer binary.
|
||||
|
||||
LOG_FILE="hetzner_install_$(date +%Y%m%d_%H%M%S).log"
|
||||
REMOTE_USER="root" # Hetzner Rescue Mode typically uses root
|
||||
REMOTE_DIR="/tmp/hetzner_installer_$$" # Temporary directory on the remote server
|
||||
BINARY_NAME="hetzner_installer"
|
||||
BUILD_DIR="build"
|
||||
|
||||
# --- Helper Functions ---
|
||||
log() {
|
||||
local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
|
||||
echo "[$timestamp] $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
cleanup_remote() {
|
||||
if [ -n "$SERVER" ]; then
|
||||
log "Cleaning up remote directory $REMOTE_DIR on $SERVER..."
|
||||
ssh "$REMOTE_USER@$SERVER" "rm -rf $REMOTE_DIR" || log "Warning: Failed to clean up remote directory (might be okay if server rebooted)."
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Main Script ---
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
log "=== Starting Hetzner Installimage Deployment ==="
|
||||
log "Log file: $LOG_FILE"
|
||||
log "IMPORTANT: Ensure the target server ($SERVER) is booted into Hetzner Rescue Mode!"
|
||||
|
||||
# Check required environment variables
|
||||
if [ -z "$SERVER" ]; then
|
||||
log "❌ ERROR: SERVER environment variable is not set."
|
||||
log "Please set it to the IP address of the target server (in Rescue Mode)."
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$HOSTNAME" ]; then
|
||||
log "❌ ERROR: HOSTNAME environment variable is not set."
|
||||
log "Please set it to the desired hostname for the installed system."
|
||||
exit 1
|
||||
fi
|
||||
# Drives are auto-detected by the binary.
|
||||
|
||||
# Validate SERVER IP (basic check)
|
||||
if ! [[ "$SERVER" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] && \
|
||||
! [[ "$SERVER" =~ ^[0-9a-fA-F:]+$ ]]; then
|
||||
log "❌ ERROR: SERVER ($SERVER) does not look like a valid IPv4 or IPv6 address."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Target Server: $SERVER"
|
||||
log "Target Hostname: $HOSTNAME"
|
||||
log "Target Drives: Auto-detected by the installer."
|
||||
|
||||
# Build the Hetzner installer binary
|
||||
log "Building $BINARY_NAME binary..."
|
||||
./build.sh | tee -a "$LOG_FILE"
|
||||
|
||||
# Check if binary exists
|
||||
BINARY_PATH="$BUILD_DIR/$BINARY_NAME"
|
||||
if [ ! -f "$BINARY_PATH" ]; then
|
||||
log "❌ ERROR: $BINARY_NAME binary not found at $BINARY_PATH after build."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Binary size:"
|
||||
ls -lh "$BINARY_PATH" | tee -a "$LOG_FILE"
|
||||
|
||||
# Set up trap for cleanup
|
||||
trap cleanup_remote EXIT
|
||||
|
||||
# Create deployment directory on server
|
||||
log "Creating temporary directory $REMOTE_DIR on server..."
|
||||
# Use -t to force pseudo-terminal allocation for mkdir (less critical but consistent)
|
||||
ssh -t "$REMOTE_USER@$SERVER" "mkdir -p $REMOTE_DIR" 2>&1 | tee -a "$LOG_FILE"
|
||||
if [ $? -ne 0 ]; then
|
||||
log "❌ ERROR: Failed to create remote directory $REMOTE_DIR on $SERVER."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Transfer the binary to the server
|
||||
log "Transferring $BINARY_NAME binary to $SERVER:$REMOTE_DIR/ ..."
|
||||
rsync -avz --progress "$BINARY_PATH" "$REMOTE_USER@$SERVER:$REMOTE_DIR/" 2>&1 | tee -a "$LOG_FILE"
|
||||
if [ $? -ne 0 ]; then
|
||||
log "❌ ERROR: Failed to transfer binary to $SERVER."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ensure binary is executable on the server
|
||||
log "Setting permissions on server..."
|
||||
# Use -t
|
||||
ssh -t "$REMOTE_USER@$SERVER" "chmod +x $REMOTE_DIR/$BINARY_NAME" 2>&1 | tee -a "$LOG_FILE" || { log "❌ ERROR: Failed to set permissions on remote binary."; exit 1; }
|
||||
# Use -t
|
||||
ssh -t "$REMOTE_USER@$SERVER" "ls -la $REMOTE_DIR/" 2>&1 | tee -a "$LOG_FILE"
|
||||
|
||||
# Construct remote command arguments (only hostname needed now)
|
||||
# Note: The binary expects -hostname
|
||||
REMOTE_CMD_ARGS="-hostname \"$HOSTNAME\""
|
||||
|
||||
# Run the Hetzner installer (Go binary) on the server
|
||||
log "Running Go installer binary $BINARY_NAME on server $SERVER..."
|
||||
REMOTE_FULL_CMD="cd $REMOTE_DIR && ./$BINARY_NAME $REMOTE_CMD_ARGS"
|
||||
log "Command: $REMOTE_FULL_CMD"
|
||||
|
||||
# Execute the command and capture output. Use -t for better output.
|
||||
INSTALL_OUTPUT=$(ssh -t "$REMOTE_USER@$SERVER" "$REMOTE_FULL_CMD" 2>&1)
|
||||
INSTALL_EXIT_CODE=$?
|
||||
|
||||
log "--- Go Installer Binary Output ---"
|
||||
echo "$INSTALL_OUTPUT" | tee -a "$LOG_FILE"
|
||||
log "--- End Go Installer Binary Output ---"
|
||||
log "Go installer binary exit code: $INSTALL_EXIT_CODE"
|
||||
|
||||
# Analyze results - relies on Go binary output now
|
||||
if [[ "$INSTALL_OUTPUT" == *"installimage command finished. System should reboot shortly if successful."* ]]; then
|
||||
log "✅ SUCCESS: Go installer reported successful initiation. The server should be rebooting into the new OS."
|
||||
log "Verification of the installed OS must be done manually after reboot."
|
||||
elif [[ "$INSTALL_OUTPUT" == *"Error during Hetzner installation"* || $INSTALL_EXIT_CODE -ne 0 ]]; then
|
||||
log "❌ ERROR: Go installer reported an error or exited with code $INSTALL_EXIT_CODE."
|
||||
log "Check the output above for details. Common issues include installimage errors or config problems."
|
||||
# Don't exit immediately, allow cleanup trap to run
|
||||
else
|
||||
# This might happen if the SSH connection is abruptly closed by the reboot during installimage
|
||||
log "⚠️ WARNING: The Go installer finished with exit code $INSTALL_EXIT_CODE, but the output might be incomplete due to server reboot."
|
||||
log "Assuming the installimage process was initiated. Manual verification is required after reboot."
|
||||
fi
|
||||
|
||||
log "=== Hetzner Installimage Deployment Script Finished ==="
|
||||
# Cleanup trap will run on exit
|
178
pkg/builders/postgresql/builder.go
Normal file
178
pkg/builders/postgresql/builder.go
Normal 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
|
||||
}
|
25
pkg/builders/postgresql/cmd/build.sh
Executable file
25
pkg/builders/postgresql/cmd/build.sh
Executable 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/
|
27
pkg/builders/postgresql/cmd/main.go
Normal file
27
pkg/builders/postgresql/cmd/main.go
Normal 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!")
|
||||
}
|
93
pkg/builders/postgresql/cmd/run.sh
Executable file
93
pkg/builders/postgresql/cmd/run.sh
Executable 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 ==="
|
55
pkg/builders/postgresql/dependencies/dependencies.go
Normal file
55
pkg/builders/postgresql/dependencies/dependencies.go
Normal 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
|
||||
}
|
172
pkg/builders/postgresql/gosp/gosp.go
Normal file
172
pkg/builders/postgresql/gosp/gosp.go
Normal 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
|
||||
}
|
50
pkg/builders/postgresql/postgres/download.go
Normal file
50
pkg/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/builders/postgresql/postgres/fs.go
Normal file
100
pkg/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/builders/postgresql/postgres/goinstall.go
Normal file
178
pkg/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/builders/postgresql/postgres/postgres.go
Normal file
505
pkg/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/builders/postgresql/postgres/tar.go
Normal file
88
pkg/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
|
||||
}
|
103
pkg/builders/postgresql/verification/verification.go
Normal file
103
pkg/builders/postgresql/verification/verification.go
Normal 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
|
||||
}
|
Reference in New Issue
Block a user