Merge pull request #119 from bnema/fix/proxy-port-label-ignored
fix: respect gordon.port label during port selection (#118)
13 files · +557 -65
M docs/config/auto-route.md +5 -5
| @@ -175,7 +175,7 @@ The preferred method for automatic routing uses Dockerfile labels. This gives yo | |||
| 175 | 175 | | Label | Description | Example | | |
| 176 | 176 | |-------|-------------|---------| | |
| 177 | 177 | | `gordon.domains` | Comma-separated list of domains | `app.example.com,www.app.example.com` | | |
| 178 | - | | `gordon.port` | Container port to proxy | `3000` | | |
| 178 | + | | `gordon.proxy.port` | Container port to proxy | `3000` | | |
| 179 | 179 | | `gordon.health` | Health check endpoint path | `/health` | | |
| 180 | 180 | | `gordon.env-file` | Path to .env file in image | `/app/.env.example` | | |
| 181 | 181 | ||
| @@ -189,7 +189,7 @@ RUN npm install | |||
| 189 | 189 | ||
| 190 | 190 | # Gordon routing labels | |
| 191 | 191 | LABEL gordon.domains="myapp.example.com,www.myapp.example.com" | |
| 192 | - | LABEL gordon.port="3000" | |
| 192 | + | LABEL gordon.proxy.port="3000" | |
| 193 | 193 | LABEL gordon.health="/api/health" | |
| 194 | 194 | LABEL gordon.env-file="/app/.env.example" | |
| 195 | 195 | ||
| @@ -203,7 +203,7 @@ When you push an image with Gordon labels: | |||
| 203 | 203 | ||
| 204 | 204 | 1. Gordon reads the `gordon.domains` label | |
| 205 | 205 | 2. Creates routes for each domain automatically | |
| 206 | - | 3. Uses `gordon.port` for the proxy target (or first EXPOSE port) | |
| 206 | + | 3. Uses `gordon.proxy.port` for the proxy target, otherwise auto-detects an exposed port preferring common HTTP ports | |
| 207 | 207 | 4. Configures health checks if `gordon.health` is set | |
| 208 | 208 | 5. Extracts environment variables from `gordon.env-file` if specified | |
| 209 | 209 | ||
| @@ -241,7 +241,7 @@ Gordon will: | |||
| 241 | 241 | ||
| 242 | 242 | ### Best Practices | |
| 243 | 243 | ||
| 244 | - | 1. **Always set `gordon.port`** - Don't rely on EXPOSE detection | |
| 244 | + | 1. **Always set `gordon.proxy.port`** - Don't rely on EXPOSE detection | |
| 245 | 245 | 2. **Use `gordon.health`** - Enables reliable deployment verification | |
| 246 | 246 | 3. **Include `.env.example`** - Self-documenting environment requirements | |
| 247 | 247 | 4. **Separate domains with commas** - No spaces around commas | |
| @@ -261,7 +261,7 @@ COPY .env.example . | |||
| 261 | 261 | ||
| 262 | 262 | # Gordon labels | |
| 263 | 263 | LABEL gordon.domains="api.mycompany.com" | |
| 264 | - | LABEL gordon.port="8080" | |
| 264 | + | LABEL gordon.proxy.port="8080" | |
| 265 | 265 | LABEL gordon.health="/health" | |
| 266 | 266 | LABEL gordon.env-file="/app/.env.example" | |
| 267 | 267 | ||
| @@ -175,7 +175,7 @@ The preferred method for automatic routing uses Dockerfile labels. This gives yo | |||||
| 175 | | Label | Description | Example | | 175 | | Label | Description | Example | | ||
| 176 | |-------|-------------|---------| | 176 | |-------|-------------|---------| | ||
| 177 | | `gordon.domains` | Comma-separated list of domains | `app.example.com,www.app.example.com` | | 177 | | `gordon.domains` | Comma-separated list of domains | `app.example.com,www.app.example.com` | | ||
| 178 | - | | `gordon.port` | Container port to proxy | `3000` | | 178 | + | | `gordon.proxy.port` | Container port to proxy | `3000` | |
| 179 | | `gordon.health` | Health check endpoint path | `/health` | | 179 | | `gordon.health` | Health check endpoint path | `/health` | | ||
| 180 | | `gordon.env-file` | Path to .env file in image | `/app/.env.example` | | 180 | | `gordon.env-file` | Path to .env file in image | `/app/.env.example` | | ||
| 181 | 181 | ||||
| @@ -189,7 +189,7 @@ RUN npm install | |||||
| 189 | 189 | ||||
| 190 | # Gordon routing labels | 190 | # Gordon routing labels | ||
| 191 | LABEL gordon.domains="myapp.example.com,www.myapp.example.com" | 191 | LABEL gordon.domains="myapp.example.com,www.myapp.example.com" | ||
| 192 | - | LABEL gordon.port="3000" | 192 | + | LABEL gordon.proxy.port="3000" |
| 193 | LABEL gordon.health="/api/health" | 193 | LABEL gordon.health="/api/health" | ||
| 194 | LABEL gordon.env-file="/app/.env.example" | 194 | LABEL gordon.env-file="/app/.env.example" | ||
| 195 | 195 | ||||
| @@ -203,7 +203,7 @@ When you push an image with Gordon labels: | |||||
| 203 | 203 | ||||
| 204 | 1. Gordon reads the `gordon.domains` label | 204 | 1. Gordon reads the `gordon.domains` label | ||
| 205 | 2. Creates routes for each domain automatically | 205 | 2. Creates routes for each domain automatically | ||
| 206 | - | 3. Uses `gordon.port` for the proxy target (or first EXPOSE port) | 206 | + | 3. Uses `gordon.proxy.port` for the proxy target, otherwise auto-detects an exposed port preferring common HTTP ports |
| 207 | 4. Configures health checks if `gordon.health` is set | 207 | 4. Configures health checks if `gordon.health` is set | ||
| 208 | 5. Extracts environment variables from `gordon.env-file` if specified | 208 | 5. Extracts environment variables from `gordon.env-file` if specified | ||
| 209 | 209 | ||||
| @@ -241,7 +241,7 @@ Gordon will: | |||||
| 241 | 241 | ||||
| 242 | ### Best Practices | 242 | ### Best Practices | ||
| 243 | 243 | ||||
| 244 | - | 1. **Always set `gordon.port`** - Don't rely on EXPOSE detection | 244 | + | 1. **Always set `gordon.proxy.port`** - Don't rely on EXPOSE detection |
| 245 | 2. **Use `gordon.health`** - Enables reliable deployment verification | 245 | 2. **Use `gordon.health`** - Enables reliable deployment verification | ||
| 246 | 3. **Include `.env.example`** - Self-documenting environment requirements | 246 | 3. **Include `.env.example`** - Self-documenting environment requirements | ||
| 247 | 4. **Separate domains with commas** - No spaces around commas | 247 | 4. **Separate domains with commas** - No spaces around commas | ||
| @@ -261,7 +261,7 @@ COPY .env.example . | |||||
| 261 | 261 | ||||
| 262 | # Gordon labels | 262 | # Gordon labels | ||
| 263 | LABEL gordon.domains="api.mycompany.com" | 263 | LABEL gordon.domains="api.mycompany.com" | ||
| 264 | - | LABEL gordon.port="8080" | 264 | + | LABEL gordon.proxy.port="8080" |
| 265 | LABEL gordon.health="/health" | 265 | LABEL gordon.health="/health" | ||
| 266 | LABEL gordon.env-file="/app/.env.example" | 266 | LABEL gordon.env-file="/app/.env.example" | ||
| 267 | 267 | ||||
M docs/reference/docker-labels.md +13 -2
| @@ -14,6 +14,17 @@ Gordon adds these labels to managed containers: | |||
| 14 | 14 | | `gordon.route` | Domain name | Route this container handles | | |
| 15 | 15 | | `gordon.created` | Timestamp | When Gordon created the container | | |
| 16 | 16 | ||
| 17 | + | ### Propagated Image Labels | |
| 18 | + | ||
| 19 | + | When an image defines these labels, Gordon copies them onto the container for runtime use (readiness probing, proxy routing): | |
| 20 | + | ||
| 21 | + | | Label | Example | Description | | |
| 22 | + | |-------|---------|-------------| | |
| 23 | + | | `gordon.proxy.port` | `"3000"` | Port the proxy routes HTTP traffic to | | |
| 24 | + | | `gordon.health` | `"/healthz"` | Health check endpoint for readiness probing | | |
| 25 | + | ||
| 26 | + | These labels only appear on containers whose source image defined them. | |
| 27 | + | ||
| 17 | 28 | ### Attachment Labels | |
| 18 | 29 | ||
| 19 | 30 | Additional labels for attachment containers: | |
| @@ -42,8 +53,7 @@ Labels you can set in your Dockerfile: | |||
| 42 | 53 | | Label | Example | Description | | |
| 43 | 54 | |-------|---------|-------------| | |
| 44 | 55 | | `gordon.domains` | `"app.example.com,www.app.example.com"` | Comma-separated domains for auto-route | | |
| 45 | - | | `gordon.port` | `"3000"` | Port to proxy HTTP traffic to | | |
| 46 | - | | `gordon.proxy.port` | `"3000"` | Port to proxy HTTP traffic to (legacy alias for `gordon.port`) | | |
| 56 | + | | `gordon.proxy.port` | `"3000"` | Port to proxy HTTP traffic to | | |
| 47 | 57 | | `gordon.health` | `"/healthz"` | HTTP health check endpoint path for readiness probing | | |
| 48 | 58 | | `gordon.env-file` | `"/app/.env.example"` | Path to env template file inside the image | | |
| 49 | 59 | ||
| @@ -108,6 +118,7 @@ Example output: | |||
| 108 | 118 | "gordon.domain": "app.mydomain.com", | |
| 109 | 119 | "gordon.image": "myapp:latest", | |
| 110 | 120 | "gordon.managed": "true", | |
| 121 | + | "gordon.proxy.port": "3000", | |
| 111 | 122 | "gordon.route": "app.mydomain.com" | |
| 112 | 123 | } | |
| 113 | 124 | ``` | |
| @@ -14,6 +14,17 @@ Gordon adds these labels to managed containers: | |||||
| 14 | | `gordon.route` | Domain name | Route this container handles | | 14 | | `gordon.route` | Domain name | Route this container handles | | ||
| 15 | | `gordon.created` | Timestamp | When Gordon created the container | | 15 | | `gordon.created` | Timestamp | When Gordon created the container | | ||
| 16 | 16 | ||||
| 17 | + | ### Propagated Image Labels | |||
| 18 | + | ||||
| 19 | + | When an image defines these labels, Gordon copies them onto the container for runtime use (readiness probing, proxy routing): | |||
| 20 | + | ||||
| 21 | + | | Label | Example | Description | | |||
| 22 | + | |-------|---------|-------------| | |||
| 23 | + | | `gordon.proxy.port` | `"3000"` | Port the proxy routes HTTP traffic to | | |||
| 24 | + | | `gordon.health` | `"/healthz"` | Health check endpoint for readiness probing | | |||
| 25 | + | ||||
| 26 | + | These labels only appear on containers whose source image defined them. | |||
| 27 | + | ||||
| 17 | ### Attachment Labels | 28 | ### Attachment Labels | ||
| 18 | 29 | ||||
| 19 | Additional labels for attachment containers: | 30 | Additional labels for attachment containers: | ||
| @@ -42,8 +53,7 @@ Labels you can set in your Dockerfile: | |||||
| 42 | | Label | Example | Description | | 53 | | Label | Example | Description | | ||
| 43 | |-------|---------|-------------| | 54 | |-------|---------|-------------| | ||
| 44 | | `gordon.domains` | `"app.example.com,www.app.example.com"` | Comma-separated domains for auto-route | | 55 | | `gordon.domains` | `"app.example.com,www.app.example.com"` | Comma-separated domains for auto-route | | ||
| 45 | - | | `gordon.port` | `"3000"` | Port to proxy HTTP traffic to | | 56 | + | | `gordon.proxy.port` | `"3000"` | Port to proxy HTTP traffic to | |
| 46 | - | | `gordon.proxy.port` | `"3000"` | Port to proxy HTTP traffic to (legacy alias for `gordon.port`) | | |||
| 47 | | `gordon.health` | `"/healthz"` | HTTP health check endpoint path for readiness probing | | 57 | | `gordon.health` | `"/healthz"` | HTTP health check endpoint path for readiness probing | | ||
| 48 | | `gordon.env-file` | `"/app/.env.example"` | Path to env template file inside the image | | 58 | | `gordon.env-file` | `"/app/.env.example"` | Path to env template file inside the image | | ||
| 49 | 59 | ||||
| @@ -108,6 +118,7 @@ Example output: | |||||
| 108 | "gordon.domain": "app.mydomain.com", | 118 | "gordon.domain": "app.mydomain.com", | ||
| 109 | "gordon.image": "myapp:latest", | 119 | "gordon.image": "myapp:latest", | ||
| 110 | "gordon.managed": "true", | 120 | "gordon.managed": "true", | ||
| 121 | + | "gordon.proxy.port": "3000", | |||
| 111 | "gordon.route": "app.mydomain.com" | 122 | "gordon.route": "app.mydomain.com" | ||
| 112 | } | 123 | } | ||
| 113 | ``` | 124 | ``` | ||
M internal/adapters/in/cli/push_test.go +11 -11
| @@ -357,20 +357,20 @@ func TestParseDockerfileLabels(t *testing.T) { | |||
| 357 | 357 | { | |
| 358 | 358 | name: "multiple labels", | |
| 359 | 359 | content: `LABEL gordon.domain="myapp.example.com" | |
| 360 | - | LABEL gordon.port="8080" | |
| 360 | + | LABEL gordon.proxy.port="8080" | |
| 361 | 361 | LABEL gordon.health="/health"`, | |
| 362 | 362 | expected: map[string]string{ | |
| 363 | - | "gordon.domain": "myapp.example.com", | |
| 364 | - | "gordon.port": "8080", | |
| 365 | - | "gordon.health": "/health", | |
| 363 | + | "gordon.domain": "myapp.example.com", | |
| 364 | + | "gordon.proxy.port": "8080", | |
| 365 | + | "gordon.health": "/health", | |
| 366 | 366 | }, | |
| 367 | 367 | }, | |
| 368 | 368 | { | |
| 369 | 369 | name: "multi-label on one line", | |
| 370 | - | content: `LABEL gordon.domain="myapp.example.com" gordon.port="8080"`, | |
| 370 | + | content: `LABEL gordon.domain="myapp.example.com" gordon.proxy.port="8080"`, | |
| 371 | 371 | expected: map[string]string{ | |
| 372 | - | "gordon.domain": "myapp.example.com", | |
| 373 | - | "gordon.port": "8080", | |
| 372 | + | "gordon.domain": "myapp.example.com", | |
| 373 | + | "gordon.proxy.port": "8080", | |
| 374 | 374 | }, | |
| 375 | 375 | }, | |
| 376 | 376 | { | |
| @@ -471,13 +471,13 @@ func TestSplitLabelPairs(t *testing.T) { | |||
| 471 | 471 | }, | |
| 472 | 472 | { | |
| 473 | 473 | name: "two pairs", | |
| 474 | - | content: `gordon.domain="test.com" gordon.port="8080"`, | |
| 475 | - | expected: []string{`gordon.domain="test.com"`, `gordon.port="8080"`}, | |
| 474 | + | content: `gordon.domain="test.com" gordon.proxy.port="8080"`, | |
| 475 | + | expected: []string{`gordon.domain="test.com"`, `gordon.proxy.port="8080"`}, | |
| 476 | 476 | }, | |
| 477 | 477 | { | |
| 478 | 478 | name: "unquoted", | |
| 479 | - | content: `gordon.domain=test.com gordon.port=8080`, | |
| 480 | - | expected: []string{`gordon.domain=test.com`, `gordon.port=8080`}, | |
| 479 | + | content: `gordon.domain=test.com gordon.proxy.port=8080`, | |
| 480 | + | expected: []string{`gordon.domain=test.com`, `gordon.proxy.port=8080`}, | |
| 481 | 481 | }, | |
| 482 | 482 | { | |
| 483 | 483 | name: "quoted with spaces in value", | |
| @@ -357,20 +357,20 @@ func TestParseDockerfileLabels(t *testing.T) { | |||||
| 357 | { | 357 | { | ||
| 358 | name: "multiple labels", | 358 | name: "multiple labels", | ||
| 359 | content: `LABEL gordon.domain="myapp.example.com" | 359 | content: `LABEL gordon.domain="myapp.example.com" | ||
| 360 | - | LABEL gordon.port="8080" | 360 | + | LABEL gordon.proxy.port="8080" |
| 361 | LABEL gordon.health="/health"`, | 361 | LABEL gordon.health="/health"`, | ||
| 362 | expected: map[string]string{ | 362 | expected: map[string]string{ | ||
| 363 | - | "gordon.domain": "myapp.example.com", | 363 | + | "gordon.domain": "myapp.example.com", |
| 364 | - | "gordon.port": "8080", | 364 | + | "gordon.proxy.port": "8080", |
| 365 | - | "gordon.health": "/health", | 365 | + | "gordon.health": "/health", |
| 366 | }, | 366 | }, | ||
| 367 | }, | 367 | }, | ||
| 368 | { | 368 | { | ||
| 369 | name: "multi-label on one line", | 369 | name: "multi-label on one line", | ||
| 370 | - | content: `LABEL gordon.domain="myapp.example.com" gordon.port="8080"`, | 370 | + | content: `LABEL gordon.domain="myapp.example.com" gordon.proxy.port="8080"`, |
| 371 | expected: map[string]string{ | 371 | expected: map[string]string{ | ||
| 372 | - | "gordon.domain": "myapp.example.com", | 372 | + | "gordon.domain": "myapp.example.com", |
| 373 | - | "gordon.port": "8080", | 373 | + | "gordon.proxy.port": "8080", |
| 374 | }, | 374 | }, | ||
| 375 | }, | 375 | }, | ||
| 376 | { | 376 | { | ||
| @@ -471,13 +471,13 @@ func TestSplitLabelPairs(t *testing.T) { | |||||
| 471 | }, | 471 | }, | ||
| 472 | { | 472 | { | ||
| 473 | name: "two pairs", | 473 | name: "two pairs", | ||
| 474 | - | content: `gordon.domain="test.com" gordon.port="8080"`, | 474 | + | content: `gordon.domain="test.com" gordon.proxy.port="8080"`, |
| 475 | - | expected: []string{`gordon.domain="test.com"`, `gordon.port="8080"`}, | 475 | + | expected: []string{`gordon.domain="test.com"`, `gordon.proxy.port="8080"`}, |
| 476 | }, | 476 | }, | ||
| 477 | { | 477 | { | ||
| 478 | name: "unquoted", | 478 | name: "unquoted", | ||
| 479 | - | content: `gordon.domain=test.com gordon.port=8080`, | 479 | + | content: `gordon.domain=test.com gordon.proxy.port=8080`, |
| 480 | - | expected: []string{`gordon.domain=test.com`, `gordon.port=8080`}, | 480 | + | expected: []string{`gordon.domain=test.com`, `gordon.proxy.port=8080`}, |
| 481 | }, | 481 | }, | ||
| 482 | { | 482 | { | ||
| 483 | name: "quoted with spaces in value", | 483 | name: "quoted with spaces in value", | ||
M internal/adapters/out/docker/runtime.go +2 -1
| @@ -890,7 +890,8 @@ func (r *Runtime) GetContainerNetworkInfo(ctx context.Context, containerID strin | |||
| 890 | 890 | return "", 0, fmt.Errorf("no EXPOSE directives found for container %s - please add EXPOSE directive to Dockerfile", containerID) | |
| 891 | 891 | } | |
| 892 | 892 | ||
| 893 | - | // Use the first exposed port | |
| 893 | + | // Sort ports with HTTP-friendly ports first, pushing SSH (22) to the end | |
| 894 | + | sortPortsHTTPFirst(exposedPorts) | |
| 894 | 895 | selectedPort := exposedPorts[0] | |
| 895 | 896 | ||
| 896 | 897 | log.Info(). | |
| @@ -890,7 +890,8 @@ func (r *Runtime) GetContainerNetworkInfo(ctx context.Context, containerID strin | |||||
| 890 | return "", 0, fmt.Errorf("no EXPOSE directives found for container %s - please add EXPOSE directive to Dockerfile", containerID) | 890 | return "", 0, fmt.Errorf("no EXPOSE directives found for container %s - please add EXPOSE directive to Dockerfile", containerID) | ||
| 891 | } | 891 | } | ||
| 892 | 892 | ||||
| 893 | - | // Use the first exposed port | 893 | + | // Sort ports with HTTP-friendly ports first, pushing SSH (22) to the end |
| 894 | + | sortPortsHTTPFirst(exposedPorts) | |||
| 894 | selectedPort := exposedPorts[0] | 895 | selectedPort := exposedPorts[0] | ||
| 895 | 896 | ||||
| 896 | log.Info(). | 897 | log.Info(). | ||
A internal/adapters/out/docker/runtime_port_sort_test.go +65 -0
| @@ -0,0 +1,65 @@ | |||
| 1 | + | package docker | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/stretchr/testify/assert" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | func Test_sortPortsHTTPFirst(t *testing.T) { | |
| 10 | + | tests := []struct { | |
| 11 | + | name string | |
| 12 | + | input []int | |
| 13 | + | expected []int | |
| 14 | + | }{ | |
| 15 | + | { | |
| 16 | + | name: "single port unchanged", | |
| 17 | + | input: []int{8080}, | |
| 18 | + | expected: []int{8080}, | |
| 19 | + | }, | |
| 20 | + | { | |
| 21 | + | name: "SSH port pushed to end", | |
| 22 | + | input: []int{22, 3000}, | |
| 23 | + | expected: []int{3000, 22}, | |
| 24 | + | }, | |
| 25 | + | { | |
| 26 | + | name: "HTTP ports sorted by priority", | |
| 27 | + | input: []int{3000, 80, 8080}, | |
| 28 | + | expected: []int{80, 8080, 3000}, | |
| 29 | + | }, | |
| 30 | + | { | |
| 31 | + | name: "SSH pushed behind non-priority ports", | |
| 32 | + | input: []int{22, 4000}, | |
| 33 | + | expected: []int{4000, 22}, | |
| 34 | + | }, | |
| 35 | + | { | |
| 36 | + | name: "mixed priority and non-priority ports", | |
| 37 | + | input: []int{22, 3000, 4000, 80}, | |
| 38 | + | expected: []int{80, 3000, 4000, 22}, | |
| 39 | + | }, | |
| 40 | + | { | |
| 41 | + | name: "all non-priority ports sorted ascending", | |
| 42 | + | input: []int{9090, 7000, 6000}, | |
| 43 | + | expected: []int{6000, 7000, 9090}, | |
| 44 | + | }, | |
| 45 | + | { | |
| 46 | + | name: "empty slice is no-op", | |
| 47 | + | input: []int{}, | |
| 48 | + | expected: []int{}, | |
| 49 | + | }, | |
| 50 | + | { | |
| 51 | + | name: "gitea-style multi-port image", | |
| 52 | + | input: []int{22, 3000}, | |
| 53 | + | expected: []int{3000, 22}, | |
| 54 | + | }, | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | for _, tt := range tests { | |
| 58 | + | t.Run(tt.name, func(t *testing.T) { | |
| 59 | + | ports := make([]int, len(tt.input)) | |
| 60 | + | copy(ports, tt.input) | |
| 61 | + | sortPortsHTTPFirst(ports) | |
| 62 | + | assert.Equal(t, tt.expected, ports) | |
| 63 | + | }) | |
| 64 | + | } | |
| 65 | + | } | |
| @@ -0,0 +1,65 @@ | |||||
| 1 | + | package docker | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/stretchr/testify/assert" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | func Test_sortPortsHTTPFirst(t *testing.T) { | |||
| 10 | + | tests := []struct { | |||
| 11 | + | name string | |||
| 12 | + | input []int | |||
| 13 | + | expected []int | |||
| 14 | + | }{ | |||
| 15 | + | { | |||
| 16 | + | name: "single port unchanged", | |||
| 17 | + | input: []int{8080}, | |||
| 18 | + | expected: []int{8080}, | |||
| 19 | + | }, | |||
| 20 | + | { | |||
| 21 | + | name: "SSH port pushed to end", | |||
| 22 | + | input: []int{22, 3000}, | |||
| 23 | + | expected: []int{3000, 22}, | |||
| 24 | + | }, | |||
| 25 | + | { | |||
| 26 | + | name: "HTTP ports sorted by priority", | |||
| 27 | + | input: []int{3000, 80, 8080}, | |||
| 28 | + | expected: []int{80, 8080, 3000}, | |||
| 29 | + | }, | |||
| 30 | + | { | |||
| 31 | + | name: "SSH pushed behind non-priority ports", | |||
| 32 | + | input: []int{22, 4000}, | |||
| 33 | + | expected: []int{4000, 22}, | |||
| 34 | + | }, | |||
| 35 | + | { | |||
| 36 | + | name: "mixed priority and non-priority ports", | |||
| 37 | + | input: []int{22, 3000, 4000, 80}, | |||
| 38 | + | expected: []int{80, 3000, 4000, 22}, | |||
| 39 | + | }, | |||
| 40 | + | { | |||
| 41 | + | name: "all non-priority ports sorted ascending", | |||
| 42 | + | input: []int{9090, 7000, 6000}, | |||
| 43 | + | expected: []int{6000, 7000, 9090}, | |||
| 44 | + | }, | |||
| 45 | + | { | |||
| 46 | + | name: "empty slice is no-op", | |||
| 47 | + | input: []int{}, | |||
| 48 | + | expected: []int{}, | |||
| 49 | + | }, | |||
| 50 | + | { | |||
| 51 | + | name: "gitea-style multi-port image", | |||
| 52 | + | input: []int{22, 3000}, | |||
| 53 | + | expected: []int{3000, 22}, | |||
| 54 | + | }, | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | for _, tt := range tests { | |||
| 58 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 59 | + | ports := make([]int, len(tt.input)) | |||
| 60 | + | copy(ports, tt.input) | |||
| 61 | + | sortPortsHTTPFirst(ports) | |||
| 62 | + | assert.Equal(t, tt.expected, ports) | |||
| 63 | + | }) | |||
| 64 | + | } | |||
| 65 | + | } | |||
M internal/domain/labels.go +3 -2
| @@ -15,7 +15,7 @@ const ( | |||
| 15 | 15 | // exposing secret values. | |
| 16 | 16 | LabelEnvHash = "gordon.env-hash" | |
| 17 | 17 | ||
| 18 | - | // Image labels (set in Dockerfile) | |
| 18 | + | // LabelProxyPort specifies the container port to proxy HTTP traffic to. | |
| 19 | 19 | LabelProxyPort = "gordon.proxy.port" | |
| 20 | 20 | ||
| 21 | 21 | // Auto-route image labels (for automatic route discovery) | |
| @@ -23,7 +23,8 @@ const ( | |||
| 23 | 23 | LabelDomains = "gordon.domains" | |
| 24 | 24 | // LabelHealth specifies the health check endpoint path. | |
| 25 | 25 | LabelHealth = "gordon.health" | |
| 26 | - | // LabelPort specifies the container port to proxy. | |
| 26 | + | // LabelPort is a deprecated alias for LabelProxyPort. | |
| 27 | + | // Kept for backward compatibility; prefer LabelProxyPort for new images. | |
| 27 | 28 | LabelPort = "gordon.port" | |
| 28 | 29 | // LabelEnvFile specifies the path to .env file inside the image. | |
| 29 | 30 | LabelEnvFile = "gordon.env-file" | |
| @@ -15,7 +15,7 @@ const ( | |||||
| 15 | // exposing secret values. | 15 | // exposing secret values. | ||
| 16 | LabelEnvHash = "gordon.env-hash" | 16 | LabelEnvHash = "gordon.env-hash" | ||
| 17 | 17 | ||||
| 18 | - | // Image labels (set in Dockerfile) | 18 | + | // LabelProxyPort specifies the container port to proxy HTTP traffic to. |
| 19 | LabelProxyPort = "gordon.proxy.port" | 19 | LabelProxyPort = "gordon.proxy.port" | ||
| 20 | 20 | ||||
| 21 | // Auto-route image labels (for automatic route discovery) | 21 | // Auto-route image labels (for automatic route discovery) | ||
| @@ -23,7 +23,8 @@ const ( | |||||
| 23 | LabelDomains = "gordon.domains" | 23 | LabelDomains = "gordon.domains" | ||
| 24 | // LabelHealth specifies the health check endpoint path. | 24 | // LabelHealth specifies the health check endpoint path. | ||
| 25 | LabelHealth = "gordon.health" | 25 | LabelHealth = "gordon.health" | ||
| 26 | - | // LabelPort specifies the container port to proxy. | 26 | + | // LabelPort is a deprecated alias for LabelProxyPort. |
| 27 | + | // Kept for backward compatibility; prefer LabelProxyPort for new images. | |||
| 27 | LabelPort = "gordon.port" | 28 | LabelPort = "gordon.port" | ||
| 28 | // LabelEnvFile specifies the path to .env file inside the image. | 29 | // LabelEnvFile specifies the path to .env file inside the image. | ||
| 29 | LabelEnvFile = "gordon.env-file" | 30 | LabelEnvFile = "gordon.env-file" | ||
M internal/usecase/container/autoroute.go +5 -2
| @@ -414,8 +414,11 @@ func parseImageLabels(configData []byte) (*domain.ImageLabels, error) { | |||
| 414 | 414 | labels.Health = strings.TrimSpace(v) | |
| 415 | 415 | } | |
| 416 | 416 | ||
| 417 | - | if v, ok := config.Config.Labels[domain.LabelPort]; ok { | |
| 418 | - | labels.Port = strings.TrimSpace(v) | |
| 417 | + | for _, key := range []string{domain.LabelProxyPort, domain.LabelPort} { | |
| 418 | + | if v, ok := config.Config.Labels[key]; ok { | |
| 419 | + | labels.Port = strings.TrimSpace(v) | |
| 420 | + | break | |
| 421 | + | } | |
| 419 | 422 | } | |
| 420 | 423 | ||
| 421 | 424 | if v, ok := config.Config.Labels[domain.LabelEnvFile]; ok { | |
| @@ -414,8 +414,11 @@ func parseImageLabels(configData []byte) (*domain.ImageLabels, error) { | |||||
| 414 | labels.Health = strings.TrimSpace(v) | 414 | labels.Health = strings.TrimSpace(v) | ||
| 415 | } | 415 | } | ||
| 416 | 416 | ||||
| 417 | - | if v, ok := config.Config.Labels[domain.LabelPort]; ok { | 417 | + | for _, key := range []string{domain.LabelProxyPort, domain.LabelPort} { |
| 418 | - | labels.Port = strings.TrimSpace(v) | 418 | + | if v, ok := config.Config.Labels[key]; ok { |
| 419 | + | labels.Port = strings.TrimSpace(v) | |||
| 420 | + | break | |||
| 421 | + | } | |||
| 419 | } | 422 | } | ||
| 420 | 423 | ||||
| 421 | if v, ok := config.Config.Labels[domain.LabelEnvFile]; ok { | 424 | if v, ok := config.Config.Labels[domain.LabelEnvFile]; ok { | ||
M internal/usecase/container/autoroute_test.go +4 -4
| @@ -263,10 +263,10 @@ func TestParseImageLabels(t *testing.T) { | |||
| 263 | 263 | config: map[string]any{ | |
| 264 | 264 | "config": map[string]any{ | |
| 265 | 265 | "Labels": map[string]any{ | |
| 266 | - | "gordon.domain": "app.example.com", | |
| 267 | - | "gordon.health": "/healthz", | |
| 268 | - | "gordon.port": "8080", | |
| 269 | - | "gordon.env-file": ".env.prod", | |
| 266 | + | "gordon.domain": "app.example.com", | |
| 267 | + | "gordon.health": "/healthz", | |
| 268 | + | "gordon.proxy.port": "8080", | |
| 269 | + | "gordon.env-file": ".env.prod", | |
| 270 | 270 | }, | |
| 271 | 271 | }, | |
| 272 | 272 | }, | |
| @@ -263,10 +263,10 @@ func TestParseImageLabels(t *testing.T) { | |||||
| 263 | config: map[string]any{ | 263 | config: map[string]any{ | ||
| 264 | "config": map[string]any{ | 264 | "config": map[string]any{ | ||
| 265 | "Labels": map[string]any{ | 265 | "Labels": map[string]any{ | ||
| 266 | - | "gordon.domain": "app.example.com", | 266 | + | "gordon.domain": "app.example.com", |
| 267 | - | "gordon.health": "/healthz", | 267 | + | "gordon.health": "/healthz", |
| 268 | - | "gordon.port": "8080", | 268 | + | "gordon.proxy.port": "8080", |
| 269 | - | "gordon.env-file": ".env.prod", | 269 | + | "gordon.env-file": ".env.prod", |
| 270 | }, | 270 | }, | ||
| 271 | }, | 271 | }, | ||
| 272 | }, | 272 | }, | ||
M internal/usecase/container/service.go +70 -28
| @@ -210,25 +210,58 @@ func (s *Service) deploymentContainerName(containerDomain string, existing *doma | |||
| 210 | 210 | } | |
| 211 | 211 | } | |
| 212 | 212 | ||
| 213 | - | func (s *Service) buildContainerConfig(containerDomain, image, actualImageRef string, exposedPorts []int, envVars []string, envHash string, volumes map[string]string, networkName string, existing *domain.Container) *domain.ContainerConfig { | |
| 214 | - | containerName := s.deploymentContainerName(containerDomain, existing) | |
| 213 | + | // containerConfigInput groups the parameters for building a container configuration. | |
| 214 | + | type containerConfigInput struct { | |
| 215 | + | Domain string | |
| 216 | + | Image string | |
| 217 | + | ImageRef string | |
| 218 | + | ExposedPorts []int | |
| 219 | + | EnvVars []string | |
| 220 | + | EnvHash string | |
| 221 | + | Volumes map[string]string | |
| 222 | + | NetworkName string | |
| 223 | + | ImageLabels map[string]string | |
| 224 | + | Existing *domain.Container | |
| 225 | + | } | |
| 226 | + | ||
| 227 | + | func (s *Service) buildContainerConfig(in containerConfigInput) *domain.ContainerConfig { | |
| 228 | + | containerName := s.deploymentContainerName(in.Domain, in.Existing) | |
| 229 | + | labels := map[string]string{ | |
| 230 | + | domain.LabelDomain: in.Domain, | |
| 231 | + | domain.LabelEnvHash: in.EnvHash, | |
| 232 | + | domain.LabelImage: in.Image, | |
| 233 | + | domain.LabelManaged: "true", | |
| 234 | + | domain.LabelRoute: in.Domain, | |
| 235 | + | } | |
| 236 | + | ||
| 237 | + | // Propagate proxy/port labels from image so readiness probes can find them | |
| 238 | + | for _, key := range []string{domain.LabelProxyPort, domain.LabelPort, domain.LabelHealth} { | |
| 239 | + | if v, ok := in.ImageLabels[key]; ok && v != "" { | |
| 240 | + | labels[key] = v | |
| 241 | + | } | |
| 242 | + | } | |
| 243 | + | ||
| 244 | + | // Ensure the label-specified proxy port is included in exposed ports | |
| 245 | + | // so Docker creates a host port binding for it. | |
| 246 | + | ports := in.ExposedPorts | |
| 247 | + | if portStr, ok := labels[domain.LabelProxyPort]; ok { | |
| 248 | + | if port, err := strconv.Atoi(portStr); err == nil && port > 0 && port <= 65535 { | |
| 249 | + | if !slices.Contains(ports, port) { | |
| 250 | + | ports = append(ports, port) | |
| 251 | + | } | |
| 252 | + | } | |
| 253 | + | } | |
| 215 | 254 | ||
| 216 | 255 | return &domain.ContainerConfig{ | |
| 217 | - | Image: actualImageRef, | |
| 256 | + | Image: in.ImageRef, | |
| 218 | 257 | Name: containerName, | |
| 219 | - | Ports: exposedPorts, | |
| 220 | - | Env: envVars, | |
| 221 | - | Volumes: volumes, | |
| 222 | - | NetworkMode: networkName, | |
| 223 | - | Hostname: containerDomain, | |
| 224 | - | Labels: map[string]string{ | |
| 225 | - | domain.LabelDomain: containerDomain, | |
| 226 | - | domain.LabelEnvHash: envHash, | |
| 227 | - | domain.LabelImage: image, | |
| 228 | - | domain.LabelManaged: "true", | |
| 229 | - | domain.LabelRoute: containerDomain, | |
| 230 | - | }, | |
| 231 | - | AutoRemove: false, | |
| 258 | + | Ports: ports, | |
| 259 | + | Env: in.EnvVars, | |
| 260 | + | Volumes: in.Volumes, | |
| 261 | + | NetworkMode: in.NetworkName, | |
| 262 | + | Hostname: in.Domain, | |
| 263 | + | Labels: labels, | |
| 264 | + | AutoRemove: false, | |
| 232 | 265 | } | |
| 233 | 266 | } | |
| 234 | 267 | ||
| @@ -416,6 +449,7 @@ type deployResources struct { | |||
| 416 | 449 | networkName string | |
| 417 | 450 | actualImageRef string | |
| 418 | 451 | exposedPorts []int | |
| 452 | + | imageLabels map[string]string | |
| 419 | 453 | envVars []string | |
| 420 | 454 | envHash string | |
| 421 | 455 | volumes map[string]string | |
| @@ -516,6 +550,12 @@ func (s *Service) prepareDeployResources(ctx context.Context, route domain.Route | |||
| 516 | 550 | exposedPorts = []int{80, 8080, 3000} | |
| 517 | 551 | } | |
| 518 | 552 | ||
| 553 | + | imageLabels, err := s.runtime.GetImageLabels(ctx, actualImageRef) | |
| 554 | + | if err != nil { | |
| 555 | + | log.Warn().Err(err).Msg("failed to get image labels, skipping label propagation") | |
| 556 | + | imageLabels = nil | |
| 557 | + | } | |
| 558 | + | ||
| 519 | 559 | envVars, err := s.loadEnvironment(ctx, route.Domain, actualImageRef) | |
| 520 | 560 | if err != nil { | |
| 521 | 561 | return nil, err | |
| @@ -531,6 +571,7 @@ func (s *Service) prepareDeployResources(ctx context.Context, route domain.Route | |||
| 531 | 571 | networkName: networkName, | |
| 532 | 572 | actualImageRef: actualImageRef, | |
| 533 | 573 | exposedPorts: exposedPorts, | |
| 574 | + | imageLabels: imageLabels, | |
| 534 | 575 | envVars: envVars, | |
| 535 | 576 | envHash: envHash, | |
| 536 | 577 | volumes: volumes, | |
| @@ -543,17 +584,18 @@ func (s *Service) createStartedContainer(ctx context.Context, route domain.Route | |||
| 543 | 584 | ||
| 544 | 585 | log := zerowrap.FromCtx(ctx) | |
| 545 | 586 | ||
| 546 | - | containerConfig := s.buildContainerConfig( | |
| 547 | - | route.Domain, | |
| 548 | - | route.Image, | |
| 549 | - | resources.actualImageRef, | |
| 550 | - | resources.exposedPorts, | |
| 551 | - | resources.envVars, | |
| 552 | - | resources.envHash, | |
| 553 | - | resources.volumes, | |
| 554 | - | resources.networkName, | |
| 555 | - | existing, | |
| 556 | - | ) | |
| 587 | + | containerConfig := s.buildContainerConfig(containerConfigInput{ | |
| 588 | + | Domain: route.Domain, | |
| 589 | + | Image: route.Image, | |
| 590 | + | ImageRef: resources.actualImageRef, | |
| 591 | + | ExposedPorts: resources.exposedPorts, | |
| 592 | + | EnvVars: resources.envVars, | |
| 593 | + | EnvHash: resources.envHash, | |
| 594 | + | Volumes: resources.volumes, | |
| 595 | + | NetworkName: resources.networkName, | |
| 596 | + | ImageLabels: resources.imageLabels, | |
| 597 | + | Existing: existing, | |
| 598 | + | }) | |
| 557 | 599 | ||
| 558 | 600 | newContainer, err := s.runtime.CreateContainer(ctx, containerConfig) | |
| 559 | 601 | if err != nil { | |
| @@ -2527,7 +2569,7 @@ func (s *Service) resolveProbeEndpoint(ctx context.Context, containerID string, | |||
| 2527 | 2569 | for _, key := range []string{domain.LabelProxyPort, domain.LabelPort} { | |
| 2528 | 2570 | if portStr, ok := labels[key]; ok && portStr != "" { | |
| 2529 | 2571 | port, err := strconv.Atoi(portStr) | |
| 2530 | - | if err == nil && port > 0 { | |
| 2572 | + | if err == nil && port > 0 && port <= 65535 { | |
| 2531 | 2573 | internalPort = port | |
| 2532 | 2574 | break | |
| 2533 | 2575 | } | |
| @@ -210,25 +210,58 @@ func (s *Service) deploymentContainerName(containerDomain string, existing *doma | |||||
| 210 | } | 210 | } | ||
| 211 | } | 211 | } | ||
| 212 | 212 | ||||
| 213 | - | func (s *Service) buildContainerConfig(containerDomain, image, actualImageRef string, exposedPorts []int, envVars []string, envHash string, volumes map[string]string, networkName string, existing *domain.Container) *domain.ContainerConfig { | 213 | + | // containerConfigInput groups the parameters for building a container configuration. |
| 214 | - | containerName := s.deploymentContainerName(containerDomain, existing) | 214 | + | type containerConfigInput struct { |
| 215 | + | Domain string | |||
| 216 | + | Image string | |||
| 217 | + | ImageRef string | |||
| 218 | + | ExposedPorts []int | |||
| 219 | + | EnvVars []string | |||
| 220 | + | EnvHash string | |||
| 221 | + | Volumes map[string]string | |||
| 222 | + | NetworkName string | |||
| 223 | + | ImageLabels map[string]string | |||
| 224 | + | Existing *domain.Container | |||
| 225 | + | } | |||
| 226 | + | ||||
| 227 | + | func (s *Service) buildContainerConfig(in containerConfigInput) *domain.ContainerConfig { | |||
| 228 | + | containerName := s.deploymentContainerName(in.Domain, in.Existing) | |||
| 229 | + | labels := map[string]string{ | |||
| 230 | + | domain.LabelDomain: in.Domain, | |||
| 231 | + | domain.LabelEnvHash: in.EnvHash, | |||
| 232 | + | domain.LabelImage: in.Image, | |||
| 233 | + | domain.LabelManaged: "true", | |||
| 234 | + | domain.LabelRoute: in.Domain, | |||
| 235 | + | } | |||
| 236 | + | ||||
| 237 | + | // Propagate proxy/port labels from image so readiness probes can find them | |||
| 238 | + | for _, key := range []string{domain.LabelProxyPort, domain.LabelPort, domain.LabelHealth} { | |||
| 239 | + | if v, ok := in.ImageLabels[key]; ok && v != "" { | |||
| 240 | + | labels[key] = v | |||
| 241 | + | } | |||
| 242 | + | } | |||
| 243 | + | ||||
| 244 | + | // Ensure the label-specified proxy port is included in exposed ports | |||
| 245 | + | // so Docker creates a host port binding for it. | |||
| 246 | + | ports := in.ExposedPorts | |||
| 247 | + | if portStr, ok := labels[domain.LabelProxyPort]; ok { | |||
| 248 | + | if port, err := strconv.Atoi(portStr); err == nil && port > 0 && port <= 65535 { | |||
| 249 | + | if !slices.Contains(ports, port) { | |||
| 250 | + | ports = append(ports, port) | |||
| 251 | + | } | |||
| 252 | + | } | |||
| 253 | + | } | |||
| 215 | 254 | ||||
| 216 | return &domain.ContainerConfig{ | 255 | return &domain.ContainerConfig{ | ||
| 217 | - | Image: actualImageRef, | 256 | + | Image: in.ImageRef, |
| 218 | Name: containerName, | 257 | Name: containerName, | ||
| 219 | - | Ports: exposedPorts, | 258 | + | Ports: ports, |
| 220 | - | Env: envVars, | 259 | + | Env: in.EnvVars, |
| 221 | - | Volumes: volumes, | 260 | + | Volumes: in.Volumes, |
| 222 | - | NetworkMode: networkName, | 261 | + | NetworkMode: in.NetworkName, |
| 223 | - | Hostname: containerDomain, | 262 | + | Hostname: in.Domain, |
| 224 | - | Labels: map[string]string{ | 263 | + | Labels: labels, |
| 225 | - | domain.LabelDomain: containerDomain, | 264 | + | AutoRemove: false, |
| 226 | - | domain.LabelEnvHash: envHash, | |||
| 227 | - | domain.LabelImage: image, | |||
| 228 | - | domain.LabelManaged: "true", | |||
| 229 | - | domain.LabelRoute: containerDomain, | |||
| 230 | - | }, | |||
| 231 | - | AutoRemove: false, | |||
| 232 | } | 265 | } | ||
| 233 | } | 266 | } | ||
| 234 | 267 | ||||
| @@ -416,6 +449,7 @@ type deployResources struct { | |||||
| 416 | networkName string | 449 | networkName string | ||
| 417 | actualImageRef string | 450 | actualImageRef string | ||
| 418 | exposedPorts []int | 451 | exposedPorts []int | ||
| 452 | + | imageLabels map[string]string | |||
| 419 | envVars []string | 453 | envVars []string | ||
| 420 | envHash string | 454 | envHash string | ||
| 421 | volumes map[string]string | 455 | volumes map[string]string | ||
| @@ -516,6 +550,12 @@ func (s *Service) prepareDeployResources(ctx context.Context, route domain.Route | |||||
| 516 | exposedPorts = []int{80, 8080, 3000} | 550 | exposedPorts = []int{80, 8080, 3000} | ||
| 517 | } | 551 | } | ||
| 518 | 552 | ||||
| 553 | + | imageLabels, err := s.runtime.GetImageLabels(ctx, actualImageRef) | |||
| 554 | + | if err != nil { | |||
| 555 | + | log.Warn().Err(err).Msg("failed to get image labels, skipping label propagation") | |||
| 556 | + | imageLabels = nil | |||
| 557 | + | } | |||
| 558 | + | ||||
| 519 | envVars, err := s.loadEnvironment(ctx, route.Domain, actualImageRef) | 559 | envVars, err := s.loadEnvironment(ctx, route.Domain, actualImageRef) | ||
| 520 | if err != nil { | 560 | if err != nil { | ||
| 521 | return nil, err | 561 | return nil, err | ||
| @@ -531,6 +571,7 @@ func (s *Service) prepareDeployResources(ctx context.Context, route domain.Route | |||||
| 531 | networkName: networkName, | 571 | networkName: networkName, | ||
| 532 | actualImageRef: actualImageRef, | 572 | actualImageRef: actualImageRef, | ||
| 533 | exposedPorts: exposedPorts, | 573 | exposedPorts: exposedPorts, | ||
| 574 | + | imageLabels: imageLabels, | |||
| 534 | envVars: envVars, | 575 | envVars: envVars, | ||
| 535 | envHash: envHash, | 576 | envHash: envHash, | ||
| 536 | volumes: volumes, | 577 | volumes: volumes, | ||
| @@ -543,17 +584,18 @@ func (s *Service) createStartedContainer(ctx context.Context, route domain.Route | |||||
| 543 | 584 | ||||
| 544 | log := zerowrap.FromCtx(ctx) | 585 | log := zerowrap.FromCtx(ctx) | ||
| 545 | 586 | ||||
| 546 | - | containerConfig := s.buildContainerConfig( | 587 | + | containerConfig := s.buildContainerConfig(containerConfigInput{ |
| 547 | - | route.Domain, | 588 | + | Domain: route.Domain, |
| 548 | - | route.Image, | 589 | + | Image: route.Image, |
| 549 | - | resources.actualImageRef, | 590 | + | ImageRef: resources.actualImageRef, |
| 550 | - | resources.exposedPorts, | 591 | + | ExposedPorts: resources.exposedPorts, |
| 551 | - | resources.envVars, | 592 | + | EnvVars: resources.envVars, |
| 552 | - | resources.envHash, | 593 | + | EnvHash: resources.envHash, |
| 553 | - | resources.volumes, | 594 | + | Volumes: resources.volumes, |
| 554 | - | resources.networkName, | 595 | + | NetworkName: resources.networkName, |
| 555 | - | existing, | 596 | + | ImageLabels: resources.imageLabels, |
| 556 | - | ) | 597 | + | Existing: existing, |
| 598 | + | }) | |||
| 557 | 599 | ||||
| 558 | newContainer, err := s.runtime.CreateContainer(ctx, containerConfig) | 600 | newContainer, err := s.runtime.CreateContainer(ctx, containerConfig) | ||
| 559 | if err != nil { | 601 | if err != nil { | ||
| @@ -2527,7 +2569,7 @@ func (s *Service) resolveProbeEndpoint(ctx context.Context, containerID string, | |||||
| 2527 | for _, key := range []string{domain.LabelProxyPort, domain.LabelPort} { | 2569 | for _, key := range []string{domain.LabelProxyPort, domain.LabelPort} { | ||
| 2528 | if portStr, ok := labels[key]; ok && portStr != "" { | 2570 | if portStr, ok := labels[key]; ok && portStr != "" { | ||
| 2529 | port, err := strconv.Atoi(portStr) | 2571 | port, err := strconv.Atoi(portStr) | ||
| 2530 | - | if err == nil && port > 0 { | 2572 | + | if err == nil && port > 0 && port <= 65535 { |
| 2531 | internalPort = port | 2573 | internalPort = port | ||
| 2532 | break | 2574 | break | ||
| 2533 | } | 2575 | } | ||
M internal/usecase/container/service_readiness_test.go +96 -0
| @@ -216,3 +216,99 @@ func TestService_WaitForHealthy_EmptyStatusIsReportedAsStarting(t *testing.T) { | |||
| 216 | 216 | assert.Error(t, err) | |
| 217 | 217 | assert.True(t, strings.Contains(err.Error(), "last status: starting")) | |
| 218 | 218 | } | |
| 219 | + | ||
| 220 | + | func TestService_WaitForReady_ProbeUsesGordonProxyPortLabel(t *testing.T) { | |
| 221 | + | runtime := mocks.NewMockContainerRuntime(t) | |
| 222 | + | svc := NewService(runtime, nil, nil, nil, Config{ | |
| 223 | + | ReadinessMode: "auto", | |
| 224 | + | HTTPProbeTimeout: 50 * time.Millisecond, | |
| 225 | + | }) | |
| 226 | + | ||
| 227 | + | ctx := testContext() | |
| 228 | + | containerID := "container-port-label" | |
| 229 | + | labelPort := 3000 | |
| 230 | + | ||
| 231 | + | probeIP, probePort := reserveEphemeralAddr(t) | |
| 232 | + | _ = probeIP | |
| 233 | + | ||
| 234 | + | // Container config has gordon.proxy.port label — should be used for probe endpoint | |
| 235 | + | containerConfig := &domain.ContainerConfig{ | |
| 236 | + | Ports: []int{22, 3000}, | |
| 237 | + | Labels: map[string]string{ | |
| 238 | + | domain.LabelProxyPort: "3000", | |
| 239 | + | }, | |
| 240 | + | } | |
| 241 | + | ||
| 242 | + | runtime.EXPECT().IsContainerRunning(mock.Anything, containerID).Return(true, nil).Once() | |
| 243 | + | runtime.EXPECT().GetContainerHealthStatus(mock.Anything, containerID).Return("", false, nil).Once() | |
| 244 | + | // Should use label port (3000), NOT call GetContainerNetworkInfo | |
| 245 | + | runtime.EXPECT().GetContainerPort(mock.Anything, containerID, labelPort).Return(probePort, nil).Once() | |
| 246 | + | ||
| 247 | + | err := svc.waitForReady(ctx, containerID, containerConfig) | |
| 248 | + | // HTTP probe will timeout (no server), but the important thing is it used port 3000 | |
| 249 | + | assert.Error(t, err) | |
| 250 | + | assert.Contains(t, err.Error(), "HTTP alive probe timeout") | |
| 251 | + | } | |
| 252 | + | ||
| 253 | + | func TestService_WaitForReady_ProbeUsesDeprecatedPortLabel(t *testing.T) { | |
| 254 | + | runtime := mocks.NewMockContainerRuntime(t) | |
| 255 | + | svc := NewService(runtime, nil, nil, nil, Config{ | |
| 256 | + | ReadinessMode: "auto", | |
| 257 | + | HTTPProbeTimeout: 50 * time.Millisecond, | |
| 258 | + | }) | |
| 259 | + | ||
| 260 | + | ctx := testContext() | |
| 261 | + | containerID := "container-proxy-port" | |
| 262 | + | labelPort := 3000 | |
| 263 | + | ||
| 264 | + | probeIP, probePort := reserveEphemeralAddr(t) | |
| 265 | + | _ = probeIP // probeIP not used directly but port is | |
| 266 | + | ||
| 267 | + | // Container config has deprecated gordon.port — should still work | |
| 268 | + | containerConfig := &domain.ContainerConfig{ | |
| 269 | + | Ports: []int{22, 3000}, | |
| 270 | + | Labels: map[string]string{ | |
| 271 | + | domain.LabelPort: "3000", | |
| 272 | + | }, | |
| 273 | + | } | |
| 274 | + | ||
| 275 | + | runtime.EXPECT().IsContainerRunning(mock.Anything, containerID).Return(true, nil).Once() | |
| 276 | + | runtime.EXPECT().GetContainerHealthStatus(mock.Anything, containerID).Return("", false, nil).Once() | |
| 277 | + | runtime.EXPECT().GetContainerPort(mock.Anything, containerID, labelPort).Return(probePort, nil).Once() | |
| 278 | + | ||
| 279 | + | err := svc.waitForReady(ctx, containerID, containerConfig) | |
| 280 | + | assert.Error(t, err) | |
| 281 | + | assert.Contains(t, err.Error(), "HTTP alive probe timeout") | |
| 282 | + | } | |
| 283 | + | ||
| 284 | + | func TestService_WaitForReady_ProxyPortTakesPriorityOverPort(t *testing.T) { | |
| 285 | + | runtime := mocks.NewMockContainerRuntime(t) | |
| 286 | + | svc := NewService(runtime, nil, nil, nil, Config{ | |
| 287 | + | ReadinessMode: "auto", | |
| 288 | + | HTTPProbeTimeout: 50 * time.Millisecond, | |
| 289 | + | }) | |
| 290 | + | ||
| 291 | + | ctx := testContext() | |
| 292 | + | containerID := "container-priority" | |
| 293 | + | ||
| 294 | + | probeIP, probePort := reserveEphemeralAddr(t) | |
| 295 | + | _ = probeIP | |
| 296 | + | ||
| 297 | + | // Both labels set — gordon.proxy.port (8080) should win over gordon.port (3000) | |
| 298 | + | containerConfig := &domain.ContainerConfig{ | |
| 299 | + | Ports: []int{22, 3000, 8080}, | |
| 300 | + | Labels: map[string]string{ | |
| 301 | + | domain.LabelProxyPort: "8080", | |
| 302 | + | domain.LabelPort: "3000", | |
| 303 | + | }, | |
| 304 | + | } | |
| 305 | + | ||
| 306 | + | runtime.EXPECT().IsContainerRunning(mock.Anything, containerID).Return(true, nil).Once() | |
| 307 | + | runtime.EXPECT().GetContainerHealthStatus(mock.Anything, containerID).Return("", false, nil).Once() | |
| 308 | + | // Should use gordon.proxy.port=8080, NOT gordon.port=3000 | |
| 309 | + | runtime.EXPECT().GetContainerPort(mock.Anything, containerID, 8080).Return(probePort, nil).Once() | |
| 310 | + | ||
| 311 | + | err := svc.waitForReady(ctx, containerID, containerConfig) | |
| 312 | + | assert.Error(t, err) | |
| 313 | + | assert.Contains(t, err.Error(), "HTTP alive probe timeout") | |
| 314 | + | } | |
| @@ -216,3 +216,99 @@ func TestService_WaitForHealthy_EmptyStatusIsReportedAsStarting(t *testing.T) { | |||||
| 216 | assert.Error(t, err) | 216 | assert.Error(t, err) | ||
| 217 | assert.True(t, strings.Contains(err.Error(), "last status: starting")) | 217 | assert.True(t, strings.Contains(err.Error(), "last status: starting")) | ||
| 218 | } | 218 | } | ||
| 219 | + | ||||
| 220 | + | func TestService_WaitForReady_ProbeUsesGordonProxyPortLabel(t *testing.T) { | |||
| 221 | + | runtime := mocks.NewMockContainerRuntime(t) | |||
| 222 | + | svc := NewService(runtime, nil, nil, nil, Config{ | |||
| 223 | + | ReadinessMode: "auto", | |||
| 224 | + | HTTPProbeTimeout: 50 * time.Millisecond, | |||
| 225 | + | }) | |||
| 226 | + | ||||
| 227 | + | ctx := testContext() | |||
| 228 | + | containerID := "container-port-label" | |||
| 229 | + | labelPort := 3000 | |||
| 230 | + | ||||
| 231 | + | probeIP, probePort := reserveEphemeralAddr(t) | |||
| 232 | + | _ = probeIP | |||
| 233 | + | ||||
| 234 | + | // Container config has gordon.proxy.port label — should be used for probe endpoint | |||
| 235 | + | containerConfig := &domain.ContainerConfig{ | |||
| 236 | + | Ports: []int{22, 3000}, | |||
| 237 | + | Labels: map[string]string{ | |||
| 238 | + | domain.LabelProxyPort: "3000", | |||
| 239 | + | }, | |||
| 240 | + | } | |||
| 241 | + | ||||
| 242 | + | runtime.EXPECT().IsContainerRunning(mock.Anything, containerID).Return(true, nil).Once() | |||
| 243 | + | runtime.EXPECT().GetContainerHealthStatus(mock.Anything, containerID).Return("", false, nil).Once() | |||
| 244 | + | // Should use label port (3000), NOT call GetContainerNetworkInfo | |||
| 245 | + | runtime.EXPECT().GetContainerPort(mock.Anything, containerID, labelPort).Return(probePort, nil).Once() | |||
| 246 | + | ||||
| 247 | + | err := svc.waitForReady(ctx, containerID, containerConfig) | |||
| 248 | + | // HTTP probe will timeout (no server), but the important thing is it used port 3000 | |||
| 249 | + | assert.Error(t, err) | |||
| 250 | + | assert.Contains(t, err.Error(), "HTTP alive probe timeout") | |||
| 251 | + | } | |||
| 252 | + | ||||
| 253 | + | func TestService_WaitForReady_ProbeUsesDeprecatedPortLabel(t *testing.T) { | |||
| 254 | + | runtime := mocks.NewMockContainerRuntime(t) | |||
| 255 | + | svc := NewService(runtime, nil, nil, nil, Config{ | |||
| 256 | + | ReadinessMode: "auto", | |||
| 257 | + | HTTPProbeTimeout: 50 * time.Millisecond, | |||
| 258 | + | }) | |||
| 259 | + | ||||
| 260 | + | ctx := testContext() | |||
| 261 | + | containerID := "container-proxy-port" | |||
| 262 | + | labelPort := 3000 | |||
| 263 | + | ||||
| 264 | + | probeIP, probePort := reserveEphemeralAddr(t) | |||
| 265 | + | _ = probeIP // probeIP not used directly but port is | |||
| 266 | + | ||||
| 267 | + | // Container config has deprecated gordon.port — should still work | |||
| 268 | + | containerConfig := &domain.ContainerConfig{ | |||
| 269 | + | Ports: []int{22, 3000}, | |||
| 270 | + | Labels: map[string]string{ | |||
| 271 | + | domain.LabelPort: "3000", | |||
| 272 | + | }, | |||
| 273 | + | } | |||
| 274 | + | ||||
| 275 | + | runtime.EXPECT().IsContainerRunning(mock.Anything, containerID).Return(true, nil).Once() | |||
| 276 | + | runtime.EXPECT().GetContainerHealthStatus(mock.Anything, containerID).Return("", false, nil).Once() | |||
| 277 | + | runtime.EXPECT().GetContainerPort(mock.Anything, containerID, labelPort).Return(probePort, nil).Once() | |||
| 278 | + | ||||
| 279 | + | err := svc.waitForReady(ctx, containerID, containerConfig) | |||
| 280 | + | assert.Error(t, err) | |||
| 281 | + | assert.Contains(t, err.Error(), "HTTP alive probe timeout") | |||
| 282 | + | } | |||
| 283 | + | ||||
| 284 | + | func TestService_WaitForReady_ProxyPortTakesPriorityOverPort(t *testing.T) { | |||
| 285 | + | runtime := mocks.NewMockContainerRuntime(t) | |||
| 286 | + | svc := NewService(runtime, nil, nil, nil, Config{ | |||
| 287 | + | ReadinessMode: "auto", | |||
| 288 | + | HTTPProbeTimeout: 50 * time.Millisecond, | |||
| 289 | + | }) | |||
| 290 | + | ||||
| 291 | + | ctx := testContext() | |||
| 292 | + | containerID := "container-priority" | |||
| 293 | + | ||||
| 294 | + | probeIP, probePort := reserveEphemeralAddr(t) | |||
| 295 | + | _ = probeIP | |||
| 296 | + | ||||
| 297 | + | // Both labels set — gordon.proxy.port (8080) should win over gordon.port (3000) | |||
| 298 | + | containerConfig := &domain.ContainerConfig{ | |||
| 299 | + | Ports: []int{22, 3000, 8080}, | |||
| 300 | + | Labels: map[string]string{ | |||
| 301 | + | domain.LabelProxyPort: "8080", | |||
| 302 | + | domain.LabelPort: "3000", | |||
| 303 | + | }, | |||
| 304 | + | } | |||
| 305 | + | ||||
| 306 | + | runtime.EXPECT().IsContainerRunning(mock.Anything, containerID).Return(true, nil).Once() | |||
| 307 | + | runtime.EXPECT().GetContainerHealthStatus(mock.Anything, containerID).Return("", false, nil).Once() | |||
| 308 | + | // Should use gordon.proxy.port=8080, NOT gordon.port=3000 | |||
| 309 | + | runtime.EXPECT().GetContainerPort(mock.Anything, containerID, 8080).Return(probePort, nil).Once() | |||
| 310 | + | ||||
| 311 | + | err := svc.waitForReady(ctx, containerID, containerConfig) | |||
| 312 | + | assert.Error(t, err) | |||
| 313 | + | assert.Contains(t, err.Error(), "HTTP alive probe timeout") | |||
| 314 | + | } | |||
M internal/usecase/container/service_test.go +125 -0
| @@ -222,6 +222,7 @@ func TestService_Deploy_Success(t *testing.T) { | |||
| 222 | 222 | ||
| 223 | 223 | // Get exposed ports | |
| 224 | 224 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | |
| 225 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:latest").Return(nil, nil) | |
| 225 | 226 | ||
| 226 | 227 | // Load environment | |
| 227 | 228 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{"FOO=bar"}, nil) | |
| @@ -298,6 +299,7 @@ func TestService_Deploy_ReadinessRecoveryWindow_AllowsTransientFlap(t *testing.T | |||
| 298 | 299 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | |
| 299 | 300 | runtime.EXPECT().PullImage(mock.Anything, "myapp:latest").Return(nil) | |
| 300 | 301 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | |
| 302 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:latest").Return(nil, nil) | |
| 301 | 303 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | |
| 302 | 304 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return([]string{}, nil) | |
| 303 | 305 | ||
| @@ -457,6 +459,7 @@ func TestService_Deploy_ReplacesExistingContainer(t *testing.T) { | |||
| 457 | 459 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | |
| 458 | 460 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | |
| 459 | 461 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | |
| 462 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:v2").Return(nil, nil) | |
| 460 | 463 | ||
| 461 | 464 | // Environment | |
| 462 | 465 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | |
| @@ -551,6 +554,7 @@ func TestService_Deploy_ResolvesExistingFromRuntime_WhenMemoryStale(t *testing.T | |||
| 551 | 554 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | |
| 552 | 555 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | |
| 553 | 556 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | |
| 557 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:v2").Return(nil, nil) | |
| 554 | 558 | ||
| 555 | 559 | // Environment | |
| 556 | 560 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | |
| @@ -630,6 +634,7 @@ func TestService_Deploy_SkipRedundantDeploy_GetImageIDError(t *testing.T) { | |||
| 630 | 634 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | |
| 631 | 635 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:latest"}, nil) | |
| 632 | 636 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | |
| 637 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:latest").Return(nil, nil) | |
| 633 | 638 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | |
| 634 | 639 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return([]string{}, nil) | |
| 635 | 640 | ||
| @@ -684,6 +689,7 @@ func TestService_Deploy_WithNetworkIsolation(t *testing.T) { | |||
| 684 | 689 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | |
| 685 | 690 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:latest"}, nil) | |
| 686 | 691 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | |
| 692 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:latest").Return(nil, nil) | |
| 687 | 693 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | |
| 688 | 694 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return([]string{}, nil) | |
| 689 | 695 | ||
| @@ -740,6 +746,7 @@ func TestService_Deploy_WithVolumeAutoCreate(t *testing.T) { | |||
| 740 | 746 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | |
| 741 | 747 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:latest"}, nil) | |
| 742 | 748 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | |
| 749 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:latest").Return(nil, nil) | |
| 743 | 750 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | |
| 744 | 751 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return([]string{}, nil) | |
| 745 | 752 | ||
| @@ -1471,6 +1478,7 @@ func TestService_Deploy_InternalDeployForcesPull(t *testing.T) { | |||
| 1471 | 1478 | ||
| 1472 | 1479 | // Get exposed ports | |
| 1473 | 1480 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "registry.example.com/myapp:latest").Return([]int{8080}, nil) | |
| 1481 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "registry.example.com/myapp:latest").Return(nil, nil) | |
| 1474 | 1482 | ||
| 1475 | 1483 | // Load environment | |
| 1476 | 1484 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | |
| @@ -1532,6 +1540,7 @@ func TestService_AutoStart_StartsNewContainers(t *testing.T) { | |||
| 1532 | 1540 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Times(2) | |
| 1533 | 1541 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp1:latest", "myapp2:latest"}, nil).Times(2) | |
| 1534 | 1542 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, mock.AnythingOfType("string")).Return([]int{8080}, nil).Times(2) | |
| 1543 | + | runtime.EXPECT().GetImageLabels(mock.Anything, mock.AnythingOfType("string")).Return(nil, nil).Times(2) | |
| 1535 | 1544 | envLoader.EXPECT().LoadEnv(mock.Anything, mock.AnythingOfType("string")).Return([]string{}, nil).Times(2) | |
| 1536 | 1545 | runtime.EXPECT().InspectImageEnv(mock.Anything, mock.AnythingOfType("string")).Return([]string{}, nil).Times(2) | |
| 1537 | 1546 | ||
| @@ -1584,6 +1593,7 @@ func TestService_AutoStart_SkipsExistingContainers(t *testing.T) { | |||
| 1584 | 1593 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Once() | |
| 1585 | 1594 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp2:latest"}, nil).Once() | |
| 1586 | 1595 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp2:latest").Return([]int{8080}, nil).Once() | |
| 1596 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp2:latest").Return(nil, nil).Once() | |
| 1587 | 1597 | envLoader.EXPECT().LoadEnv(mock.Anything, "app2.example.com").Return([]string{}, nil).Once() | |
| 1588 | 1598 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp2:latest").Return([]string{}, nil).Once() | |
| 1589 | 1599 | runtime.EXPECT().CreateContainer(mock.Anything, mock.AnythingOfType("*domain.ContainerConfig")).Return(&domain.Container{ | |
| @@ -1661,6 +1671,7 @@ func TestService_AutoStart_UsesInternalDeployContext(t *testing.T) { | |||
| 1661 | 1671 | runtime.EXPECT().TagImage(mock.Anything, "localhost:5000/myapp:latest", "reg.example.com/myapp:latest").Return(nil) | |
| 1662 | 1672 | runtime.EXPECT().UntagImage(mock.Anything, "localhost:5000/myapp:latest").Return(nil) | |
| 1663 | 1673 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "reg.example.com/myapp:latest").Return([]int{8080}, nil) | |
| 1674 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "reg.example.com/myapp:latest").Return(nil, nil) | |
| 1664 | 1675 | envLoader.EXPECT().LoadEnv(mock.Anything, "app1.example.com").Return([]string{}, nil) | |
| 1665 | 1676 | runtime.EXPECT().InspectImageEnv(mock.Anything, "reg.example.com/myapp:latest").Return([]string{}, nil) | |
| 1666 | 1677 | runtime.EXPECT().CreateContainer(mock.Anything, mock.AnythingOfType("*domain.ContainerConfig")).Return(&domain.Container{ | |
| @@ -1724,6 +1735,7 @@ func TestService_Deploy_OrphanCleanupSkipsTrackedContainer(t *testing.T) { | |||
| 1724 | 1735 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | |
| 1725 | 1736 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | |
| 1726 | 1737 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | |
| 1738 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:v2").Return(nil, nil) | |
| 1727 | 1739 | ||
| 1728 | 1740 | // Environment | |
| 1729 | 1741 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | |
| @@ -1815,6 +1827,7 @@ func TestService_Deploy_OrphanCleanupRemovesTrueOrphans(t *testing.T) { | |||
| 1815 | 1827 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | |
| 1816 | 1828 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v1").Return(nil) | |
| 1817 | 1829 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v1").Return([]int{8080}, nil) | |
| 1830 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:v1").Return(nil, nil) | |
| 1818 | 1831 | ||
| 1819 | 1832 | // Environment | |
| 1820 | 1833 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | |
| @@ -1892,6 +1905,7 @@ func TestService_Deploy_OrphanCleanupRemovesStaleNewContainer(t *testing.T) { | |||
| 1892 | 1905 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | |
| 1893 | 1906 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | |
| 1894 | 1907 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | |
| 1908 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:v2").Return(nil, nil) | |
| 1895 | 1909 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | |
| 1896 | 1910 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:v2").Return([]string{}, nil) | |
| 1897 | 1911 | ||
| @@ -1955,6 +1969,7 @@ func TestService_Deploy_TrackedTempContainerUsesAlternateTempName(t *testing.T) | |||
| 1955 | 1969 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | |
| 1956 | 1970 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | |
| 1957 | 1971 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | |
| 1972 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:v2").Return(nil, nil) | |
| 1958 | 1973 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | |
| 1959 | 1974 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:v2").Return([]string{}, nil) | |
| 1960 | 1975 | ||
| @@ -2138,6 +2153,7 @@ func TestService_Deploy_ConcurrentSameDomain(t *testing.T) { | |||
| 2138 | 2153 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Times(2) | |
| 2139 | 2154 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:latest"}, nil).Times(2) | |
| 2140 | 2155 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil).Times(2) | |
| 2156 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:latest").Return(nil, nil).Times(2) | |
| 2141 | 2157 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return(nil, nil).Times(2) | |
| 2142 | 2158 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return(nil, nil).Times(2) | |
| 2143 | 2159 | ||
| @@ -2277,6 +2293,7 @@ func TestService_Deploy_CacheInvalidationBeforeOldContainerStop(t *testing.T) { | |||
| 2277 | 2293 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | |
| 2278 | 2294 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:v2"}, nil) | |
| 2279 | 2295 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | |
| 2296 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:v2").Return(nil, nil) | |
| 2280 | 2297 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | |
| 2281 | 2298 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:v2").Return([]string{}, nil) | |
| 2282 | 2299 | ||
| @@ -2348,6 +2365,7 @@ func TestService_Deploy_NilCacheInvalidator(t *testing.T) { | |||
| 2348 | 2365 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | |
| 2349 | 2366 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:v2"}, nil) | |
| 2350 | 2367 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | |
| 2368 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:v2").Return(nil, nil) | |
| 2351 | 2369 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | |
| 2352 | 2370 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:v2").Return([]string{}, nil) | |
| 2353 | 2371 | ||
| @@ -2407,6 +2425,7 @@ func TestService_Deploy_SkipsRedundantDeploy(t *testing.T) { | |||
| 2407 | 2425 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | |
| 2408 | 2426 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:latest"}, nil) | |
| 2409 | 2427 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | |
| 2428 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:latest").Return(nil, nil) | |
| 2410 | 2429 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | |
| 2411 | 2430 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return([]string{}, nil) | |
| 2412 | 2431 | ||
| @@ -2463,6 +2482,7 @@ func TestService_Deploy_SkipsRedundantDeploy_WhenTrackedImageIDMissing(t *testin | |||
| 2463 | 2482 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | |
| 2464 | 2483 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:latest"}, nil) | |
| 2465 | 2484 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | |
| 2485 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:latest").Return(nil, nil) | |
| 2466 | 2486 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | |
| 2467 | 2487 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return([]string{}, nil) | |
| 2468 | 2488 | ||
| @@ -2517,6 +2537,7 @@ func TestService_Deploy_DoesNotSkipWhenImageIDDiffers(t *testing.T) { | |||
| 2517 | 2537 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | |
| 2518 | 2538 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:latest"}, nil) | |
| 2519 | 2539 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | |
| 2540 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:latest").Return(nil, nil) | |
| 2520 | 2541 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | |
| 2521 | 2542 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return([]string{}, nil) | |
| 2522 | 2543 | ||
| @@ -2619,6 +2640,7 @@ func TestService_Deploy_SkipRedundantDeploy_ContainerNotRunning(t *testing.T) { | |||
| 2619 | 2640 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | |
| 2620 | 2641 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:latest"}, nil) | |
| 2621 | 2642 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | |
| 2643 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:latest").Return(nil, nil) | |
| 2622 | 2644 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | |
| 2623 | 2645 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return([]string{}, nil) | |
| 2624 | 2646 | ||
| @@ -2678,6 +2700,7 @@ func TestService_Deploy_DoesNotSkip_WhenEnvHashDiffers(t *testing.T) { | |||
| 2678 | 2700 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | |
| 2679 | 2701 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:latest"}, nil) | |
| 2680 | 2702 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | |
| 2703 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:latest").Return(nil, nil) | |
| 2681 | 2704 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | |
| 2682 | 2705 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return([]string{}, nil) | |
| 2683 | 2706 | ||
| @@ -2732,6 +2755,7 @@ func TestService_Deploy_DoesNotSkip_WhenEnvHashMissing(t *testing.T) { | |||
| 2732 | 2755 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | |
| 2733 | 2756 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:latest"}, nil) | |
| 2734 | 2757 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | |
| 2758 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:latest").Return(nil, nil) | |
| 2735 | 2759 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | |
| 2736 | 2760 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return([]string{}, nil) | |
| 2737 | 2761 | ||
| @@ -2873,6 +2897,7 @@ func TestService_Deploy_OrphanCleanup_NeverKillsRunningCanonical(t *testing.T) { | |||
| 2873 | 2897 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | |
| 2874 | 2898 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | |
| 2875 | 2899 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | |
| 2900 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:v2").Return(nil, nil) | |
| 2876 | 2901 | ||
| 2877 | 2902 | // Environment | |
| 2878 | 2903 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | |
| @@ -2958,6 +2983,7 @@ func TestService_Deploy_RollbackOnPostSwitchCrash(t *testing.T) { | |||
| 2958 | 2983 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | |
| 2959 | 2984 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | |
| 2960 | 2985 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | |
| 2986 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:v2").Return(nil, nil) | |
| 2961 | 2987 | ||
| 2962 | 2988 | // Environment | |
| 2963 | 2989 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | |
| @@ -3050,6 +3076,7 @@ func TestService_Deploy_StabilizationSuccess(t *testing.T) { | |||
| 3050 | 3076 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | |
| 3051 | 3077 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | |
| 3052 | 3078 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | |
| 3079 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:v2").Return(nil, nil) | |
| 3053 | 3080 | ||
| 3054 | 3081 | // Environment | |
| 3055 | 3082 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | |
| @@ -3135,6 +3162,7 @@ func TestService_Deploy_ZeroDowntime_OldNeverStoppedBeforeNewReady(t *testing.T) | |||
| 3135 | 3162 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | |
| 3136 | 3163 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | |
| 3137 | 3164 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | |
| 3165 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:v2").Return(nil, nil) | |
| 3138 | 3166 | ||
| 3139 | 3167 | // Environment | |
| 3140 | 3168 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | |
| @@ -3226,6 +3254,7 @@ func TestService_Deploy_ZeroDowntime_ReadinessFailure_OldUntouched(t *testing.T) | |||
| 3226 | 3254 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | |
| 3227 | 3255 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | |
| 3228 | 3256 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | |
| 3257 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:v2").Return(nil, nil) | |
| 3229 | 3258 | ||
| 3230 | 3259 | // Environment | |
| 3231 | 3260 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | |
| @@ -3303,6 +3332,7 @@ func TestService_Deploy_ZeroDowntime_NoExisting_FirstDeploy(t *testing.T) { | |||
| 3303 | 3332 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | |
| 3304 | 3333 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | |
| 3305 | 3334 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | |
| 3335 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:v2").Return(nil, nil) | |
| 3306 | 3336 | ||
| 3307 | 3337 | // Environment | |
| 3308 | 3338 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | |
| @@ -3345,3 +3375,98 @@ func TestService_Deploy_ZeroDowntime_NoExisting_FirstDeploy(t *testing.T) { | |||
| 3345 | 3375 | assert.True(t, exists) | |
| 3346 | 3376 | assert.Equal(t, "first-container", tracked.ID) | |
| 3347 | 3377 | } | |
| 3378 | + | ||
| 3379 | + | func TestService_Deploy_PropagatesImageLabelsToContainerConfig(t *testing.T) { | |
| 3380 | + | runtime := mocks.NewMockContainerRuntime(t) | |
| 3381 | + | envLoader := mocks.NewMockEnvLoader(t) | |
| 3382 | + | eventBus := mocks.NewMockEventPublisher(t) | |
| 3383 | + | ||
| 3384 | + | config := Config{ | |
| 3385 | + | NetworkIsolation: false, | |
| 3386 | + | VolumeAutoCreate: false, | |
| 3387 | + | ReadinessDelay: time.Millisecond, | |
| 3388 | + | DrainDelay: time.Millisecond, | |
| 3389 | + | DrainDelayConfigured: true, | |
| 3390 | + | } | |
| 3391 | + | ||
| 3392 | + | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 3393 | + | ctx := testContext() | |
| 3394 | + | ||
| 3395 | + | route := domain.Route{ | |
| 3396 | + | Domain: "gitea.example.com", | |
| 3397 | + | Image: "gitea/gitea:latest", | |
| 3398 | + | } | |
| 3399 | + | ||
| 3400 | + | // No container in memory — runtime resolution returns nothing | |
| 3401 | + | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{}, nil) | |
| 3402 | + | // No orphaned containers | |
| 3403 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | |
| 3404 | + | ||
| 3405 | + | // Image is found locally | |
| 3406 | + | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"gitea/gitea:latest"}, nil) | |
| 3407 | + | ||
| 3408 | + | // Exposed ports — image exposes both SSH and HTTP | |
| 3409 | + | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "gitea/gitea:latest").Return([]int{22, 3000}, nil) | |
| 3410 | + | ||
| 3411 | + | // Image labels include gordon.proxy.port and gordon.health | |
| 3412 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "gitea/gitea:latest").Return(map[string]string{ | |
| 3413 | + | domain.LabelProxyPort: "3000", | |
| 3414 | + | domain.LabelHealth: "/healthz", | |
| 3415 | + | "some.other.label": "ignored", | |
| 3416 | + | }, nil) | |
| 3417 | + | ||
| 3418 | + | // Load environment | |
| 3419 | + | envLoader.EXPECT().LoadEnv(mock.Anything, "gitea.example.com").Return(nil, nil) | |
| 3420 | + | runtime.EXPECT().InspectImageEnv(mock.Anything, "gitea/gitea:latest").Return(nil, nil) | |
| 3421 | + | ||
| 3422 | + | // Create container — ASSERT that labels include propagated image labels | |
| 3423 | + | createdContainer := &domain.Container{ | |
| 3424 | + | ID: "c-gitea-123", | |
| 3425 | + | Name: "gordon-gitea.example.com", | |
| 3426 | + | Image: "gitea/gitea:latest", | |
| 3427 | + | Status: "created", | |
| 3428 | + | } | |
| 3429 | + | runtime.EXPECT().CreateContainer(mock.Anything, mock.MatchedBy(func(cfg *domain.ContainerConfig) bool { | |
| 3430 | + | // Verify gordon.proxy.port was propagated from image labels | |
| 3431 | + | if cfg.Labels[domain.LabelProxyPort] != "3000" { | |
| 3432 | + | return false | |
| 3433 | + | } | |
| 3434 | + | // Verify gordon.health was propagated from image labels | |
| 3435 | + | if cfg.Labels[domain.LabelHealth] != "/healthz" { | |
| 3436 | + | return false | |
| 3437 | + | } | |
| 3438 | + | // Verify non-gordon labels were NOT propagated | |
| 3439 | + | if _, exists := cfg.Labels["some.other.label"]; exists { | |
| 3440 | + | return false | |
| 3441 | + | } | |
| 3442 | + | // Verify standard container labels are still present | |
| 3443 | + | if cfg.Labels[domain.LabelManaged] != "true" { | |
| 3444 | + | return false | |
| 3445 | + | } | |
| 3446 | + | return true | |
| 3447 | + | })).Return(createdContainer, nil) | |
| 3448 | + | ||
| 3449 | + | runtime.EXPECT().StartContainer(mock.Anything, "c-gitea-123").Return(nil) | |
| 3450 | + | ||
| 3451 | + | // Readiness: delay mode (minimal) | |
| 3452 | + | runtime.EXPECT().IsContainerRunning(mock.Anything, "c-gitea-123").Return(true, nil).Times(2) | |
| 3453 | + | ||
| 3454 | + | // Re-inspect | |
| 3455 | + | runningContainer := &domain.Container{ | |
| 3456 | + | ID: "c-gitea-123", | |
| 3457 | + | Name: "gordon-gitea.example.com", | |
| 3458 | + | Image: "gitea/gitea:latest", | |
| 3459 | + | Status: "running", | |
| 3460 | + | Ports: []int{22, 3000}, | |
| 3461 | + | } | |
| 3462 | + | runtime.EXPECT().InspectContainer(mock.Anything, "c-gitea-123").Return(runningContainer, nil) | |
| 3463 | + | ||
| 3464 | + | // Publish event | |
| 3465 | + | eventBus.EXPECT().Publish(domain.EventContainerDeployed, mock.AnythingOfType("*domain.ContainerEventPayload")).Return(nil) | |
| 3466 | + | ||
| 3467 | + | result, err := svc.Deploy(ctx, route) | |
| 3468 | + | ||
| 3469 | + | require.NoError(t, err) | |
| 3470 | + | assert.Equal(t, "c-gitea-123", result.ID) | |
| 3471 | + | assert.Equal(t, "running", result.Status) | |
| 3472 | + | } | |
| @@ -222,6 +222,7 @@ func TestService_Deploy_Success(t *testing.T) { | |||||
| 222 | 222 | ||||
| 223 | // Get exposed ports | 223 | // Get exposed ports | ||
| 224 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | 224 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | ||
| 225 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:latest").Return(nil, nil) | |||
| 225 | 226 | ||||
| 226 | // Load environment | 227 | // Load environment | ||
| 227 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{"FOO=bar"}, nil) | 228 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{"FOO=bar"}, nil) | ||
| @@ -298,6 +299,7 @@ func TestService_Deploy_ReadinessRecoveryWindow_AllowsTransientFlap(t *testing.T | |||||
| 298 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | 299 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | ||
| 299 | runtime.EXPECT().PullImage(mock.Anything, "myapp:latest").Return(nil) | 300 | runtime.EXPECT().PullImage(mock.Anything, "myapp:latest").Return(nil) | ||
| 300 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | 301 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | ||
| 302 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:latest").Return(nil, nil) | |||
| 301 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | 303 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | ||
| 302 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return([]string{}, nil) | 304 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return([]string{}, nil) | ||
| 303 | 305 | ||||
| @@ -457,6 +459,7 @@ func TestService_Deploy_ReplacesExistingContainer(t *testing.T) { | |||||
| 457 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | 459 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | ||
| 458 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | 460 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | ||
| 459 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | 461 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | ||
| 462 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:v2").Return(nil, nil) | |||
| 460 | 463 | ||||
| 461 | // Environment | 464 | // Environment | ||
| 462 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | 465 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | ||
| @@ -551,6 +554,7 @@ func TestService_Deploy_ResolvesExistingFromRuntime_WhenMemoryStale(t *testing.T | |||||
| 551 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | 554 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | ||
| 552 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | 555 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | ||
| 553 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | 556 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | ||
| 557 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:v2").Return(nil, nil) | |||
| 554 | 558 | ||||
| 555 | // Environment | 559 | // Environment | ||
| 556 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | 560 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | ||
| @@ -630,6 +634,7 @@ func TestService_Deploy_SkipRedundantDeploy_GetImageIDError(t *testing.T) { | |||||
| 630 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | 634 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | ||
| 631 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:latest"}, nil) | 635 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:latest"}, nil) | ||
| 632 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | 636 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | ||
| 637 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:latest").Return(nil, nil) | |||
| 633 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | 638 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | ||
| 634 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return([]string{}, nil) | 639 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return([]string{}, nil) | ||
| 635 | 640 | ||||
| @@ -684,6 +689,7 @@ func TestService_Deploy_WithNetworkIsolation(t *testing.T) { | |||||
| 684 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | 689 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | ||
| 685 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:latest"}, nil) | 690 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:latest"}, nil) | ||
| 686 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | 691 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | ||
| 692 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:latest").Return(nil, nil) | |||
| 687 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | 693 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | ||
| 688 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return([]string{}, nil) | 694 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return([]string{}, nil) | ||
| 689 | 695 | ||||
| @@ -740,6 +746,7 @@ func TestService_Deploy_WithVolumeAutoCreate(t *testing.T) { | |||||
| 740 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | 746 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | ||
| 741 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:latest"}, nil) | 747 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:latest"}, nil) | ||
| 742 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | 748 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | ||
| 749 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:latest").Return(nil, nil) | |||
| 743 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | 750 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | ||
| 744 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return([]string{}, nil) | 751 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return([]string{}, nil) | ||
| 745 | 752 | ||||
| @@ -1471,6 +1478,7 @@ func TestService_Deploy_InternalDeployForcesPull(t *testing.T) { | |||||
| 1471 | 1478 | ||||
| 1472 | // Get exposed ports | 1479 | // Get exposed ports | ||
| 1473 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "registry.example.com/myapp:latest").Return([]int{8080}, nil) | 1480 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "registry.example.com/myapp:latest").Return([]int{8080}, nil) | ||
| 1481 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "registry.example.com/myapp:latest").Return(nil, nil) | |||
| 1474 | 1482 | ||||
| 1475 | // Load environment | 1483 | // Load environment | ||
| 1476 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | 1484 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | ||
| @@ -1532,6 +1540,7 @@ func TestService_AutoStart_StartsNewContainers(t *testing.T) { | |||||
| 1532 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Times(2) | 1540 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Times(2) | ||
| 1533 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp1:latest", "myapp2:latest"}, nil).Times(2) | 1541 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp1:latest", "myapp2:latest"}, nil).Times(2) | ||
| 1534 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, mock.AnythingOfType("string")).Return([]int{8080}, nil).Times(2) | 1542 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, mock.AnythingOfType("string")).Return([]int{8080}, nil).Times(2) | ||
| 1543 | + | runtime.EXPECT().GetImageLabels(mock.Anything, mock.AnythingOfType("string")).Return(nil, nil).Times(2) | |||
| 1535 | envLoader.EXPECT().LoadEnv(mock.Anything, mock.AnythingOfType("string")).Return([]string{}, nil).Times(2) | 1544 | envLoader.EXPECT().LoadEnv(mock.Anything, mock.AnythingOfType("string")).Return([]string{}, nil).Times(2) | ||
| 1536 | runtime.EXPECT().InspectImageEnv(mock.Anything, mock.AnythingOfType("string")).Return([]string{}, nil).Times(2) | 1545 | runtime.EXPECT().InspectImageEnv(mock.Anything, mock.AnythingOfType("string")).Return([]string{}, nil).Times(2) | ||
| 1537 | 1546 | ||||
| @@ -1584,6 +1593,7 @@ func TestService_AutoStart_SkipsExistingContainers(t *testing.T) { | |||||
| 1584 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Once() | 1593 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Once() | ||
| 1585 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp2:latest"}, nil).Once() | 1594 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp2:latest"}, nil).Once() | ||
| 1586 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp2:latest").Return([]int{8080}, nil).Once() | 1595 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp2:latest").Return([]int{8080}, nil).Once() | ||
| 1596 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp2:latest").Return(nil, nil).Once() | |||
| 1587 | envLoader.EXPECT().LoadEnv(mock.Anything, "app2.example.com").Return([]string{}, nil).Once() | 1597 | envLoader.EXPECT().LoadEnv(mock.Anything, "app2.example.com").Return([]string{}, nil).Once() | ||
| 1588 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp2:latest").Return([]string{}, nil).Once() | 1598 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp2:latest").Return([]string{}, nil).Once() | ||
| 1589 | runtime.EXPECT().CreateContainer(mock.Anything, mock.AnythingOfType("*domain.ContainerConfig")).Return(&domain.Container{ | 1599 | runtime.EXPECT().CreateContainer(mock.Anything, mock.AnythingOfType("*domain.ContainerConfig")).Return(&domain.Container{ | ||
| @@ -1661,6 +1671,7 @@ func TestService_AutoStart_UsesInternalDeployContext(t *testing.T) { | |||||
| 1661 | runtime.EXPECT().TagImage(mock.Anything, "localhost:5000/myapp:latest", "reg.example.com/myapp:latest").Return(nil) | 1671 | runtime.EXPECT().TagImage(mock.Anything, "localhost:5000/myapp:latest", "reg.example.com/myapp:latest").Return(nil) | ||
| 1662 | runtime.EXPECT().UntagImage(mock.Anything, "localhost:5000/myapp:latest").Return(nil) | 1672 | runtime.EXPECT().UntagImage(mock.Anything, "localhost:5000/myapp:latest").Return(nil) | ||
| 1663 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "reg.example.com/myapp:latest").Return([]int{8080}, nil) | 1673 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "reg.example.com/myapp:latest").Return([]int{8080}, nil) | ||
| 1674 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "reg.example.com/myapp:latest").Return(nil, nil) | |||
| 1664 | envLoader.EXPECT().LoadEnv(mock.Anything, "app1.example.com").Return([]string{}, nil) | 1675 | envLoader.EXPECT().LoadEnv(mock.Anything, "app1.example.com").Return([]string{}, nil) | ||
| 1665 | runtime.EXPECT().InspectImageEnv(mock.Anything, "reg.example.com/myapp:latest").Return([]string{}, nil) | 1676 | runtime.EXPECT().InspectImageEnv(mock.Anything, "reg.example.com/myapp:latest").Return([]string{}, nil) | ||
| 1666 | runtime.EXPECT().CreateContainer(mock.Anything, mock.AnythingOfType("*domain.ContainerConfig")).Return(&domain.Container{ | 1677 | runtime.EXPECT().CreateContainer(mock.Anything, mock.AnythingOfType("*domain.ContainerConfig")).Return(&domain.Container{ | ||
| @@ -1724,6 +1735,7 @@ func TestService_Deploy_OrphanCleanupSkipsTrackedContainer(t *testing.T) { | |||||
| 1724 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | 1735 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | ||
| 1725 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | 1736 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | ||
| 1726 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | 1737 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | ||
| 1738 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:v2").Return(nil, nil) | |||
| 1727 | 1739 | ||||
| 1728 | // Environment | 1740 | // Environment | ||
| 1729 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | 1741 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | ||
| @@ -1815,6 +1827,7 @@ func TestService_Deploy_OrphanCleanupRemovesTrueOrphans(t *testing.T) { | |||||
| 1815 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | 1827 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | ||
| 1816 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v1").Return(nil) | 1828 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v1").Return(nil) | ||
| 1817 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v1").Return([]int{8080}, nil) | 1829 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v1").Return([]int{8080}, nil) | ||
| 1830 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:v1").Return(nil, nil) | |||
| 1818 | 1831 | ||||
| 1819 | // Environment | 1832 | // Environment | ||
| 1820 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | 1833 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | ||
| @@ -1892,6 +1905,7 @@ func TestService_Deploy_OrphanCleanupRemovesStaleNewContainer(t *testing.T) { | |||||
| 1892 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | 1905 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | ||
| 1893 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | 1906 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | ||
| 1894 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | 1907 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | ||
| 1908 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:v2").Return(nil, nil) | |||
| 1895 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | 1909 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | ||
| 1896 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:v2").Return([]string{}, nil) | 1910 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:v2").Return([]string{}, nil) | ||
| 1897 | 1911 | ||||
| @@ -1955,6 +1969,7 @@ func TestService_Deploy_TrackedTempContainerUsesAlternateTempName(t *testing.T) | |||||
| 1955 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | 1969 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | ||
| 1956 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | 1970 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | ||
| 1957 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | 1971 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | ||
| 1972 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:v2").Return(nil, nil) | |||
| 1958 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | 1973 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | ||
| 1959 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:v2").Return([]string{}, nil) | 1974 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:v2").Return([]string{}, nil) | ||
| 1960 | 1975 | ||||
| @@ -2138,6 +2153,7 @@ func TestService_Deploy_ConcurrentSameDomain(t *testing.T) { | |||||
| 2138 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Times(2) | 2153 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Times(2) | ||
| 2139 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:latest"}, nil).Times(2) | 2154 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:latest"}, nil).Times(2) | ||
| 2140 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil).Times(2) | 2155 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil).Times(2) | ||
| 2156 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:latest").Return(nil, nil).Times(2) | |||
| 2141 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return(nil, nil).Times(2) | 2157 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return(nil, nil).Times(2) | ||
| 2142 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return(nil, nil).Times(2) | 2158 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return(nil, nil).Times(2) | ||
| 2143 | 2159 | ||||
| @@ -2277,6 +2293,7 @@ func TestService_Deploy_CacheInvalidationBeforeOldContainerStop(t *testing.T) { | |||||
| 2277 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | 2293 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | ||
| 2278 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:v2"}, nil) | 2294 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:v2"}, nil) | ||
| 2279 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | 2295 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | ||
| 2296 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:v2").Return(nil, nil) | |||
| 2280 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | 2297 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | ||
| 2281 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:v2").Return([]string{}, nil) | 2298 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:v2").Return([]string{}, nil) | ||
| 2282 | 2299 | ||||
| @@ -2348,6 +2365,7 @@ func TestService_Deploy_NilCacheInvalidator(t *testing.T) { | |||||
| 2348 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | 2365 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | ||
| 2349 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:v2"}, nil) | 2366 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:v2"}, nil) | ||
| 2350 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | 2367 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | ||
| 2368 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:v2").Return(nil, nil) | |||
| 2351 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | 2369 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | ||
| 2352 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:v2").Return([]string{}, nil) | 2370 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:v2").Return([]string{}, nil) | ||
| 2353 | 2371 | ||||
| @@ -2407,6 +2425,7 @@ func TestService_Deploy_SkipsRedundantDeploy(t *testing.T) { | |||||
| 2407 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | 2425 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | ||
| 2408 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:latest"}, nil) | 2426 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:latest"}, nil) | ||
| 2409 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | 2427 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | ||
| 2428 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:latest").Return(nil, nil) | |||
| 2410 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | 2429 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | ||
| 2411 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return([]string{}, nil) | 2430 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return([]string{}, nil) | ||
| 2412 | 2431 | ||||
| @@ -2463,6 +2482,7 @@ func TestService_Deploy_SkipsRedundantDeploy_WhenTrackedImageIDMissing(t *testin | |||||
| 2463 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | 2482 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | ||
| 2464 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:latest"}, nil) | 2483 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:latest"}, nil) | ||
| 2465 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | 2484 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | ||
| 2485 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:latest").Return(nil, nil) | |||
| 2466 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | 2486 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | ||
| 2467 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return([]string{}, nil) | 2487 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return([]string{}, nil) | ||
| 2468 | 2488 | ||||
| @@ -2517,6 +2537,7 @@ func TestService_Deploy_DoesNotSkipWhenImageIDDiffers(t *testing.T) { | |||||
| 2517 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | 2537 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | ||
| 2518 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:latest"}, nil) | 2538 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:latest"}, nil) | ||
| 2519 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | 2539 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | ||
| 2540 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:latest").Return(nil, nil) | |||
| 2520 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | 2541 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | ||
| 2521 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return([]string{}, nil) | 2542 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return([]string{}, nil) | ||
| 2522 | 2543 | ||||
| @@ -2619,6 +2640,7 @@ func TestService_Deploy_SkipRedundantDeploy_ContainerNotRunning(t *testing.T) { | |||||
| 2619 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | 2640 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | ||
| 2620 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:latest"}, nil) | 2641 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:latest"}, nil) | ||
| 2621 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | 2642 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | ||
| 2643 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:latest").Return(nil, nil) | |||
| 2622 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | 2644 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | ||
| 2623 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return([]string{}, nil) | 2645 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return([]string{}, nil) | ||
| 2624 | 2646 | ||||
| @@ -2678,6 +2700,7 @@ func TestService_Deploy_DoesNotSkip_WhenEnvHashDiffers(t *testing.T) { | |||||
| 2678 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | 2700 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | ||
| 2679 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:latest"}, nil) | 2701 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:latest"}, nil) | ||
| 2680 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | 2702 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | ||
| 2703 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:latest").Return(nil, nil) | |||
| 2681 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | 2704 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | ||
| 2682 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return([]string{}, nil) | 2705 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return([]string{}, nil) | ||
| 2683 | 2706 | ||||
| @@ -2732,6 +2755,7 @@ func TestService_Deploy_DoesNotSkip_WhenEnvHashMissing(t *testing.T) { | |||||
| 2732 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | 2755 | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | ||
| 2733 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:latest"}, nil) | 2756 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"myapp:latest"}, nil) | ||
| 2734 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | 2757 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:latest").Return([]int{8080}, nil) | ||
| 2758 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:latest").Return(nil, nil) | |||
| 2735 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | 2759 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | ||
| 2736 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return([]string{}, nil) | 2760 | runtime.EXPECT().InspectImageEnv(mock.Anything, "myapp:latest").Return([]string{}, nil) | ||
| 2737 | 2761 | ||||
| @@ -2873,6 +2897,7 @@ func TestService_Deploy_OrphanCleanup_NeverKillsRunningCanonical(t *testing.T) { | |||||
| 2873 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | 2897 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | ||
| 2874 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | 2898 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | ||
| 2875 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | 2899 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | ||
| 2900 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:v2").Return(nil, nil) | |||
| 2876 | 2901 | ||||
| 2877 | // Environment | 2902 | // Environment | ||
| 2878 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | 2903 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | ||
| @@ -2958,6 +2983,7 @@ func TestService_Deploy_RollbackOnPostSwitchCrash(t *testing.T) { | |||||
| 2958 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | 2983 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | ||
| 2959 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | 2984 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | ||
| 2960 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | 2985 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | ||
| 2986 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:v2").Return(nil, nil) | |||
| 2961 | 2987 | ||||
| 2962 | // Environment | 2988 | // Environment | ||
| 2963 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | 2989 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | ||
| @@ -3050,6 +3076,7 @@ func TestService_Deploy_StabilizationSuccess(t *testing.T) { | |||||
| 3050 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | 3076 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | ||
| 3051 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | 3077 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | ||
| 3052 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | 3078 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | ||
| 3079 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:v2").Return(nil, nil) | |||
| 3053 | 3080 | ||||
| 3054 | // Environment | 3081 | // Environment | ||
| 3055 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | 3082 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | ||
| @@ -3135,6 +3162,7 @@ func TestService_Deploy_ZeroDowntime_OldNeverStoppedBeforeNewReady(t *testing.T) | |||||
| 3135 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | 3162 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | ||
| 3136 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | 3163 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | ||
| 3137 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | 3164 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | ||
| 3165 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:v2").Return(nil, nil) | |||
| 3138 | 3166 | ||||
| 3139 | // Environment | 3167 | // Environment | ||
| 3140 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | 3168 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | ||
| @@ -3226,6 +3254,7 @@ func TestService_Deploy_ZeroDowntime_ReadinessFailure_OldUntouched(t *testing.T) | |||||
| 3226 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | 3254 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | ||
| 3227 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | 3255 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | ||
| 3228 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | 3256 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | ||
| 3257 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:v2").Return(nil, nil) | |||
| 3229 | 3258 | ||||
| 3230 | // Environment | 3259 | // Environment | ||
| 3231 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | 3260 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | ||
| @@ -3303,6 +3332,7 @@ func TestService_Deploy_ZeroDowntime_NoExisting_FirstDeploy(t *testing.T) { | |||||
| 3303 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | 3332 | runtime.EXPECT().ListImages(mock.Anything).Return([]string{}, nil) | ||
| 3304 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | 3333 | runtime.EXPECT().PullImage(mock.Anything, "myapp:v2").Return(nil) | ||
| 3305 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | 3334 | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "myapp:v2").Return([]int{8080}, nil) | ||
| 3335 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:v2").Return(nil, nil) | |||
| 3306 | 3336 | ||||
| 3307 | // Environment | 3337 | // Environment | ||
| 3308 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | 3338 | envLoader.EXPECT().LoadEnv(mock.Anything, "test.example.com").Return([]string{}, nil) | ||
| @@ -3345,3 +3375,98 @@ func TestService_Deploy_ZeroDowntime_NoExisting_FirstDeploy(t *testing.T) { | |||||
| 3345 | assert.True(t, exists) | 3375 | assert.True(t, exists) | ||
| 3346 | assert.Equal(t, "first-container", tracked.ID) | 3376 | assert.Equal(t, "first-container", tracked.ID) | ||
| 3347 | } | 3377 | } | ||
| 3378 | + | ||||
| 3379 | + | func TestService_Deploy_PropagatesImageLabelsToContainerConfig(t *testing.T) { | |||
| 3380 | + | runtime := mocks.NewMockContainerRuntime(t) | |||
| 3381 | + | envLoader := mocks.NewMockEnvLoader(t) | |||
| 3382 | + | eventBus := mocks.NewMockEventPublisher(t) | |||
| 3383 | + | ||||
| 3384 | + | config := Config{ | |||
| 3385 | + | NetworkIsolation: false, | |||
| 3386 | + | VolumeAutoCreate: false, | |||
| 3387 | + | ReadinessDelay: time.Millisecond, | |||
| 3388 | + | DrainDelay: time.Millisecond, | |||
| 3389 | + | DrainDelayConfigured: true, | |||
| 3390 | + | } | |||
| 3391 | + | ||||
| 3392 | + | svc := NewService(runtime, envLoader, eventBus, nil, config) | |||
| 3393 | + | ctx := testContext() | |||
| 3394 | + | ||||
| 3395 | + | route := domain.Route{ | |||
| 3396 | + | Domain: "gitea.example.com", | |||
| 3397 | + | Image: "gitea/gitea:latest", | |||
| 3398 | + | } | |||
| 3399 | + | ||||
| 3400 | + | // No container in memory — runtime resolution returns nothing | |||
| 3401 | + | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{}, nil) | |||
| 3402 | + | // No orphaned containers | |||
| 3403 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | |||
| 3404 | + | ||||
| 3405 | + | // Image is found locally | |||
| 3406 | + | runtime.EXPECT().ListImages(mock.Anything).Return([]string{"gitea/gitea:latest"}, nil) | |||
| 3407 | + | ||||
| 3408 | + | // Exposed ports — image exposes both SSH and HTTP | |||
| 3409 | + | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "gitea/gitea:latest").Return([]int{22, 3000}, nil) | |||
| 3410 | + | ||||
| 3411 | + | // Image labels include gordon.proxy.port and gordon.health | |||
| 3412 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "gitea/gitea:latest").Return(map[string]string{ | |||
| 3413 | + | domain.LabelProxyPort: "3000", | |||
| 3414 | + | domain.LabelHealth: "/healthz", | |||
| 3415 | + | "some.other.label": "ignored", | |||
| 3416 | + | }, nil) | |||
| 3417 | + | ||||
| 3418 | + | // Load environment | |||
| 3419 | + | envLoader.EXPECT().LoadEnv(mock.Anything, "gitea.example.com").Return(nil, nil) | |||
| 3420 | + | runtime.EXPECT().InspectImageEnv(mock.Anything, "gitea/gitea:latest").Return(nil, nil) | |||
| 3421 | + | ||||
| 3422 | + | // Create container — ASSERT that labels include propagated image labels | |||
| 3423 | + | createdContainer := &domain.Container{ | |||
| 3424 | + | ID: "c-gitea-123", | |||
| 3425 | + | Name: "gordon-gitea.example.com", | |||
| 3426 | + | Image: "gitea/gitea:latest", | |||
| 3427 | + | Status: "created", | |||
| 3428 | + | } | |||
| 3429 | + | runtime.EXPECT().CreateContainer(mock.Anything, mock.MatchedBy(func(cfg *domain.ContainerConfig) bool { | |||
| 3430 | + | // Verify gordon.proxy.port was propagated from image labels | |||
| 3431 | + | if cfg.Labels[domain.LabelProxyPort] != "3000" { | |||
| 3432 | + | return false | |||
| 3433 | + | } | |||
| 3434 | + | // Verify gordon.health was propagated from image labels | |||
| 3435 | + | if cfg.Labels[domain.LabelHealth] != "/healthz" { | |||
| 3436 | + | return false | |||
| 3437 | + | } | |||
| 3438 | + | // Verify non-gordon labels were NOT propagated | |||
| 3439 | + | if _, exists := cfg.Labels["some.other.label"]; exists { | |||
| 3440 | + | return false | |||
| 3441 | + | } | |||
| 3442 | + | // Verify standard container labels are still present | |||
| 3443 | + | if cfg.Labels[domain.LabelManaged] != "true" { | |||
| 3444 | + | return false | |||
| 3445 | + | } | |||
| 3446 | + | return true | |||
| 3447 | + | })).Return(createdContainer, nil) | |||
| 3448 | + | ||||
| 3449 | + | runtime.EXPECT().StartContainer(mock.Anything, "c-gitea-123").Return(nil) | |||
| 3450 | + | ||||
| 3451 | + | // Readiness: delay mode (minimal) | |||
| 3452 | + | runtime.EXPECT().IsContainerRunning(mock.Anything, "c-gitea-123").Return(true, nil).Times(2) | |||
| 3453 | + | ||||
| 3454 | + | // Re-inspect | |||
| 3455 | + | runningContainer := &domain.Container{ | |||
| 3456 | + | ID: "c-gitea-123", | |||
| 3457 | + | Name: "gordon-gitea.example.com", | |||
| 3458 | + | Image: "gitea/gitea:latest", | |||
| 3459 | + | Status: "running", | |||
| 3460 | + | Ports: []int{22, 3000}, | |||
| 3461 | + | } | |||
| 3462 | + | runtime.EXPECT().InspectContainer(mock.Anything, "c-gitea-123").Return(runningContainer, nil) | |||
| 3463 | + | ||||
| 3464 | + | // Publish event | |||
| 3465 | + | eventBus.EXPECT().Publish(domain.EventContainerDeployed, mock.AnythingOfType("*domain.ContainerEventPayload")).Return(nil) | |||
| 3466 | + | ||||
| 3467 | + | result, err := svc.Deploy(ctx, route) | |||
| 3468 | + | ||||
| 3469 | + | require.NoError(t, err) | |||
| 3470 | + | assert.Equal(t, "c-gitea-123", result.ID) | |||
| 3471 | + | assert.Equal(t, "running", result.Status) | |||
| 3472 | + | } | |||
M internal/usecase/proxy/service.go +19 -10
| @@ -211,13 +211,18 @@ func (s *Service) GetTarget(ctx context.Context, domainName string) (target *dom | |||
| 211 | 211 | ||
| 212 | 212 | if s.isRunningInContainer() { | |
| 213 | 213 | // Gordon is in a container - use container network | |
| 214 | - | containerIP, containerPort, err := s.runtime.GetContainerNetworkInfo(ctx, container.ID) | |
| 214 | + | // Determine target port: check for explicit label first, then fall back to network detection | |
| 215 | + | targetPort, err := s.getProxyPort(ctx, container.Image) | |
| 216 | + | if err != nil { | |
| 217 | + | return nil, log.WrapErr(err, "failed to determine proxy port") | |
| 218 | + | } | |
| 219 | + | containerIP, _, err := s.runtime.GetContainerNetworkInfo(ctx, container.ID) | |
| 215 | 220 | if err != nil { | |
| 216 | 221 | return nil, log.WrapErrWithFields(err, "failed to get container network info", map[string]any{zerowrap.FieldEntityID: container.ID}) | |
| 217 | 222 | } | |
| 218 | 223 | target = &domain.ProxyTarget{ | |
| 219 | 224 | Host: containerIP, | |
| 220 | - | Port: containerPort, | |
| 225 | + | Port: targetPort, | |
| 221 | 226 | ContainerID: container.ID, | |
| 222 | 227 | Scheme: "http", | |
| 223 | 228 | } | |
| @@ -688,23 +693,27 @@ func (s *Service) isRunningInContainer() bool { | |||
| 688 | 693 | } | |
| 689 | 694 | ||
| 690 | 695 | // getProxyPort determines the port to proxy to for an image. | |
| 691 | - | // It checks for the gordon.proxy.port label first, then falls back to the first exposed port. | |
| 696 | + | // It checks for gordon.proxy.port first, then the deprecated gordon.port alias, then falls back to the first exposed port. | |
| 692 | 697 | func (s *Service) getProxyPort(ctx context.Context, imageRef string) (int, error) { | |
| 693 | 698 | log := zerowrap.FromCtx(ctx) | |
| 694 | 699 | ||
| 695 | 700 | log.Debug().Str("image_ref", imageRef).Msg("determining proxy port for image") | |
| 696 | 701 | ||
| 697 | - | // Check for explicit port label | |
| 702 | + | // Check for explicit port label (gordon.proxy.port preferred, gordon.port as deprecated alias) | |
| 698 | 703 | labels, err := s.runtime.GetImageLabels(ctx, imageRef) | |
| 699 | 704 | if err != nil { | |
| 700 | 705 | log.Debug().Err(err).Msg("failed to get image labels, falling back to exposed ports") | |
| 701 | - | } else if portStr, ok := labels[domain.LabelProxyPort]; ok { | |
| 702 | - | port, err := strconv.Atoi(portStr) | |
| 703 | - | if err == nil && port > 0 { | |
| 704 | - | log.Debug().Int("port", port).Msg("using proxy port from image label") | |
| 705 | - | return port, nil | |
| 706 | + | } else { | |
| 707 | + | for _, key := range []string{domain.LabelProxyPort, domain.LabelPort} { | |
| 708 | + | if portStr, ok := labels[key]; ok && portStr != "" { | |
| 709 | + | port, err := strconv.Atoi(portStr) | |
| 710 | + | if err == nil && port > 0 && port <= 65535 { | |
| 711 | + | log.Debug().Int("port", port).Str("label", key).Msg("using proxy port from image label") | |
| 712 | + | return port, nil | |
| 713 | + | } | |
| 714 | + | log.Warn().Str("label", key).Str("port_value", portStr).Msg("invalid port label value") | |
| 715 | + | } | |
| 706 | 716 | } | |
| 707 | - | log.Warn().Str("port_value", portStr).Msg("invalid gordon.proxy.port label value") | |
| 708 | 717 | } | |
| 709 | 718 | ||
| 710 | 719 | // Fall back to first exposed port | |
| @@ -211,13 +211,18 @@ func (s *Service) GetTarget(ctx context.Context, domainName string) (target *dom | |||||
| 211 | 211 | ||||
| 212 | if s.isRunningInContainer() { | 212 | if s.isRunningInContainer() { | ||
| 213 | // Gordon is in a container - use container network | 213 | // Gordon is in a container - use container network | ||
| 214 | - | containerIP, containerPort, err := s.runtime.GetContainerNetworkInfo(ctx, container.ID) | 214 | + | // Determine target port: check for explicit label first, then fall back to network detection |
| 215 | + | targetPort, err := s.getProxyPort(ctx, container.Image) | |||
| 216 | + | if err != nil { | |||
| 217 | + | return nil, log.WrapErr(err, "failed to determine proxy port") | |||
| 218 | + | } | |||
| 219 | + | containerIP, _, err := s.runtime.GetContainerNetworkInfo(ctx, container.ID) | |||
| 215 | if err != nil { | 220 | if err != nil { | ||
| 216 | return nil, log.WrapErrWithFields(err, "failed to get container network info", map[string]any{zerowrap.FieldEntityID: container.ID}) | 221 | return nil, log.WrapErrWithFields(err, "failed to get container network info", map[string]any{zerowrap.FieldEntityID: container.ID}) | ||
| 217 | } | 222 | } | ||
| 218 | target = &domain.ProxyTarget{ | 223 | target = &domain.ProxyTarget{ | ||
| 219 | Host: containerIP, | 224 | Host: containerIP, | ||
| 220 | - | Port: containerPort, | 225 | + | Port: targetPort, |
| 221 | ContainerID: container.ID, | 226 | ContainerID: container.ID, | ||
| 222 | Scheme: "http", | 227 | Scheme: "http", | ||
| 223 | } | 228 | } | ||
| @@ -688,23 +693,27 @@ func (s *Service) isRunningInContainer() bool { | |||||
| 688 | } | 693 | } | ||
| 689 | 694 | ||||
| 690 | // getProxyPort determines the port to proxy to for an image. | 695 | // getProxyPort determines the port to proxy to for an image. | ||
| 691 | - | // It checks for the gordon.proxy.port label first, then falls back to the first exposed port. | 696 | + | // It checks for gordon.proxy.port first, then the deprecated gordon.port alias, then falls back to the first exposed port. |
| 692 | func (s *Service) getProxyPort(ctx context.Context, imageRef string) (int, error) { | 697 | func (s *Service) getProxyPort(ctx context.Context, imageRef string) (int, error) { | ||
| 693 | log := zerowrap.FromCtx(ctx) | 698 | log := zerowrap.FromCtx(ctx) | ||
| 694 | 699 | ||||
| 695 | log.Debug().Str("image_ref", imageRef).Msg("determining proxy port for image") | 700 | log.Debug().Str("image_ref", imageRef).Msg("determining proxy port for image") | ||
| 696 | 701 | ||||
| 697 | - | // Check for explicit port label | 702 | + | // Check for explicit port label (gordon.proxy.port preferred, gordon.port as deprecated alias) |
| 698 | labels, err := s.runtime.GetImageLabels(ctx, imageRef) | 703 | labels, err := s.runtime.GetImageLabels(ctx, imageRef) | ||
| 699 | if err != nil { | 704 | if err != nil { | ||
| 700 | log.Debug().Err(err).Msg("failed to get image labels, falling back to exposed ports") | 705 | log.Debug().Err(err).Msg("failed to get image labels, falling back to exposed ports") | ||
| 701 | - | } else if portStr, ok := labels[domain.LabelProxyPort]; ok { | 706 | + | } else { |
| 702 | - | port, err := strconv.Atoi(portStr) | 707 | + | for _, key := range []string{domain.LabelProxyPort, domain.LabelPort} { |
| 703 | - | if err == nil && port > 0 { | 708 | + | if portStr, ok := labels[key]; ok && portStr != "" { |
| 704 | - | log.Debug().Int("port", port).Msg("using proxy port from image label") | 709 | + | port, err := strconv.Atoi(portStr) |
| 705 | - | return port, nil | 710 | + | if err == nil && port > 0 && port <= 65535 { |
| 711 | + | log.Debug().Int("port", port).Str("label", key).Msg("using proxy port from image label") | |||
| 712 | + | return port, nil | |||
| 713 | + | } | |||
| 714 | + | log.Warn().Str("label", key).Str("port_value", portStr).Msg("invalid port label value") | |||
| 715 | + | } | |||
| 706 | } | 716 | } | ||
| 707 | - | log.Warn().Str("port_value", portStr).Msg("invalid gordon.proxy.port label value") | |||
| 708 | } | 717 | } | ||
| 709 | 718 | ||||
| 710 | // Fall back to first exposed port | 719 | // Fall back to first exposed port | ||
M internal/usecase/proxy/service_test.go +139 -0
| @@ -571,3 +571,142 @@ func TestDrainRegistryInFlightTimeout(t *testing.T) { | |||
| 571 | 571 | t.Fatal("expected DrainRegistryInFlight to return false on timeout, got true") | |
| 572 | 572 | } | |
| 573 | 573 | } | |
| 574 | + | ||
| 575 | + | func TestService_GetTarget_HostMode_UsesProxyPortLabel(t *testing.T) { | |
| 576 | + | runtime := outmocks.NewMockContainerRuntime(t) | |
| 577 | + | containerSvc := inmocks.NewMockContainerService(t) | |
| 578 | + | configSvc := inmocks.NewMockConfigService(t) | |
| 579 | + | ||
| 580 | + | svc := NewService(runtime, containerSvc, configSvc, Config{}) | |
| 581 | + | ctx := testContext() | |
| 582 | + | ||
| 583 | + | // No external routes | |
| 584 | + | configSvc.EXPECT().GetExternalRoutes().Return(map[string]string{}) | |
| 585 | + | ||
| 586 | + | // Container exists for this domain | |
| 587 | + | container := &domain.Container{ | |
| 588 | + | ID: "c-gitea", | |
| 589 | + | Image: "gitea/gitea:latest", | |
| 590 | + | } | |
| 591 | + | containerSvc.EXPECT().Get(mock.Anything, "git.example.com").Return(container, true) | |
| 592 | + | ||
| 593 | + | // Route exists | |
| 594 | + | configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{ | |
| 595 | + | {Domain: "git.example.com", Image: "gitea/gitea:latest"}, | |
| 596 | + | }) | |
| 597 | + | ||
| 598 | + | // Image has gordon.proxy.port=3000 label | |
| 599 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "gitea/gitea:latest").Return(map[string]string{ | |
| 600 | + | domain.LabelProxyPort: "3000", | |
| 601 | + | }, nil) | |
| 602 | + | ||
| 603 | + | // Host port mapping for internal port 3000 | |
| 604 | + | runtime.EXPECT().GetContainerPort(mock.Anything, "c-gitea", 3000).Return(32000, nil) | |
| 605 | + | ||
| 606 | + | result, err := svc.GetTarget(ctx, "git.example.com") | |
| 607 | + | ||
| 608 | + | assert.NoError(t, err) | |
| 609 | + | assert.Equal(t, "localhost", result.Host) | |
| 610 | + | assert.Equal(t, 32000, result.Port) | |
| 611 | + | assert.Equal(t, "c-gitea", result.ContainerID) | |
| 612 | + | } | |
| 613 | + | ||
| 614 | + | func TestService_GetTarget_HostMode_UsesDeprecatedPortLabel(t *testing.T) { | |
| 615 | + | runtime := outmocks.NewMockContainerRuntime(t) | |
| 616 | + | containerSvc := inmocks.NewMockContainerService(t) | |
| 617 | + | configSvc := inmocks.NewMockConfigService(t) | |
| 618 | + | ||
| 619 | + | svc := NewService(runtime, containerSvc, configSvc, Config{}) | |
| 620 | + | ctx := testContext() | |
| 621 | + | ||
| 622 | + | configSvc.EXPECT().GetExternalRoutes().Return(map[string]string{}) | |
| 623 | + | ||
| 624 | + | container := &domain.Container{ | |
| 625 | + | ID: "c-app", | |
| 626 | + | Image: "myapp:latest", | |
| 627 | + | } | |
| 628 | + | containerSvc.EXPECT().Get(mock.Anything, "app.example.com").Return(container, true) | |
| 629 | + | ||
| 630 | + | configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{ | |
| 631 | + | {Domain: "app.example.com", Image: "myapp:latest"}, | |
| 632 | + | }) | |
| 633 | + | ||
| 634 | + | // Image has deprecated gordon.port label — should still work | |
| 635 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:latest").Return(map[string]string{ | |
| 636 | + | domain.LabelPort: "8080", | |
| 637 | + | }, nil) | |
| 638 | + | ||
| 639 | + | runtime.EXPECT().GetContainerPort(mock.Anything, "c-app", 8080).Return(33000, nil) | |
| 640 | + | ||
| 641 | + | result, err := svc.GetTarget(ctx, "app.example.com") | |
| 642 | + | ||
| 643 | + | assert.NoError(t, err) | |
| 644 | + | assert.Equal(t, 33000, result.Port) | |
| 645 | + | } | |
| 646 | + | ||
| 647 | + | func TestService_GetTarget_HostMode_ProxyPortWinsOverPort(t *testing.T) { | |
| 648 | + | runtime := outmocks.NewMockContainerRuntime(t) | |
| 649 | + | containerSvc := inmocks.NewMockContainerService(t) | |
| 650 | + | configSvc := inmocks.NewMockConfigService(t) | |
| 651 | + | ||
| 652 | + | svc := NewService(runtime, containerSvc, configSvc, Config{}) | |
| 653 | + | ctx := testContext() | |
| 654 | + | ||
| 655 | + | configSvc.EXPECT().GetExternalRoutes().Return(map[string]string{}) | |
| 656 | + | ||
| 657 | + | container := &domain.Container{ | |
| 658 | + | ID: "c-dual", | |
| 659 | + | Image: "dualapp:latest", | |
| 660 | + | } | |
| 661 | + | containerSvc.EXPECT().Get(mock.Anything, "dual.example.com").Return(container, true) | |
| 662 | + | ||
| 663 | + | configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{ | |
| 664 | + | {Domain: "dual.example.com", Image: "dualapp:latest"}, | |
| 665 | + | }) | |
| 666 | + | ||
| 667 | + | // Both labels set — gordon.proxy.port=9000 should win over gordon.port=3000 | |
| 668 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "dualapp:latest").Return(map[string]string{ | |
| 669 | + | domain.LabelProxyPort: "9000", | |
| 670 | + | domain.LabelPort: "3000", | |
| 671 | + | }, nil) | |
| 672 | + | ||
| 673 | + | // Should use 9000 (gordon.proxy.port), NOT 3000 (gordon.port) | |
| 674 | + | runtime.EXPECT().GetContainerPort(mock.Anything, "c-dual", 9000).Return(34000, nil) | |
| 675 | + | ||
| 676 | + | result, err := svc.GetTarget(ctx, "dual.example.com") | |
| 677 | + | ||
| 678 | + | assert.NoError(t, err) | |
| 679 | + | assert.Equal(t, 34000, result.Port) | |
| 680 | + | } | |
| 681 | + | ||
| 682 | + | func TestService_GetTarget_HostMode_FallsBackToExposedPort(t *testing.T) { | |
| 683 | + | runtime := outmocks.NewMockContainerRuntime(t) | |
| 684 | + | containerSvc := inmocks.NewMockContainerService(t) | |
| 685 | + | configSvc := inmocks.NewMockConfigService(t) | |
| 686 | + | ||
| 687 | + | svc := NewService(runtime, containerSvc, configSvc, Config{}) | |
| 688 | + | ctx := testContext() | |
| 689 | + | ||
| 690 | + | configSvc.EXPECT().GetExternalRoutes().Return(map[string]string{}) | |
| 691 | + | ||
| 692 | + | container := &domain.Container{ | |
| 693 | + | ID: "c-plain", | |
| 694 | + | Image: "plain:latest", | |
| 695 | + | } | |
| 696 | + | containerSvc.EXPECT().Get(mock.Anything, "plain.example.com").Return(container, true) | |
| 697 | + | ||
| 698 | + | configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{ | |
| 699 | + | {Domain: "plain.example.com", Image: "plain:latest"}, | |
| 700 | + | }) | |
| 701 | + | ||
| 702 | + | // No port labels — should fall back to first exposed port | |
| 703 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "plain:latest").Return(map[string]string{}, nil) | |
| 704 | + | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "plain:latest").Return([]int{8080}, nil) | |
| 705 | + | ||
| 706 | + | runtime.EXPECT().GetContainerPort(mock.Anything, "c-plain", 8080).Return(35000, nil) | |
| 707 | + | ||
| 708 | + | result, err := svc.GetTarget(ctx, "plain.example.com") | |
| 709 | + | ||
| 710 | + | assert.NoError(t, err) | |
| 711 | + | assert.Equal(t, 35000, result.Port) | |
| 712 | + | } | |
| @@ -571,3 +571,142 @@ func TestDrainRegistryInFlightTimeout(t *testing.T) { | |||||
| 571 | t.Fatal("expected DrainRegistryInFlight to return false on timeout, got true") | 571 | t.Fatal("expected DrainRegistryInFlight to return false on timeout, got true") | ||
| 572 | } | 572 | } | ||
| 573 | } | 573 | } | ||
| 574 | + | ||||
| 575 | + | func TestService_GetTarget_HostMode_UsesProxyPortLabel(t *testing.T) { | |||
| 576 | + | runtime := outmocks.NewMockContainerRuntime(t) | |||
| 577 | + | containerSvc := inmocks.NewMockContainerService(t) | |||
| 578 | + | configSvc := inmocks.NewMockConfigService(t) | |||
| 579 | + | ||||
| 580 | + | svc := NewService(runtime, containerSvc, configSvc, Config{}) | |||
| 581 | + | ctx := testContext() | |||
| 582 | + | ||||
| 583 | + | // No external routes | |||
| 584 | + | configSvc.EXPECT().GetExternalRoutes().Return(map[string]string{}) | |||
| 585 | + | ||||
| 586 | + | // Container exists for this domain | |||
| 587 | + | container := &domain.Container{ | |||
| 588 | + | ID: "c-gitea", | |||
| 589 | + | Image: "gitea/gitea:latest", | |||
| 590 | + | } | |||
| 591 | + | containerSvc.EXPECT().Get(mock.Anything, "git.example.com").Return(container, true) | |||
| 592 | + | ||||
| 593 | + | // Route exists | |||
| 594 | + | configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{ | |||
| 595 | + | {Domain: "git.example.com", Image: "gitea/gitea:latest"}, | |||
| 596 | + | }) | |||
| 597 | + | ||||
| 598 | + | // Image has gordon.proxy.port=3000 label | |||
| 599 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "gitea/gitea:latest").Return(map[string]string{ | |||
| 600 | + | domain.LabelProxyPort: "3000", | |||
| 601 | + | }, nil) | |||
| 602 | + | ||||
| 603 | + | // Host port mapping for internal port 3000 | |||
| 604 | + | runtime.EXPECT().GetContainerPort(mock.Anything, "c-gitea", 3000).Return(32000, nil) | |||
| 605 | + | ||||
| 606 | + | result, err := svc.GetTarget(ctx, "git.example.com") | |||
| 607 | + | ||||
| 608 | + | assert.NoError(t, err) | |||
| 609 | + | assert.Equal(t, "localhost", result.Host) | |||
| 610 | + | assert.Equal(t, 32000, result.Port) | |||
| 611 | + | assert.Equal(t, "c-gitea", result.ContainerID) | |||
| 612 | + | } | |||
| 613 | + | ||||
| 614 | + | func TestService_GetTarget_HostMode_UsesDeprecatedPortLabel(t *testing.T) { | |||
| 615 | + | runtime := outmocks.NewMockContainerRuntime(t) | |||
| 616 | + | containerSvc := inmocks.NewMockContainerService(t) | |||
| 617 | + | configSvc := inmocks.NewMockConfigService(t) | |||
| 618 | + | ||||
| 619 | + | svc := NewService(runtime, containerSvc, configSvc, Config{}) | |||
| 620 | + | ctx := testContext() | |||
| 621 | + | ||||
| 622 | + | configSvc.EXPECT().GetExternalRoutes().Return(map[string]string{}) | |||
| 623 | + | ||||
| 624 | + | container := &domain.Container{ | |||
| 625 | + | ID: "c-app", | |||
| 626 | + | Image: "myapp:latest", | |||
| 627 | + | } | |||
| 628 | + | containerSvc.EXPECT().Get(mock.Anything, "app.example.com").Return(container, true) | |||
| 629 | + | ||||
| 630 | + | configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{ | |||
| 631 | + | {Domain: "app.example.com", Image: "myapp:latest"}, | |||
| 632 | + | }) | |||
| 633 | + | ||||
| 634 | + | // Image has deprecated gordon.port label — should still work | |||
| 635 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "myapp:latest").Return(map[string]string{ | |||
| 636 | + | domain.LabelPort: "8080", | |||
| 637 | + | }, nil) | |||
| 638 | + | ||||
| 639 | + | runtime.EXPECT().GetContainerPort(mock.Anything, "c-app", 8080).Return(33000, nil) | |||
| 640 | + | ||||
| 641 | + | result, err := svc.GetTarget(ctx, "app.example.com") | |||
| 642 | + | ||||
| 643 | + | assert.NoError(t, err) | |||
| 644 | + | assert.Equal(t, 33000, result.Port) | |||
| 645 | + | } | |||
| 646 | + | ||||
| 647 | + | func TestService_GetTarget_HostMode_ProxyPortWinsOverPort(t *testing.T) { | |||
| 648 | + | runtime := outmocks.NewMockContainerRuntime(t) | |||
| 649 | + | containerSvc := inmocks.NewMockContainerService(t) | |||
| 650 | + | configSvc := inmocks.NewMockConfigService(t) | |||
| 651 | + | ||||
| 652 | + | svc := NewService(runtime, containerSvc, configSvc, Config{}) | |||
| 653 | + | ctx := testContext() | |||
| 654 | + | ||||
| 655 | + | configSvc.EXPECT().GetExternalRoutes().Return(map[string]string{}) | |||
| 656 | + | ||||
| 657 | + | container := &domain.Container{ | |||
| 658 | + | ID: "c-dual", | |||
| 659 | + | Image: "dualapp:latest", | |||
| 660 | + | } | |||
| 661 | + | containerSvc.EXPECT().Get(mock.Anything, "dual.example.com").Return(container, true) | |||
| 662 | + | ||||
| 663 | + | configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{ | |||
| 664 | + | {Domain: "dual.example.com", Image: "dualapp:latest"}, | |||
| 665 | + | }) | |||
| 666 | + | ||||
| 667 | + | // Both labels set — gordon.proxy.port=9000 should win over gordon.port=3000 | |||
| 668 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "dualapp:latest").Return(map[string]string{ | |||
| 669 | + | domain.LabelProxyPort: "9000", | |||
| 670 | + | domain.LabelPort: "3000", | |||
| 671 | + | }, nil) | |||
| 672 | + | ||||
| 673 | + | // Should use 9000 (gordon.proxy.port), NOT 3000 (gordon.port) | |||
| 674 | + | runtime.EXPECT().GetContainerPort(mock.Anything, "c-dual", 9000).Return(34000, nil) | |||
| 675 | + | ||||
| 676 | + | result, err := svc.GetTarget(ctx, "dual.example.com") | |||
| 677 | + | ||||
| 678 | + | assert.NoError(t, err) | |||
| 679 | + | assert.Equal(t, 34000, result.Port) | |||
| 680 | + | } | |||
| 681 | + | ||||
| 682 | + | func TestService_GetTarget_HostMode_FallsBackToExposedPort(t *testing.T) { | |||
| 683 | + | runtime := outmocks.NewMockContainerRuntime(t) | |||
| 684 | + | containerSvc := inmocks.NewMockContainerService(t) | |||
| 685 | + | configSvc := inmocks.NewMockConfigService(t) | |||
| 686 | + | ||||
| 687 | + | svc := NewService(runtime, containerSvc, configSvc, Config{}) | |||
| 688 | + | ctx := testContext() | |||
| 689 | + | ||||
| 690 | + | configSvc.EXPECT().GetExternalRoutes().Return(map[string]string{}) | |||
| 691 | + | ||||
| 692 | + | container := &domain.Container{ | |||
| 693 | + | ID: "c-plain", | |||
| 694 | + | Image: "plain:latest", | |||
| 695 | + | } | |||
| 696 | + | containerSvc.EXPECT().Get(mock.Anything, "plain.example.com").Return(container, true) | |||
| 697 | + | ||||
| 698 | + | configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{ | |||
| 699 | + | {Domain: "plain.example.com", Image: "plain:latest"}, | |||
| 700 | + | }) | |||
| 701 | + | ||||
| 702 | + | // No port labels — should fall back to first exposed port | |||
| 703 | + | runtime.EXPECT().GetImageLabels(mock.Anything, "plain:latest").Return(map[string]string{}, nil) | |||
| 704 | + | runtime.EXPECT().GetImageExposedPorts(mock.Anything, "plain:latest").Return([]int{8080}, nil) | |||
| 705 | + | ||||
| 706 | + | runtime.EXPECT().GetContainerPort(mock.Anything, "c-plain", 8080).Return(35000, nil) | |||
| 707 | + | ||||
| 708 | + | result, err := svc.GetTarget(ctx, "plain.example.com") | |||
| 709 | + | ||||
| 710 | + | assert.NoError(t, err) | |||
| 711 | + | assert.Equal(t, 35000, result.Port) | |||
| 712 | + | } | |||