...
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