package resourcediscovery import ( "bytes" "context" "encoding/json" "fmt" "net/http" "time" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes" 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/cloudflare" "github.com/sankofa/crossplane-provider-proxmox/pkg/discovery" "github.com/sankofa/crossplane-provider-proxmox/pkg/proxmox" ) // ResourceDiscoveryReconciler reconciles a ResourceDiscovery object type ResourceDiscoveryReconciler struct { client.Client Scheme *runtime.Scheme K8sClient kubernetes.Interface InventoryAPI string // API endpoint for resource inventory } //+kubebuilder:rbac:groups=proxmox.sankofa.nexus,resources=resourcediscoveries,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=proxmox.sankofa.nexus,resources=resourcediscoveries/status,verbs=get;update;patch //+kubebuilder:rbac:groups=proxmox.sankofa.nexus,resources=resourcediscoveries/finalizers,verbs=update // Reconcile is part of the main kubernetes reconciliation loop func (r *ResourceDiscoveryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := log.FromContext(ctx) var rd proxmoxv1alpha1.ResourceDiscovery if err := r.Get(ctx, req.NamespacedName, &rd); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } // Check if discovery is enabled if !rd.Spec.Enabled { logger.Info("Resource discovery is disabled, skipping") return ctrl.Result{RequeueAfter: time.Duration(rd.Spec.SyncInterval) * time.Second}, nil } // Check if we need to sync (based on sync interval) syncInterval := time.Duration(rd.Spec.SyncInterval) * time.Second if rd.Status.LastSyncTime != nil { timeSinceLastSync := time.Since(rd.Status.LastSyncTime.Time) if timeSinceLastSync < syncInterval { requeueAfter := syncInterval - timeSinceLastSync logger.Info("Sync interval not reached, requeuing", "requeueAfter", requeueAfter) return ctrl.Result{RequeueAfter: requeueAfter}, nil } } // Update status to IN_PROGRESS rd.Status.SyncStatus = "IN_PROGRESS" rd.Status.LastSyncError = "" if err := r.Status().Update(ctx, &rd); err != nil { logger.Error(err, "failed to update status") } // Perform discovery based on provider var discoveredResources []discovery.DiscoveredResource var err error switch rd.Spec.Provider { case "PROXMOX": discoveredResources, err = r.discoverProxmoxResources(ctx, &rd) case "KUBERNETES": discoveredResources, err = r.discoverKubernetesResources(ctx, &rd) case "CLOUDFLARE": discoveredResources, err = r.discoverCloudflareResources(ctx, &rd) default: err = fmt.Errorf("unsupported provider: %s", rd.Spec.Provider) } if err != nil { logger.Error(err, "discovery failed") rd.Status.SyncStatus = "FAILED" rd.Status.LastSyncError = err.Error() if updateErr := r.Status().Update(ctx, &rd); updateErr != nil { logger.Error(updateErr, "failed to update status with error") } return ctrl.Result{RequeueAfter: syncInterval}, err } // Sync discovered resources to inventory API syncedCount, err := r.syncResourcesToAPI(ctx, discoveredResources, &rd) if err != nil { logger.Error(err, "failed to sync resources to API") rd.Status.SyncStatus = "FAILED" rd.Status.LastSyncError = err.Error() if updateErr := r.Status().Update(ctx, &rd); updateErr != nil { logger.Error(updateErr, "failed to update status with error") } return ctrl.Result{RequeueAfter: syncInterval}, err } // Update status now := metav1.Now() rd.Status.LastSyncTime = &now rd.Status.ResourcesDiscovered = syncedCount rd.Status.SyncStatus = "SUCCESS" rd.Status.LastSyncError = "" if err := r.Status().Update(ctx, &rd); err != nil { logger.Error(err, "failed to update status") return ctrl.Result{}, err } logger.Info("Discovery completed successfully", "resources", syncedCount) return ctrl.Result{RequeueAfter: syncInterval}, nil } func (r *ResourceDiscoveryReconciler) discoverProxmoxResources( ctx context.Context, rd *proxmoxv1alpha1.ResourceDiscovery, ) ([]discovery.DiscoveredResource, error) { // Validate ProviderConfigReference if rd.Spec.ProviderConfigReference == nil { return []discovery.DiscoveredResource{}, errors.New("providerConfigRef is required") } if rd.Spec.ProviderConfigReference.Name == "" { return []discovery.DiscoveredResource{}, errors.New("providerConfigRef.name is required") } // Get ProviderConfig var providerConfig proxmoxv1alpha1.ProviderConfig if err := r.Get(ctx, client.ObjectKey{Name: rd.Spec.ProviderConfigReference.Name}, &providerConfig); err != nil { return []discovery.DiscoveredResource{}, errors.Wrapf(err, "cannot get provider config") } // Get credentials from ProviderConfig var credentials struct { Username string Password string Endpoint string } // Try to get credentials from ProviderConfig if providerConfig.Spec.Credentials.SecretRef != nil { secretRef := providerConfig.Spec.Credentials.SecretRef secret := &corev1.Secret{} secretKey := client.ObjectKey{ Namespace: secretRef.Namespace, Name: secretRef.Name, } if err := r.Get(ctx, secretKey, secret); err == nil { if userData, ok := secret.Data["username"]; ok { credentials.Username = string(userData) } if passData, ok := secret.Data["password"]; ok { credentials.Password = string(passData) } if endpointData, ok := secret.Data["endpoint"]; ok { credentials.Endpoint = string(endpointData) } } } // Find endpoint from ProviderConfig Sites var endpoint string var insecureSkipTLS bool if rd.Spec.Site != "" { // Find the site matching rd.Spec.Site for _, site := range providerConfig.Spec.Sites { if site.Name == rd.Spec.Site { endpoint = site.Endpoint insecureSkipTLS = site.InsecureSkipTLSVerify break } } } else if len(providerConfig.Spec.Sites) > 0 { // Use first site if no site specified endpoint = providerConfig.Spec.Sites[0].Endpoint insecureSkipTLS = providerConfig.Spec.Sites[0].InsecureSkipTLSVerify } // Override with endpoint from credentials if provided if credentials.Endpoint != "" { endpoint = credentials.Endpoint } if endpoint == "" { return []discovery.DiscoveredResource{}, errors.New("no endpoint found in ProviderConfig sites or credentials") } client, err := proxmox.NewClient(endpoint, credentials.Username, credentials.Password, insecureSkipTLS) if err != nil { return []discovery.DiscoveredResource{}, errors.Wrap(err, "failed to create Proxmox client") } agent := discovery.NewProxmoxDiscoveryAgent(client, rd.Spec.Site, rd.Spec.Region) return agent.DiscoverAll(ctx) } func (r *ResourceDiscoveryReconciler) discoverKubernetesResources( ctx context.Context, rd *proxmoxv1alpha1.ResourceDiscovery, ) ([]discovery.DiscoveredResource, error) { if r.K8sClient == nil { return nil, fmt.Errorf("kubernetes client not configured") } agent := discovery.NewKubernetesDiscoveryAgent(r.K8sClient, rd.Spec.Site, rd.Spec.Region) return agent.DiscoverAll(ctx) } func (r *ResourceDiscoveryReconciler) discoverCloudflareResources( ctx context.Context, rd *proxmoxv1alpha1.ResourceDiscovery, ) ([]discovery.DiscoveredResource, error) { // Get ProviderConfig var providerConfig proxmoxv1alpha1.ProviderConfig if err := r.Get(ctx, client.ObjectKey{Name: rd.Spec.ProviderConfigReference.Name}, &providerConfig); err != nil { return nil, errors.Wrapf(err, "cannot get provider config") } // Get credentials from ProviderConfig var apiToken, accountID string if providerConfig.Spec.Credentials.SecretRef != nil { secretRef := providerConfig.Spec.Credentials.SecretRef secret := &corev1.Secret{} secretKey := client.ObjectKey{ Namespace: secretRef.Namespace, Name: secretRef.Name, } if err := r.Get(ctx, secretKey, secret); err == nil { if tokenData, ok := secret.Data["apiToken"]; ok { apiToken = string(tokenData) } if accountData, ok := secret.Data["accountID"]; ok { accountID = string(accountData) } } } client := cloudflare.NewClient(apiToken, accountID) agent := discovery.NewCloudflareDiscoveryAgent(client, rd.Spec.Site, rd.Spec.Region) return agent.DiscoverAll(ctx) } // syncResourcesToAPI syncs discovered resources to the inventory API func (r *ResourceDiscoveryReconciler) syncResourcesToAPI( ctx context.Context, resources []discovery.DiscoveredResource, rd *proxmoxv1alpha1.ResourceDiscovery, ) (int, error) { if r.InventoryAPI == "" { // If no API endpoint configured, just return count return len(resources), nil } // Prepare resources for API type ResourceInventoryItem struct { ResourceType string `json:"resourceType"` Provider string `json:"provider"` ProviderID string `json:"providerId"` ProviderResourceID string `json:"providerResourceId"` Name string `json:"name"` Region string `json:"region"` SiteID string `json:"siteId"` Metadata map[string]interface{} `json:"metadata"` Tags []string `json:"tags"` } items := make([]ResourceInventoryItem, len(resources)) for i, res := range resources { items[i] = ResourceInventoryItem{ ResourceType: res.ResourceType, Provider: res.Provider, ProviderID: res.ProviderID, ProviderResourceID: res.ProviderResourceID, Name: res.Name, Region: res.Region, SiteID: res.SiteID, Metadata: res.Metadata, Tags: res.Tags, } } // Make API call to sync resources jsonData, err := json.Marshal(map[string]interface{}{ "provider": rd.Spec.Provider, "resources": items, }) if err != nil { return 0, errors.Wrap(err, "failed to marshal resources") } req, err := http.NewRequestWithContext(ctx, "POST", r.InventoryAPI+"/sync", bytes.NewBuffer(jsonData)) if err != nil { return 0, errors.Wrap(err, "failed to create request") } req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return 0, errors.Wrap(err, "failed to send request") } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return 0, fmt.Errorf("API returned status %d", resp.StatusCode) } var result struct { Synced int `json:"synced"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { // If response doesn't match expected format, assume all were synced return len(resources), nil } return result.Synced, nil } // SetupWithManager sets up the controller with the Manager. func (r *ResourceDiscoveryReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&proxmoxv1alpha1.ResourceDiscovery{}). Complete(r) }