tcp stream (und damit: tls passthrough) ist implementiert
This commit is contained in:
1 parent
6a5cb95f92
commit
fdcd06c93f
16 files changed
+1008
-75
No files matched your search
+46
-19
@@ -13,13 +13,19 @@ import (
|
||||
"sultan/pkg/registry"
|
||||
)
|
||||
|
||||
const servicesPrefix = "traefik.http.services."
|
||||
// protocols sind die Label-Ebenen mit Routern und Services (HTTP und TCP
|
||||
// für TLS-Passthrough). Router- und Service-Namen sind je Ebene getrennt.
|
||||
var protocols = []string{"http", "tcp"}
|
||||
|
||||
func routersPrefix(proto string) string { return "traefik." + proto + ".routers." }
|
||||
func servicesPrefix(proto string) string { return "traefik." + proto + ".services." }
|
||||
|
||||
var unsafeName = regexp.MustCompile(`[^A-Za-z0-9_-]`)
|
||||
|
||||
// Workloads übersetzt Container in Workloads für service_state.
|
||||
//
|
||||
// Berücksichtigt werden nur Container mit traefik.enable=true. Weil der
|
||||
// Berücksichtigt werden nur Container mit traefik.enable=true. Die Regeln
|
||||
// gelten für traefik.http.* und traefik.tcp.* (TLS-Passthrough) getrennt. Weil der
|
||||
// Ingress auf einem anderen Host sitzt, wird loadbalancer.server.port (Port
|
||||
// im Container) auf den veröffentlichten Host-Port umgerechnet. Fehlt das
|
||||
// Port-Label, gilt wie bei Traefiks Docker-Provider der einzige
|
||||
@@ -36,8 +42,8 @@ func Workloads(containers []docker.Container, host string) (workloads []registry
|
||||
type candidate struct {
|
||||
workload registry.Workload
|
||||
running bool
|
||||
routers []string
|
||||
services []string
|
||||
routers []string // "<proto>:<name>"
|
||||
services []string // "<proto>:<name>"
|
||||
}
|
||||
var candidates []candidate
|
||||
taken := map[string]bool{} // Namen laufender Container, "r:"/"s:" + klein
|
||||
@@ -53,8 +59,14 @@ func Workloads(containers []docker.Container, host string) (workloads []registry
|
||||
cand := candidate{
|
||||
workload: registry.Workload{Name: c.Name(), Host: host, Labels: labels},
|
||||
running: c.State == "running",
|
||||
routers: labelNames(labels, "traefik.http.routers."),
|
||||
services: labelNames(labels, servicesPrefix),
|
||||
}
|
||||
for _, proto := range protocols {
|
||||
for _, r := range labelNames(labels, routersPrefix(proto)) {
|
||||
cand.routers = append(cand.routers, proto+":"+r)
|
||||
}
|
||||
for _, s := range labelNames(labels, servicesPrefix(proto)) {
|
||||
cand.services = append(cand.services, proto+":"+s)
|
||||
}
|
||||
}
|
||||
if cand.running {
|
||||
for _, r := range cand.routers {
|
||||
@@ -84,7 +96,10 @@ func Workloads(containers []docker.Container, host string) (workloads []registry
|
||||
if collides(c) {
|
||||
continue
|
||||
}
|
||||
down = append(down, c.services...)
|
||||
for _, s := range c.services {
|
||||
_, name, _ := strings.Cut(s, ":")
|
||||
down = append(down, name)
|
||||
}
|
||||
}
|
||||
workloads = append(workloads, c.workload)
|
||||
}
|
||||
@@ -142,6 +157,18 @@ type serviceLabels struct {
|
||||
func translatePorts(c docker.Container) (map[string]string, string) {
|
||||
labels := maps.Clone(c.Labels)
|
||||
reachable := reachablePorts(c.Ports)
|
||||
for _, proto := range protocols {
|
||||
if warning := translateProtoPorts(c, labels, reachable, proto); warning != "" {
|
||||
return nil, warning
|
||||
}
|
||||
}
|
||||
return labels, ""
|
||||
}
|
||||
|
||||
// translateProtoPorts rechnet die Service-Ports einer Ebene (http/tcp) in
|
||||
// labels auf veröffentlichte Host-Ports um.
|
||||
func translateProtoPorts(c docker.Container, labels map[string]string, reachable map[uint16]uint16, proto string) string {
|
||||
svcPrefix := servicesPrefix(proto)
|
||||
single := func() (uint16, bool) {
|
||||
for _, pub := range reachable {
|
||||
return pub, len(reachable) == 1
|
||||
@@ -153,10 +180,10 @@ func translatePorts(c docker.Container) (map[string]string, string) {
|
||||
hasRouters := false
|
||||
for k := range labels {
|
||||
lk := strings.ToLower(k)
|
||||
if strings.HasPrefix(lk, "traefik.http.routers.") {
|
||||
if strings.HasPrefix(lk, routersPrefix(proto)) {
|
||||
hasRouters = true
|
||||
}
|
||||
rest, ok := strings.CutPrefix(lk, servicesPrefix)
|
||||
rest, ok := strings.CutPrefix(lk, svcPrefix)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
@@ -166,7 +193,7 @@ func translatePorts(c docker.Container) (map[string]string, string) {
|
||||
}
|
||||
s := services[svc]
|
||||
if s == nil {
|
||||
name, _, _ := strings.Cut(k[len(servicesPrefix):], ".")
|
||||
name, _, _ := strings.Cut(k[len(svcPrefix):], ".")
|
||||
s = &serviceLabels{name: name}
|
||||
services[svc] = s
|
||||
}
|
||||
@@ -177,35 +204,35 @@ func translatePorts(c docker.Container) (map[string]string, string) {
|
||||
|
||||
if len(services) == 0 {
|
||||
if !hasRouters {
|
||||
return labels, ""
|
||||
return ""
|
||||
}
|
||||
pub, ok := single()
|
||||
if !ok {
|
||||
return nil, fmt.Sprintf("Container %s: %d erreichbare Ports – Label loadbalancer.server.port angeben", c.Name(), len(reachable))
|
||||
return fmt.Sprintf("Container %s: %d erreichbare Ports – Label loadbalancer.server.port angeben", c.Name(), len(reachable))
|
||||
}
|
||||
svc := unsafeName.ReplaceAllString(c.Name(), "-")
|
||||
labels[servicesPrefix+svc+".loadbalancer.server.port"] = strconv.Itoa(int(pub))
|
||||
return labels, ""
|
||||
labels[svcPrefix+svc+".loadbalancer.server.port"] = strconv.Itoa(int(pub))
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, s := range services {
|
||||
if s.portKey == "" {
|
||||
pub, ok := single()
|
||||
if !ok {
|
||||
return nil, fmt.Sprintf("Container %s, Service %s: %d erreichbare Ports – Label loadbalancer.server.port angeben", c.Name(), s.name, len(reachable))
|
||||
return fmt.Sprintf("Container %s, Service %s: %d erreichbare Ports – Label loadbalancer.server.port angeben", c.Name(), s.name, len(reachable))
|
||||
}
|
||||
labels[servicesPrefix+s.name+".loadbalancer.server.port"] = strconv.Itoa(int(pub))
|
||||
labels[svcPrefix+s.name+".loadbalancer.server.port"] = strconv.Itoa(int(pub))
|
||||
continue
|
||||
}
|
||||
private, err := strconv.Atoi(strings.TrimSpace(labels[s.portKey]))
|
||||
if err != nil {
|
||||
return nil, fmt.Sprintf("Container %s: Port %q nicht lesbar", c.Name(), labels[s.portKey])
|
||||
return fmt.Sprintf("Container %s: Port %q nicht lesbar", c.Name(), labels[s.portKey])
|
||||
}
|
||||
pub, ok := reachable[uint16(private)]
|
||||
if !ok {
|
||||
return nil, fmt.Sprintf("Container %s: Port %d ist von außen nicht erreichbar (nicht veröffentlicht oder nur 127.0.0.1)", c.Name(), private)
|
||||
return fmt.Sprintf("Container %s: Port %d ist von außen nicht erreichbar (nicht veröffentlicht oder nur 127.0.0.1)", c.Name(), private)
|
||||
}
|
||||
labels[s.portKey] = strconv.Itoa(int(pub))
|
||||
}
|
||||
return labels, ""
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"sultan/pkg/docker"
|
||||
)
|
||||
|
||||
func caLabels() map[string]string {
|
||||
return map[string]string{
|
||||
"traefik.enable": "true",
|
||||
"traefik.tcp.routers.ca.rule": "HostSNI(`ca.lab.kunde.de`)",
|
||||
"traefik.tcp.routers.ca.tls.passthrough": "true",
|
||||
"traefik.tcp.services.ca.loadbalancer.server.port": "9000",
|
||||
}
|
||||
}
|
||||
|
||||
// TLS-Passthrough: Auch TCP-Services zeigen auf den veröffentlichten Host-Port.
|
||||
func TestWorkloads_TCPTranslatesPublishedPort(t *testing.T) {
|
||||
got, _, warnings := Workloads([]docker.Container{ctr("tinyca", caLabels(), pub(9000, 19000))}, "h")
|
||||
if len(warnings) != 0 || len(got) != 1 {
|
||||
t.Fatalf("got=%+v warnings=%v", got, warnings)
|
||||
}
|
||||
if p := got[0].Labels["traefik.tcp.services.ca.loadbalancer.server.port"]; p != "19000" {
|
||||
t.Errorf("Port = %q", p)
|
||||
}
|
||||
|
||||
_, _, warnings = Workloads([]docker.Container{ctr("tinyca", caLabels(), pub(80, 8080))}, "h")
|
||||
if len(warnings) != 1 {
|
||||
t.Errorf("unerreichbarer TCP-Port: Warnungen = %v", warnings)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkloads_TCPDefaultService(t *testing.T) {
|
||||
labels := map[string]string{"traefik.enable": "true", "traefik.tcp.routers.ca.rule": "HostSNI(`ca.lab.kunde.de`)"}
|
||||
got, _, warnings := Workloads([]docker.Container{ctr("tiny.ca", labels, pub(9000, 19000))}, "h")
|
||||
if len(warnings) != 0 || len(got) != 1 {
|
||||
t.Fatalf("got=%+v warnings=%v", got, warnings)
|
||||
}
|
||||
if got[0].Labels["traefik.tcp.services.tiny-ca.loadbalancer.server.port"] != "19000" {
|
||||
t.Errorf("Default-Service fehlt: %v", got[0].Labels)
|
||||
}
|
||||
if _, ok := got[0].Labels["traefik.http.services.tiny-ca.loadbalancer.server.port"]; ok {
|
||||
t.Error("ohne HTTP-Router kein HTTP-Service")
|
||||
}
|
||||
}
|
||||
|
||||
// HTTP (z. B. Redirect auf :80) und TCP im selben Container.
|
||||
func TestWorkloads_HTTPAndTCP(t *testing.T) {
|
||||
labels := caLabels()
|
||||
labels["traefik.http.routers.ca-http.rule"] = "Host(`ca.lab.kunde.de`)"
|
||||
labels["traefik.http.services.ca-http.loadbalancer.server.port"] = "80"
|
||||
got, _, warnings := Workloads([]docker.Container{ctr("tinyca", labels, pub(80, 8080), pub(9000, 19000))}, "h")
|
||||
if len(warnings) != 0 || len(got) != 1 {
|
||||
t.Fatalf("got=%+v warnings=%v", got, warnings)
|
||||
}
|
||||
l := got[0].Labels
|
||||
if l["traefik.http.services.ca-http.loadbalancer.server.port"] != "8080" || l["traefik.tcp.services.ca.loadbalancer.server.port"] != "19000" {
|
||||
t.Errorf("Labels = %v", l)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkloads_TCPStoppedIsDown(t *testing.T) {
|
||||
_, down, _ := Workloads([]docker.Container{stopped(ctr("tinyca", caLabels(), pub(9000, 19000)), "exited")}, "h")
|
||||
if !reflect.DeepEqual(down, []string{"ca"}) {
|
||||
t.Errorf("down = %v", down)
|
||||
}
|
||||
|
||||
// Ein laufender Container mit demselben TCP-Router verdrängt den gestoppten.
|
||||
got, down, _ := Workloads([]docker.Container{
|
||||
stopped(ctr("a-alt", caLabels(), pub(9000, 19001)), "exited"),
|
||||
ctr("b-neu", caLabels(), pub(9000, 19000)),
|
||||
}, "h")
|
||||
if len(got) != 1 || got[0].Name != "b-neu" || len(down) != 0 {
|
||||
t.Errorf("got=%+v down=%v", got, down)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user