feat: add PBS/PMG stubs to test harness and implement HTTP config fetch

Resolves two remaining TODOs from codebase audit.

## 1. PBS/PMG Test Harness Stubs

**Location:** internal/monitoring/harness_integration.go:149-151

**Changes:**
- Added PBS client stub registration: `monitor.pbsClients[inst.Name] = &pbs.Client{}`
- Added PMG client stub registration: `monitor.pmgClients[inst.Name] = &pmg.Client{}`
- Added imports for pkg/pbs and pkg/pmg

**Purpose:**
Enables integration test scenarios to include PBS and PMG instance types
alongside existing PVE support. Stubs allow scheduler to register and
execute tasks for these instance types during integration testing.

**Testing:**
 TestAdaptiveSchedulerIntegration passes (55.5s)
 Integration test harness now supports all three instance types

## 2. HTTP Config URL Fetch

**Location:** cmd/pulse/config.go:226-261

**Problem:**
`PULSE_INIT_CONFIG_URL` was recognized but not implemented, returning
"URL import not yet implemented" error.

**Implementation:**
- URL validation (http/https schemes only)
- HTTP client with 15 second timeout
- Status code validation (2xx required)
- Empty response detection
- Base64 decoding with fallback to raw data
- Matches existing env-var behavior for `PULSE_INIT_CONFIG_DATA`

**Security:**
- Both HTTP and HTTPS supported (HTTPS recommended for production)
- URL scheme validation prevents file:// or other protocols
- Timeout prevents hanging on unresponsive servers

**Usage:**
```bash
export PULSE_INIT_CONFIG_URL="https://config-server/encrypted-config"
export PULSE_INIT_CONFIG_PASSPHRASE="secret"
pulse config auto-import
```

**Testing:**
 Code compiles cleanly
 Follows same pattern as existing PULSE_INIT_CONFIG_DATA handling

## Impact

- Completes integration test infrastructure for all instance types
- Enables automated config distribution via HTTP(S) for container deployments
- Removes last TODOs from codebase (no TODO/FIXME remaining in Go files)
This commit is contained in:
rcourtman
2025-10-20 16:05:45 +00:00
parent 656ae0d254
commit dec85a4efe
2 changed files with 72 additions and 32 deletions

View File

