v0.33.7_docker_hostnames

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-22 19:00:26 +02:00
1 parent f462ca7194
commit 7e625c5876
6 files changed
+90 -73

No files matched your search

+4
View File
@@ -44,3 +44,7 @@ go build -trimpath -ldflags='-s -w' -o rustproxy-docker-agent .
The inventory endpoint is `GET /v1/inventory` with `Authorization: Bearer <token>`. `/healthz` does not expose Docker inventory. Both are served over HTTPS by default.
`RUSTPROXY_DOCKER_ADVERTISE_IP` marks a preferred address but does not suppress automatic discovery of the host's other addresses. `RUSTPROXY_DOCKER_PUBLIC_IP_URL` adds the externally observed address when enabled. RustProxy can choose a concrete reported address independently for every Docker-backed vhost.
## Default vhost hostname suggestions
The agent reports a DNS-safe Docker container name as `technical_name` (or the Compose service name when the container name is unavailable). RustProxy pre-fills this name for newly published vhosts. For example, `/grafana` suggests `grafana`, not `grafana-3000`. If multiple published services have the same normalized container name, the agent appends the container's internal port; remaining collisions get a deterministic identifier. The stable `service_key` is unchanged. Already saved vhost hostnames are never rewritten by discovery.
+43 -6
View File
@@ -2,6 +2,7 @@ package main
import (
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"io"
@@ -92,20 +93,18 @@ func serviceIdentity(c dockerContainer, p dockerPort) (key, technical string) {
project := c.Labels["com.docker.compose.project"]
composeService := c.Labels["com.docker.compose.service"]
replica := c.Labels["com.docker.compose.container-number"]
suffix := strconv.Itoa(int(p.PrivatePort))
if project != "" && composeService != "" {
if replica == "" {
replica = name
}
key = fmt.Sprintf("compose:%s/%s/%s:%d/%s", project, composeService, replica, p.PrivatePort, p.Type)
technicalBase := project + "-" + composeService
if replica != "1" {
technicalBase += "-" + replica
technical = dnsLabel(name)
if name == "container" {
technical = dnsLabel(composeService)
}
technical = dnsLabel(technicalBase + "-" + suffix)
} else {
key = fmt.Sprintf("container:%s:%d/%s", name, p.PrivatePort, p.Type)
technical = dnsLabel(name + "-" + suffix)
technical = dnsLabel(name)
}
return key, technical
}
@@ -163,5 +162,43 @@ func servicesFrom(containers []dockerContainer) []service {
out = append(out, *item)
}
sort.Slice(out, func(i, j int) bool { return out[i].Key < out[j].Key })
// Prefer the Docker container name. Add the internal port only when names
// collide; retain stable service keys and prevent any duplicate hostnames.
counts := make(map[string]int, len(out))
for _, item := range out {
counts[item.TechnicalName]++
}
for i := range out {
if counts[out[i].TechnicalName] > 1 {
out[i].TechnicalName = dnsLabel(out[i].TechnicalName + "-" + strconv.Itoa(int(out[i].PrivatePort)))
}
}
// Port suffixes can still collide after normalization/truncation.
// Resolve these deterministically using a short hash of the stable key.
counts = make(map[string]int, len(out))
for _, item := range out {
counts[item.TechnicalName]++
}
used := make(map[string]struct{}, len(out))
for i := range out {
name := out[i].TechnicalName
if counts[name] > 1 {
digest := sha256.Sum256([]byte(out[i].Key))
name = fmt.Sprintf("%s-%x", strings.TrimRight(name[:min(len(name), 48)], "-"), digest[:6])
}
// Extremely unlikely hash collisions are still resolved without
// changing the stable key or depending on Docker listing order.
for attempt := 0; ; attempt++ {
candidate := name
if attempt > 0 {
candidate = fmt.Sprintf("%s-%d", name[:min(len(name), 56)], attempt)
}
if _, exists := used[candidate]; !exists {
out[i].TechnicalName = candidate
used[candidate] = struct{}{}
break
}
}
}
return out
}
+37 -1
View File
@@ -41,7 +41,7 @@ func TestServicesFromUsesStableComposeIdentity(t *testing.T) {
if len(got) != 1 {
t.Fatalf("got %d services", len(got))
}
if got[0].Key != "compose:lab/web/1:8080/tcp" || got[0].TechnicalName != "lab-web-8080" {
if got[0].Key != "compose:lab/web/1:8080/tcp" || got[0].TechnicalName != "web-1" {
t.Fatalf("unexpected service %#v", got[0])
}
if len(got[0].Bindings) != 1 || got[0].Bindings[0].PublicPort != 18080 {
@@ -72,6 +72,42 @@ func TestServicesFromKeepsComposeReplicasAndAllBindings(t *testing.T) {
}
}
func TestServiceNamesPreferContainerAndOnlyAppendPortsForCollisions(t *testing.T) {
items := []dockerContainer{
{ID: "a", Names: []string{"/grafana"}, Labels: map[string]string{}, Ports: []dockerPort{{PrivatePort: 3000, PublicPort: 3000, Type: "tcp", IP: "0.0.0.0"}}},
{ID: "b", Names: []string{"/frontend"}, Labels: map[string]string{"com.docker.compose.project": "example", "com.docker.compose.service": "web", "com.docker.compose.container-number": "1"}, Ports: []dockerPort{{PrivatePort: 80, PublicPort: 8080, Type: "tcp", IP: "0.0.0.0"}}},
{ID: "c", Names: []string{"/shared"}, Ports: []dockerPort{{PrivatePort: 80, PublicPort: 8081, Type: "tcp", IP: "0.0.0.0"}, {PrivatePort: 443, PublicPort: 8443, Type: "tcp", IP: "0.0.0.0"}}},
}
got := servicesFrom(items)
names := map[string]string{}
for _, item := range got {
names[item.Key] = item.TechnicalName
}
if names["container:grafana:3000/tcp"] != "grafana" || names["compose:example/web/1:80/tcp"] != "frontend" {
t.Fatalf("incorrect short names: %#v", names)
}
if names["container:shared:80/tcp"] != "shared-80" || names["container:shared:443/tcp"] != "shared-443" {
t.Fatalf("incorrect collision names: %#v", names)
}
}
func TestServiceNamesRemainUniqueAfterDNSNormalization(t *testing.T) {
items := []dockerContainer{
{Names: []string{"/A_B"}, Ports: []dockerPort{{PrivatePort: 80, PublicPort: 18080, Type: "tcp"}}},
{Names: []string{"/a-b"}, Ports: []dockerPort{{PrivatePort: 80, PublicPort: 18081, Type: "tcp"}}},
}
got := servicesFrom(items)
if len(got) != 2 || got[0].TechnicalName == got[1].TechnicalName {
t.Fatalf("names not unique: %#v", got)
}
reversed := servicesFrom([]dockerContainer{items[1], items[0]})
for i := range got {
if got[i].TechnicalName != reversed[i].TechnicalName {
t.Fatalf("unstable names: %#v vs %#v", got, reversed)
}
}
}
func TestServicesFromSkipsLoopbackOnlyBindings(t *testing.T) {
items := []dockerContainer{{ID: "abc", Names: []string{"/app"}, Labels: map[string]string{}, Ports: []dockerPort{
{IP: "127.0.0.1", PrivatePort: 8080, PublicPort: 18080, Type: "tcp"},