Update .gitignore, remove package-lock.json, and enhance Cloudflare and Proxmox adapters
- Added lock file exclusions for pnpm in .gitignore. - Removed obsolete package-lock.json from the api and portal directories. - Enhanced Cloudflare adapter with additional interfaces for zones and tunnels. - Improved Proxmox adapter error handling and logging for API requests. - Updated Proxmox VM parameters with validation rules in the API schema. - Enhanced documentation for Proxmox VM specifications and examples.
This commit is contained in:
@@ -19,6 +19,7 @@ import (
|
||||
proxmoxv1alpha1 "github.com/sankofa/crossplane-provider-proxmox/apis/v1alpha1"
|
||||
"github.com/sankofa/crossplane-provider-proxmox/pkg/proxmox"
|
||||
"github.com/sankofa/crossplane-provider-proxmox/pkg/quota"
|
||||
"github.com/sankofa/crossplane-provider-proxmox/pkg/utils"
|
||||
)
|
||||
|
||||
// ProxmoxVMReconciler reconciles a ProxmoxVM object
|
||||
@@ -92,23 +93,123 @@ func (r *ProxmoxVMReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
|
||||
return ctrl.Result{}, errors.Wrap(err, "cannot create Proxmox client")
|
||||
}
|
||||
|
||||
// Check node health before proceeding
|
||||
if err := proxmoxClient.CheckNodeHealth(ctx, vm.Spec.ForProvider.Node); err != nil {
|
||||
logger.Error(err, "node health check failed", "node", vm.Spec.ForProvider.Node)
|
||||
// Update status with error condition
|
||||
vm.Status.Conditions = append(vm.Status.Conditions, metav1.Condition{
|
||||
Type: "NodeUnhealthy",
|
||||
Status: "True",
|
||||
Reason: "HealthCheckFailed",
|
||||
Message: err.Error(),
|
||||
LastTransitionTime: metav1.Now(),
|
||||
})
|
||||
r.Status().Update(ctx, &vm)
|
||||
return ctrl.Result{RequeueAfter: 2 * time.Minute}, nil
|
||||
}
|
||||
// Check node health before proceeding
|
||||
if err := proxmoxClient.CheckNodeHealth(ctx, vm.Spec.ForProvider.Node); err != nil {
|
||||
logger.Error(err, "node health check failed", "node", vm.Spec.ForProvider.Node)
|
||||
// Update status with error condition
|
||||
vm.Status.Conditions = append(vm.Status.Conditions, metav1.Condition{
|
||||
Type: "NodeUnhealthy",
|
||||
Status: "True",
|
||||
Reason: "HealthCheckFailed",
|
||||
Message: err.Error(),
|
||||
LastTransitionTime: metav1.Now(),
|
||||
})
|
||||
r.Status().Update(ctx, &vm)
|
||||
return ctrl.Result{RequeueAfter: 2 * time.Minute}, nil
|
||||
}
|
||||
|
||||
// Validate network bridge exists on node
|
||||
if vm.Spec.ForProvider.Network != "" {
|
||||
networkExists, err := proxmoxClient.NetworkExists(ctx, vm.Spec.ForProvider.Node, vm.Spec.ForProvider.Network)
|
||||
if err != nil {
|
||||
logger.Error(err, "failed to check network bridge", "node", vm.Spec.ForProvider.Node, "network", vm.Spec.ForProvider.Network)
|
||||
// Don't fail on check error - network might exist but API call failed
|
||||
} else if !networkExists {
|
||||
err := fmt.Errorf("network bridge '%s' does not exist on node '%s'", vm.Spec.ForProvider.Network, vm.Spec.ForProvider.Node)
|
||||
logger.Error(err, "network bridge validation failed")
|
||||
vm.Status.Conditions = append(vm.Status.Conditions, metav1.Condition{
|
||||
Type: "ValidationFailed",
|
||||
Status: "True",
|
||||
Reason: "NetworkNotFound",
|
||||
Message: err.Error(),
|
||||
LastTransitionTime: metav1.Now(),
|
||||
})
|
||||
r.Status().Update(ctx, &vm)
|
||||
return ctrl.Result{}, errors.Wrap(err, "network bridge validation failed")
|
||||
}
|
||||
}
|
||||
|
||||
// Reconcile VM
|
||||
if vm.Status.VMID == 0 {
|
||||
// Validate VM specification before creation
|
||||
if err := utils.ValidateVMName(vm.Spec.ForProvider.Name); err != nil {
|
||||
logger.Error(err, "invalid VM name")
|
||||
vm.Status.Conditions = append(vm.Status.Conditions, metav1.Condition{
|
||||
Type: "ValidationFailed",
|
||||
Status: "True",
|
||||
Reason: "InvalidVMName",
|
||||
Message: err.Error(),
|
||||
LastTransitionTime: metav1.Now(),
|
||||
})
|
||||
r.Status().Update(ctx, &vm)
|
||||
return ctrl.Result{}, errors.Wrap(err, "invalid VM name")
|
||||
}
|
||||
|
||||
if err := utils.ValidateMemory(vm.Spec.ForProvider.Memory); err != nil {
|
||||
logger.Error(err, "invalid memory specification")
|
||||
vm.Status.Conditions = append(vm.Status.Conditions, metav1.Condition{
|
||||
Type: "ValidationFailed",
|
||||
Status: "True",
|
||||
Reason: "InvalidMemory",
|
||||
Message: err.Error(),
|
||||
LastTransitionTime: metav1.Now(),
|
||||
})
|
||||
r.Status().Update(ctx, &vm)
|
||||
return ctrl.Result{}, errors.Wrap(err, "invalid memory specification")
|
||||
}
|
||||
|
||||
if err := utils.ValidateDisk(vm.Spec.ForProvider.Disk); err != nil {
|
||||
logger.Error(err, "invalid disk specification")
|
||||
vm.Status.Conditions = append(vm.Status.Conditions, metav1.Condition{
|
||||
Type: "ValidationFailed",
|
||||
Status: "True",
|
||||
Reason: "InvalidDisk",
|
||||
Message: err.Error(),
|
||||
LastTransitionTime: metav1.Now(),
|
||||
})
|
||||
r.Status().Update(ctx, &vm)
|
||||
return ctrl.Result{}, errors.Wrap(err, "invalid disk specification")
|
||||
}
|
||||
|
||||
if err := utils.ValidateCPU(vm.Spec.ForProvider.CPU); err != nil {
|
||||
logger.Error(err, "invalid CPU specification")
|
||||
vm.Status.Conditions = append(vm.Status.Conditions, metav1.Condition{
|
||||
Type: "ValidationFailed",
|
||||
Status: "True",
|
||||
Reason: "InvalidCPU",
|
||||
Message: err.Error(),
|
||||
LastTransitionTime: metav1.Now(),
|
||||
})
|
||||
r.Status().Update(ctx, &vm)
|
||||
return ctrl.Result{}, errors.Wrap(err, "invalid CPU specification")
|
||||
}
|
||||
|
||||
if err := utils.ValidateNetworkBridge(vm.Spec.ForProvider.Network); err != nil {
|
||||
logger.Error(err, "invalid network bridge specification")
|
||||
vm.Status.Conditions = append(vm.Status.Conditions, metav1.Condition{
|
||||
Type: "ValidationFailed",
|
||||
Status: "True",
|
||||
Reason: "InvalidNetwork",
|
||||
Message: err.Error(),
|
||||
LastTransitionTime: metav1.Now(),
|
||||
})
|
||||
r.Status().Update(ctx, &vm)
|
||||
return ctrl.Result{}, errors.Wrap(err, "invalid network bridge specification")
|
||||
}
|
||||
|
||||
if err := utils.ValidateImageSpec(vm.Spec.ForProvider.Image); err != nil {
|
||||
logger.Error(err, "invalid image specification")
|
||||
vm.Status.Conditions = append(vm.Status.Conditions, metav1.Condition{
|
||||
Type: "ValidationFailed",
|
||||
Status: "True",
|
||||
Reason: "InvalidImage",
|
||||
Message: err.Error(),
|
||||
LastTransitionTime: metav1.Now(),
|
||||
})
|
||||
r.Status().Update(ctx, &vm)
|
||||
return ctrl.Result{}, errors.Wrap(err, "invalid image specification")
|
||||
}
|
||||
|
||||
// Create VM
|
||||
logger.Info("Creating VM", "name", vm.Name, "node", vm.Spec.ForProvider.Node)
|
||||
|
||||
@@ -137,8 +238,8 @@ func (r *ProxmoxVMReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
|
||||
quotaClient := quota.NewQuotaClient(apiURL, apiToken)
|
||||
|
||||
// Parse memory from string (e.g., "8Gi" -> 8)
|
||||
memoryGB := parseMemoryToGB(vm.Spec.ForProvider.Memory)
|
||||
diskGB := parseDiskToGB(vm.Spec.ForProvider.Disk)
|
||||
memoryGB := utils.ParseMemoryToGB(vm.Spec.ForProvider.Memory)
|
||||
diskGB := utils.ParseDiskToGB(vm.Spec.ForProvider.Disk)
|
||||
|
||||
resourceRequest := quota.ResourceRequest{
|
||||
Compute: "a.ComputeRequest{
|
||||
@@ -236,8 +337,10 @@ func (r *ProxmoxVMReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
|
||||
}
|
||||
|
||||
vm.Status.VMID = createdVM.ID
|
||||
vm.Status.State = createdVM.Status
|
||||
vm.Status.IPAddress = createdVM.IP
|
||||
// Set initial status conservatively - VM is created but may not be running yet
|
||||
vm.Status.State = "created" // Use "created" instead of actual status until verified
|
||||
// IP address may not be available immediately - will be updated in next reconcile
|
||||
vm.Status.IPAddress = ""
|
||||
|
||||
// Clear any previous failure conditions
|
||||
for i := len(vm.Status.Conditions) - 1; i >= 0; i-- {
|
||||
@@ -487,66 +590,7 @@ func (r *ProxmoxVMReconciler) findSite(config *proxmoxv1alpha1.ProviderConfig, s
|
||||
return nil, fmt.Errorf("site %s not found", siteName)
|
||||
}
|
||||
|
||||
// Helper functions for quota enforcement
|
||||
func parseMemoryToGB(memory string) int {
|
||||
if memory == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Remove whitespace and convert to lowercase
|
||||
memory = strings.TrimSpace(strings.ToLower(memory))
|
||||
|
||||
// Parse memory string (e.g., "8Gi", "8G", "8192Mi")
|
||||
if strings.HasSuffix(memory, "gi") || strings.HasSuffix(memory, "g") {
|
||||
value, err := strconv.Atoi(strings.TrimSuffix(strings.TrimSuffix(memory, "gi"), "g"))
|
||||
if err == nil {
|
||||
return value
|
||||
}
|
||||
} else if strings.HasSuffix(memory, "mi") || strings.HasSuffix(memory, "m") {
|
||||
value, err := strconv.Atoi(strings.TrimSuffix(strings.TrimSuffix(memory, "mi"), "m"))
|
||||
if err == nil {
|
||||
return value / 1024 // Convert MiB to GiB
|
||||
}
|
||||
} else {
|
||||
// Try parsing as number (assume GB)
|
||||
value, err := strconv.Atoi(memory)
|
||||
if err == nil {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func parseDiskToGB(disk string) int {
|
||||
if disk == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Remove whitespace and convert to lowercase
|
||||
disk = strings.TrimSpace(strings.ToLower(disk))
|
||||
|
||||
// Parse disk string (e.g., "100Gi", "100G", "100Ti")
|
||||
if strings.HasSuffix(disk, "gi") || strings.HasSuffix(disk, "g") {
|
||||
value, err := strconv.Atoi(strings.TrimSuffix(strings.TrimSuffix(disk, "gi"), "g"))
|
||||
if err == nil {
|
||||
return value
|
||||
}
|
||||
} else if strings.HasSuffix(disk, "ti") || strings.HasSuffix(disk, "t") {
|
||||
value, err := strconv.Atoi(strings.TrimSuffix(strings.TrimSuffix(disk, "ti"), "t"))
|
||||
if err == nil {
|
||||
return value * 1024 // Convert TiB to GiB
|
||||
}
|
||||
} else {
|
||||
// Try parsing as number (assume GB)
|
||||
value, err := strconv.Atoi(disk)
|
||||
if err == nil {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
// Helper functions for quota enforcement (use shared utils)
|
||||
|
||||
func intPtr(i int) *int {
|
||||
return &i
|
||||
|
||||
@@ -74,12 +74,27 @@ func categorizeError(errorStr string) ErrorCategory {
|
||||
}
|
||||
}
|
||||
|
||||
// Authentication errors (non-retryable without credential fix)
|
||||
if strings.Contains(errorStr, "authentication") ||
|
||||
strings.Contains(errorStr, "unauthorized") ||
|
||||
strings.Contains(errorStr, "401") ||
|
||||
strings.Contains(errorStr, "invalid credentials") ||
|
||||
strings.Contains(errorStr, "forbidden") ||
|
||||
strings.Contains(errorStr, "403") {
|
||||
return ErrorCategory{
|
||||
Type: "AuthenticationError",
|
||||
Reason: "AuthenticationFailed",
|
||||
}
|
||||
}
|
||||
|
||||
// Network/Connection errors (retryable)
|
||||
if strings.Contains(errorStr, "network") ||
|
||||
strings.Contains(errorStr, "connection") ||
|
||||
strings.Contains(errorStr, "timeout") ||
|
||||
strings.Contains(errorStr, "502") ||
|
||||
strings.Contains(errorStr, "503") {
|
||||
strings.Contains(errorStr, "503") ||
|
||||
strings.Contains(errorStr, "connection refused") ||
|
||||
strings.Contains(errorStr, "connection reset") {
|
||||
return ErrorCategory{
|
||||
Type: "NetworkError",
|
||||
Reason: "TransientNetworkFailure",
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
package virtualmachine
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCategorizeError(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
errorStr string
|
||||
wantType string
|
||||
wantReason string
|
||||
}{
|
||||
// API not supported errors
|
||||
{
|
||||
name: "501 error",
|
||||
errorStr: "501 Not Implemented",
|
||||
wantType: "APINotSupported",
|
||||
wantReason: "ImportDiskAPINotImplemented",
|
||||
},
|
||||
{
|
||||
name: "not implemented",
|
||||
errorStr: "importdisk API is not implemented",
|
||||
wantType: "APINotSupported",
|
||||
wantReason: "ImportDiskAPINotImplemented",
|
||||
},
|
||||
{
|
||||
name: "importdisk error",
|
||||
errorStr: "failed to use importdisk",
|
||||
wantType: "APINotSupported",
|
||||
wantReason: "ImportDiskAPINotImplemented",
|
||||
},
|
||||
|
||||
// Configuration errors
|
||||
{
|
||||
name: "cannot get provider config",
|
||||
errorStr: "cannot get provider config",
|
||||
wantType: "ConfigurationError",
|
||||
wantReason: "InvalidConfiguration",
|
||||
},
|
||||
{
|
||||
name: "cannot get credentials",
|
||||
errorStr: "cannot get credentials",
|
||||
wantType: "ConfigurationError",
|
||||
wantReason: "InvalidConfiguration",
|
||||
},
|
||||
{
|
||||
name: "cannot find site",
|
||||
errorStr: "cannot find site",
|
||||
wantType: "ConfigurationError",
|
||||
wantReason: "InvalidConfiguration",
|
||||
},
|
||||
{
|
||||
name: "cannot create proxmox client",
|
||||
errorStr: "cannot create Proxmox client",
|
||||
wantType: "ConfigurationError",
|
||||
wantReason: "InvalidConfiguration",
|
||||
},
|
||||
|
||||
// Quota errors
|
||||
{
|
||||
name: "quota exceeded",
|
||||
errorStr: "quota exceeded",
|
||||
wantType: "QuotaExceeded",
|
||||
wantReason: "ResourceQuotaExceeded",
|
||||
},
|
||||
{
|
||||
name: "resource exceeded",
|
||||
errorStr: "resource exceeded",
|
||||
wantType: "QuotaExceeded",
|
||||
wantReason: "ResourceQuotaExceeded",
|
||||
},
|
||||
|
||||
// Node health errors
|
||||
{
|
||||
name: "node unhealthy",
|
||||
errorStr: "node is unhealthy",
|
||||
wantType: "NodeUnhealthy",
|
||||
wantReason: "NodeHealthCheckFailed",
|
||||
},
|
||||
{
|
||||
name: "node not reachable",
|
||||
errorStr: "node is not reachable",
|
||||
wantType: "NodeUnhealthy",
|
||||
wantReason: "NodeHealthCheckFailed",
|
||||
},
|
||||
{
|
||||
name: "node offline",
|
||||
errorStr: "node is offline",
|
||||
wantType: "NodeUnhealthy",
|
||||
wantReason: "NodeHealthCheckFailed",
|
||||
},
|
||||
|
||||
// Image errors
|
||||
{
|
||||
name: "image not found",
|
||||
errorStr: "image not found in storage",
|
||||
wantType: "ImageNotFound",
|
||||
wantReason: "ImageNotFoundInStorage",
|
||||
},
|
||||
{
|
||||
name: "cannot find image",
|
||||
errorStr: "cannot find image",
|
||||
wantType: "ImageNotFound",
|
||||
wantReason: "ImageNotFoundInStorage",
|
||||
},
|
||||
|
||||
// Lock errors
|
||||
{
|
||||
name: "lock file error",
|
||||
errorStr: "lock file timeout",
|
||||
wantType: "LockError",
|
||||
wantReason: "LockFileTimeout",
|
||||
},
|
||||
{
|
||||
name: "timeout error",
|
||||
errorStr: "operation timeout",
|
||||
wantType: "LockError",
|
||||
wantReason: "LockFileTimeout",
|
||||
},
|
||||
|
||||
// Authentication errors
|
||||
{
|
||||
name: "authentication error",
|
||||
errorStr: "authentication failed",
|
||||
wantType: "AuthenticationError",
|
||||
wantReason: "AuthenticationFailed",
|
||||
},
|
||||
{
|
||||
name: "unauthorized",
|
||||
errorStr: "unauthorized access",
|
||||
wantType: "AuthenticationError",
|
||||
wantReason: "AuthenticationFailed",
|
||||
},
|
||||
{
|
||||
name: "401 error",
|
||||
errorStr: "401 Unauthorized",
|
||||
wantType: "AuthenticationError",
|
||||
wantReason: "AuthenticationFailed",
|
||||
},
|
||||
{
|
||||
name: "invalid credentials",
|
||||
errorStr: "invalid credentials",
|
||||
wantType: "AuthenticationError",
|
||||
wantReason: "AuthenticationFailed",
|
||||
},
|
||||
{
|
||||
name: "forbidden",
|
||||
errorStr: "forbidden",
|
||||
wantType: "AuthenticationError",
|
||||
wantReason: "AuthenticationFailed",
|
||||
},
|
||||
{
|
||||
name: "403 error",
|
||||
errorStr: "403 Forbidden",
|
||||
wantType: "AuthenticationError",
|
||||
wantReason: "AuthenticationFailed",
|
||||
},
|
||||
|
||||
// Network errors
|
||||
{
|
||||
name: "network error",
|
||||
errorStr: "network connection failed",
|
||||
wantType: "NetworkError",
|
||||
wantReason: "TransientNetworkFailure",
|
||||
},
|
||||
{
|
||||
name: "connection error",
|
||||
errorStr: "connection refused",
|
||||
wantType: "NetworkError",
|
||||
wantReason: "TransientNetworkFailure",
|
||||
},
|
||||
{
|
||||
name: "connection reset",
|
||||
errorStr: "connection reset",
|
||||
wantType: "NetworkError",
|
||||
wantReason: "TransientNetworkFailure",
|
||||
},
|
||||
{
|
||||
name: "502 error",
|
||||
errorStr: "502 Bad Gateway",
|
||||
wantType: "NetworkError",
|
||||
wantReason: "TransientNetworkFailure",
|
||||
},
|
||||
{
|
||||
name: "503 error",
|
||||
errorStr: "503 Service Unavailable",
|
||||
wantType: "NetworkError",
|
||||
wantReason: "TransientNetworkFailure",
|
||||
},
|
||||
|
||||
// Creation failures
|
||||
{
|
||||
name: "cannot create vm",
|
||||
errorStr: "cannot create VM",
|
||||
wantType: "CreationFailed",
|
||||
wantReason: "VMCreationFailed",
|
||||
},
|
||||
{
|
||||
name: "failed to create",
|
||||
errorStr: "failed to create VM",
|
||||
wantType: "CreationFailed",
|
||||
wantReason: "VMCreationFailed",
|
||||
},
|
||||
|
||||
// Unknown errors
|
||||
{
|
||||
name: "unknown error",
|
||||
errorStr: "something went wrong",
|
||||
wantType: "Failed",
|
||||
wantReason: "UnknownError",
|
||||
},
|
||||
{
|
||||
name: "empty error",
|
||||
errorStr: "",
|
||||
wantType: "Failed",
|
||||
wantReason: "UnknownError",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := categorizeError(tt.errorStr)
|
||||
if result.Type != tt.wantType {
|
||||
t.Errorf("categorizeError(%q).Type = %q, want %q", tt.errorStr, result.Type, tt.wantType)
|
||||
}
|
||||
if result.Reason != tt.wantReason {
|
||||
t.Errorf("categorizeError(%q).Reason = %q, want %q", tt.errorStr, result.Reason, tt.wantReason)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCategorizeError_CaseInsensitive(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
errorStr string
|
||||
wantType string
|
||||
}{
|
||||
{"uppercase", "AUTHENTICATION FAILED", "AuthenticationError"},
|
||||
{"mixed case", "AuThEnTiCaTiOn FaIlEd", "AuthenticationError"},
|
||||
{"lowercase", "authentication failed", "AuthenticationError"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := categorizeError(tt.errorStr)
|
||||
if result.Type != tt.wantType {
|
||||
t.Errorf("categorizeError(%q).Type = %q, want %q", tt.errorStr, result.Type, tt.wantType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
// +build integration
|
||||
|
||||
package virtualmachine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
||||
|
||||
proxmoxv1alpha1 "github.com/sankofa/crossplane-provider-proxmox/apis/v1alpha1"
|
||||
)
|
||||
|
||||
// Integration tests for VM creation scenarios
|
||||
// These tests require a test environment with Proxmox API access
|
||||
// Run with: go test -tags=integration ./pkg/controller/virtualmachine/...
|
||||
|
||||
func TestVMCreationWithTemplateCloning(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test")
|
||||
}
|
||||
|
||||
// This is a placeholder for integration test
|
||||
// In a real scenario, this would:
|
||||
// 1. Set up test environment
|
||||
// 2. Create a template VM
|
||||
// 3. Create a ProxmoxVM with template ID
|
||||
// 4. Verify VM is created correctly
|
||||
// 5. Clean up
|
||||
|
||||
t.Log("Integration test: VM creation with template cloning")
|
||||
t.Skip("Requires Proxmox test environment")
|
||||
}
|
||||
|
||||
func TestVMCreationWithCloudImageImport(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test")
|
||||
}
|
||||
|
||||
t.Log("Integration test: VM creation with cloud image import")
|
||||
t.Skip("Requires Proxmox test environment with importdisk API support")
|
||||
}
|
||||
|
||||
func TestVMCreationWithPreImportedImages(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test")
|
||||
}
|
||||
|
||||
t.Log("Integration test: VM creation with pre-imported images")
|
||||
t.Skip("Requires Proxmox test environment")
|
||||
}
|
||||
|
||||
func TestVMValidationScenarios(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
vm *proxmoxv1alpha1.ProxmoxVM
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid VM spec",
|
||||
vm: &proxmoxv1alpha1.ProxmoxVM{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-vm-valid",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: proxmoxv1alpha1.ProxmoxVMSpec{
|
||||
ForProvider: proxmoxv1alpha1.ProxmoxVMParameters{
|
||||
Node: "test-node",
|
||||
Name: "test-vm",
|
||||
CPU: 2,
|
||||
Memory: "4Gi",
|
||||
Disk: "50Gi",
|
||||
Storage: "local-lvm",
|
||||
Network: "vmbr0",
|
||||
Image: "100", // Template ID
|
||||
Site: "test-site",
|
||||
},
|
||||
ProviderConfigReference: &proxmoxv1alpha1.ProviderConfigReference{
|
||||
Name: "test-provider-config",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid VM name",
|
||||
vm: &proxmoxv1alpha1.ProxmoxVM{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-vm-invalid-name",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: proxmoxv1alpha1.ProxmoxVMSpec{
|
||||
ForProvider: proxmoxv1alpha1.ProxmoxVMParameters{
|
||||
Node: "test-node",
|
||||
Name: "vm@invalid", // Invalid character
|
||||
CPU: 2,
|
||||
Memory: "4Gi",
|
||||
Disk: "50Gi",
|
||||
Storage: "local-lvm",
|
||||
Network: "vmbr0",
|
||||
Image: "100",
|
||||
Site: "test-site",
|
||||
},
|
||||
ProviderConfigReference: &proxmoxv1alpha1.ProviderConfigReference{
|
||||
Name: "test-provider-config",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// This would test validation in a real integration scenario
|
||||
// For now, we just verify the test structure
|
||||
require.NotNil(t, tt.vm)
|
||||
t.Logf("Test case: %s", tt.name)
|
||||
})
|
||||
}
|
||||
|
||||
t.Skip("Requires Proxmox test environment")
|
||||
}
|
||||
|
||||
func TestMultiSiteVMDeployment(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test")
|
||||
}
|
||||
|
||||
// Test VM creation across different sites
|
||||
t.Log("Integration test: Multi-site VM deployment")
|
||||
t.Skip("Requires multiple Proxmox sites configured")
|
||||
}
|
||||
|
||||
func TestNetworkBridgeValidation(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
network string
|
||||
expectExists bool
|
||||
}{
|
||||
{"existing bridge", "vmbr0", true},
|
||||
{"non-existent bridge", "vmbr999", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// In real test, would call NetworkExists and verify
|
||||
t.Logf("Test network bridge: %s, expect exists: %v", tt.network, tt.expectExists)
|
||||
})
|
||||
}
|
||||
|
||||
t.Skip("Requires Proxmox test environment")
|
||||
}
|
||||
|
||||
func TestErrorRecoveryScenarios(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test")
|
||||
}
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
errorType string
|
||||
shouldRetry bool
|
||||
}{
|
||||
{"network error", "NetworkError", true},
|
||||
{"authentication error", "AuthenticationError", false},
|
||||
{"quota exceeded", "QuotaExceeded", false},
|
||||
{"node unhealthy", "NodeUnhealthy", true},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
// Test error recovery logic
|
||||
t.Logf("Test error scenario: %s, should retry: %v", scenario.name, scenario.shouldRetry)
|
||||
})
|
||||
}
|
||||
|
||||
t.Skip("Requires Proxmox test environment")
|
||||
}
|
||||
|
||||
func TestCloudInitConfiguration(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test")
|
||||
}
|
||||
|
||||
t.Log("Integration test: Cloud-init configuration")
|
||||
t.Skip("Requires Proxmox test environment with cloud-init support")
|
||||
}
|
||||
|
||||
// setupTestEnvironment creates a test Kubernetes environment
|
||||
// This is a placeholder - in real tests, this would use envtest
|
||||
func setupTestEnvironment(t *testing.T) (*envtest.Environment, client.Client, func()) {
|
||||
t.Helper()
|
||||
|
||||
// Placeholder - would set up envtest environment
|
||||
// env := &envtest.Environment{}
|
||||
// cfg, err := env.Start()
|
||||
// require.NoError(t, err)
|
||||
|
||||
// client, err := client.New(cfg, client.Options{})
|
||||
// require.NoError(t, err)
|
||||
|
||||
// cleanup := func() {
|
||||
// require.NoError(t, env.Stop())
|
||||
// }
|
||||
|
||||
// return env, client, cleanup
|
||||
|
||||
t.Skip("Test environment setup not implemented")
|
||||
return nil, nil, func() {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user