mirror of
https://github.com/tinyauthapp/tinyauth.git
synced 2026-09-24 03:33:31 +08:00
feat(k8s): Support new Gateway api
This commit is contained in:
@@ -19,17 +19,17 @@ import (
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
type ingressKey struct {
|
||||
type resourceKey struct {
|
||||
namespace string
|
||||
name string
|
||||
}
|
||||
|
||||
type ingressAppKey struct {
|
||||
ingressKey
|
||||
type resourceAppKey struct {
|
||||
resourceKey
|
||||
appName string
|
||||
}
|
||||
|
||||
type ingressApp struct {
|
||||
type resourceApp struct {
|
||||
domain string
|
||||
appName string
|
||||
app model.App
|
||||
@@ -42,9 +42,22 @@ type KubernetesService struct {
|
||||
client dynamic.Interface
|
||||
started bool
|
||||
mu sync.RWMutex
|
||||
ingressApps map[ingressKey][]ingressApp
|
||||
domainIndex map[string]ingressAppKey
|
||||
appNameIndex map[string]ingressAppKey
|
||||
resourceApps map[resourceKey][]resourceApp
|
||||
domainIndex map[string]resourceAppKey
|
||||
appNameIndex map[string]resourceAppKey
|
||||
}
|
||||
|
||||
var watchedGVRs = []schema.GroupVersionResource{
|
||||
{
|
||||
Group: "networking.k8s.io",
|
||||
Version: "v1",
|
||||
Resource: "ingresses",
|
||||
},
|
||||
{
|
||||
Group: "gateway.networking.k8s.io",
|
||||
Version: "v1",
|
||||
Resource: "httproutes",
|
||||
},
|
||||
}
|
||||
|
||||
func NewKubernetesService(
|
||||
@@ -62,74 +75,75 @@ func NewKubernetesService(
|
||||
return nil, fmt.Errorf("failed to create kubernetes client: %w", err)
|
||||
}
|
||||
|
||||
gvr := schema.GroupVersionResource{
|
||||
Group: "networking.k8s.io",
|
||||
Version: "v1",
|
||||
Resource: "ingresses",
|
||||
service := &KubernetesService{
|
||||
log: log,
|
||||
ctx: ctx,
|
||||
client: client,
|
||||
resourceApps: make(map[resourceKey][]resourceApp),
|
||||
domainIndex: make(map[string]resourceAppKey),
|
||||
appNameIndex: make(map[string]resourceAppKey),
|
||||
}
|
||||
|
||||
accessCtx, accessCancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer accessCancel()
|
||||
|
||||
_, err = client.Resource(gvr).List(accessCtx, metav1.ListOptions{Limit: 1})
|
||||
if err != nil {
|
||||
log.App.Warn().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Failed to access Ingress API, Kubernetes label provider will be disabled")
|
||||
return nil, fmt.Errorf("failed to access ingress api: %w", err)
|
||||
started := 0
|
||||
for _, gvr := range watchedGVRs {
|
||||
_, err = client.Resource(gvr).List(accessCtx, metav1.ListOptions{Limit: 1})
|
||||
if err != nil {
|
||||
log.App.Warn().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Failed to access API, skipping watcher")
|
||||
continue
|
||||
}
|
||||
log.App.Debug().Str("api", gvr.GroupVersion().String()).Msg("Successfully accessed API, starting watcher")
|
||||
gvrCopy := gvr
|
||||
wg.Go(func() {
|
||||
service.watchGVR(gvrCopy)
|
||||
})
|
||||
started++
|
||||
}
|
||||
|
||||
log.App.Debug().Str("api", gvr.GroupVersion().String()).Msg("Successfully accessed Ingress API, starting watcher")
|
||||
|
||||
service := &KubernetesService{
|
||||
log: log,
|
||||
ctx: ctx,
|
||||
client: client,
|
||||
ingressApps: make(map[ingressKey][]ingressApp),
|
||||
domainIndex: make(map[string]ingressAppKey),
|
||||
appNameIndex: make(map[string]ingressAppKey),
|
||||
if started == 0 {
|
||||
return nil, fmt.Errorf("failed to access any supported kubernetes API (ingresses, httproutes)")
|
||||
}
|
||||
|
||||
wg.Go(func() {
|
||||
service.watchGVR(gvr)
|
||||
})
|
||||
|
||||
service.started = true
|
||||
log.App.Debug().Msg("Kubernetes label provider started successfully")
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
func (k *KubernetesService) addIngressApps(namespace, name string, apps []ingressApp) {
|
||||
func (k *KubernetesService) addResourceApps(namespace, name string, apps []resourceApp) {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
|
||||
key := ingressKey{namespace, name}
|
||||
// Remove existing entries for this ingress
|
||||
if existing, ok := k.ingressApps[key]; ok {
|
||||
key := resourceKey{namespace, name}
|
||||
// Remove existing entries for this resource
|
||||
if existing, ok := k.resourceApps[key]; ok {
|
||||
for _, app := range existing {
|
||||
delete(k.domainIndex, app.domain)
|
||||
delete(k.appNameIndex, app.appName)
|
||||
}
|
||||
}
|
||||
// Add new entries
|
||||
k.ingressApps[key] = apps
|
||||
k.resourceApps[key] = apps
|
||||
for _, app := range apps {
|
||||
appKey := ingressAppKey{key, app.appName}
|
||||
appKey := resourceAppKey{key, app.appName}
|
||||
k.domainIndex[app.domain] = appKey
|
||||
k.appNameIndex[app.appName] = appKey
|
||||
}
|
||||
}
|
||||
|
||||
func (k *KubernetesService) removeIngress(namespace, name string) {
|
||||
func (k *KubernetesService) removeResource(namespace, name string) {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
|
||||
key := ingressKey{namespace, name}
|
||||
if apps, ok := k.ingressApps[key]; ok {
|
||||
key := resourceKey{namespace, name}
|
||||
if apps, ok := k.resourceApps[key]; ok {
|
||||
for _, app := range apps {
|
||||
delete(k.domainIndex, app.domain)
|
||||
delete(k.appNameIndex, app.appName)
|
||||
}
|
||||
delete(k.ingressApps, key)
|
||||
delete(k.resourceApps, key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +152,7 @@ func (k *KubernetesService) getByDomain(domain string) *model.App {
|
||||
defer k.mu.RUnlock()
|
||||
|
||||
if appKey, ok := k.domainIndex[domain]; ok {
|
||||
if apps, ok := k.ingressApps[appKey.ingressKey]; ok {
|
||||
if apps, ok := k.resourceApps[appKey.resourceKey]; ok {
|
||||
for i := range apps {
|
||||
app := &apps[i]
|
||||
if app.domain == domain && app.appName == appKey.appName {
|
||||
@@ -155,7 +169,7 @@ func (k *KubernetesService) getByAppName(appName string) *model.App {
|
||||
defer k.mu.RUnlock()
|
||||
|
||||
if appKey, ok := k.appNameIndex[appName]; ok {
|
||||
if apps, ok := k.ingressApps[appKey.ingressKey]; ok {
|
||||
if apps, ok := k.resourceApps[appKey.resourceKey]; ok {
|
||||
for i := range apps {
|
||||
app := &apps[i]
|
||||
if app.appName == appName {
|
||||
@@ -172,30 +186,30 @@ func (k *KubernetesService) updateFromItem(item *unstructured.Unstructured) {
|
||||
name := item.GetName()
|
||||
annotations := item.GetAnnotations()
|
||||
if annotations == nil {
|
||||
k.removeIngress(namespace, name)
|
||||
k.removeResource(namespace, name)
|
||||
return
|
||||
}
|
||||
labels, err := decoders.DecodeLabels[model.Apps](annotations, "apps")
|
||||
if err != nil {
|
||||
k.log.App.Warn().Err(err).Str("namespace", namespace).Str("name", name).Msg("Failed to decode ingress labels, skipping")
|
||||
k.removeIngress(namespace, name)
|
||||
k.log.App.Warn().Err(err).Str("namespace", namespace).Str("name", name).Msg("Failed to decode labels, skipping")
|
||||
k.removeResource(namespace, name)
|
||||
return
|
||||
}
|
||||
var apps []ingressApp
|
||||
var apps []resourceApp
|
||||
for appName, appLabels := range labels.Apps {
|
||||
if appLabels.Config.Domain == "" {
|
||||
continue
|
||||
}
|
||||
apps = append(apps, ingressApp{
|
||||
apps = append(apps, resourceApp{
|
||||
domain: appLabels.Config.Domain,
|
||||
appName: appName,
|
||||
app: appLabels,
|
||||
})
|
||||
}
|
||||
if len(apps) == 0 {
|
||||
k.removeIngress(namespace, name)
|
||||
k.removeResource(namespace, name)
|
||||
} else {
|
||||
k.addIngressApps(namespace, name, apps)
|
||||
k.addResourceApps(namespace, name, apps)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,7 +253,7 @@ func (k *KubernetesService) runWatcher(gvr schema.GroupVersionResource, w watch.
|
||||
case watch.Added, watch.Modified:
|
||||
k.updateFromItem(item)
|
||||
case watch.Deleted:
|
||||
k.removeIngress(item.GetNamespace(), item.GetName())
|
||||
k.removeResource(item.GetNamespace(), item.GetName())
|
||||
}
|
||||
case <-resyncTicker.C:
|
||||
if err := k.resyncGVR(gvr); err != nil {
|
||||
|
||||
@@ -25,7 +25,7 @@ func TestKubernetesService(t *testing.T) {
|
||||
description: "Cache by domain returns app and misses unknown domain",
|
||||
run: func(t *testing.T, svc *KubernetesService) {
|
||||
app := model.App{Config: model.AppConfig{Domain: "foo.example.com"}}
|
||||
svc.addIngressApps("default", "my-ingress", []ingressApp{
|
||||
svc.addResourceApps("default", "my-ingress", []resourceApp{
|
||||
{domain: "foo.example.com", appName: "foo", app: app},
|
||||
})
|
||||
|
||||
@@ -41,7 +41,7 @@ func TestKubernetesService(t *testing.T) {
|
||||
description: "Cache by app name returns app and misses unknown name",
|
||||
run: func(t *testing.T, svc *KubernetesService) {
|
||||
app := model.App{Config: model.AppConfig{Domain: "bar.example.com"}}
|
||||
svc.addIngressApps("default", "my-ingress", []ingressApp{
|
||||
svc.addResourceApps("default", "my-ingress", []resourceApp{
|
||||
{domain: "bar.example.com", appName: "bar", app: app},
|
||||
})
|
||||
|
||||
@@ -54,14 +54,14 @@ func TestKubernetesService(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "RemoveIngress clears domain and app name entries",
|
||||
description: "RemoveResource clears domain and app name entries",
|
||||
run: func(t *testing.T, svc *KubernetesService) {
|
||||
app := model.App{Config: model.AppConfig{Domain: "baz.example.com"}}
|
||||
svc.addIngressApps("default", "my-ingress", []ingressApp{
|
||||
svc.addResourceApps("default", "my-ingress", []resourceApp{
|
||||
{domain: "baz.example.com", appName: "baz", app: app},
|
||||
})
|
||||
|
||||
svc.removeIngress("default", "my-ingress")
|
||||
svc.removeResource("default", "my-ingress")
|
||||
|
||||
got := svc.getByDomain("baz.example.com")
|
||||
assert.Nil(t, got)
|
||||
@@ -70,15 +70,15 @@ func TestKubernetesService(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "AddIngressApps replaces stale entries for the same ingress",
|
||||
description: "AddResourceApps replaces stale entries for the same resource",
|
||||
run: func(t *testing.T, svc *KubernetesService) {
|
||||
old := model.App{Config: model.AppConfig{Domain: "old.example.com"}}
|
||||
svc.addIngressApps("default", "my-ingress", []ingressApp{
|
||||
svc.addResourceApps("default", "my-ingress", []resourceApp{
|
||||
{domain: "old.example.com", appName: "old", app: old},
|
||||
})
|
||||
|
||||
updated := model.App{Config: model.AppConfig{Domain: "new.example.com"}}
|
||||
svc.addIngressApps("default", "my-ingress", []ingressApp{
|
||||
svc.addResourceApps("default", "my-ingress", []resourceApp{
|
||||
{domain: "new.example.com", appName: "new", app: updated},
|
||||
})
|
||||
|
||||
@@ -96,7 +96,7 @@ func TestKubernetesService(t *testing.T) {
|
||||
svc.started = true
|
||||
|
||||
app := model.App{Config: model.AppConfig{Domain: "hit.example.com"}}
|
||||
svc.addIngressApps("default", "ing", []ingressApp{
|
||||
svc.addResourceApps("default", "ing", []resourceApp{
|
||||
{domain: "hit.example.com", appName: "hit", app: app},
|
||||
})
|
||||
|
||||
@@ -121,7 +121,7 @@ func TestKubernetesService(t *testing.T) {
|
||||
svc.started = true
|
||||
|
||||
app := model.App{Config: model.AppConfig{Domain: "myapp.internal.example.com"}}
|
||||
svc.addIngressApps("default", "ing", []ingressApp{
|
||||
svc.addResourceApps("default", "ing", []resourceApp{
|
||||
{domain: "myapp.internal.example.com", appName: "myapp", app: app},
|
||||
})
|
||||
|
||||
@@ -139,7 +139,7 @@ func TestKubernetesService(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "UpdateFromItem parses annotations and populates cache",
|
||||
description: "UpdateFromItem parses annotations and populates cache from ingress",
|
||||
run: func(t *testing.T, svc *KubernetesService) {
|
||||
item := unstructured.Unstructured{}
|
||||
item.SetNamespace("default")
|
||||
@@ -157,11 +157,30 @@ func TestKubernetesService(t *testing.T) {
|
||||
assert.Equal(t, "alice", got.Users.Allow)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "UpdateFromItem parses annotations and populates cache from httproute",
|
||||
run: func(t *testing.T, svc *KubernetesService) {
|
||||
item := unstructured.Unstructured{}
|
||||
item.SetNamespace("default")
|
||||
item.SetName("test-httproute")
|
||||
item.SetAnnotations(map[string]string{
|
||||
"tinyauth.apps.gwapp.config.domain": "gwapp.example.com",
|
||||
"tinyauth.apps.gwapp.users.allow": "bob",
|
||||
})
|
||||
|
||||
svc.updateFromItem(&item)
|
||||
|
||||
got := svc.getByDomain("gwapp.example.com")
|
||||
require.NotNil(t, got)
|
||||
assert.Equal(t, "gwapp.example.com", got.Config.Domain)
|
||||
assert.Equal(t, "bob", got.Users.Allow)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "UpdateFromItem with no annotations removes existing cache entries",
|
||||
run: func(t *testing.T, svc *KubernetesService) {
|
||||
app := model.App{Config: model.AppConfig{Domain: "todelete.example.com"}}
|
||||
svc.addIngressApps("default", "test-ingress", []ingressApp{
|
||||
svc.addResourceApps("default", "test-ingress", []resourceApp{
|
||||
{domain: "todelete.example.com", appName: "todelete", app: app},
|
||||
})
|
||||
|
||||
@@ -175,14 +194,43 @@ func TestKubernetesService(t *testing.T) {
|
||||
assert.Nil(t, got)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ingress and HTTPRoute apps coexist in cache",
|
||||
run: func(t *testing.T, svc *KubernetesService) {
|
||||
ingress := unstructured.Unstructured{}
|
||||
ingress.SetNamespace("default")
|
||||
ingress.SetName("my-ingress")
|
||||
ingress.SetAnnotations(map[string]string{
|
||||
"tinyauth.apps.ingapp.config.domain": "ingapp.example.com",
|
||||
})
|
||||
|
||||
httproute := unstructured.Unstructured{}
|
||||
httproute.SetNamespace("default")
|
||||
httproute.SetName("my-httproute")
|
||||
httproute.SetAnnotations(map[string]string{
|
||||
"tinyauth.apps.gwapp.config.domain": "gwapp.example.com",
|
||||
})
|
||||
|
||||
svc.updateFromItem(&ingress)
|
||||
svc.updateFromItem(&httproute)
|
||||
|
||||
got := svc.getByDomain("ingapp.example.com")
|
||||
require.NotNil(t, got)
|
||||
assert.Equal(t, "ingapp.example.com", got.Config.Domain)
|
||||
|
||||
got = svc.getByDomain("gwapp.example.com")
|
||||
require.NotNil(t, got)
|
||||
assert.Equal(t, "gwapp.example.com", got.Config.Domain)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
svc := &KubernetesService{
|
||||
ingressApps: make(map[ingressKey][]ingressApp),
|
||||
domainIndex: make(map[string]ingressAppKey),
|
||||
appNameIndex: make(map[string]ingressAppKey),
|
||||
resourceApps: make(map[resourceKey][]resourceApp),
|
||||
domainIndex: make(map[string]resourceAppKey),
|
||||
appNameIndex: make(map[string]resourceAppKey),
|
||||
log: log,
|
||||
}
|
||||
test.run(t, svc)
|
||||
|
||||
Reference in New Issue
Block a user