Skip to content

Commit b723369

Browse files
committed
🐛 fix(notifications): use detected daemon host name for controller server identity (#296)
On Docker Compose / Synology setups the process hostname inside the drydock container is the container ID, so the notification prefix rendered as [189093dae256] instead of the actual host. Read the daemon host name via dockerApi.info().Name during local watcher startup and cache it as the detected server name, falling back to os.hostname() when the daemon info call is unavailable. - configuration/index.ts: getServerName precedence is now DD_SERVER_NAME > detectedServerName > os.hostname(). Add setDetectedServerName setter for the watcher to populate. - watchers/providers/docker/docker-remote-auth.ts: call dockerApi.info() only for local (non-remote) watchers so remote agents don't hijack the controller identity; the call is best-effort and silently falls back on failure. - Regression tests in configuration/index.test.ts and watchers/providers/docker/docker-remote-auth.test.ts - README.md + agents/triggers docs reflect the new precedence order Fixes: #296
1 parent d027441 commit b723369

File tree

7 files changed

+74
-4
lines changed

7 files changed

+74
-4
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ See the [Quick Start guide](https://getdrydock.com/docs/quickstart) for Docker C
152152
- **Identity-keyed container tracking** — Containers tracked by stable identity key (agent::watcher::name) across renames/replacements, preventing cross-host status contamination.
153153
- **Watcher next-run schedule visibility** — Watcher API and Agents view now show when each watcher will next poll for updates.
154154
- **Notification delivery failure audit trail** — Failed notification deliveries surface in the notification bell dropdown for visibility without leaving the UI.
155-
- **Multi-server notification identification** — Notifications automatically include `[server-name]` prefix when agents are registered, identifying which server each update comes from. Configurable via `DD_SERVER_NAME` (defaults to hostname). Custom templates can use `container.notificationServerName`.
155+
- **Multi-server notification identification** — Notifications automatically include `[server-name]` prefix when agents are registered, identifying which server each update comes from. Configurable via `DD_SERVER_NAME` (defaults to the detected daemon host name, then the process hostname). Custom templates can use `container.notificationServerName`.
156156
- **System log viewer overhaul** — Pinned toolbar, line wrapping, sort toggle (newest/oldest), filter mode (funnel icon shows matches only), auto-apply filters, component dropdown from API, aligned columns, floating copy button.
157157
- **Hide Pinned containers** — Checkbox toggle in the container filter bar hides version-pinned containers. Persisted in user preferences.
158158
- **Combined batch+digest notifications**`MODE=batch+digest` sends both immediate batch emails and scheduled digest summaries.

app/configuration/index.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ function getTestDirectory() {
1717

1818
const TEST_DIRECTORY = getTestDirectory();
1919

20+
afterEach(() => {
21+
configuration.setDetectedServerName(undefined);
22+
});
23+
2024
test('getVersion should return dd version', async () => {
2125
configuration.ddEnvVars.DD_VERSION = 'x.y.z';
2226
expect(configuration.getVersion()).toStrictEqual('x.y.z');
@@ -438,6 +442,13 @@ test('getServerName should fall back to hostname when DD_SERVER_NAME is empty',
438442
delete configuration.ddEnvVars.DD_SERVER_NAME;
439443
});
440444

445+
test('getServerName should prefer detected server name when DD_SERVER_NAME is not set', () => {
446+
delete configuration.ddEnvVars.DD_SERVER_NAME;
447+
configuration.setDetectedServerName('datavault');
448+
449+
expect(configuration.getServerName()).toBe('datavault');
450+
});
451+
441452
test('getServerConfiguration should allow enabling identity-aware rate-limit keys', async () => {
442453
configuration.ddEnvVars.DD_SERVER_RATELIMIT_IDENTITYKEYING = 'true';
443454
const config = configuration.getServerConfiguration();

app/configuration/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ const warnedLegacyTriggerEnvVars = new Set<string>();
6969
const triggerLegacyPrefixUsage = new Set<string>();
7070
let packageVersionCache: string | undefined;
7171
let packageVersionResolved = false;
72+
let detectedServerName: string | undefined;
7273

7374
// First, collect legacy WUD_ vars and remap to DD_ keys
7475
Object.keys(process.env)
@@ -134,16 +135,24 @@ export function getVersion() {
134135

135136
/**
136137
* Get the server name used to identify this Drydock instance in notifications.
137-
* Configured via DD_SERVER_NAME, falls back to os.hostname().
138+
* Configured via DD_SERVER_NAME, then a detected daemon host name, then os.hostname().
138139
*/
139140
export function getServerName(): string {
140141
const configured = ddEnvVars.DD_SERVER_NAME?.trim();
141142
if (configured) {
142143
return configured;
143144
}
145+
if (detectedServerName) {
146+
return detectedServerName;
147+
}
144148
return hostname();
145149
}
146150

151+
export function setDetectedServerName(name: string | undefined): void {
152+
const trimmed = typeof name === 'string' ? name.trim() : '';
153+
detectedServerName = trimmed || undefined;
154+
}
155+
147156
export function getLogLevel() {
148157
return ddEnvVars.DD_LOG_LEVEL || 'info';
149158
}

app/watchers/providers/docker/docker-remote-auth.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const {
55
mockReadFileSync,
66
mockResolveConfiguredPath,
77
mockGetErrorMessage,
8+
mockSetDetectedServerName,
89
mockInitializeRemoteOidcStateFromConfiguration,
910
mockIsRemoteOidcTokenRefreshRequired,
1011
mockRefreshRemoteOidcAccessToken,
@@ -15,6 +16,7 @@ const {
1516
mockReadFileSync: vi.fn(),
1617
mockResolveConfiguredPath: vi.fn((value: string) => `/resolved/${value}`),
1718
mockGetErrorMessage: vi.fn((_: unknown, fallback: string) => fallback),
19+
mockSetDetectedServerName: vi.fn(),
1820
mockInitializeRemoteOidcStateFromConfiguration: vi.fn(),
1921
mockIsRemoteOidcTokenRefreshRequired: vi.fn(() => false),
2022
mockRefreshRemoteOidcAccessToken: vi.fn(),
@@ -36,6 +38,10 @@ vi.mock('../../../runtime/paths.js', () => ({
3638
resolveConfiguredPath: mockResolveConfiguredPath,
3739
}));
3840

41+
vi.mock('../../../configuration/index.js', () => ({
42+
setDetectedServerName: mockSetDetectedServerName,
43+
}));
44+
3945
vi.mock('./docker-helpers.js', () => ({
4046
getErrorMessage: mockGetErrorMessage,
4147
}));
@@ -136,6 +142,28 @@ describe('docker remote auth module', () => {
136142
expect(watcher.dockerApi).toBe(dockerApi);
137143
});
138144

145+
test('initWatcherWithRemoteAuth captures the local daemon host name for notifications', async () => {
146+
const dockerApi = {
147+
modem: { headers: {} },
148+
info: vi.fn().mockResolvedValue({ Name: 'datavault' }),
149+
};
150+
mockDockerodeCtor.mockImplementation(function DockerodeMock() {
151+
return dockerApi;
152+
});
153+
154+
const watcher = createWatcher({
155+
configuration: {
156+
socket: '/var/run/docker.sock',
157+
port: 0,
158+
},
159+
});
160+
161+
await initWatcherWithRemoteAuth(watcher as any);
162+
163+
expect(dockerApi.info).toHaveBeenCalledTimes(1);
164+
expect(mockSetDetectedServerName).toHaveBeenCalledWith('datavault');
165+
});
166+
139167
test('initWatcherWithRemoteAuth pins API version when probe succeeds', async () => {
140168
const dockerApi = { modem: { headers: {} } };
141169
mockDockerodeCtor.mockImplementation(function DockerodeMock() {

app/watchers/providers/docker/docker-remote-auth.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import fs from 'node:fs';
22
import Dockerode from 'dockerode';
3+
import { setDetectedServerName } from '../../../configuration/index.js';
34
import { resolveConfiguredPath } from '../../../runtime/paths.js';
45
import { disableSocketRedirects } from './disable-socket-redirects.js';
56
import { getErrorMessage } from './docker-helpers.js';
@@ -47,6 +48,26 @@ interface DockerRemoteAuthWatcher {
4748
setRemoteAuthorizationHeader: (authorizationValue: string) => void;
4849
}
4950

51+
async function detectLocalDaemonServerName(watcher: DockerRemoteAuthWatcher): Promise<void> {
52+
if (watcher.configuration.host || typeof watcher.dockerApi?.info !== 'function') {
53+
return;
54+
}
55+
56+
try {
57+
const info = await watcher.dockerApi.info();
58+
if (!info || typeof info !== 'object') {
59+
return;
60+
}
61+
62+
const daemonName = (info as { Name?: unknown }).Name;
63+
if (typeof daemonName === 'string') {
64+
setDetectedServerName(daemonName);
65+
}
66+
} catch {
67+
// Server-name detection is best-effort. Fall back to os.hostname() when unavailable.
68+
}
69+
}
70+
5071
export async function initWatcherWithRemoteAuth(watcher: DockerRemoteAuthWatcher): Promise<void> {
5172
const options: Dockerode.DockerOptions = {};
5273
watcher.remoteAuthBlockedReason = undefined;
@@ -104,6 +125,7 @@ export async function initWatcherWithRemoteAuth(watcher: DockerRemoteAuthWatcher
104125
watcher.dockerApi = new Dockerode(options);
105126
if (!watcher.configuration.host) {
106127
disableSocketRedirects(watcher.dockerApi);
128+
await detectLocalDaemonServerName(watcher);
107129
}
108130
}
109131

content/docs/current/configuration/agents/index.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,4 +221,4 @@ services:
221221
- **Registries**: Configured on the Agent to check for updates.
222222
- **Triggers**:
223223
- `docker` and `dockercompose` triggers are configured and executed **on the Agent** (allowing update of remote containers). The controller automatically proxies update requests to the correct agent.
224-
- Notification triggers (e.g. `smtp`, `discord`) are configured and executed **on the Controller**. Notifications automatically include a `[server-name]` prefix identifying which server each update comes from. The controller name defaults to the hostname and can be overridden with `DD_SERVER_NAME`.
224+
- Notification triggers (e.g. `smtp`, `discord`) are configured and executed **on the Controller**. Notifications automatically include a `[server-name]` prefix identifying which server each update comes from. The controller name defaults to the detected Docker or Podman daemon host name when available, then the process hostname, and can be overridden with `DD_SERVER_NAME`.

content/docs/current/configuration/triggers/index.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ Template strings use `${expression}` placeholders. Drydock provides two sets of
157157
| `container.watcher` | Watcher name | `local` |
158158
| `container.agent` | Agent name (when using distributed agents) | `remote-1` |
159159
| `container.notificationAgentPrefix` | Server identification prefix: `[agentName]` + space for agent containers, `[controllerName]` + space for controller containers when agents exist, empty for single-server setups | `[prod-server]` |
160-
| `container.notificationServerName` | Server identity — agent name for agent containers, controller name (from `DD_SERVER_NAME` or hostname) for controller containers. Always resolved regardless of setup | `prod-server` |
160+
| `container.notificationServerName` | Server identity — agent name for agent containers, controller name (from `DD_SERVER_NAME`, otherwise the detected daemon host name when available, then the process hostname) for controller containers. Always resolved regardless of setup | `prod-server` |
161161
| `container.image.name` | Image name | `library/nginx` |
162162
| `container.image.registry.name` | Registry provider name | `hub` |
163163
| `container.image.registry.url` | Registry URL | `https://registry-1.docker.io` |

0 commit comments

Comments
 (0)