refactor: use typed objects for kubernetes

This commit is contained in:
Stavros
2026-09-20 21:07:06 +03:00
parent 5a685c4a6b
commit b43cf76d9b
7 changed files with 437 additions and 301 deletions
+18
View File
@@ -28,9 +28,11 @@ require (
golang.org/x/oauth2 v0.36.0 golang.org/x/oauth2 v0.36.0
golang.org/x/tools v0.49.0 golang.org/x/tools v0.49.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.37.0
k8s.io/apimachinery v0.37.0 k8s.io/apimachinery v0.37.0
k8s.io/client-go v0.37.0 k8s.io/client-go v0.37.0
modernc.org/sqlite v1.58.0 modernc.org/sqlite v1.58.0
sigs.k8s.io/gateway-api v1.6.2
) )
require ( require (
@@ -69,6 +71,7 @@ require (
github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fxamacker/cbor/v2 v2.9.1 // indirect github.com/fxamacker/cbor/v2 v2.9.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect
@@ -76,11 +79,26 @@ require (
github.com/go-asn1-ber/asn1-ber v1.5.8 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8 // indirect
github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v1.0.0 // indirect
github.com/go-openapi/jsonreference v1.0.0 // indirect
github.com/go-openapi/swag v0.27.1 // indirect
github.com/go-openapi/swag/cmdutils v0.27.1 // indirect
github.com/go-openapi/swag/conv v0.27.1 // indirect
github.com/go-openapi/swag/fileutils v0.27.1 // indirect
github.com/go-openapi/swag/jsonutils v0.27.1 // indirect
github.com/go-openapi/swag/loading v0.27.1 // indirect
github.com/go-openapi/swag/mangling v0.27.1 // indirect
github.com/go-openapi/swag/netutils v0.27.1 // indirect
github.com/go-openapi/swag/pools v0.27.1 // indirect
github.com/go-openapi/swag/stringutils v0.27.1 // indirect
github.com/go-openapi/swag/typeutils v0.27.1 // indirect
github.com/go-openapi/swag/yamlutils v0.27.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect github.com/goccy/go-yaml v1.19.2 // indirect
github.com/google/gnostic-models v0.7.1 // indirect
github.com/huandu/xstrings v1.5.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa // indirect github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
+10 -2
View File
@@ -138,6 +138,8 @@ github.com/go-openapi/swag/fileutils v0.27.1 h1:QQqBSoi5mW4XpU85nS0mLcA+zAE6vLzr
github.com/go-openapi/swag/fileutils v0.27.1/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8= github.com/go-openapi/swag/fileutils v0.27.1/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8=
github.com/go-openapi/swag/jsonutils v0.27.1 h1:SVgK3i4USzCU5mibOOS/l4ea2h9UQXy7J7RNLTjuXjU= github.com/go-openapi/swag/jsonutils v0.27.1 h1:SVgK3i4USzCU5mibOOS/l4ea2h9UQXy7J7RNLTjuXjU=
github.com/go-openapi/swag/jsonutils v0.27.1/go.mod h1:tdlEpZqdcQ17uj6J4YdK9vd8It5qWMwjWXOs0tjpRlk= github.com/go-openapi/swag/jsonutils v0.27.1/go.mod h1:tdlEpZqdcQ17uj6J4YdK9vd8It5qWMwjWXOs0tjpRlk=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.1 h1:mJu3COL9WEaZVp/Kf2PRMi7tPszPEJfSr/OO75ynCs8=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.1/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY=
github.com/go-openapi/swag/loading v0.27.1 h1:/DxUgDXKbBX4bcn7r9uEXfJyzN5XpiJmZplzQTjrRCY= github.com/go-openapi/swag/loading v0.27.1 h1:/DxUgDXKbBX4bcn7r9uEXfJyzN5XpiJmZplzQTjrRCY=
github.com/go-openapi/swag/loading v0.27.1/go.mod h1:jvGh3iA2+zyUUycB5fgJWzeHnhrpvGnJJM0RVE9ZShE= github.com/go-openapi/swag/loading v0.27.1/go.mod h1:jvGh3iA2+zyUUycB5fgJWzeHnhrpvGnJJM0RVE9ZShE=
github.com/go-openapi/swag/mangling v0.27.1 h1:yC9D0HyUE8gbP+BfmGx9+AA89ikwZTMjESK3OnnoaqA= github.com/go-openapi/swag/mangling v0.27.1 h1:yC9D0HyUE8gbP+BfmGx9+AA89ikwZTMjESK3OnnoaqA=
@@ -152,6 +154,10 @@ github.com/go-openapi/swag/typeutils v0.27.1 h1:KSTdFlfnse4r6dP9IrEnwMldjE+zs71U
github.com/go-openapi/swag/typeutils v0.27.1/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ= github.com/go-openapi/swag/typeutils v0.27.1/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ=
github.com/go-openapi/swag/yamlutils v0.27.1 h1:ftxv6xvXb1E3zohUc+okZ9nSqNb9StQX/FXnKZ98sQA= github.com/go-openapi/swag/yamlutils v0.27.1 h1:ftxv6xvXb1E3zohUc+okZ9nSqNb9StQX/FXnKZ98sQA=
github.com/go-openapi/swag/yamlutils v0.27.1/go.mod h1:bnxFIB1qewGRiZHypXGZ3fNgf13/0HfRgnS/iZBDrOo= github.com/go-openapi/swag/yamlutils v0.27.1/go.mod h1:bnxFIB1qewGRiZHypXGZ3fNgf13/0HfRgnS/iZBDrOo=
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0=
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo=
github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug=
github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -168,8 +174,8 @@ github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63Y
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA= github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE= github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
@@ -440,6 +446,8 @@ modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
sigs.k8s.io/gateway-api v1.6.2 h1:vh5YzKlbdBivEaLX61+APKLGRq4tZ7Fj4XfGkv08xB4=
sigs.k8s.io/gateway-api v1.6.2/go.mod h1:FVfx3t389ybeXOqvDghLbdvJdSCfI/PReqCUI3lu3mY=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
+3 -3
View File
@@ -46,7 +46,7 @@ func NewAccessControlsService(i AccessControlServiceInput) *AccessControlsServic
} }
} }
func (service *AccessControlsService) ensureAscii(str string) bool { func ensureAscii(str string) bool {
for i := 0; i < len(str); i++ { for i := 0; i < len(str); i++ {
if str[i] > unicode.MaxASCII { if str[i] > unicode.MaxASCII {
return false return false
@@ -64,7 +64,7 @@ func (service *AccessControlsService) normalizeDomain(domain string) string {
} }
func (service *AccessControlsService) getACLs(domain string, lookup func(locator func(name string, app *model.App) bool) error) (*model.App, error) { func (service *AccessControlsService) getACLs(domain string, lookup func(locator func(name string, app *model.App) bool) error) (*model.App, error) {
if !service.ensureAscii(domain) { if !ensureAscii(domain) {
return nil, errors.New("domain contains non-ascii characters") return nil, errors.New("domain contains non-ascii characters")
} }
@@ -80,7 +80,7 @@ func (service *AccessControlsService) getACLs(domain string, lookup func(locator
locatorFunc := func(name string, app *model.App) bool { locatorFunc := func(name string, app *model.App) bool {
if app.Config.Domain != "" { if app.Config.Domain != "" {
if !service.ensureAscii(app.Config.Domain) { if !ensureAscii(app.Config.Domain) {
service.log.App.Warn().Str("name", name).Str("domain", app.Config.Domain).Msg("Domain contains non-ascii characters, skipping") service.log.App.Warn().Str("name", name).Str("domain", app.Config.Domain).Msg("Domain contains non-ascii characters, skipping")
return false return false
} }
@@ -0,0 +1,47 @@
package service
import (
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
gateway "sigs.k8s.io/gateway-api/apis/v1"
)
type KubernetesGRPCRouteExtractor struct {
log *logger.Logger
}
type KubernetesGRPCRouteExtractorInput struct {
Log *logger.Logger
}
func NewKubernetesGRPCRouteExtractor(i KubernetesGRPCRouteExtractorInput) *KubernetesGRPCRouteExtractor {
return &KubernetesGRPCRouteExtractor{
log: i.Log,
}
}
func (k *KubernetesGRPCRouteExtractor) getHosts(hostnames []gateway.Hostname) []string {
var hosts []string
for _, hostname := range hostnames {
if hostname != "" {
hosts = append(hosts, string(hostname))
}
}
return nil
}
func (k *KubernetesGRPCRouteExtractor) Extract(route *gateway.GRPCRoute) *ExtractionResult {
hosts := k.getHosts(route.Spec.Hostnames)
namespace := route.GetNamespace()
name := route.GetName()
annotations := route.GetAnnotations()
return &ExtractionResult{
typ: ResourceTypeGRPCRoute,
name: name,
namespace: namespace,
hosts: hosts,
annotations: annotations,
}
}
@@ -0,0 +1,91 @@
package service
import (
"slices"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
gateway "sigs.k8s.io/gateway-api/apis/v1"
)
type KubernetesHTTPRouteExtractor struct {
log *logger.Logger
}
type KubernetesHTTPRouteExtractorInput struct {
Log *logger.Logger
}
func NewKubernetesHTTPRouteExtractor(i KubernetesHTTPRouteExtractorInput) *KubernetesHTTPRouteExtractor {
return &KubernetesHTTPRouteExtractor{
log: i.Log,
}
}
func (k *KubernetesHTTPRouteExtractor) getHosts(hostnames []gateway.Hostname) []string {
var hosts []string
for _, hostname := range hostnames {
if hostname != "" {
hosts = append(hosts, string(hostname))
}
}
return nil
}
func (k *KubernetesHTTPRouteExtractor) getRuleMatchers(matchers []gateway.HTTPRouteMatch) []string {
var res []string
for _, m := range matchers {
pathType := m.Path.Type
if pathType == nil {
pathType = new(gateway.PathMatchPathPrefix)
}
pathValue := m.Path.Value
if pathValue == nil {
pathValue = new("/")
}
if *pathType != gateway.PathMatchPathPrefix {
continue
}
res = append(res, *pathValue)
}
return res
}
func (k *KubernetesHTTPRouteExtractor) getPaths(rules []gateway.HTTPRouteRule) []string {
var paths []string
for _, rule := range rules {
matchers := k.getRuleMatchers(rule.Matches)
paths = append(paths, matchers...)
}
return paths
}
func (k *KubernetesHTTPRouteExtractor) Extract(route *gateway.HTTPRoute) *ExtractionResult {
hosts := k.getHosts(route.Spec.Hostnames)
paths := k.getPaths(route.Spec.Rules)
namespace := route.GetNamespace()
name := route.GetName()
annotations := route.GetAnnotations()
if !slices.Contains(paths, "/") {
k.log.App.Warn().Str("namespace", namespace).Str("name", name).Strs("paths", paths).Msg("Route does not contain a catch-all path, another route may be able to bypass auth checks if it routes the same host with a different path. Consider adding a catch-all path to this route to ensure auth checks are applied to all paths for this host.")
}
return &ExtractionResult{
typ: ResourceTypeHTTPRoute,
name: name,
namespace: namespace,
hosts: hosts,
annotations: annotations,
}
}
@@ -0,0 +1,68 @@
package service
import (
"slices"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
networking "k8s.io/api/networking/v1"
)
type KubernetesIngressExtractor struct {
log *logger.Logger
}
type KubernetesIngressExtractorInput struct {
Log *logger.Logger
}
func NewKubernetesIngressExtractor(i KubernetesIngressExtractorInput) *KubernetesIngressExtractor {
return &KubernetesIngressExtractor{
log: i.Log,
}
}
func (k *KubernetesIngressExtractor) getPaths(rule networking.IngressRule) []string {
var paths []string
for _, path := range rule.HTTP.Paths {
paths = append(paths, path.Path)
}
return paths
}
func (k *KubernetesIngressExtractor) getHosts(rules []networking.IngressRule) []string {
var hosts []string
for _, rule := range rules {
hosts = append(hosts, rule.Host)
paths := k.getPaths(rule)
if len(paths) == 0 {
continue
}
if !slices.Contains(paths, "/") {
k.log.App.Warn().Strs("hosts", hosts).Strs("paths", paths).Msg("Ingress rule does not contain a catch-all path, another ingress may be able to bypass auth checks if it routes the same host with a different path. Consider adding a catch-all path to this rule to ensure auth checks are applied to all paths for this host.")
}
}
return nil
}
func (k *KubernetesIngressExtractor) Extract(ingress *networking.Ingress) *ExtractionResult {
annotations := ingress.GetAnnotations()
if len(annotations) == 0 {
return nil
}
hosts := k.getHosts(ingress.Spec.Rules)
return &ExtractionResult{
typ: ResourceTypeIngress,
name: ingress.GetName(),
namespace: ingress.GetNamespace(),
hosts: hosts,
annotations: annotations,
}
}
+200 -296
View File
@@ -12,42 +12,43 @@ import (
"github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils/decoders" "github.com/tinyauthapp/tinyauth/internal/utils/decoders"
"github.com/tinyauthapp/tinyauth/internal/utils/logger" "github.com/tinyauthapp/tinyauth/internal/utils/logger"
"github.com/tinyauthapp/tinyauth/pkg/validators"
"go.uber.org/dig" "go.uber.org/dig"
networking "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/watch" "k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
gateway "sigs.k8s.io/gateway-api/apis/v1"
) )
// watchedResource describes a kind of resource that can carry tinyauth
// annotations, along with the specifics of extracting the hosts it routes.
type watchedResource struct { type watchedResource struct {
gvr schema.GroupVersionResource gvr schema.GroupVersionResource
// gatewayAPI resources declare their hosts in spec.hostnames instead of typ ResourceType
// spec.rules[].host and may use the wildcard label (`*.`).
gatewayAPI bool
// httpPaths marks resources that route on HTTP paths, which means another
// resource may claim the same host on a different path.
httpPaths bool
} }
// api returns a human readable identifier for the watched resource. func (w watchedResource) pretty() string {
func (r watchedResource) api() string { return w.gvr.Group + "/" + w.gvr.Version + "/" + w.gvr.Resource
return r.gvr.GroupVersion().String() + "/" + r.gvr.Resource
} }
var watchedResources = []watchedResource{ type ResourceType string
const (
ResourceTypeIngress ResourceType = "ingress"
ResourceTypeGRPCRoute ResourceType = "grpcroute"
ResourceTypeHTTPRoute ResourceType = "httproute"
)
var supportedResources = []watchedResource{
{ {
gvr: schema.GroupVersionResource{ gvr: schema.GroupVersionResource{
Group: "networking.k8s.io", Group: "networking.k8s.io",
Version: "v1", Version: "v1",
Resource: "ingresses", Resource: "ingresses",
}, },
httpPaths: true, typ: ResourceTypeIngress,
}, },
{ {
gvr: schema.GroupVersionResource{ gvr: schema.GroupVersionResource{
@@ -55,8 +56,7 @@ var watchedResources = []watchedResource{
Version: "v1", Version: "v1",
Resource: "httproutes", Resource: "httproutes",
}, },
gatewayAPI: true, typ: ResourceTypeHTTPRoute,
httpPaths: true,
}, },
{ {
gvr: schema.GroupVersionResource{ gvr: schema.GroupVersionResource{
@@ -64,26 +64,97 @@ var watchedResources = []watchedResource{
Version: "v1", Version: "v1",
Resource: "grpcroutes", Resource: "grpcroutes",
}, },
gatewayAPI: true, typ: ResourceTypeGRPCRoute,
}, },
} }
func hostMatchesHostname(host string, hostname string) bool {
host = strings.ToLower(host)
if suffix, ok := strings.CutPrefix(host, "*."); ok {
return strings.HasSuffix(hostname, "."+suffix)
}
return host == hostname
}
func hostCoversName(host string, name string) bool {
host = strings.ToLower(host)
if strings.HasPrefix(host, "*.") {
return true
}
return strings.HasPrefix(host, strings.ToLower(name+"."))
}
type ExtractionResult struct {
typ ResourceType
name string
namespace string
hosts []string
annotations map[string]string
}
type typedItem struct {
typ ResourceType
ingress *networking.Ingress
route *gateway.HTTPRoute
grpc *gateway.GRPCRoute
}
func convertFromUnstructured[T any](obj *unstructured.Unstructured) (*T, error) {
var typed *T
err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &typed)
if err != nil {
var zero *T
return zero, fmt.Errorf("failed to convert ingress to typed object: %w", err)
}
return typed, nil
}
func (ti *typedItem) fromUnstructured(typ ResourceType, obj *unstructured.Unstructured) (*typedItem, error) {
switch typ {
case ResourceTypeIngress:
typed, err := convertFromUnstructured[networking.Ingress](obj)
if err != nil {
return nil, err
}
return &typedItem{
typ: ResourceTypeIngress,
ingress: typed,
}, nil
case ResourceTypeHTTPRoute:
typed, err := convertFromUnstructured[gateway.HTTPRoute](obj)
if err != nil {
return nil, err
}
return &typedItem{
typ: ResourceTypeHTTPRoute,
route: typed,
}, nil
case ResourceTypeGRPCRoute:
typed, err := convertFromUnstructured[gateway.GRPCRoute](obj)
if err != nil {
return nil, err
}
return &typedItem{
typ: ResourceTypeGRPCRoute,
grpc: typed,
}, nil
default:
return nil, fmt.Errorf("unknown resource type %s", typ)
}
}
type resourceEntry struct { type resourceEntry struct {
name string name string
app model.App app model.App
} }
// routedApps holds the apps annotated on a resource along with the hosts that
// resource routes, which bound the domains those apps may define ACLs for.
type routedApps struct { type routedApps struct {
hosts []string hosts []string
entries []resourceEntry entries []resourceEntry
} }
// resourceKey identifies a watched resource. The kind is part of the key
// because an Ingress and an HTTPRoute may share a name within a namespace.
type resourceKey struct { type resourceKey struct {
resource string typ ResourceType
namespace string namespace string
name string name string
} }
@@ -91,10 +162,16 @@ type resourceKey struct {
type KubernetesService struct { type KubernetesService struct {
log *logger.Logger log *logger.Logger
client dynamic.Interface apps map[resourceKey]routedApps
connected bool client dynamic.Interface
mu sync.RWMutex mu sync.RWMutex
resourceApps map[resourceKey]routedApps connected bool
extractors struct {
ingress *KubernetesIngressExtractor
httproute *KubernetesHTTPRouteExtractor
grpc *KubernetesGRPCRouteExtractor
}
} }
type KubernetesServiceInput struct { type KubernetesServiceInput struct {
@@ -117,35 +194,45 @@ func NewKubernetesService(i KubernetesServiceInput) (*KubernetesService, error)
} }
service := &KubernetesService{ service := &KubernetesService{
log: i.Log, log: i.Log,
client: client, client: client,
resourceApps: make(map[resourceKey]routedApps), apps: make(map[resourceKey]routedApps),
} }
watching := 0 service.extractors.ingress = NewKubernetesIngressExtractor(KubernetesIngressExtractorInput{
Log: i.Log,
})
service.extractors.httproute = NewKubernetesHTTPRouteExtractor(KubernetesHTTPRouteExtractorInput{
Log: i.Log,
})
service.extractors.grpc = NewKubernetesGRPCRouteExtractor(KubernetesGRPCRouteExtractorInput{
Log: i.Log,
})
for _, res := range watchedResources { watchedGVRs := make(map[string]bool)
accessCtx, accessCancel := context.WithTimeout(i.Ctx, 5*time.Second)
_, err := client.Resource(res.gvr).List(accessCtx, metav1.ListOptions{Limit: 1}) for _, res := range supportedResources {
accessCancel() ctx, cancel := context.WithTimeout(i.Ctx, 5*time.Second)
_, err := client.Resource(res.gvr).List(ctx, metav1.ListOptions{Limit: 1})
cancel()
if err != nil { if err != nil {
// The Gateway API CRDs are not installed on every cluster, so a // The Gateway API CRDs are not installed on every cluster, so a
// single unreachable API is not fatal // single unreachable resource is not fatal
i.Log.App.Warn().Err(err).Str("api", res.api()).Msg("Failed to access API, skipping watcher") i.Log.App.Warn().Err(err).Str("res", res.pretty()).Msg("Failed to access resource, skipping watcher")
continue continue
} }
i.Log.App.Debug().Str("api", res.api()).Msg("Successfully accessed API, starting watcher") i.Log.App.Debug().Str("res", res.pretty()).Msg("Successfully accessed resource, starting watcher")
i.Ding.Go(func(ctx context.Context) { i.Ding.Go(func(ctx context.Context) {
service.watchGVR(res, ctx) service.watchGVR(res, ctx)
}, ding.RingMajor) }, ding.RingMajor)
watching++ watchedGVRs[res.gvr.Resource] = true
} }
if watching == 0 { if len(watchedGVRs) == 0 {
return nil, fmt.Errorf("failed to access any supported kubernetes api (ingresses, httproutes, grpcroutes)") return nil, fmt.Errorf("failed to access any supported kubernetes api (ingresses, httproutes, grpcroutes)")
} }
@@ -158,7 +245,7 @@ func NewKubernetesService(i KubernetesServiceInput) (*KubernetesService, error)
func (k *KubernetesService) addResourceEntries(key resourceKey, hosts []string, entries []resourceEntry) { func (k *KubernetesService) addResourceEntries(key resourceKey, hosts []string, entries []resourceEntry) {
k.mu.Lock() k.mu.Lock()
defer k.mu.Unlock() defer k.mu.Unlock()
k.resourceApps[key] = routedApps{ k.apps[key] = routedApps{
hosts: hosts, hosts: hosts,
entries: entries, entries: entries,
} }
@@ -167,15 +254,12 @@ func (k *KubernetesService) addResourceEntries(key resourceKey, hosts []string,
func (k *KubernetesService) removeResource(key resourceKey) { func (k *KubernetesService) removeResource(key resourceKey) {
k.mu.Lock() k.mu.Lock()
defer k.mu.Unlock() defer k.mu.Unlock()
delete(k.resourceApps, key) delete(k.apps, key)
} }
func (k *KubernetesService) getEntry(domain string, locator func(name string, app *model.App) bool) { func (k *KubernetesService) getEntry(domain string, locator func(name string, app *model.App) bool) {
v := validators.NewDomainValidator(validators.DomainValidatorOptions{}) if !ensureAscii(domain) {
k.log.App.Debug().Str("domain", domain).Msg("Domain is invalid, skipping lookup")
hostname, err := v.SafeHostname(domain)
if err != nil {
k.log.App.Debug().Err(err).Str("domain", domain).Msg("Domain is invalid, skipping lookup")
return return
} }
@@ -183,15 +267,13 @@ func (k *KubernetesService) getEntry(domain string, locator func(name string, ap
defer k.mu.RUnlock() defer k.mu.RUnlock()
// O(n^2) is not great but the number of resource entries is expected to be small // O(n^2) is not great but the number of resource entries is expected to be small
for _, apps := range k.resourceApps { for _, app := range k.apps {
// Only a resource that routes the domain may define its ACLs, otherwise if !slices.ContainsFunc(app.hosts, func(host string) bool {
// an app could claim any domain that happens to start with its name return hostMatchesHostname(host, domain)
if !slices.ContainsFunc(apps.hosts, func(host string) bool {
return hostMatches(host, hostname)
}) { }) {
continue continue
} }
for _, entry := range apps.entries { for _, entry := range app.entries {
if ok := locator(entry.name, &entry.app); ok { if ok := locator(entry.name, &entry.app); ok {
return return
} }
@@ -199,230 +281,48 @@ func (k *KubernetesService) getEntry(domain string, locator func(name string, ap
} }
} }
// hostMatches reports whether hostname is routed by host. It honours the func (k *KubernetesService) updateFromItem(res watchedResource, typedItem *typedItem) {
// Gateway API wildcard label (`*.`), which is a suffix match, so var result *ExtractionResult
// `*.example.com` matches `test.example.com` and `foo.test.example.com` but
// not `example.com`.
func hostMatches(host string, hostname string) bool {
host = strings.ToLower(host)
if suffix, ok := strings.CutPrefix(host, "*."); ok { switch typedItem.typ {
return strings.HasSuffix(hostname, "."+suffix) case ResourceTypeIngress:
} if typedItem.ingress != nil {
k.log.App.Warn().Str("res", res.pretty()).Msg("Ingress is nil, skipping")
return host == hostname
}
// hostCoversName reports whether an app name could resolve to a host routed by
// the resource. A wildcard host covers any app name since `*.example.com`
// routes `<name>.example.com` for every name.
func hostCoversName(host string, name string) bool {
host = strings.ToLower(host)
if strings.HasPrefix(host, "*.") {
return true
}
return strings.HasPrefix(host, strings.ToLower(name+"."))
}
func (k *KubernetesService) extractPaths(rule map[string]any) ([]string, error) {
http, found, err := unstructured.NestedMap(rule, "http")
if err != nil {
return nil, fmt.Errorf("reading http from rule: %w", err)
}
if !found {
return nil, nil
}
paths, found, err := unstructured.NestedSlice(http, "paths")
if err != nil {
return nil, fmt.Errorf("reading http.paths: %w", err)
}
if !found {
return nil, nil
}
var result []string
for _, p := range paths {
path, ok := p.(map[string]any)
if !ok {
continue
}
if p, ok := path["path"].(string); ok && p != "" {
result = append(result, p)
}
}
return result, nil
}
// extractRoutePaths returns the paths matched by a Gateway API route rule and
// whether the rule matches every path for its hosts.
func (k *KubernetesService) extractRoutePaths(rule map[string]any) ([]string, bool, error) {
matches, found, err := unstructured.NestedSlice(rule, "matches")
if err != nil {
return nil, false, fmt.Errorf("reading matches from rule: %w", err)
}
if !found || len(matches) == 0 {
// An omitted matches list defaults to a PathPrefix match on "/"
return nil, true, nil
}
var result []string
catchAll := false
for _, m := range matches {
match, ok := m.(map[string]any)
if !ok {
continue
}
path, ok := match["path"].(map[string]any)
if !ok {
// A match without a path constrains something else, such as headers
// or a gRPC method, and leaves the path unrestricted
catchAll = true
continue
}
// Both fields are optional and default to a PathPrefix match on "/"
pathType, ok := path["type"].(string)
if !ok || pathType == "" {
pathType = "PathPrefix"
}
value, ok := path["value"].(string)
if !ok || value == "" {
value = "/"
}
result = append(result, value)
if pathType == "PathPrefix" && value == "/" {
catchAll = true
}
}
return result, catchAll, nil
}
// warnMissingCatchAllPath warns when a Gateway API route does not match every
// path for the hosts it routes. Unlike an Ingress, a route declares its hosts
// once for all of its rules, so the rules are checked as a whole.
func (k *KubernetesService) warnMissingCatchAllPath(item *unstructured.Unstructured) {
rules, found, err := unstructured.NestedSlice(item.Object, "spec", "rules")
if err != nil {
// This is purely to warn users
// It doesn't affect our ability to extract hosts, so we won't fail the whole operation
k.log.App.Warn().Err(err).Str("namespace", item.GetNamespace()).Str("name", item.GetName()).Msg("Failed to extract paths from route rules")
return
}
if !found || len(rules) == 0 {
return
}
var paths []string
for _, r := range rules {
rule, ok := r.(map[string]any)
if !ok {
continue
}
rulePaths, catchAll, err := k.extractRoutePaths(rule)
if err != nil {
k.log.App.Warn().Err(err).Str("namespace", item.GetNamespace()).Str("name", item.GetName()).Msg("Failed to extract paths from route rule")
continue
}
if catchAll {
return return
} }
paths = append(paths, rulePaths...) result = k.extractors.ingress.Extract(typedItem.ingress)
case ResourceTypeHTTPRoute:
if typedItem.route != nil {
k.log.App.Warn().Str("res", res.pretty()).Msg("HTTPRoute is nil, skipping")
return
}
result = k.extractors.httproute.Extract(typedItem.route)
case ResourceTypeGRPCRoute:
if typedItem.grpc != nil {
k.log.App.Warn().Str("res", res.pretty()).Msg("GRPCRoute is nil, skipping")
return
}
result = k.extractors.grpc.Extract(typedItem.grpc)
} }
if len(paths) == 0 {
if result == nil {
k.log.App.Warn().Str("res", res.pretty()).Msg("Failed to extract resource, skipping")
return return
} }
k.log.App.Warn().Str("namespace", item.GetNamespace()).Str("name", item.GetName()).Strs("paths", paths).Msg("Route does not contain a catch-all path, another route may be able to bypass auth checks if it routes the same host with a different path. Consider adding a catch-all path to this route to ensure auth checks are applied to all paths for this host.")
}
func (k *KubernetesService) extractIngressHosts(item *unstructured.Unstructured) ([]string, error) {
rules, found, err := unstructured.NestedSlice(item.Object, "spec", "rules")
if err != nil {
return nil, fmt.Errorf("reading spec.rules: %w", err)
}
if !found {
return nil, nil
}
var hosts []string
for _, r := range rules {
rule, ok := r.(map[string]any)
if !ok {
continue
}
if host, ok := rule["host"].(string); ok && host != "" {
hosts = append(hosts, host)
}
paths, err := k.extractPaths(rule)
if err != nil {
// This is purely to warn users
// It doesn't affect our ability to extract hosts, so we won't fail the whole operation
k.log.App.Warn().Err(err).Str("namespace", item.GetNamespace()).Str("name", item.GetName()).Msg("Failed to extract paths from ingress rule")
continue
}
if len(paths) == 0 {
continue
}
if !slices.Contains(paths, "/") {
k.log.App.Warn().Str("namespace", item.GetNamespace()).Str("name", item.GetName()).Strs("paths", paths).Msg("Ingress rule does not contain a catch-all path, another ingress may be able to bypass auth checks if it routes the same host with a different path. Consider adding a catch-all path to this rule to ensure auth checks are applied to all paths for this host.")
}
}
k.log.App.Trace().Strs("hosts", hosts).Msg("Extracted hosts from ingress rules")
return hosts, nil
}
func (k *KubernetesService) extractRouteHosts(res watchedResource, item *unstructured.Unstructured) ([]string, error) {
hostnames, found, err := unstructured.NestedStringSlice(item.Object, "spec", "hostnames")
if err != nil {
return nil, fmt.Errorf("reading spec.hostnames: %w", err)
}
if !found {
// A route without hostnames inherits the ones of the gateway listeners
// it attaches to, which we cannot resolve from the route alone
return nil, nil
}
var hosts []string
for _, hostname := range hostnames {
if hostname != "" {
hosts = append(hosts, hostname)
}
}
if res.httpPaths {
k.warnMissingCatchAllPath(item)
}
k.log.App.Trace().Strs("hosts", hosts).Msg("Extracted hosts from route hostnames")
return hosts, nil
}
func (k *KubernetesService) extractHosts(res watchedResource, item *unstructured.Unstructured) ([]string, error) {
if res.gatewayAPI {
return k.extractRouteHosts(res, item)
}
return k.extractIngressHosts(item)
}
func (k *KubernetesService) updateFromItem(res watchedResource, item *unstructured.Unstructured) {
key := resourceKey{ key := resourceKey{
resource: res.gvr.Resource, typ: res.typ,
namespace: item.GetNamespace(), namespace: result.namespace,
name: item.GetName(), name: result.name,
} }
annotations := item.GetAnnotations() if len(result.hosts) == 0 {
if annotations == nil { k.log.App.Warn().Str("res", res.pretty()).Str("namespace", key.namespace).Str("name", key.name).Msg("No hosts found in resource, skipping")
k.removeResource(key) k.removeResource(key)
return return
} }
hosts, err := k.extractHosts(res, item) labels, err := decoders.DecodeLabels[model.Apps](result.annotations, "apps")
if err != nil {
k.removeResource(key)
return
}
if len(hosts) == 0 {
k.log.App.Warn().Str("api", res.api()).Str("namespace", key.namespace).Str("name", key.name).Msg("No hosts found in resource, skipping")
k.removeResource(key)
return
}
labels, err := decoders.DecodeLabels[model.Apps](annotations, "apps")
if err != nil { if err != nil {
k.log.App.Warn().Err(err).Str("namespace", key.namespace).Str("name", key.name).Msg("Failed to decode resource labels, skipping") k.log.App.Warn().Err(err).Str("namespace", key.namespace).Str("name", key.name).Msg("Failed to decode resource labels, skipping")
k.removeResource(key) k.removeResource(key)
@@ -431,25 +331,24 @@ func (k *KubernetesService) updateFromItem(res watchedResource, item *unstructur
var entries []resourceEntry var entries []resourceEntry
v := validators.NewDomainValidator(validators.DomainValidatorOptions{})
for name, config := range labels.Apps { for name, config := range labels.Apps {
if config.Config.Domain != "" { if config.Config.Domain != "" {
hostname, err := v.SafeHostname(config.Config.Domain) if !ensureAscii(config.Config.Domain) {
if err != nil {
k.log.App.Warn().Err(err).Str("namespace", key.namespace).Str("name", key.name).Str("domain", config.Config.Domain).Msg("Domain is invalid, matching will rely on app name") k.log.App.Warn().Err(err).Str("namespace", key.namespace).Str("name", key.name).Str("domain", config.Config.Domain).Msg("Domain is invalid, matching will rely on app name")
} else if slices.ContainsFunc(hosts, func(host string) bool { } else {
return hostMatches(host, hostname) if slices.ContainsFunc(result.hosts, func(host string) bool {
}) { return hostMatchesHostname(host, config.Config.Domain)
entries = append(entries, resourceEntry{ }) {
name: name, entries = append(entries, resourceEntry{
app: config, name: name,
}) app: config,
continue })
continue
}
} }
} }
if slices.ContainsFunc(hosts, func(host string) bool { if slices.ContainsFunc(result.hosts, func(host string) bool {
return hostCoversName(host, name) return hostCoversName(host, name)
}) { }) {
entries = append(entries, resourceEntry{ entries = append(entries, resourceEntry{
@@ -464,7 +363,7 @@ func (k *KubernetesService) updateFromItem(res watchedResource, item *unstructur
return return
} }
k.addResourceEntries(key, hosts, entries) k.addResourceEntries(key, result.hosts, entries)
} }
func (k *KubernetesService) resyncGVR(res watchedResource, ctx context.Context) error { func (k *KubernetesService) resyncGVR(res watchedResource, ctx context.Context) error {
@@ -473,18 +372,21 @@ func (k *KubernetesService) resyncGVR(res watchedResource, ctx context.Context)
list, err := k.client.Resource(res.gvr).List(ctx, metav1.ListOptions{}) list, err := k.client.Resource(res.gvr).List(ctx, metav1.ListOptions{})
if err != nil { if err != nil {
k.log.App.Warn().Err(err).Str("api", res.api()).Msg("Failed to list resources for resync") k.log.App.Warn().Err(err).Str("res", res.pretty()).Msg("Failed to list resources for resync")
return err return err
} }
for i := range list.Items { for _, item := range list.Items {
k.updateFromItem(res, &list.Items[i]) newTypedItem, err := new(typedItem).fromUnstructured(res.typ, &item)
if err != nil {
k.log.App.Warn().Err(err).Str("res", res.pretty()).Msg("Failed to decode resource, skipping")
continue
}
k.updateFromItem(res, newTypedItem)
} }
k.log.App.Debug().Str("api", res.api()).Int("count", len(list.Items)).Msg("Resync complete") k.log.App.Debug().Str("res", res.pretty()).Int("count", len(list.Items)).Msg("Resync complete")
return nil return nil
} }
// runWatcher drains events from an active watcher until it closes or the context is done.
// Returns true if the caller should restart the watcher, false if it should exit.
func (k *KubernetesService) runWatcher(res watchedResource, w watch.Interface, resyncTicker *time.Ticker, ctx context.Context) bool { func (k *KubernetesService) runWatcher(res watchedResource, w watch.Interface, resyncTicker *time.Ticker, ctx context.Context) bool {
for { for {
select { select {
@@ -493,29 +395,34 @@ func (k *KubernetesService) runWatcher(res watchedResource, w watch.Interface, r
return false return false
case event, ok := <-w.ResultChan(): case event, ok := <-w.ResultChan():
if !ok { if !ok {
k.log.App.Warn().Str("api", res.api()).Msg("Watcher channel closed, restarting watcher") k.log.App.Warn().Str("res", res.pretty()).Msg("Watcher channel closed, restarting watcher")
w.Stop() w.Stop()
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
return true return true
} }
item, ok := event.Object.(*unstructured.Unstructured) item, ok := event.Object.(*unstructured.Unstructured)
if !ok { if !ok {
k.log.App.Warn().Str("api", res.api()).Msg("Received unexpected event object, skipping") k.log.App.Warn().Str("res", res.pretty()).Msg("Received unexpected event object, skipping")
continue
}
newTypedItem, err := new(typedItem).fromUnstructured(res.typ, item)
if err != nil {
k.log.App.Warn().Err(err).Str("res", res.pretty()).Msg("Failed to decode resource, skipping")
continue continue
} }
switch event.Type { switch event.Type {
case watch.Added, watch.Modified: case watch.Added, watch.Modified:
k.updateFromItem(res, item) k.updateFromItem(res, newTypedItem)
case watch.Deleted: case watch.Deleted:
k.removeResource(resourceKey{ k.removeResource(resourceKey{
resource: res.gvr.Resource, typ: res.typ,
namespace: item.GetNamespace(), namespace: item.GetNamespace(),
name: item.GetName(), name: item.GetName(),
}) })
} }
case <-resyncTicker.C: case <-resyncTicker.C:
if err := k.resyncGVR(res, ctx); err != nil { if err := k.resyncGVR(res, ctx); err != nil {
k.log.App.Warn().Err(err).Str("api", res.api()).Msg("Periodic resync failed during watcher run") k.log.App.Warn().Err(err).Str("res", res.pretty()).Msg("Periodic resync failed during watcher run")
} }
} }
} }
@@ -526,29 +433,29 @@ func (k *KubernetesService) watchGVR(res watchedResource, ctx context.Context) {
defer resyncTicker.Stop() defer resyncTicker.Stop()
if err := k.resyncGVR(res, ctx); err != nil { if err := k.resyncGVR(res, ctx); err != nil {
k.log.App.Warn().Err(err).Str("api", res.api()).Msg("Initial resync failed, will retry") k.log.App.Warn().Err(err).Str("res", res.pretty()).Msg("Initial resync failed, will retry")
time.Sleep(30 * time.Second) time.Sleep(30 * time.Second)
} }
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
k.log.App.Debug().Str("api", res.api()).Msg("Shutting down kubernetes watcher") k.log.App.Debug().Str("res", res.pretty()).Msg("Shutting down kubernetes watcher")
return return
case <-resyncTicker.C: case <-resyncTicker.C:
if err := k.resyncGVR(res, ctx); err != nil { if err := k.resyncGVR(res, ctx); err != nil {
k.log.App.Warn().Err(err).Str("api", res.api()).Msg("Periodic resync failed, will retry") k.log.App.Warn().Err(err).Str("res", res.pretty()).Msg("Periodic resync failed, will retry")
} }
default: default:
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
watcher, err := k.client.Resource(res.gvr).Watch(ctx, metav1.ListOptions{}) watcher, err := k.client.Resource(res.gvr).Watch(ctx, metav1.ListOptions{})
if err != nil { if err != nil {
k.log.App.Warn().Err(err).Str("api", res.api()).Msg("Failed to start watcher, will retry") k.log.App.Warn().Err(err).Str("res", res.pretty()).Msg("Failed to start watcher, will retry")
cancel() cancel()
time.Sleep(10 * time.Second) time.Sleep(10 * time.Second)
continue continue
} }
k.log.App.Debug().Str("api", res.api()).Msg("Watcher started successfully") k.log.App.Debug().Str("res", res.pretty()).Msg("Watcher started successfully")
if !k.runWatcher(res, watcher, resyncTicker, ctx) { if !k.runWatcher(res, watcher, resyncTicker, ctx) {
cancel() cancel()
return return
@@ -558,9 +465,6 @@ func (k *KubernetesService) watchGVR(res watchedResource, ctx context.Context) {
} }
} }
// Lookup yields the apps annotated on the resources that route domain. Apps
// annotated on any other resource are withheld, since they are served
// elsewhere and must not define the ACLs of this domain.
func (k *KubernetesService) Lookup(domain string, locator func(name string, app *model.App) bool) error { func (k *KubernetesService) Lookup(domain string, locator func(name string, app *model.App) bool) error {
if !k.connected { if !k.connected {
k.log.App.Debug().Msg("Kubernetes label provider not started, skipping") k.log.App.Debug().Msg("Kubernetes label provider not started, skipping")