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

+1
View File
@@ -5,6 +5,7 @@
## 0.33.7 ## 0.33.7
- Docker agent: suggest container names rather than name-port for newly published vhosts. Append port only for conflicts; preserve stable service keys and existing vhost hostnames.
- Replace the redundant **Arrange Overview** visual editor modal with an - Replace the redundant **Arrange Overview** visual editor modal with an
account-scoped Dashboard preset manager. Save, load/overwrite and delete up account-scoped Dashboard preset manager. Save, load/overwrite and delete up
to 20 named presets containing Dashboard density, card order/visibility and to 20 named presets containing Dashboard density, card order/visibility and
+5 -5
View File
@@ -2,7 +2,7 @@
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example 4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore 9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
c793a8be7f4b76350e04ab36dbeec3792bb2dcf33c87a86f76fa2f330e26dd9f AGENTS.md c793a8be7f4b76350e04ab36dbeec3792bb2dcf33c87a86f76fa2f330e26dd9f AGENTS.md
4d86a27c6b8f8931e4c43bbbd0da1a5880ffafd17bd139bd9ba93fa1e7c02af4 CHANGELOG.md 538acee273827bd09041e25fe7ee804284eb66ade85537cf3dcd7b968ec33b43 CHANGELOG.md
d5dc2b778af98f52ad52a7515e2d1b4ba8c2e01a04cc6e02564377075b01c501 Cargo.lock d5dc2b778af98f52ad52a7515e2d1b4ba8c2e01a04cc6e02564377075b01c501 Cargo.lock
7fd5dd1ccae8fac77bbbea4bbf960f5cf762f712ff17a2e629a5dd8629eb7ff9 Cargo.toml 7fd5dd1ccae8fac77bbbea4bbf960f5cf762f712ff17a2e629a5dd8629eb7ff9 Cargo.toml
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile 25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
@@ -13,14 +13,14 @@ f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs
ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md
4834c278430ddf3bf842995735195368f1ac2c3b5c1d749df4f9238caf1e2b25 deploy/rustproxy.service 4834c278430ddf3bf842995735195368f1ac2c3b5c1d749df4f9238caf1e2b25 deploy/rustproxy.service
c111f5b20484707b80efb7dd9176acbaa27b63ac78db497261ac234821e1fade dev.sh c111f5b20484707b80efb7dd9176acbaa27b63ac78db497261ac234821e1fade dev.sh
5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md 82fbf6deba6841f843ea5fb419efa03ee820c7fb2af00b1bf4d8f6cddad52b9e docker-agent/README.md
7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go 7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go
5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example 5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example
bf687a851374d8f1fa59d050f3f76532f7c12f16a6daa7d3c36102c2d9a8ec9c docker-agent/docker.go dcdf8b060d9529ceb8c286d0a592fa4bd876bd7fdcd1a178e4295e13cd88e6c0 docker-agent/docker.go
9e9f3f17f38f9b0730f598f65988ce8ef562d2182ef6778fc7292b990e9cf22f docker-agent/go.mod 9e9f3f17f38f9b0730f598f65988ce8ef562d2182ef6778fc7292b990e9cf22f docker-agent/go.mod
9d77c90767fa929ee8338734d72ce83284d3d965673270f62cb2b1613d587a33 docker-agent/install.sh 9d77c90767fa929ee8338734d72ce83284d3d965673270f62cb2b1613d587a33 docker-agent/install.sh
b68234472bec5cedd67c7cfe1de7d8806a965dfd4101f944aa6320132f18c14e docker-agent/main.go b68234472bec5cedd67c7cfe1de7d8806a965dfd4101f944aa6320132f18c14e docker-agent/main.go
3d4051a1f805d145df3877476e4b87773e74be8e123045065955a5133afdc68c docker-agent/main_test.go 34552a436bc88a49d1a82a60875d4bd1c0c0b808759b6a099a7f954f44701ab0 docker-agent/main_test.go
68ad007eac216f115809695d3c2c6651e2850ff3d7334685ae6e49bc21b01087 docker-agent/model.go 68ad007eac216f115809695d3c2c6651e2850ff3d7334685ae6e49bc21b01087 docker-agent/model.go
e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/network.go e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/network.go
634790f35ff4627df2a78cec2ed371f6e9ec9df00afb386075595fc7f3ed72eb docker-agent/rustproxy-docker-agent.service 634790f35ff4627df2a78cec2ed371f6e9ec9df00afb386075595fc7f3ed72eb docker-agent/rustproxy-docker-agent.service
@@ -64,7 +64,7 @@ ec7e32fd19e6ba6ed1732f1f3dd92ac0860609e8c0bbd3c1998840cdfb24bf54 docs/SECURITY.
007537ee971463072b2482dfa4fdcf1955f837ced0b711d4eef3b6267d16d4f6 docs/SECURITY_AUDIT_0.31.0.md 007537ee971463072b2482dfa4fdcf1955f837ced0b711d4eef3b6267d16d4f6 docs/SECURITY_AUDIT_0.31.0.md
84d1523a18dc98d266a290ed7fb8fe2624bb37fd54f4f0b991ba37a7624d8a3a docs/SETTINGS.md 84d1523a18dc98d266a290ed7fb8fe2624bb37fd54f4f0b991ba37a7624d8a3a docs/SETTINGS.md
8fc112cb45a2f35ef5b0dd4371aa6f43aca4df85db62c6ae02e56d74a3b702ae docs/STREAMS.md 8fc112cb45a2f35ef5b0dd4371aa6f43aca4df85db62c6ae02e56d74a3b702ae docs/STREAMS.md
420d8e3976882ef42c2b7dde409560825db72a22405b023876f9c39055671754 docs/TEST_REPORT.md 193704656501643c86470be699d2e1c05b5fbb678f68ffac1b3f5c51172bf433 docs/TEST_REPORT.md
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
751c9b13b47d0959a8575d6e82bdecba988b950a374ba0b084fbba08b4b65ebe docs/TRANSFORM.md 751c9b13b47d0959a8575d6e82bdecba988b950a374ba0b084fbba08b4b65ebe docs/TRANSFORM.md
fe3ec6ca16ff933239c12dacf39dd7b45717c0551a8b9f6ce601bf78136c9e37 docs/TRANSPORT.md fe3ec6ca16ff933239c12dacf39dd7b45717c0551a8b9f6ce601bf78136c9e37 docs/TRANSPORT.md
+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. 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. `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 ( import (
"context" "context"
"crypto/sha256"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@@ -92,20 +93,18 @@ func serviceIdentity(c dockerContainer, p dockerPort) (key, technical string) {
project := c.Labels["com.docker.compose.project"] project := c.Labels["com.docker.compose.project"]
composeService := c.Labels["com.docker.compose.service"] composeService := c.Labels["com.docker.compose.service"]
replica := c.Labels["com.docker.compose.container-number"] replica := c.Labels["com.docker.compose.container-number"]
suffix := strconv.Itoa(int(p.PrivatePort))
if project != "" && composeService != "" { if project != "" && composeService != "" {
if replica == "" { if replica == "" {
replica = name replica = name
} }
key = fmt.Sprintf("compose:%s/%s/%s:%d/%s", project, composeService, replica, p.PrivatePort, p.Type) key = fmt.Sprintf("compose:%s/%s/%s:%d/%s", project, composeService, replica, p.PrivatePort, p.Type)
technicalBase := project + "-" + composeService technical = dnsLabel(name)
if replica != "1" { if name == "container" {
technicalBase += "-" + replica technical = dnsLabel(composeService)
} }
technical = dnsLabel(technicalBase + "-" + suffix)
} else { } else {
key = fmt.Sprintf("container:%s:%d/%s", name, p.PrivatePort, p.Type) key = fmt.Sprintf("container:%s:%d/%s", name, p.PrivatePort, p.Type)
technical = dnsLabel(name + "-" + suffix) technical = dnsLabel(name)
} }
return key, technical return key, technical
} }
@@ -163,5 +162,43 @@ func servicesFrom(containers []dockerContainer) []service {
out = append(out, *item) out = append(out, *item)
} }
sort.Slice(out, func(i, j int) bool { return out[i].Key < out[j].Key }) 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 return out
} }
+37 -1
View File
@@ -41,7 +41,7 @@ func TestServicesFromUsesStableComposeIdentity(t *testing.T) {
if len(got) != 1 { if len(got) != 1 {
t.Fatalf("got %d services", len(got)) 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]) t.Fatalf("unexpected service %#v", got[0])
} }
if len(got[0].Bindings) != 1 || got[0].Bindings[0].PublicPort != 18080 { 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) { func TestServicesFromSkipsLoopbackOnlyBindings(t *testing.T) {
items := []dockerContainer{{ID: "abc", Names: []string{"/app"}, Labels: map[string]string{}, Ports: []dockerPort{ items := []dockerContainer{{ID: "abc", Names: []string{"/app"}, Labels: map[string]string{}, Ports: []dockerPort{
{IP: "127.0.0.1", PrivatePort: 8080, PublicPort: 18080, Type: "tcp"}, {IP: "127.0.0.1", PrivatePort: 8080, PublicPort: 18080, Type: "tcp"},
-61
View File
@@ -1,61 +0,0 @@
# RustProxy 0.33.7 - verification report
## Scope (2026-10-15)
0.33.7 changes the Dashboard UI without changing proxy traffic handling or
telemetry collection:
- Replace the duplicated **Arrange Overview** editor modal with an account-scoped
**Dashboard presets** manager. A named preset stores Dashboard density, card
order, visibility and optional custom widths; presets can be saved/updated,
loaded and deleted through the existing administrator preferences document.
- Keep actual Dashboard editing in the existing inline **Unlock layout** flow and
Settings visibility/order controls.
- Use the same monospace font family as **CONNECTION ACTIVITY** for Overview chart
titles, including compact chart cards and wide charts.
- No SQLite schema migration, new telemetry polling loop or proxy-path change is
introduced.
## Checks completed
- **PASS:** `python scripts/tests/dashboard_presets_0337_browser_test.py` against
the real bundled HTML/CSS/JS with a simulated preferences API at 320, 375,
768 and 1440px. Covered save/update snapshot data, load, delete, responsive
modal layout, removal of the former duplicated editor controls, and chart-title
font parity with **CONNECTION ACTIVITY**.
- **PASS:** `python scripts/tests/dashboard_modal_0333_browser_test.py` after the
0.33.7 preset conversion at 320, 375, 550, 620, 800, 900, 960, 1158, 1440 and
1920px with no horizontal viewport overflow.
- **PASS:** updated `dashboard_layout_0331_browser_test.py` and
`dashboard_resizing_0333_browser_test.py`; inline editing still covers
add/hide/reorder/resize, live refresh, cancel/save and persisted custom widths,
while opening/closing the preset modal does not mutate the active layout.
- **PASS:** all 13 current `scripts/tests/*browser_test.py` simulated-browser
regressions, including chart heading/width/fixed CPU/stable scale coverage,
progress bars, sidebar, RAM, Web observability and Web Server live metrics.
The obsolete CPU autoscale regression was removed because 0.33.5 intentionally
replaced that behavior with a fixed 0-100% CPU scale and has its own current
regression test.
- **PASS:** `node scripts/tests/overview_samples_0330_ui_test.mjs`,
`node scripts/tests/dashboard_resume_rate_charts_ui_test.mjs` and
`node scripts/tests/dashboard_metric_visuals_ui_test.mjs`.
- **PASS:** JSON parsing for `src/admin/openapi.json`, `langs/en_US.json` and
`langs/pl_pl.json`; JavaScript syntax checks for the changed preferences/preset
files; Python syntax checks for changed browser tests; `python scripts/preflight.py`.
- **PASS:** `./dev.sh doctor` reports the source layout as OK and confirms that a
C compiler and Python are available.
- **PASS:** all eight `migrations/*.sql` applied successfully in two successive
cycles to a disposable in-memory SQLite database. This verifies SQL script
execution/idempotency in this context, not a production-data upgrade.
- **PASS:** parsed `scripts/deploy/upgrade.py --help` and
`scripts/migration/migrate_npm.py --help`; no live external upgrade/migration
was performed.
## Not verified in this environment
- `./dev.sh check` and `./dev.sh test` exit 127 because Cargo/rustc are not
installed. Therefore Rust compilation, Rust unit/integration tests, an actual
RustProxy process, real SSE traffic, production database upgrade and live load
testing are not verified here.
- The real-runtime acceptance gate remains open until Rust builds and runs against
representative test traffic.