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:
defiQUG
2025-12-12 19:29:01 -08:00
parent 9daf1fd378
commit 7cd7022f6e
66 changed files with 5892 additions and 14502 deletions

View File

@@ -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: &quota.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