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"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
"syscall"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/spf13/cobra"
@@ -223,8 +226,39 @@ var configAutoImportCmd = &cobra.Command{
// Get data from URL or direct data
if configURL != "" {
// TODO: Implement HTTP fetch for config URL
return fmt.Errorf("URL import not yet implemented")
parsedURL, err := url.Parse(configURL)
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 != "" {
// Decode base64 if needed
if decoded, err := base64.StdEncoding.DecodeString(configData); err == nil {

View File

@@ -11,6 +11,8 @@ import (
"time"
"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"
)
@@ -80,29 +82,29 @@ type ResourceStats struct {
// HarnessReport is returned after a harness run completes.
type HarnessReport struct {
Scenario HarnessScenario
PerInstanceStats map[string]InstanceStats
QueueStats QueueStats
StalenessStats StalenessStats
ResourceStats ResourceStats
Health SchedulerHealthResponse
MaxStaleness time.Duration
RuntimeSamples []runtimeSnapshot
Scenario HarnessScenario
PerInstanceStats map[string]InstanceStats
QueueStats QueueStats
StalenessStats StalenessStats
ResourceStats ResourceStats
Health SchedulerHealthResponse
MaxStaleness time.Duration
RuntimeSamples []runtimeSnapshot
}
// Harness orchestrates the integration run.
type Harness struct {
Monitor *Monitor
Executor *fakeExecutor
cancel context.CancelFunc
scenario HarnessScenario
dataPath string
queueMax int
queueSum int
queueSamples int
maxStaleness time.Duration
sampleEvery time.Duration
runtimeSamples []runtimeSnapshot
Monitor *Monitor
Executor *fakeExecutor
cancel context.CancelFunc
scenario HarnessScenario
dataPath string
queueMax int
queueSum int
queueSamples int
maxStaleness time.Duration
sampleEvery time.Duration
runtimeSamples []runtimeSnapshot
lastRuntimeSample time.Time
}
@@ -146,9 +148,9 @@ func NewHarness(scenario HarnessScenario) *Harness {
case "pve":
monitor.pveClients[inst.Name] = noopPVEClient{}
case "pbs":
// TODO: add PBS stub when needed.
monitor.pbsClients[inst.Name] = &pbs.Client{}
case "pmg":
// TODO: add PMG stub when needed.
monitor.pmgClients[inst.Name] = &pmg.Client{}
default:
// Unsupported types are ignored for now.
}
@@ -251,12 +253,12 @@ loop:
GCCountStart: startSample.NumGC,
GCCountEnd: endSample.NumGC,
},
Health: health,
MaxStaleness: h.maxStaleness,
Health: health,
MaxStaleness: h.maxStaleness,
RuntimeSamples: runtimeSamplesCopy,
}
return report
return report
}
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) {
return nil, nil
}
func (noopPVEClient) GetStorage(ctx context.Context, node string) ([]proxmox.Storage, 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) GetStorage(ctx context.Context, node string) ([]proxmox.Storage, 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) {
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) {
return nil, nil
}
func (noopPVEClient) GetDisks(ctx context.Context, node string) ([]proxmox.Disk, 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 }
func (noopPVEClient) GetDisks(ctx context.Context, node string) ([]proxmox.Disk, 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 }