+49
.gitea/workflows/docker.yaml
+49
.gitea/workflows/docker.yaml
···
1
+
name: Docker
2
+
3
+
on:
4
+
push:
5
+
branches: [ "main" ]
6
+
# Publish semver tags as releases.
7
+
tags: [ 'v*.*.*' ]
8
+
pull_request:
9
+
branches: [ "main" ]
10
+
11
+
jobs:
12
+
build:
13
+
runs-on: ubuntu-latest
14
+
permissions:
15
+
contents: read
16
+
packages: write
17
+
steps:
18
+
- name: Checkout repository
19
+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
20
+
21
+
- name: Set up Docker Buildx
22
+
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
23
+
24
+
- name: Log into registry
25
+
if: github.event_name != 'pull_request'
26
+
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
27
+
with:
28
+
registry: git.xeserv.us
29
+
username: mimi
30
+
password: ${{ secrets.RELEASE_GITEA_TOKEN }}
31
+
32
+
- name: Build and push
33
+
if: github.event_name != 'pull_request'
34
+
id: build
35
+
uses: docker/bake-action@76f9fa3a758507623da19f6092dc4089a7e61592 # v6.6.0
36
+
with:
37
+
source: .
38
+
push: true
39
+
set: |
40
+
hythlodaeus.tags=git.xeserv.us/techaro/hythlodaeus:latest
41
+
42
+
- name: Build Docker image
43
+
if: github.event_name == 'pull_request'
44
+
uses: docker/bake-action@76f9fa3a758507623da19f6092dc4089a7e61592 # v6.6.0
45
+
with:
46
+
source: .
47
+
push: false
48
+
set: |
49
+
hythlodaeus.tags=git.xeserv.us/techaro/hythlodaeus:latest
+1
-1
LICENSE
+1
-1
LICENSE
···
1
1
MIT License
2
2
3
-
Copyright (c) 2025 xe
3
+
Copyright (c) 2025 Caleb Doxsey and Xe Iaso
4
4
5
5
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
6
+2
README.md
+2
README.md
+66
cmd/hythlodaeus/main.go
+66
cmd/hythlodaeus/main.go
···
1
+
package main
2
+
3
+
import (
4
+
"context"
5
+
"flag"
6
+
"log"
7
+
"os"
8
+
"path/filepath"
9
+
10
+
"git.xeserv.us/Techaro/hythlodaeus/server"
11
+
"git.xeserv.us/Techaro/hythlodaeus/watcher"
12
+
"github.com/facebookgo/flagenv"
13
+
"golang.org/x/sync/errgroup"
14
+
"k8s.io/client-go/kubernetes"
15
+
"k8s.io/client-go/rest"
16
+
"k8s.io/client-go/tools/clientcmd"
17
+
)
18
+
19
+
var (
20
+
httpBind = flag.String("http-bind", ":80", "the host:port to bind plain HTTP")
21
+
httpsBind = flag.String("https-bind", ":443", "the host:port to bind secure HTTPS")
22
+
)
23
+
24
+
func main() {
25
+
flagenv.Parse()
26
+
flag.Parse()
27
+
28
+
client, err := kubernetes.NewForConfig(getKubernetesConfig())
29
+
if err != nil {
30
+
log.Fatalf("can't make kubernetes client: %v", err)
31
+
}
32
+
33
+
s := server.New(server.WithHTTPBind(*httpBind), server.WithHTTPSBind(*httpsBind))
34
+
w := watcher.New(client, func(payload *watcher.Payload) {
35
+
s.Update(payload)
36
+
})
37
+
38
+
eg, ctx := errgroup.WithContext(context.Background())
39
+
eg.Go(func() error {
40
+
return s.Run(ctx)
41
+
})
42
+
eg.Go(func() error {
43
+
return w.Run(ctx)
44
+
})
45
+
if err := eg.Wait(); err != nil {
46
+
log.Fatal(err)
47
+
}
48
+
}
49
+
50
+
func getKubernetesConfig() *rest.Config {
51
+
config, err := rest.InClusterConfig()
52
+
if err != nil {
53
+
config, err = clientcmd.BuildConfigFromFlags("", filepath.Join(homeDir(), ".kube", "config"))
54
+
}
55
+
if err != nil {
56
+
log.Fatalf("can't get kubernetes config: %v", err)
57
+
}
58
+
return config
59
+
}
60
+
61
+
func homeDir() string {
62
+
if h := os.Getenv("HOME"); h != "" {
63
+
return h
64
+
}
65
+
return os.Getenv("USERPROFILE") // windows
66
+
}
+27
docker-bake.hcl
+27
docker-bake.hcl
···
1
+
variable "ALPINE_VERSION" { default = "edge" }
2
+
variable "GO_VERSION" { default = "1.24" }
3
+
variable "GITHUB_SHA" { default = "devel" }
4
+
5
+
group "default" {
6
+
targets = [
7
+
"hythlodaeus",
8
+
]
9
+
}
10
+
11
+
target "hythlodaeus" {
12
+
args = {
13
+
ALPINE_VERSION = null
14
+
GO_VERSION = null
15
+
COMPONENT = "hythlodaeus"
16
+
}
17
+
context = "."
18
+
dockerfile = "./docker/alpine.Dockerfile"
19
+
platforms = [
20
+
"linux/amd64",
21
+
"linux/arm64",
22
+
]
23
+
pull = true
24
+
tags = [
25
+
"git.xeserv.us/techaro/hythlodaeus:${GITHUB_SHA}"
26
+
]
27
+
}
+28
docker/alpine.Dockerfile
+28
docker/alpine.Dockerfile
···
1
+
ARG GO_VERSION=1.24
2
+
ARG ALPINE_VERSION=edge
3
+
FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine AS build
4
+
5
+
ARG TARGETOS
6
+
ARG TARGETARCH
7
+
ARG COMPONENT
8
+
9
+
WORKDIR /app
10
+
11
+
COPY go.mod go.sum ./
12
+
RUN go mod download
13
+
14
+
COPY . .
15
+
RUN --mount=type=cache,target=/root/.cache GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=0 go build -gcflags "all=-N -l" -o /app/bin/${COMPONENT} ./cmd/${COMPONENT}
16
+
17
+
FROM alpine:${ALPINE_VERSION} AS run
18
+
WORKDIR /app
19
+
20
+
ARG COMPONENT
21
+
22
+
RUN apk -U add ca-certificates mailcap
23
+
24
+
COPY --from=build /app/bin/${COMPONENT} /app/bin/${COMPONENT}
25
+
26
+
CMD ["/app/bin/${COMPONENT}"]
27
+
28
+
LABEL org.opencontainers.image.source="https://git.xeserv.us/techaro/hivemind"
+18
-10
go.mod
+18
-10
go.mod
···
3
3
go 1.24.3
4
4
5
5
require (
6
+
github.com/bep/debounce v1.2.0
7
+
github.com/exaring/ja4plus v0.0.1
6
8
github.com/facebookgo/flagenv v0.0.0-20160425205200-fcd59fca7456
9
+
github.com/google/uuid v1.6.0
10
+
golang.org/x/net v0.40.0
11
+
golang.org/x/sync v0.14.0
12
+
k8s.io/api v0.33.1
13
+
k8s.io/apimachinery v0.33.1
7
14
k8s.io/client-go v0.33.1
8
15
)
9
16
···
17
24
github.com/Microsoft/go-winio v0.6.2 // indirect
18
25
github.com/ProtonMail/go-crypto v1.1.6 // indirect
19
26
github.com/Songmu/gitconfig v0.2.0 // indirect
20
-
github.com/TecharoHQ/yeet v0.2.2 // indirect
27
+
github.com/TecharoHQ/yeet v0.2.3 // indirect
21
28
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
22
29
github.com/cavaliergopher/cpio v1.0.1 // indirect
23
30
github.com/cli/go-gh v0.1.0 // indirect
···
49
56
github.com/google/go-cmp v0.7.0 // indirect
50
57
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect
51
58
github.com/google/rpmpack v0.6.1-0.20240329070804-c2247cbb881a // indirect
52
-
github.com/google/uuid v1.6.0 // indirect
53
59
github.com/goreleaser/chglog v0.7.0 // indirect
54
60
github.com/goreleaser/fileglob v1.3.0 // indirect
55
61
github.com/goreleaser/nfpm/v2 v2.42.0 // indirect
···
79
85
github.com/x448/float16 v0.8.4 // indirect
80
86
github.com/xanzy/ssh-agent v0.3.3 // indirect
81
87
gitlab.com/digitalxero/go-conventional-commit v1.0.7 // indirect
82
-
golang.org/x/crypto v0.37.0 // indirect
88
+
golang.org/x/crypto v0.38.0 // indirect
83
89
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
84
-
golang.org/x/net v0.39.0 // indirect
90
+
golang.org/x/mod v0.24.0 // indirect
85
91
golang.org/x/oauth2 v0.27.0 // indirect
86
-
golang.org/x/sys v0.32.0 // indirect
87
-
golang.org/x/term v0.31.0 // indirect
88
-
golang.org/x/text v0.24.0 // indirect
92
+
golang.org/x/sys v0.33.0 // indirect
93
+
golang.org/x/term v0.32.0 // indirect
94
+
golang.org/x/text v0.25.0 // indirect
89
95
golang.org/x/time v0.9.0 // indirect
96
+
golang.org/x/tools v0.33.0 // indirect
90
97
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
91
98
google.golang.org/protobuf v1.36.5 // indirect
92
99
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
93
100
gopkg.in/inf.v0 v0.9.1 // indirect
94
101
gopkg.in/warnings.v0 v0.1.2 // indirect
95
102
gopkg.in/yaml.v3 v3.0.1 // indirect
96
-
k8s.io/api v0.33.1 // indirect
97
-
k8s.io/apimachinery v0.33.1 // indirect
98
103
k8s.io/klog/v2 v2.130.1 // indirect
99
104
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
100
105
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
···
104
109
sigs.k8s.io/yaml v1.4.0 // indirect
105
110
)
106
111
107
-
tool github.com/TecharoHQ/yeet/cmd/yeet
112
+
tool (
113
+
github.com/TecharoHQ/yeet/cmd/yeet
114
+
golang.org/x/tools/cmd/goimports
115
+
)
+24
-14
go.sum
+24
-14
go.sum
···
24
24
github.com/ProtonMail/gopenpgp/v2 v2.7.1/go.mod h1:/BU5gfAVwqyd8EfC3Eu7zmuhwYQpKs+cGD8M//iiaxs=
25
25
github.com/Songmu/gitconfig v0.2.0 h1:pX2++u4KUq+K2k/ZCzGXLtkD3ceCqIdi0tDyb+IbSyo=
26
26
github.com/Songmu/gitconfig v0.2.0/go.mod h1:cB5bYJer+pl7W8g6RHFwL/0X6aJROVrYuHlvc7PT+hE=
27
-
github.com/TecharoHQ/yeet v0.2.2 h1:L5pc2LhlR9PaMgGVmvK0pPtRrUVYkec54KDcEyIRf2Q=
28
-
github.com/TecharoHQ/yeet v0.2.2/go.mod h1:avLiwxZpNY37A/o35XledvdmGnTkm3G7+Oskxca6Z7Y=
27
+
github.com/TecharoHQ/yeet v0.2.3 h1:Pcsnq5HTnk4Xntlu/FNEidH7x55bIx+f5Mk1hpVIngs=
28
+
github.com/TecharoHQ/yeet v0.2.3/go.mod h1:avLiwxZpNY37A/o35XledvdmGnTkm3G7+Oskxca6Z7Y=
29
29
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
30
30
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
31
31
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
32
32
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
33
+
github.com/bep/debounce v1.2.0 h1:wXds8Kq8qRfwAOpAxHrJDbCXgC5aHSzgQb/0gKsHQqo=
34
+
github.com/bep/debounce v1.2.0/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
33
35
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=
34
36
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
35
37
github.com/caarlos0/testfs v0.4.4 h1:3PHvzHi5Lt+g332CiShwS8ogTgS3HjrmzZxCm6JCDr8=
···
62
64
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
63
65
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
64
66
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
67
+
github.com/exaring/ja4plus v0.0.1 h1:JqFV1Jxi7Of48NpS5qAoueZq+xF1uaCz3wwnoxTU7Mk=
68
+
github.com/exaring/ja4plus v0.0.1/go.mod h1:W9UnA4hC2x6dL+WvwphbNDUH0FWVTHfF0p+vk0my5SY=
65
69
github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c h1:8ISkoahWXwZR41ois5lSJBSVw4D0OV19Ht/JSTzvSv0=
66
70
github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64=
67
71
github.com/facebookgo/flagenv v0.0.0-20160425205200-fcd59fca7456 h1:CkmB2l68uhvRlwOTPrwnuitSxi/S3Cg4L5QYOcL9MBc=
···
257
261
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
258
262
gitlab.com/digitalxero/go-conventional-commit v1.0.7 h1:8/dO6WWG+98PMhlZowt/YjuiKhqhGlOCwlIV8SqqGh8=
259
263
gitlab.com/digitalxero/go-conventional-commit v1.0.7/go.mod h1:05Xc2BFsSyC5tKhK0y+P3bs0AwUtNuTp+mTpbCU/DZ0=
264
+
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
265
+
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
260
266
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
261
267
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
262
268
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
263
269
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
264
-
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
265
-
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
270
+
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
271
+
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
266
272
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
267
273
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
268
274
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
269
275
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
276
+
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
277
+
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
270
278
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
271
279
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
272
280
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
273
281
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
274
282
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
275
-
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
276
-
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
283
+
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
284
+
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
277
285
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
278
286
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
279
287
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
280
288
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
281
289
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
290
+
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
291
+
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
282
292
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
283
293
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
284
294
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
···
296
306
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
297
307
golang.org/x/sys v0.0.0-20220818161305-2296e01440c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
298
308
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
299
-
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
300
-
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
309
+
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
310
+
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
301
311
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
302
-
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
303
-
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
312
+
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
313
+
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
304
314
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
305
315
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
306
316
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
307
317
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
308
-
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
309
-
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
318
+
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
319
+
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
310
320
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
311
321
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
312
322
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
313
323
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
314
324
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
315
325
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
316
-
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
317
-
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
326
+
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
327
+
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
318
328
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
319
329
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
320
330
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-307
main.go
-307
main.go
···
1
-
package main
2
-
3
-
import (
4
-
"context"
5
-
"crypto/tls"
6
-
"errors"
7
-
"flag"
8
-
"fmt"
9
-
"log"
10
-
"log/slog"
11
-
"net/url"
12
-
"path/filepath"
13
-
"regexp"
14
-
"sync"
15
-
"time"
16
-
17
-
"github.com/bep/debounce"
18
-
"github.com/facebookgo/flagenv"
19
-
networkingv1 "k8s.io/api/networking/v1"
20
-
"k8s.io/apimachinery/pkg/labels"
21
-
"k8s.io/client-go/informers"
22
-
"k8s.io/client-go/kubernetes"
23
-
"k8s.io/client-go/tools/cache"
24
-
"k8s.io/client-go/tools/clientcmd"
25
-
"k8s.io/client-go/util/homedir"
26
-
)
27
-
28
-
var (
29
-
kubeconfig = flag.String("kubeconfig", defaultKubeconfig(), "absolute path to the Kubernetes client configuration file")
30
-
)
31
-
32
-
func defaultKubeconfig() string {
33
-
if home := homedir.HomeDir(); home != "" {
34
-
return filepath.Join(home, ".kube", "config")
35
-
}
36
-
37
-
return ""
38
-
}
39
-
40
-
func main() {
41
-
flagenv.Parse()
42
-
flag.Parse()
43
-
44
-
config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
45
-
if err != nil {
46
-
log.Fatal(err)
47
-
}
48
-
49
-
client, err := kubernetes.NewForConfig(config)
50
-
if err != nil {
51
-
panic(err.Error())
52
-
}
53
-
54
-
}
55
-
56
-
type Payload struct {
57
-
Ingresses []IngressPayload
58
-
TLSCerts map[string]*tls.Certificate
59
-
}
60
-
61
-
type IngressPayload struct {
62
-
Ingress *networkingv1.Ingress
63
-
ServicePorts map[string]map[string]int
64
-
}
65
-
66
-
type Watcher struct {
67
-
cli kubernetes.Interface
68
-
onChange func(*Payload)
69
-
}
70
-
71
-
func p[T any](t T) *T { return &t }
72
-
73
-
func (w *Watcher) update() {
74
-
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
75
-
defer cancel()
76
-
77
-
if err := w.updateInner(ctx); err != nil {
78
-
slog.Error("can't handle changes", "err", err)
79
-
}
80
-
}
81
-
82
-
func (w *Watcher) updateInner(ctx context.Context) error {
83
-
factory := informers.NewSharedInformerFactory(w.cli, time.Minute)
84
-
secretLister := factory.Core().V1().Secrets().Lister()
85
-
serviceLister := factory.Core().V1().Services().Lister()
86
-
ingressLister := factory.Networking().V1().Ingresses().Lister()
87
-
88
-
ingresses, err := ingressLister.List(labels.Everything())
89
-
if err != nil {
90
-
return fmt.Errorf("failed to list ingresses: %w", err)
91
-
}
92
-
93
-
result := &Payload{
94
-
TLSCerts: map[string]*tls.Certificate{},
95
-
}
96
-
97
-
var errs []error
98
-
99
-
addBackend := func(ingp *IngressPayload, backend *networkingv1.IngressBackend) error {
100
-
svc, err := serviceLister.Services(ingp.Ingress.Namespace).Get(backend.Service.Port.Name)
101
-
if err != nil {
102
-
return fmt.Errorf("can't get secret: namespace=%q name=%q: %w", ingp.Ingress.Namespace, backend.Service.Port.Name, err)
103
-
}
104
-
105
-
ports := map[string]int{}
106
-
for _, port := range svc.Spec.Ports {
107
-
ports[port.Name] = int(port.Port)
108
-
}
109
-
110
-
ingp.ServicePorts[svc.Name] = ports
111
-
112
-
return nil
113
-
}
114
-
115
-
for _, ingress := range ingresses {
116
-
if ingress.Spec.IngressClassName != nil && *ingress.Spec.IngressClassName != "hythlodaeus" {
117
-
continue
118
-
}
119
-
120
-
ingp := &IngressPayload{
121
-
ServicePorts: map[string]map[string]int{},
122
-
}
123
-
var ingerrs []error
124
-
125
-
for _, tlsSpec := range ingress.Spec.TLS {
126
-
if tlsSpec.SecretName != "" {
127
-
sec, err := secretLister.Secrets(ingress.Namespace).Get(tlsSpec.SecretName)
128
-
if err != nil {
129
-
ingerrs = append(ingerrs, err)
130
-
slog.Error("can't read secret", "namespace", ingress.Namespace, "secret", tlsSpec.SecretName, "err", err)
131
-
continue
132
-
}
133
-
134
-
cert, err := tls.X509KeyPair(sec.Data["tls.crt"], sec.Data["tls.key"])
135
-
if err != nil {
136
-
ingerrs = append(ingerrs, err)
137
-
slog.Error("can't load TLS keypair", "namespace", ingress.Namespace, "secret", tlsSpec.SecretName, "err", err)
138
-
continue
139
-
}
140
-
141
-
result.TLSCerts[tlsSpec.SecretName] = &cert
142
-
}
143
-
}
144
-
145
-
if ingress.Spec.DefaultBackend != nil {
146
-
addBackend(ingp, ingress.Spec.DefaultBackend)
147
-
}
148
-
149
-
for _, rule := range ingress.Spec.Rules {
150
-
if rule.HTTP != nil {
151
-
continue
152
-
}
153
-
154
-
for _, path := range rule.HTTP.Paths {
155
-
addBackend(ingp, &path.Backend)
156
-
}
157
-
}
158
-
159
-
if len(ingerrs) != 0 {
160
-
errs = append(errs, fmt.Errorf("can't make ingress payload: namespace=%q name=%q %w", ingress.Namespace, ingress.Name, errors.Join(ingerrs...)))
161
-
continue
162
-
}
163
-
164
-
result.Ingresses = append(result.Ingresses, *ingp)
165
-
}
166
-
167
-
if len(errs) != 0 {
168
-
return errors.Join(errs...)
169
-
}
170
-
171
-
w.onChange(result)
172
-
173
-
return nil
174
-
}
175
-
176
-
func (w *Watcher) Run(ctx context.Context) error {
177
-
factory := informers.NewSharedInformerFactory(w.cli, 15*time.Second)
178
-
179
-
debounced := debounce.New(time.Second)
180
-
handler := cache.ResourceEventHandlerFuncs{
181
-
AddFunc: func(obj interface{}) {
182
-
debounced(w.update)
183
-
},
184
-
UpdateFunc: func(oldObj, newObj interface{}) {
185
-
debounced(w.update)
186
-
},
187
-
DeleteFunc: func(obj interface{}) {
188
-
debounced(w.update)
189
-
},
190
-
}
191
-
192
-
var wg sync.WaitGroup
193
-
wg.Add(1)
194
-
go func() {
195
-
informer := factory.Core().V1().Secrets().Informer()
196
-
informer.AddEventHandler(handler)
197
-
informer.Run(ctx.Done())
198
-
wg.Done()
199
-
}()
200
-
201
-
wg.Add(1)
202
-
go func() {
203
-
informer := factory.Networking().V1().Ingresses().Informer()
204
-
informer.AddEventHandler(handler)
205
-
informer.Run(ctx.Done())
206
-
wg.Done()
207
-
}()
208
-
209
-
wg.Add(1)
210
-
go func() {
211
-
informer := factory.Core().V1().Services().Informer()
212
-
informer.AddEventHandler(handler)
213
-
informer.Run(ctx.Done())
214
-
wg.Done()
215
-
}()
216
-
217
-
wg.Wait()
218
-
return nil
219
-
}
220
-
221
-
type routingTableBackend struct {
222
-
pathRE *regexp.Regexp
223
-
url *url.URL
224
-
}
225
-
226
-
func newRoutingTableBackend(scheme string, path string, serviceName string, servicePort int) (routingTableBackend, error) {
227
-
rtb := routingTableBackend{
228
-
url: &url.URL{
229
-
Scheme: scheme,
230
-
Host: fmt.Sprintf("%s:%d", serviceName, servicePort),
231
-
},
232
-
}
233
-
var err error
234
-
if path != "" {
235
-
rtb.pathRE, err = regexp.Compile(path)
236
-
}
237
-
return rtb, err
238
-
}
239
-
240
-
func (rtb routingTableBackend) matches(path string) bool {
241
-
if rtb.pathRE == nil {
242
-
return true
243
-
}
244
-
return rtb.pathRE.MatchString(path)
245
-
}
246
-
247
-
type RoutingTable struct {
248
-
certsByHost map[string]map[string]*tls.Certificate
249
-
backendsByHost map[string][]routingTableBackend
250
-
}
251
-
252
-
func NewRoutingTable(payload *Payload) *RoutingTable {
253
-
result := &RoutingTable{
254
-
certsByHost: make(map[string]map[string]*tls.Certificate),
255
-
backendsByHost: make(map[string][]routingTableBackend),
256
-
}
257
-
258
-
result.init(payload)
259
-
260
-
return result
261
-
}
262
-
263
-
func (rt *RoutingTable) init(payload *Payload) {
264
-
if payload == nil {
265
-
return
266
-
}
267
-
268
-
for _, ingp := range payload.Ingresses {
269
-
for _, rule := range ingp.Ingress.Spec.Rules {
270
-
certs, ok := rt.certsByHost[rule.Host]
271
-
if !ok {
272
-
certs = make(map[string]*tls.Certificate)
273
-
rt.certsByHost[rule.Host] = certs
274
-
}
275
-
276
-
for _, tlsSpec := range ingp.Ingress.Spec.TLS {
277
-
for _, h := range tlsSpec.Hosts {
278
-
cert, ok := payload.TLSCerts[tlsSpec.SecretName]
279
-
if ok {
280
-
certs[h] = cert
281
-
}
282
-
}
283
-
}
284
-
rt.addBackend(ingp, rule)
285
-
}
286
-
}
287
-
}
288
-
289
-
func (rt *RoutingTable) addBackend(ingp IngressPayload, rule networkingv1.IngressRule) {
290
-
scheme := "http"
291
-
292
-
if rule.HTTP == nil {
293
-
if ingp.Ingress.Spec.DefaultBackend != nil {
294
-
backend := ingp.Ingress.Spec.DefaultBackend
295
-
rtb, err := newRoutingTableBackend(scheme, "", backend.Service.Name, int(backend.Service.Port.Number))
296
-
if err != nil {
297
-
slog.Error("this should not happen, can't create routing table backend", "err", err)
298
-
}
299
-
rt.backendsByHost[rule.Host] = append(rt.backendsByHost[rule.Host], rtb)
300
-
}
301
-
} else {
302
-
for _, path := range rule.HTTP.Paths {
303
-
backend := path.Backend
304
-
rtb, err :=
305
-
}
306
-
}
307
-
}
+2
-2
package-lock.json
+2
-2
package-lock.json
···
1
1
{
2
-
"name": "@xe/hythlodaeus",
2
+
"name": "@techaro/hythlodaeus",
3
3
"version": "0.1.0",
4
4
"lockfileVersion": 3,
5
5
"requires": true,
6
6
"packages": {
7
7
"": {
8
-
"name": "@xe/hythlodaeus",
8
+
"name": "@techaro/hythlodaeus",
9
9
"version": "0.1.0",
10
10
"license": "MIT",
11
11
"devDependencies": {
+1
-1
package.json
+1
-1
package.json
+28
server/config.go
+28
server/config.go
···
1
+
package server
2
+
3
+
type config struct {
4
+
httpBind string
5
+
httpsBind string
6
+
}
7
+
8
+
func defaultConfig() *config {
9
+
return &config{
10
+
httpBind: ":80",
11
+
httpsBind: ":443",
12
+
}
13
+
}
14
+
15
+
// An Option modifies the config.
16
+
type Option func(*config)
17
+
18
+
func WithHTTPBind(httpBind string) Option {
19
+
return func(cfg *config) {
20
+
cfg.httpBind = httpBind
21
+
}
22
+
}
23
+
24
+
func WithHTTPSBind(httpsBind string) Option {
25
+
return func(cfg *config) {
26
+
cfg.httpsBind = httpsBind
27
+
}
28
+
}
+34
server/event.go
+34
server/event.go
···
1
+
package server
2
+
3
+
import (
4
+
"context"
5
+
"sync"
6
+
)
7
+
8
+
// An Event is used to communicate that something has happened.
9
+
type Event struct {
10
+
once sync.Once
11
+
C chan struct{}
12
+
}
13
+
14
+
// NewEvent creates a new Event.
15
+
func NewEvent() *Event {
16
+
return &Event{
17
+
C: make(chan struct{}),
18
+
}
19
+
}
20
+
21
+
// Set sets the event by closing the C channel. After the first time, calls to set are a no-op.
22
+
func (e *Event) Set() {
23
+
e.once.Do(func() {
24
+
close(e.C)
25
+
})
26
+
}
27
+
28
+
// Wait waits for the event to get set.
29
+
func (e *Event) Wait(ctx context.Context) {
30
+
select {
31
+
case <-ctx.Done():
32
+
case <-e.C:
33
+
}
34
+
}
+170
server/route.go
+170
server/route.go
···
1
+
package server
2
+
3
+
import (
4
+
"crypto/tls"
5
+
"errors"
6
+
"fmt"
7
+
"log/slog"
8
+
"net/url"
9
+
"regexp"
10
+
"strings"
11
+
12
+
"git.xeserv.us/Techaro/hythlodaeus/watcher"
13
+
networking "k8s.io/api/networking/v1"
14
+
"k8s.io/apimachinery/pkg/util/intstr"
15
+
)
16
+
17
+
const BackendProtocolAnnotation = "kubernetes-simple-ingress-controller/backend-protocol"
18
+
19
+
// A RoutingTable contains the information needed to route a request.
20
+
type RoutingTable struct {
21
+
certificatesByHost map[string]map[string]*tls.Certificate
22
+
backendsByHost map[string][]routingTableBackend
23
+
}
24
+
25
+
type routingTableBackend struct {
26
+
pathRE *regexp.Regexp
27
+
url *url.URL
28
+
}
29
+
30
+
func newRoutingTableBackend(scheme, path, serviceName, serviceNamespace string, servicePort int) (routingTableBackend, error) {
31
+
rtb := routingTableBackend{
32
+
url: &url.URL{
33
+
Scheme: scheme,
34
+
Host: fmt.Sprintf("%s.%s.svc.cluster.local", serviceName, serviceNamespace),
35
+
},
36
+
}
37
+
38
+
if servicePort != 0 {
39
+
rtb.url.Host += fmt.Sprintf(":%d", servicePort)
40
+
}
41
+
42
+
var err error
43
+
if path != "" {
44
+
rtb.pathRE, err = regexp.Compile(path)
45
+
}
46
+
return rtb, err
47
+
}
48
+
49
+
func (rtb routingTableBackend) matches(path string) bool {
50
+
if rtb.pathRE == nil {
51
+
return true
52
+
}
53
+
return rtb.pathRE.MatchString(path)
54
+
}
55
+
56
+
// NewRoutingTable creates a new RoutingTable.
57
+
func NewRoutingTable(payload *watcher.Payload) *RoutingTable {
58
+
rt := &RoutingTable{
59
+
certificatesByHost: make(map[string]map[string]*tls.Certificate),
60
+
backendsByHost: make(map[string][]routingTableBackend),
61
+
}
62
+
rt.init(payload)
63
+
return rt
64
+
}
65
+
66
+
func (rt *RoutingTable) init(payload *watcher.Payload) {
67
+
if payload == nil {
68
+
return
69
+
}
70
+
for _, ingressPayload := range payload.Ingresses {
71
+
for _, rule := range ingressPayload.Ingress.Spec.Rules {
72
+
m, ok := rt.certificatesByHost[rule.Host]
73
+
if !ok {
74
+
m = make(map[string]*tls.Certificate)
75
+
rt.certificatesByHost[rule.Host] = m
76
+
}
77
+
for _, t := range ingressPayload.Ingress.Spec.TLS {
78
+
for _, h := range t.Hosts {
79
+
cert, ok := payload.TLSCertificates[t.SecretName]
80
+
if ok {
81
+
m[h] = cert
82
+
}
83
+
}
84
+
}
85
+
rt.addBackend(ingressPayload, rule)
86
+
}
87
+
}
88
+
}
89
+
90
+
func (rt *RoutingTable) addBackend(ingressPayload watcher.IngressPayload, rule networking.IngressRule) {
91
+
scheme, ok := ingressPayload.Ingress.Annotations[BackendProtocolAnnotation]
92
+
if !ok {
93
+
scheme = "http"
94
+
}
95
+
scheme = strings.ToLower(scheme)
96
+
97
+
if rule.HTTP == nil {
98
+
if ingressPayload.Ingress.Spec.DefaultBackend != nil {
99
+
backend := ingressPayload.Ingress.Spec.DefaultBackend
100
+
rtb, err := newRoutingTableBackend(scheme, "", backend.Service.Name, ingressPayload.Ingress.Namespace,
101
+
rt.getServicePort(ingressPayload, backend.Service.Name, intstr.FromInt(int(backend.Service.Port.Number))))
102
+
if err != nil {
103
+
// this shouldn't happen
104
+
slog.Error("[unexpected] can't add routing table backend", "err", err)
105
+
return
106
+
}
107
+
rt.backendsByHost[rule.Host] = append(rt.backendsByHost[rule.Host], rtb)
108
+
}
109
+
} else {
110
+
for _, path := range rule.HTTP.Paths {
111
+
backend := path.Backend
112
+
rtb, err := newRoutingTableBackend(scheme, path.Path, backend.Service.Name, ingressPayload.Ingress.Namespace,
113
+
rt.getServicePort(ingressPayload, backend.Service.Name, intstr.FromInt(int(backend.Service.Port.Number))))
114
+
if err != nil {
115
+
slog.Error("can't add routing table backend", "err", err)
116
+
continue
117
+
}
118
+
rt.backendsByHost[rule.Host] = append(rt.backendsByHost[rule.Host], rtb)
119
+
}
120
+
}
121
+
}
122
+
123
+
func (rt *RoutingTable) getServicePort(ingressPayload watcher.IngressPayload, serviceName string, servicePort intstr.IntOrString) int {
124
+
if servicePort.Type == intstr.Int && servicePort.IntVal != 0 {
125
+
return servicePort.IntValue()
126
+
}
127
+
if m, ok := ingressPayload.ServicePorts[serviceName]; ok {
128
+
return m[servicePort.String()]
129
+
}
130
+
return 80
131
+
}
132
+
133
+
func (rt *RoutingTable) matches(sni string, certHost string) bool {
134
+
for strings.HasPrefix(certHost, "*.") {
135
+
if idx := strings.IndexByte(sni, '.'); idx >= 0 {
136
+
sni = sni[idx+1:]
137
+
} else {
138
+
return false
139
+
}
140
+
certHost = certHost[2:]
141
+
}
142
+
return sni == certHost
143
+
}
144
+
145
+
// GetCertificate gets a certificate.
146
+
func (rt *RoutingTable) GetCertificate(sni string) (*tls.Certificate, error) {
147
+
hostCerts, ok := rt.certificatesByHost[sni]
148
+
if ok {
149
+
for h, cert := range hostCerts {
150
+
if rt.matches(sni, h) {
151
+
return cert, nil
152
+
}
153
+
}
154
+
}
155
+
return nil, fmt.Errorf("certificate not found for %s", sni)
156
+
}
157
+
158
+
// GetBackend gets the backend for the given host and path.
159
+
func (rt *RoutingTable) GetBackend(host, path string) (*url.URL, error) {
160
+
if idx := strings.IndexByte(host, ':'); idx > 0 {
161
+
host = host[:idx]
162
+
}
163
+
backends := rt.backendsByHost[host]
164
+
for _, backend := range backends {
165
+
if backend.matches(path) {
166
+
return backend.url, nil
167
+
}
168
+
}
169
+
return nil, errors.New("backend not found")
170
+
}
+121
server/server.go
+121
server/server.go
···
1
+
package server
2
+
3
+
import (
4
+
"context"
5
+
"crypto/tls"
6
+
"fmt"
7
+
"log/slog"
8
+
"net/http"
9
+
"net/http/httputil"
10
+
"sync/atomic"
11
+
12
+
"git.xeserv.us/Techaro/hythlodaeus/watcher"
13
+
"github.com/exaring/ja4plus"
14
+
"github.com/google/uuid"
15
+
"golang.org/x/net/http2"
16
+
"golang.org/x/sync/errgroup"
17
+
)
18
+
19
+
// A Server serves HTTP pages.
20
+
type Server struct {
21
+
cfg *config
22
+
routingTable atomic.Value
23
+
ja4m *ja4plus.JA4Middleware
24
+
25
+
ready *Event
26
+
}
27
+
28
+
// New creates a new Server.
29
+
func New(options ...Option) *Server {
30
+
cfg := defaultConfig()
31
+
for _, o := range options {
32
+
o(cfg)
33
+
}
34
+
s := &Server{
35
+
cfg: cfg,
36
+
ready: NewEvent(),
37
+
ja4m: &ja4plus.JA4Middleware{},
38
+
}
39
+
s.routingTable.Store(NewRoutingTable(nil))
40
+
return s
41
+
}
42
+
43
+
// Run runs the server.
44
+
func (s *Server) Run(ctx context.Context) error {
45
+
// don't start listening until the first payload
46
+
s.ready.Wait(ctx)
47
+
48
+
var eg errgroup.Group
49
+
eg.Go(func() error {
50
+
srv := http.Server{
51
+
Addr: s.cfg.httpsBind,
52
+
Handler: s.ja4m.Wrap(s),
53
+
ConnState: s.ja4m.ConnStateCallback,
54
+
TLSConfig: &tls.Config{
55
+
GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
56
+
return s.routingTable.Load().(*RoutingTable).GetCertificate(hello.ServerName)
57
+
},
58
+
GetConfigForClient: func(chi *tls.ClientHelloInfo) (*tls.Config, error) {
59
+
s.ja4m.StoreFingerprintFromClientHello(chi)
60
+
return nil, nil
61
+
},
62
+
},
63
+
}
64
+
slog.Info("starting server", "protocol", "http", "addr", srv.Addr)
65
+
err := srv.ListenAndServeTLS("", "")
66
+
if err != nil {
67
+
return fmt.Errorf("error serving tls: %w", err)
68
+
}
69
+
return nil
70
+
})
71
+
eg.Go(func() error {
72
+
srv := http.Server{
73
+
Addr: s.cfg.httpBind,
74
+
Handler: s,
75
+
}
76
+
slog.Info("starting server", "protocol", "https", "addr", srv.Addr)
77
+
err := srv.ListenAndServe()
78
+
if err != nil {
79
+
return fmt.Errorf("error serving non-tls: %w", err)
80
+
}
81
+
return nil
82
+
})
83
+
return eg.Wait()
84
+
}
85
+
86
+
// ServeHTTP serves an HTTP request.
87
+
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
88
+
reqID := uuid.Must(uuid.NewV7()).String()
89
+
90
+
ja4 := ja4plus.JA4FromContext(r.Context())
91
+
if ja4 != "" {
92
+
r.Header.Set("X-Tls-Fingerprint-Ja4", ja4)
93
+
}
94
+
95
+
r.Header.Set("X-Forwarded-Host", r.Host)
96
+
r.Header.Set("X-Forwarded-Proto", "https")
97
+
r.Header.Set("X-Forwarded-Scheme", "https")
98
+
r.Header.Set("X-Request-Id", reqID)
99
+
r.Header.Set("X-Scheme", "https")
100
+
r.Header.Set("X-HTTP-Protocol", r.Proto)
101
+
102
+
backendURL, err := s.routingTable.Load().(*RoutingTable).GetBackend(r.Host, r.URL.Path)
103
+
if err != nil {
104
+
http.Error(w, "upstream server not found", http.StatusNotFound)
105
+
return
106
+
}
107
+
slog.Info("proxying request", "host", r.Host, "path", r.URL.Path, "backend", backendURL)
108
+
p := httputil.NewSingleHostReverseProxy(backendURL)
109
+
if backendURL.Scheme == "https" {
110
+
p.Transport = &http2.Transport{
111
+
AllowHTTP: true,
112
+
}
113
+
}
114
+
p.ServeHTTP(w, r)
115
+
}
116
+
117
+
// Update updates the server with new ingress rules.
118
+
func (s *Server) Update(payload *watcher.Payload) {
119
+
s.routingTable.Store(NewRoutingTable(payload))
120
+
s.ready.Set()
121
+
}
+157
watcher/watcher.go
+157
watcher/watcher.go
···
1
+
package watcher
2
+
3
+
import (
4
+
"context"
5
+
"crypto/tls"
6
+
"log/slog"
7
+
"sync"
8
+
"time"
9
+
10
+
"github.com/bep/debounce"
11
+
networking "k8s.io/api/networking/v1"
12
+
"k8s.io/apimachinery/pkg/labels"
13
+
"k8s.io/client-go/informers"
14
+
"k8s.io/client-go/kubernetes"
15
+
"k8s.io/client-go/tools/cache"
16
+
)
17
+
18
+
// A Payload is a collection of Kubernetes data loaded by the watcher.
19
+
type Payload struct {
20
+
Ingresses []IngressPayload
21
+
TLSCertificates map[string]*tls.Certificate
22
+
}
23
+
24
+
// An IngressPayload is an ingress + its service ports.
25
+
type IngressPayload struct {
26
+
Ingress *networking.Ingress
27
+
ServicePorts map[string]map[string]int
28
+
}
29
+
30
+
// A Watcher watches for ingresses in the kubernetes cluster
31
+
type Watcher struct {
32
+
client kubernetes.Interface
33
+
onChange func(*Payload)
34
+
}
35
+
36
+
// New creates a new Watcher.
37
+
func New(client kubernetes.Interface, onChange func(*Payload)) *Watcher {
38
+
return &Watcher{
39
+
client: client,
40
+
onChange: onChange,
41
+
}
42
+
}
43
+
44
+
// Run runs the watcher.
45
+
func (w *Watcher) Run(ctx context.Context) error {
46
+
factory := informers.NewSharedInformerFactory(w.client, time.Minute)
47
+
secretLister := factory.Core().V1().Secrets().Lister()
48
+
serviceLister := factory.Core().V1().Services().Lister()
49
+
ingressLister := factory.Networking().V1().Ingresses().Lister()
50
+
51
+
addBackend := func(ingressPayload *IngressPayload, backend networking.IngressBackend) {
52
+
slog.Debug("adding backend", "namespace", ingressPayload.Ingress.Namespace, "ingress", ingressPayload.Ingress.Name)
53
+
svc, err := serviceLister.Services(ingressPayload.Ingress.Namespace).Get(backend.Service.Name)
54
+
if err != nil {
55
+
slog.Error("unknown service", "namespace", ingressPayload.Ingress.Namespace, "name", backend.Service.Name, "err", err)
56
+
} else {
57
+
m := make(map[string]int)
58
+
for _, port := range svc.Spec.Ports {
59
+
m[port.Name] = int(port.Port)
60
+
}
61
+
ingressPayload.ServicePorts[svc.Name] = m
62
+
}
63
+
}
64
+
65
+
onChange := func() {
66
+
payload := &Payload{
67
+
TLSCertificates: make(map[string]*tls.Certificate),
68
+
}
69
+
70
+
ingresses, err := ingressLister.List(labels.Everything())
71
+
if err != nil {
72
+
slog.Error("failed to list ingresses", "err", err)
73
+
return
74
+
}
75
+
76
+
for _, ingress := range ingresses {
77
+
ingressPayload := IngressPayload{
78
+
Ingress: ingress,
79
+
ServicePorts: make(map[string]map[string]int),
80
+
}
81
+
payload.Ingresses = append(payload.Ingresses, ingressPayload)
82
+
83
+
if ingress.Spec.DefaultBackend != nil {
84
+
addBackend(&ingressPayload, *ingress.Spec.DefaultBackend)
85
+
}
86
+
for _, rule := range ingress.Spec.Rules {
87
+
if rule.HTTP != nil {
88
+
continue
89
+
}
90
+
for _, path := range rule.HTTP.Paths {
91
+
addBackend(&ingressPayload, path.Backend)
92
+
}
93
+
}
94
+
95
+
for _, rec := range ingress.Spec.TLS {
96
+
if rec.SecretName != "" {
97
+
secret, err := secretLister.Secrets(ingress.Namespace).Get(rec.SecretName)
98
+
if err != nil {
99
+
slog.Error("unknown secret", "namespace", ingress.Namespace, "name", rec.SecretName, "err", err)
100
+
continue
101
+
}
102
+
103
+
cert, err := tls.X509KeyPair(secret.Data["tls.crt"], secret.Data["tls.key"])
104
+
if err != nil {
105
+
slog.Error("invalid TLS certificate", "namespace", ingress.Namespace, "name", rec.SecretName, "err", err)
106
+
continue
107
+
}
108
+
109
+
payload.TLSCertificates[rec.SecretName] = &cert
110
+
}
111
+
}
112
+
}
113
+
114
+
w.onChange(payload)
115
+
}
116
+
117
+
debounced := debounce.New(time.Second)
118
+
handler := cache.ResourceEventHandlerFuncs{
119
+
AddFunc: func(obj interface{}) {
120
+
debounced(onChange)
121
+
},
122
+
UpdateFunc: func(oldObj, newObj interface{}) {
123
+
debounced(onChange)
124
+
},
125
+
DeleteFunc: func(obj interface{}) {
126
+
debounced(onChange)
127
+
},
128
+
}
129
+
130
+
var wg sync.WaitGroup
131
+
wg.Add(1)
132
+
go func() {
133
+
informer := factory.Core().V1().Secrets().Informer()
134
+
informer.AddEventHandler(handler)
135
+
informer.Run(ctx.Done())
136
+
wg.Done()
137
+
}()
138
+
139
+
wg.Add(1)
140
+
go func() {
141
+
informer := factory.Networking().V1().Ingresses().Informer()
142
+
informer.AddEventHandler(handler)
143
+
informer.Run(ctx.Done())
144
+
wg.Done()
145
+
}()
146
+
147
+
wg.Add(1)
148
+
go func() {
149
+
informer := factory.Core().V1().Services().Informer()
150
+
informer.AddEventHandler(handler)
151
+
informer.Run(ctx.Done())
152
+
wg.Done()
153
+
}()
154
+
155
+
wg.Wait()
156
+
return nil
157
+
}