Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
419da20
feat(k8s): Support new Gateway api
contre95 May 18, 2026
2769725
feat(k8s): Support for GRPCRoute
contre95 May 18, 2026
3a216e9
Merge branch 'main' into feat/k8s_gateways
contre95 Aug 19, 2026
e1b1e72
fix(acl): only let a label provider define ACLs for domains it routes
contre95 Aug 19, 2026
5a685c4
Merge branch 'main' into feat/k8s_gateways
steveiliop56 Sep 20, 2026
b43cf76
refactor: use typed objects for kubernetes
steveiliop56 Sep 20, 2026
45e165f
tests: add tests for kubernetes service and extractors
steveiliop56 Sep 20, 2026
34456b9
chore: add missing acls service modification for domain normalization
steveiliop56 Sep 20, 2026
90e898a
Merge branch 'main' into feat/k8s_gateways
steveiliop56 Sep 21, 2026
996316b
chore: remove gateway extractors
steveiliop56 Sep 21, 2026
261d882
feat: init kube types
steveiliop56 Sep 21, 2026
a62ef39
feat: watch for both ingresses and tinyauth crds
steveiliop56 Sep 21, 2026
f98c844
feat: register crd in kube watcher
steveiliop56 Sep 22, 2026
b29e1bd
feat: load basic auth password from secret
steveiliop56 Sep 22, 2026
314758b
chore: add controller gen to codegen tools
steveiliop56 Sep 22, 2026
fb86f44
chore: disable kube tests for now
steveiliop56 Sep 22, 2026
fcc9125
fix: auto generate crd
steveiliop56 Sep 22, 2026
ac2a418
feat: support for loading config from kubeconfig
steveiliop56 Sep 24, 2026
52661a9
Merge branch 'main' into feat/k8s-crds
steveiliop56 Sep 27, 2026
805859c
tests: add tests for kube service and extractors
steveiliop56 Sep 27, 2026
1fe845d
Merge branch 'main' into feat/k8s-crds
steveiliop56 Oct 9, 2026
6203950
fix: rabbit comments
steveiliop56 Oct 10, 2026
520f304
Merge branch 'main' into feat/k8s-crds
steveiliop56 Oct 10, 2026
91be9b4
refactor: run locator for all available apps
steveiliop56 Oct 10, 2026
ed9cac5
refactor: remove stale apps, don't wait for janitor
steveiliop56 Oct 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ jobs:
with:
sqlc-version: "1.31.1"

- name: Setup controller-gen
run: go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.22.0

