feat(go-traefik-certmanager): initial commit
This commit is contained in:
parent
913eaceaa4
commit
c10b760c0b
14 changed files with 835 additions and 0 deletions
153
pkg/certmanager/certificate.go
Normal file
153
pkg/certmanager/certificate.go
Normal file
|
@ -0,0 +1,153 @@
|
|||
package certmanager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
cmv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
|
||||
cmmetav1 "github.com/cert-manager/cert-manager/pkg/apis/meta/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/types"
|
||||
)
|
||||
|
||||
const (
|
||||
certGroup = "cert-manager.io"
|
||||
certVersion = "v1"
|
||||
certKind = "Certificate"
|
||||
certResource = "certificates"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCertificateAlreadyExist = errors.New("certificate already exists")
|
||||
ErrCertificateToUnstructured = errors.New("certificate cannot get converted to unstructured")
|
||||
ErrCertificateCreation = errors.New("certificate creation error")
|
||||
ErrCertificateToJSON = errors.New("certificate cannot get converted to JSON")
|
||||
)
|
||||
|
||||
type certificateClient struct {
|
||||
client *Client
|
||||
gvr schema.GroupVersionResource
|
||||
}
|
||||
|
||||
func newCertificateClient(client *Client) certificateClient {
|
||||
return certificateClient{
|
||||
client: client,
|
||||
gvr: schema.GroupVersionResource{
|
||||
Group: certGroup,
|
||||
Version: certVersion,
|
||||
Resource: certResource,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *certificateClient) Create(
|
||||
ctx context.Context,
|
||||
namespace, secretName string,
|
||||
routes []map[string]interface{},
|
||||
) error {
|
||||
_, err := c.client.crdClient.Resource(c.gvr).Namespace(namespace).Get(ctx, secretName, metav1.GetOptions{})
|
||||
if err == nil {
|
||||
return ErrCertificateAlreadyExist
|
||||
}
|
||||
|
||||
hosts := extractHosts(routes)
|
||||
|
||||
cert := cmv1.Certificate{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: certKind,
|
||||
APIVersion: fmt.Sprintf("%s/%s", certGroup, certVersion),
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: secretName,
|
||||
},
|
||||
Spec: cmv1.CertificateSpec{
|
||||
DNSNames: hosts,
|
||||
SecretName: secretName,
|
||||
IssuerRef: cmmetav1.ObjectReference{
|
||||
Name: c.client.certIssuerName,
|
||||
Kind: c.client.certIssuerKind,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(cert)
|
||||
if err != nil {
|
||||
return errors.Join(ErrCertificateToUnstructured, err)
|
||||
}
|
||||
|
||||
_, err = c.client.crdClient.Resource(c.gvr).Namespace(namespace).Create(
|
||||
ctx,
|
||||
&unstructured.Unstructured{Object: obj},
|
||||
metav1.CreateOptions{},
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Join(ErrCertificateCreation, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *certificateClient) Delete(ctx context.Context, namespace, name string) error {
|
||||
return c.client.crdClient.Resource(c.gvr).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{})
|
||||
}
|
||||
|
||||
func (c *certificateClient) Patch(ctx context.Context, namespace, name string, cert cmv1.Certificate) error {
|
||||
data, err := json.Marshal(cert)
|
||||
if err != nil {
|
||||
return errors.Join(ErrCertificateToJSON, err)
|
||||
}
|
||||
|
||||
_, err = c.client.crdClient.Resource(c.gvr).Namespace(namespace).Patch(
|
||||
ctx,
|
||||
name,
|
||||
types.JSONPatchType,
|
||||
data,
|
||||
metav1.PatchOptions{},
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *certificateClient) PatchSecretName(ctx context.Context, namespace, name, secretName string) error {
|
||||
cert := cmv1.Certificate{
|
||||
Spec: cmv1.CertificateSpec{
|
||||
SecretName: secretName,
|
||||
},
|
||||
}
|
||||
|
||||
return c.Patch(ctx, namespace, name, cert)
|
||||
}
|
||||
|
||||
func extractHosts(routes []map[string]interface{}) []string {
|
||||
var hosts []string
|
||||
re := regexp.MustCompile(`Host\(([^)]*)\)`)
|
||||
|
||||
for _, route := range routes {
|
||||
var (
|
||||
kind string
|
||||
match string
|
||||
ok bool
|
||||
)
|
||||
|
||||
kind, ok = route["kind"].(string)
|
||||
if !ok || kind != "Rule" {
|
||||
continue
|
||||
}
|
||||
|
||||
if match, ok = route["match"].(string); ok {
|
||||
hostMatches := re.FindAllStringSubmatch(match, -1)
|
||||
for _, match := range hostMatches {
|
||||
if len(match) > 1 {
|
||||
hosts = append(hosts, strings.Split(match[1], ",")...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hosts
|
||||
}
|
41
pkg/certmanager/client.go
Normal file
41
pkg/certmanager/client.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
package certmanager
|
||||
|
||||
import (
|
||||
"k8s.io/client-go/dynamic"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
crdClient dynamic.DynamicClient
|
||||
certIssuerName string
|
||||
certIssuerKind string
|
||||
|
||||
Certificates certificateClient
|
||||
}
|
||||
|
||||
type ClientOption func(*Client)
|
||||
|
||||
func WithCertIssuerName(name string) ClientOption {
|
||||
return func(c *Client) {
|
||||
c.certIssuerName = name
|
||||
}
|
||||
}
|
||||
|
||||
func WithCertIssuerKind(kind string) ClientOption {
|
||||
return func(c *Client) {
|
||||
c.certIssuerKind = kind
|
||||
}
|
||||
}
|
||||
|
||||
func NewClient(crdClient dynamic.DynamicClient, options ...ClientOption) *Client {
|
||||
client := &Client{
|
||||
crdClient: crdClient,
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
option(client)
|
||||
}
|
||||
|
||||
client.Certificates = newCertificateClient(client)
|
||||
|
||||
return client
|
||||
}
|
37
pkg/ingressroute/client.go
Normal file
37
pkg/ingressroute/client.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package ingressroute
|
||||
|
||||
import (
|
||||
"git.ar21.de/yolokube/go-traefik-certmanager/pkg/certmanager"
|
||||
"k8s.io/client-go/dynamic"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
crdClient dynamic.DynamicClient
|
||||
certmanager *certmanager.Client
|
||||
certCleanup bool
|
||||
|
||||
IngressRoutes ingressRouteClient
|
||||
}
|
||||
|
||||
type ClientOption func(*Client)
|
||||
|
||||
func WithCertCleanup() ClientOption {
|
||||
return func(c *Client) {
|
||||
c.certCleanup = true
|
||||
}
|
||||
}
|
||||
|
||||
func NewClient(crdClient dynamic.DynamicClient, cmClient *certmanager.Client, options ...ClientOption) *Client {
|
||||
client := &Client{
|
||||
crdClient: crdClient,
|
||||
certmanager: cmClient,
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
option(client)
|
||||
}
|
||||
|
||||
client.IngressRoutes = ingressRouteClient{client: client}
|
||||
|
||||
return client
|
||||
}
|
161
pkg/ingressroute/ingressroute.go
Normal file
161
pkg/ingressroute/ingressroute.go
Normal file
|
@ -0,0 +1,161 @@
|
|||
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 {
|
||||
return
|
||||
}
|
||||
|
||||
convObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
rawRoutes, found, err := unstructured.NestedSlice(convObj, "spec", "routes")
|
||||
if err != nil || !found {
|
||||
return
|
||||
}
|
||||
|
||||
routes, err := routeInterfaceToMapSlice(rawRoutes)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
queue.Add(event{key: key, eventType: watch.Added, routes: routes})
|
||||
},
|
||||
UpdateFunc: func(_, newObj interface{}) {
|
||||
key, err := cache.MetaNamespaceKeyFunc(newObj)
|
||||
if err == nil {
|
||||
queue.Add(event{key: key, eventType: watch.Modified})
|
||||
}
|
||||
},
|
||||
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})
|
||||
}
|
||||
},
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
//nolint:exhaustive // ignore missing switch cases
|
||||
switch event.eventType {
|
||||
case watch.Added, watch.Modified:
|
||||
createErr := i.client.certmanager.Certificates.Create(context.Background(), namespace, name, event.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)
|
||||
}
|
||||
}
|
||||
case watch.Deleted:
|
||||
if deleteErr := i.client.certmanager.Certificates.Delete(context.Background(), namespace, name); deleteErr != nil {
|
||||
log.Printf("Failed to delete certificate %s: %v", event.key, deleteErr)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type event struct {
|
||||
key string
|
||||
eventType watch.EventType
|
||||
routes []map[string]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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue