v0.33.7_docker_hostnames
This commit is contained in:
1 parent
f462ca7194
commit
7e625c5876
6 files changed
+90
-73
No files matched your search
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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"},
|
||||
|
||||
Reference in new issue
Block a user