package vmscaleset import ( "context" "fmt" "os" "time" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" proxmoxv1alpha1 "github.com/sankofa/crossplane-provider-proxmox/apis/v1alpha1" "github.com/sankofa/crossplane-provider-proxmox/pkg/metrics" "github.com/sankofa/crossplane-provider-proxmox/pkg/proxmox" "github.com/sankofa/crossplane-provider-proxmox/pkg/scaling" ) // ProxmoxVMScaleSetReconciler reconciles a ProxmoxVMScaleSet object type ProxmoxVMScaleSetReconciler struct { client.Client Scheme *runtime.Scheme } //+kubebuilder:rbac:groups=proxmox.sankofa.nexus,resources=proxmoxvmscalesets,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=proxmox.sankofa.nexus,resources=proxmoxvmscalesets/status,verbs=get;update;patch // Reconcile is part of the main kubernetes reconciliation loop func (r *ProxmoxVMScaleSetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := log.FromContext(ctx) var vmss proxmoxv1alpha1.ProxmoxVMScaleSet if err := r.Get(ctx, req.NamespacedName, &vmss); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } // Validate ProviderConfigReference if vmss.Spec.ProviderConfigReference == nil { return ctrl.Result{}, errors.New("providerConfigRef is required") } if vmss.Spec.ProviderConfigReference.Name == "" { return ctrl.Result{}, errors.New("providerConfigRef.name is required") } // Get ProviderConfig var providerConfig proxmoxv1alpha1.ProviderConfig if err := r.Get(ctx, client.ObjectKey{Name: vmss.Spec.ProviderConfigReference.Name}, &providerConfig); err != nil { return ctrl.Result{}, errors.Wrapf(err, "cannot get provider config") } // Get credentials from secret (similar to virtualmachine controller) creds, err := r.getCredentials(ctx, &providerConfig) if err != nil { logger.Error(err, "cannot get credentials") return ctrl.Result{RequeueAfter: 30 * time.Second}, errors.Wrap(err, "cannot get credentials") } // Find the site configuration (use first site or from spec if available) var site *proxmoxv1alpha1.ProxmoxSite if len(providerConfig.Spec.Sites) > 0 { site = &providerConfig.Spec.Sites[0] } else { return ctrl.Result{}, errors.New("no sites configured in provider config") } // Create Proxmox client with proper credentials proxmoxClient, err := proxmox.NewClient( site.Endpoint, creds.Username, creds.Password, site.InsecureSkipTLSVerify, ) if err != nil { return ctrl.Result{}, errors.Wrap(err, "cannot create Proxmox client") } // Create metrics collector with Prometheus client // Get Prometheus endpoint from environment or ProviderConfig // For now, we'll use a default endpoint - in production this should come from config prometheusEndpoint := "http://prometheus:9090" if prometheusURL := os.Getenv("PROMETHEUS_ENDPOINT"); prometheusURL != "" { prometheusEndpoint = prometheusURL } prometheusClient := metrics.NewPrometheusAPIClient(prometheusEndpoint) metricsCollector := metrics.NewCollector(prometheusClient) // Create policy engine policyEngine := scaling.NewPolicyEngine(metricsCollector) // Create instance manager instanceManager := scaling.NewInstanceManager(proxmoxClient) // Evaluate scaling policies decision, err := policyEngine.Evaluate(ctx, vmss.Spec, vmss.Status, vmss.Status.Instances) if err != nil { logger.Error(err, "failed to evaluate scaling policies") return ctrl.Result{RequeueAfter: 30 * time.Second}, err } // Check cooldown period if vmss.Status.LastScaleTime != nil { cooldownPeriod := time.Duration(vmss.Spec.CooldownPeriod) * time.Second timeSinceLastScale := time.Since(vmss.Status.LastScaleTime.Time) if timeSinceLastScale < cooldownPeriod && decision.Action != "NO_ACTION" { logger.Info("Cooldown period active, skipping scaling", "timeSinceLastScale", timeSinceLastScale) return ctrl.Result{RequeueAfter: cooldownPeriod - timeSinceLastScale}, nil } } // Execute scaling decision if decision.Action != "NO_ACTION" { logger.Info("Scaling decision", "action", decision.Action, "newReplicas", decision.NewReplicas, "reason", decision.Reason) // Scale instances newInstances, err := instanceManager.ScaleTo( ctx, vmss.Spec.Template, vmss.Status.Instances, decision.NewReplicas, ) if err != nil { logger.Error(err, "failed to scale instances") return ctrl.Result{RequeueAfter: 30 * time.Second}, err } // Update status now := metav1.Now() vmss.Status.CurrentReplicas = len(newInstances) vmss.Status.DesiredReplicas = decision.NewReplicas vmss.Status.Instances = newInstances vmss.Status.LastScaleTime = &now // Add scaling event event := proxmoxv1alpha1.ScalingEvent{ Type: decision.Action, OldReplicas: len(vmss.Status.Instances), NewReplicas: decision.NewReplicas, Reason: decision.Reason, Timestamp: now, } vmss.Status.ScalingEvents = append(vmss.Status.ScalingEvents, event) // Keep only last 10 events if len(vmss.Status.ScalingEvents) > 10 { vmss.Status.ScalingEvents = vmss.Status.ScalingEvents[len(vmss.Status.ScalingEvents)-10:] } // Count ready replicas readyCount := 0 for _, instance := range newInstances { if instanceManager.HealthCheck(ctx, instance) { readyCount++ } } vmss.Status.ReadyReplicas = readyCount if err := r.Status().Update(ctx, &vmss); err != nil { logger.Error(err, "failed to update status") return ctrl.Result{}, err } } // Requeue for periodic evaluation return ctrl.Result{RequeueAfter: 30 * time.Second}, nil } // getCredentials retrieves credentials from the provider config secret func (r *ProxmoxVMScaleSetReconciler) getCredentials(ctx context.Context, config *proxmoxv1alpha1.ProviderConfig) (*credentials, error) { if config.Spec.Credentials.SecretRef == nil { return nil, fmt.Errorf("no secret reference in provider config") } secretRef := config.Spec.Credentials.SecretRef // Get secret from Kubernetes secret := &corev1.Secret{} secretKey := client.ObjectKey{ Namespace: secretRef.Namespace, Name: secretRef.Name, } if err := r.Get(ctx, secretKey, secret); err != nil { return nil, errors.Wrap(err, "cannot get secret") } // Parse credentials from secret var username, password string // Try username/password format first if userData, ok := secret.Data["username"]; ok { username = string(userData) } if passData, ok := secret.Data["password"]; ok { password = string(passData) } // Try token format (for Proxmox API tokens) if tokenData, ok := secret.Data["token"]; ok { if userData, ok := secret.Data["tokenid"]; ok { username = string(userData) } password = string(tokenData) } if username == "" || password == "" { return nil, fmt.Errorf("username/password or token missing in secret") } return &credentials{ Username: username, Password: password, }, nil } type credentials struct { Username string Password string } // SetupWithManager sets up the controller with the Manager. func (r *ProxmoxVMScaleSetReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&proxmoxv1alpha1.ProxmoxVMScaleSet{}). Complete(r) }