@@ -5,9 +5,12 @@ import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http"
"net/url"
"os" "os"
"strings" "strings"
"syscall" "syscall"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/config" "github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -223,8 +226,39 @@ var configAutoImportCmd = &cobra.Command{
// Get data from URL or direct data // Get data from URL or direct data
if configURL != "" { if configURL != "" {
// TODO: Implement HTTP fetch for config URL parsedURL, err := url.Parse(configURL)
return fmt.Errorf("URL import not yet implemented") if err != nil {
return fmt.Errorf("invalid PULSE_INIT_CONFIG_URL: %w", err)
}
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
return fmt.Errorf("unsupported URL scheme %q for PULSE_INIT_CONFIG_URL", parsedURL.Scheme)
}
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Get(configURL)
if err != nil {
return fmt.Errorf("failed to fetch configuration from URL: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("failed to fetch configuration from URL: %s", resp.Status)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read configuration response: %w", err)
}
if len(body) == 0 {
return fmt.Errorf("configuration response from URL was empty")
}
trimmed := strings.TrimSpace(string(body))
if decoded, err := base64.StdEncoding.DecodeString(trimmed); err == nil {
encryptedData = string(decoded)
} else {
encryptedData = string(body)
}
} else if configData != "" { } else if configData != "" {
// Decode base64 if needed // Decode base64 if needed
if decoded, err := base64.StdEncoding.DecodeString(configData); err == nil { if decoded, err := base64.StdEncoding.DecodeString(configData); err == nil {

View File

@@ -11,6 +11,8 @@ import (
"time" "time"
"github.com/rcourtman/pulse-go-rewrite/internal/config" "github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/rcourtman/pulse-go-rewrite/pkg/pbs"
"github.com/rcourtman/pulse-go-rewrite/pkg/pmg"
"github.com/rcourtman/pulse-go-rewrite/pkg/proxmox" "github.com/rcourtman/pulse-go-rewrite/pkg/proxmox"
) )
@@ -80,29 +82,29 @@ type ResourceStats struct {
// HarnessReport is returned after a harness run completes. // HarnessReport is returned after a harness run completes.
type HarnessReport struct { type HarnessReport struct {
Scenario HarnessScenario Scenario HarnessScenario
PerInstanceStats map[string]InstanceStats PerInstanceStats map[string]InstanceStats
QueueStats QueueStats QueueStats QueueStats
StalenessStats StalenessStats StalenessStats StalenessStats
ResourceStats ResourceStats ResourceStats ResourceStats
Health SchedulerHealthResponse Health SchedulerHealthResponse
MaxStaleness time.Duration MaxStaleness time.Duration
RuntimeSamples []runtimeSnapshot RuntimeSamples []runtimeSnapshot
} }
// Harness orchestrates the integration run. // Harness orchestrates the integration run.
type Harness struct { type Harness struct {
Monitor *Monitor Monitor *Monitor
Executor *fakeExecutor Executor *fakeExecutor
cancel context.CancelFunc cancel context.CancelFunc
scenario HarnessScenario scenario HarnessScenario
dataPath string dataPath string
queueMax int queueMax int
queueSum int queueSum int
queueSamples int queueSamples int
maxStaleness time.Duration maxStaleness time.Duration
sampleEvery time.Duration sampleEvery time.Duration
runtimeSamples []runtimeSnapshot runtimeSamples []runtimeSnapshot
lastRuntimeSample time.Time lastRuntimeSample time.Time
} }
@@ -146,9 +148,9 @@ func NewHarness(scenario HarnessScenario) *Harness {
case "pve": case "pve":
monitor.pveClients[inst.Name] = noopPVEClient{} monitor.pveClients[inst.Name] = noopPVEClient{}
case "pbs": case "pbs":
// TODO: add PBS stub when needed. monitor.pbsClients[inst.Name] = &pbs.Client{}
case "pmg": case "pmg":
// TODO: add PMG stub when needed. monitor.pmgClients[inst.Name] = &pmg.Client{}
default: default:
// Unsupported types are ignored for now. // Unsupported types are ignored for now.
} }
@@ -251,12 +253,12 @@ loop:
GCCountStart: startSample.NumGC, GCCountStart: startSample.NumGC,
GCCountEnd: endSample.NumGC, GCCountEnd: endSample.NumGC,
}, },
Health: health, Health: health,
MaxStaleness: h.maxStaleness, MaxStaleness: h.maxStaleness,
RuntimeSamples: runtimeSamplesCopy, RuntimeSamples: runtimeSamplesCopy,
} }
return report return report
} }
func (h *Harness) schedule(now time.Time) { func (h *Harness) schedule(now time.Time) {
@@ -386,9 +388,11 @@ func (noopPVEClient) GetVMs(ctx context.Context, node string) ([]proxmox.VM, err
func (noopPVEClient) GetContainers(ctx context.Context, node string) ([]proxmox.Container, error) { func (noopPVEClient) GetContainers(ctx context.Context, node string) ([]proxmox.Container, error) {
return nil, nil return nil, nil
} }
func (noopPVEClient) GetStorage(ctx context.Context, node string) ([]proxmox.Storage, error) { return nil, nil } func (noopPVEClient) GetStorage(ctx context.Context, node string) ([]proxmox.Storage, error) {
func (noopPVEClient) GetAllStorage(ctx context.Context) ([]proxmox.Storage, error) { return nil, nil } return nil, nil
func (noopPVEClient) GetBackupTasks(ctx context.Context) ([]proxmox.Task, error) { return nil, nil } }
func (noopPVEClient) GetAllStorage(ctx context.Context) ([]proxmox.Storage, error) { return nil, nil }
func (noopPVEClient) GetBackupTasks(ctx context.Context) ([]proxmox.Task, error) { return nil, nil }
func (noopPVEClient) GetStorageContent(ctx context.Context, node, storage string) ([]proxmox.StorageContent, error) { func (noopPVEClient) GetStorageContent(ctx context.Context, node, storage string) ([]proxmox.StorageContent, error) {
return nil, nil return nil, nil
} }
@@ -423,6 +427,8 @@ func (noopPVEClient) GetZFSPoolStatus(ctx context.Context, node string) ([]proxm
func (noopPVEClient) GetZFSPoolsWithDetails(ctx context.Context, node string) ([]proxmox.ZFSPoolInfo, error) { func (noopPVEClient) GetZFSPoolsWithDetails(ctx context.Context, node string) ([]proxmox.ZFSPoolInfo, error) {
return nil, nil return nil, nil
} }
func (noopPVEClient) GetDisks(ctx context.Context, node string) ([]proxmox.Disk, error) { return nil, nil } func (noopPVEClient) GetDisks(ctx context.Context, node string) ([]proxmox.Disk, error) {
func (noopPVEClient) GetCephStatus(ctx context.Context) (*proxmox.CephStatus, error) { return nil, nil } return nil, nil
func (noopPVEClient) GetCephDF(ctx context.Context) (*proxmox.CephDF, error) { return nil, nil } }
func (noopPVEClient) GetCephStatus(ctx context.Context) (*proxmox.CephStatus, error) { return nil, nil }
func (noopPVEClient) GetCephDF(ctx context.Context) (*proxmox.CephDF, error) { return nil, nil }