package ingressroute import ( "context" "errors" "fmt" "log" "time" "git.ar21.de/yolokube/go-traefik-certmanager/pkg/certmanager" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/tools/cache" "k8s.io/client-go/util/workqueue" ) const ( group = "traefik.io" version = "v1alpha1" resource = "ingressroutes" ) type ingressRouteClient struct { client *Client } func (i *ingressRouteClient) Watch(stopCh chan struct{}) { gvr := schema.GroupVersionResource{ Group: group, Version: version, Resource: resource, } listWatch := &cache.ListWatch{ ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { return i.client.crdClient.Resource(gvr).Namespace(corev1.NamespaceAll).List(context.Background(), options) }, WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { return i.client.crdClient.Resource(gvr).Namespace(corev1.NamespaceAll).Watch(context.Background(), options) }, } queue := workqueue.NewTypedRateLimitingQueue(workqueue.DefaultTypedControllerRateLimiter[any]()) informer := cache.NewSharedInformer(listWatch, &unstructured.Unstructured{}, 0) _, err := informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { key, err := cache.MetaNamespaceKeyFunc(obj) if err == nil { queue.Add(event{key: key, eventType: watch.Added, object: &obj}) } }, UpdateFunc: func(_, newObj interface{}) { key, err := cache.MetaNamespaceKeyFunc(newObj) if err == nil { queue.Add(event{key: key, eventType: watch.Modified, object: &newObj}) } }, DeleteFunc: func(obj interface{}) { if !i.client.certCleanup { return } key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj) if err == nil { queue.Add(event{key: key, eventType: watch.Deleted, object: &obj}) } }, }) if err != nil { log.Printf("Cannot add event handler: %v", err) } go informer.Run(stopCh) wait.Until(func() { for i.processNextItem(queue) { } }, time.Second, stopCh) } func (i *ingressRouteClient) processNextItem(queue workqueue.TypedRateLimitingInterface[any]) bool { item, quit := queue.Get() if quit { return false } defer queue.Done(item) log.Printf("Processing key %v", item) event, ok := item.(event) if !ok { log.Printf("Invalid data struct: %v", item) return true } namespace, name, err := cache.SplitMetaNamespaceKey(event.key) if err != nil { log.Printf("Failed to split namespace and name: %v", err) return true } convObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(event.object) if err != nil { log.Printf("Failed to convert unstructured object for key %s: %v", event.key, err) return true } rawRoutes, found, err := unstructured.NestedSlice(convObj, "spec", "routes") if err != nil || !found { log.Printf("No routes found for key %s", event.key) return true } secretName, found, err := unstructured.NestedString(convObj, "spec", "tls", "secretName") if err != nil { log.Printf("Failed to scrape secret name for %s: %v", event.key, err) return true } if !found { log.Printf("No secret name found, using ingressroute name %s", name) secretName = name } routes, err := routeInterfaceToMapSlice(rawRoutes) if err != nil { log.Printf("Failed to convert routes for key %s: %v", event.key, err) return true } //nolint:exhaustive // ignore missing switch cases switch event.eventType { case watch.Added, watch.Modified: createErr := i.client.certmanager.Certificates.Create(context.Background(), namespace, secretName, routes) if createErr != nil { if errors.Is(createErr, certmanager.ErrCertificateAlreadyExist) { log.Printf("Certificate %s for %s already exists", secretName, event.key) } else { log.Printf("Failed to create certificate %s: %v", event.key, createErr) } } else { log.Printf("Certificate %s for %s created", secretName, event.key) } case watch.Deleted: deleteErr := i.client.certmanager.Certificates.Delete(context.Background(), namespace, secretName) if deleteErr != nil { log.Printf("Failed to delete certificate %s: %v", event.key, deleteErr) } else { log.Printf("Certificate %s for %s deleted", secretName, event.key) } } return true } type event struct { key string eventType watch.EventType object *interface{} } func routeInterfaceToMapSlice(input []interface{}) ([]map[string]interface{}, error) { var result []map[string]interface{} for _, item := range input { match, ok := item.(map[string]interface{}) if !ok { return nil, fmt.Errorf("item is not of type map[string]interface{}: %v", item) } result = append(result, match) } return result, nil }