- name: Check codegen is up to date
run: |
sqlc generate
Expand Down
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ PROD_COMPOSE := $(shell test -f "docker-compose.test.prod.yml" && echo "docker-c

.DEFAULT_GOAL := binary

.PHONY: deps clean-data clean-webui webui binary binary-linux-amd64 binary-linux-arm64 test vet test-race dev dev-infisical prod prod-infisical sql generate docker docker-distroless
.PHONY: deps clean-data clean-webui webui binary binary-linux-amd64 binary-linux-arm64 test vet test-race dev dev-infisical prod prod-infisical sql generate docker docker-distroless tools

# Tools
tools:
go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.22.0
go install github.com/sqlc-dev/sqlc/cmd/sqlc@v1.31.1

# Deps
deps:
Expand Down
17 changes: 17 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,28 @@ require (
github.com/docker/go-connections v0.7.0 // indirect
github.com/docker/go-units v0.5.0 // 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.1.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8 // indirect
github.com/go-logr/logr v1.4.4 // 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/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.1 // indirect
Expand Down Expand Up @@ -117,6 +132,7 @@ require (
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
Expand All @@ -141,6 +157,7 @@ require (
golang.org/x/text v0.42.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gotest.tools/v3 v3.5.2 // indirect
k8s.io/klog/v2 v2.140.0 // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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/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/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/go.mod h1:jvGh3iA2+zyUUycB5fgJWzeHnhrpvGnJJM0RVE9ZShE=
github.com/go-openapi/swag/mangling v0.27.1 h1:yC9D0HyUE8gbP+BfmGx9+AA89ikwZTMjESK3OnnoaqA=
Expand All @@ -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/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/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/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
Expand Down
56 changes: 27 additions & 29 deletions internal/service/access_controls_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
)

type LabelProvider interface {
Lookup(locator func(name string, app *model.App) bool) error
Lookup(locator func(name string, app *model.App)) error
}

type AccessControlsService struct {
Expand Down Expand Up @@ -59,7 +59,7 @@ func normalizeDomain(domain string) string {
return strings.ToLower(domain)
}

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)) error) (*model.App, error) {
if !ensureAscii(domain) {
return nil, errors.New("domain contains non-ascii characters")
}
Expand All @@ -70,60 +70,56 @@ func (service *AccessControlsService) getACLs(domain string, lookup func(locator
return nil, fmt.Errorf("domain does not match cookie domain, expected %s (or a subdomain), got %s", service.runtime.CookieDomain, domain)
}

var domainMatch *model.App
var nameMatch *model.App
var nameMatchedApps []string
var domainMatches []model.App
var nameMatches []model.App

locatorFunc := func(name string, app *model.App) bool {
locatorFunc := func(name string, app *model.App) {
if 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")
return false
return
}
if normalizedDomain == normalizeDomain(app.Config.Domain) {
service.log.App.Debug().Str("name", name).Msg("Found matching container by domain")
domainMatch = app
return true
domainMatches = append(domainMatches, *app)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return
}
return false
return
}
if strings.HasPrefix(normalizedDomain, strings.ToLower(name+".")) {
service.log.App.Debug().Str("name", name).Msg("Found matching container by app name")
nameMatch = app
nameMatchedApps = append(nameMatchedApps, name)
nameMatches = append(nameMatches, *app)
}
return false
}

err := lookup(locatorFunc)
if err != nil {
return nil, err
}

if domainMatch != nil {
if len(domainMatches) > 0 {
if len(domainMatches) > 1 {
return nil, fmt.Errorf("domain matched multiple apps, use explicit domain config")
}
service.log.App.Debug().Str("domain", domain).Msg("Found matching app by domain")
return domainMatch, nil
}

if nameMatch == nil {
service.log.App.Debug().Str("domain", domain).Msg("No match found for domain, skipping")
return nil, nil
return &domainMatches[0], nil
}

if len(nameMatchedApps) > 1 {
return nil, fmt.Errorf("domain matched multiple apps by name prefix, use explicit domain config")
if len(nameMatches) > 0 {
if len(nameMatches) > 1 {
return nil, fmt.Errorf("domain matched multiple apps by name prefix, use explicit domain config")
}
service.log.App.Debug().Str("domain", domain).Msg("Found matching app by app name")
return &nameMatches[0], nil
}

service.log.App.Debug().Str("domain", domain).Msg("Found matching app by app name")
return nameMatch, nil
return nil, nil
}

func (service *AccessControlsService) lookupStaticACLs(domain string) (*model.App, error) {
return service.getACLs(domain, func(locator func(name string, app *model.App) bool) error {
return service.getACLs(domain, func(locator func(name string, app *model.App)) error {
for app, config := range service.config.Apps {
if ok := locator(app, &config); ok {
return nil
}
locator(app, &config)
}
return nil
})
Expand All @@ -145,7 +141,9 @@ func (service *AccessControlsService) GetAccessControls(domain string) (*model.A

// If we have a label provider configured, try to get ACLs from it
if service.labelProvider != nil {
return service.getACLs(domain, service.labelProvider.Lookup)
return service.getACLs(domain, func(locator func(name string, app *model.App)) error {
return service.labelProvider.Lookup(locator)
})
}

// No labels
Expand Down
6 changes: 2 additions & 4 deletions internal/service/access_controls_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,12 @@ func newMockProvider(acls map[string]model.App, shouldError bool) *mockProvider
return &mockProvider{acls: acls, shouldError: shouldError}
}

func (m *mockProvider) Lookup(locator func(name string, app *model.App) bool) error {
func (m *mockProvider) Lookup(locator func(name string, app *model.App)) error {
if m.shouldError {
return errors.New("mock error")
}
for name, app := range m.acls {
if ok := locator(name, &app); ok {
return nil
}
locator(name, &app)
}
return nil
}
Expand Down
6 changes: 2 additions & 4 deletions internal/service/docker_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ func (docker *DockerService) inspectContainer(containerId string) (container.Ins
return docker.client.ContainerInspect(docker.context, containerId)
}

func (docker *DockerService) Lookup(locator func(name string, app *model.App) bool) error {
func (docker *DockerService) Lookup(locator func(name string, app *model.App)) error {
if !docker.isConnected {
docker.log.App.Debug().Msg("Docker service not connected, returning empty labels")
return nil
Expand All @@ -144,9 +144,7 @@ func (docker *DockerService) Lookup(locator func(name string, app *model.App) bo
}

for app, config := range labels.Apps {
if ok := locator(app, &config); ok {
return nil
}
locator(app, &config)
}
}

Expand Down
98 changes: 98 additions & 0 deletions internal/service/kubernetes_crd_extractor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package service

import (
"context"
"fmt"
"time"

"github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
"github.com/tinyauthapp/tinyauth/pkg/apis/tinyauth/v1alpha1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)

type KubernetesCRDInput struct {
Log *logger.Logger
Ctx context.Context
Client kubernetes.Interface
}

type KubernetesCRDExtractor struct {
log *logger.Logger
ctx context.Context
client kubernetes.Interface
}

func NewKubernetesCRDExtractor(i KubernetesCRDInput) *KubernetesCRDExtractor {
return &KubernetesCRDExtractor{
log: i.Log,
ctx: i.Ctx,
client: i.Client,
}
}

func (k *KubernetesCRDExtractor) Extract(app *v1alpha1.Application) ExtractionResult {
meta := &ResourceMeta{
Typ: ResourceTypeCRD,
Name: app.GetName(),
Namespace: app.GetNamespace(),
}

if !ensureResourceMeta(meta) {
k.log.App.Warn().Str("namespace", meta.Namespace).Str("name", meta.Name).Msg("Resource has no namespace or name, skipping")
return ExtractionResult{}
}

if app.Spec.Config.Domain == "" {
k.log.App.Warn().Str("name", meta.Name).Str("namespace", meta.Namespace).Msg("Application has no domain, skipping")
return ExtractionResult{
Meta: meta,
}
}

if !ensureAscii(app.Spec.Config.Domain) {
k.log.App.Warn().Str("name", meta.Name).Str("namespace", meta.Namespace).Str("domain", app.Spec.Config.Domain).Msg("Domain is invalid, skipping")
return ExtractionResult{
Meta: meta,
}
}

// Convert the CRD to the internal representation.
internalApp := app.Spec.ToInternalApp()

if ref := app.Spec.Response.BasicAuth.PasswordSecretRef; ref != nil {
ctx, cancel := context.WithTimeout(k.ctx, 10*time.Second)
secret, err := k.client.CoreV1().Secrets(meta.Namespace).Get(ctx, ref.Name, metav1.GetOptions{})
cancel()

missingKey := false
if err == nil {
if password, ok := secret.Data[ref.Key]; ok {
internalApp.Response.BasicAuth.Password = string(password)
} else {
missingKey = true
err = fmt.Errorf("key %q not found in Secret %q", ref.Key, ref.Name)
}
}

optional := ref.Optional != nil && *ref.Optional
if err != nil && !(optional && (missingKey || apierrors.IsNotFound(err))) {
k.log.App.Warn().Err(err).
Str("namespace", meta.Namespace).
Str("name", meta.Name).
Str("secret", ref.Name).
Str("key", ref.Key).
Msg("Failed to resolve basic auth password Secret, skipping")

return ExtractionResult{Meta: meta}
}
}
return ExtractionResult{
Meta: meta,
Apps: map[string]model.App{
meta.Name: internalApp,
},
}
}
Loading
Loading