- Add comprehensive database migrations (001-024) for schema evolution - Enhance API schema with expanded type definitions and resolvers - Add new middleware: audit logging, rate limiting, MFA enforcement, security, tenant auth - Implement new services: AI optimization, billing, blockchain, compliance, marketplace - Add adapter layer for cloud integrations (Cloudflare, Kubernetes, Proxmox, storage) - Update Crossplane provider with enhanced VM management capabilities - Add comprehensive test suite for API endpoints and services - Update frontend components with improved GraphQL subscriptions and real-time updates - Enhance security configurations and headers (CSP, CORS, etc.) - Update documentation and configuration files - Add new CI/CD workflows and validation scripts - Implement design system improvements and UI enhancements
231 lines
7.2 KiB
Go
231 lines
7.2 KiB
Go
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)
|
|
}
|
|
|