go-traefik-certmanager/pkg/ingressroute/ingressroute.go
Tom Neuber 689d5e8d77
All checks were successful
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
chore(linter): add "golden config" for golangci linter
2025-01-24 20:46:00 +01:00

177 lines
4.8 KiB
Go

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
}