Skip to content

Commit 096d49a

Browse files
committed
add automatic compose file location fetching
1 parent 9f45f8a commit 096d49a

File tree

5 files changed

+194
-3
lines changed

5 files changed

+194
-3
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
1111
## [Unreleased]
1212

13+
### Added
14+
15+
- **Compose-native auto-compose discovery** — Added `dd.compose.native` / `wud.compose.native` container labels to enable deriving compose file paths from native Compose labels (`com.docker.compose.project.config_files` + `com.docker.compose.project.working_dir`) when `dd.compose.file` is not set.
16+
- **Watcher-wide compose-native default** — Added `DD_WATCHER_DOCKER_{name}_COMPOSENATIVE=true` to enable compose-native path discovery for all containers watched by a Docker watcher, with per-container `dd.compose.native` still taking precedence.
17+
1318
## [1.3.2] — 2026-02-16
1419

1520
### Added

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,12 +402,15 @@ When using the Docker Compose trigger, container labels can override trigger set
402402
| `dd.compose.prune` | `wud.compose.prune` | `DD_TRIGGER_DOCKERCOMPOSE_xxx_PRUNE` | `true` / `false` |
403403
| `dd.compose.dryrun` | `wud.compose.dryrun` | `DD_TRIGGER_DOCKERCOMPOSE_xxx_DRYRUN` | `true` / `false` |
404404
| `dd.compose.auto` | `wud.compose.auto` | `DD_TRIGGER_DOCKERCOMPOSE_xxx_AUTO` | `true` / `false` |
405+
| `dd.compose.native` | `wud.compose.native` | `DD_WATCHER_DOCKER_xxx_COMPOSENATIVE` | `true` / `false` |
405406
| `dd.compose.threshold` | `wud.compose.threshold` | `DD_TRIGGER_DOCKERCOMPOSE_xxx_THRESHOLD` | `all` / `major` / `minor` / `patch` |
406407

407408
Behavior notes:
408409

409410
- `dd.compose.file` / `wud.compose.file` causes drydock to create (or reuse) a scoped `dockercompose` trigger for that container.
410411
- That generated compose trigger is set with `requireinclude=true` and auto-appended to the container include list, so it only runs for explicitly associated containers.
412+
- `dd.compose.native` / `wud.compose.native` enables deriving compose file paths from native Compose labels (`com.docker.compose.project.config_files` and `com.docker.compose.project.working_dir`).
413+
- `DD_WATCHER_DOCKER_xxx_COMPOSENATIVE=true` enables compose-native lookup by default for all containers in that watcher (container label can still override).
411414
- If `dd.compose.auto` is omitted, normal trigger default applies (`auto=true`).
412415

413416
`dd.*` labels take precedence when both `dd.*` and `wud.*` are present.

app/watchers/providers/docker/Docker.test.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Docker, {
1010
testable_filterBySegmentCount,
1111
testable_getContainerDisplayName,
1212
testable_getContainerName,
13+
testable_getComposeFilePathFromLabels,
1314
testable_getCurrentPrefix,
1415
testable_getFirstDigitIndex,
1516
testable_getImageForRegistryLookup,
@@ -2864,6 +2865,76 @@ describe('Docker Watcher', () => {
28642865
expect(result.triggerInclude).toBe('dockercompose.tmp-test-container-wud');
28652866
});
28662867

2868+
test('should auto-include dockercompose trigger from compose-native labels when dd.compose.native is true', async () => {
2869+
const container = await setupContainerDetailTest(docker, {
2870+
container: {
2871+
Image: 'nginx:1.0.0',
2872+
Names: ['/test-container-native'],
2873+
Labels: {
2874+
'dd.compose.native': 'true',
2875+
'com.docker.compose.project.working_dir': '/opt/my-stack',
2876+
'com.docker.compose.project.config_files': 'docker-compose.yml',
2877+
},
2878+
},
2879+
});
2880+
2881+
const result = await docker.addImageDetailsToContainer(container);
2882+
2883+
expect(registry.ensureDockercomposeTriggerForContainer).toHaveBeenCalledWith(
2884+
'test-container-native',
2885+
'/opt/my-stack/docker-compose.yml',
2886+
{},
2887+
);
2888+
expect(result.triggerInclude).toBe('dockercompose.my-stack-test-container-native');
2889+
});
2890+
2891+
test('should auto-include dockercompose trigger from compose-native labels when watcher composenative is enabled', async () => {
2892+
const container = await setupContainerDetailTest(docker, {
2893+
registerConfig: {
2894+
composenative: true,
2895+
},
2896+
container: {
2897+
Image: 'nginx:1.0.0',
2898+
Names: ['/test-container-native-global'],
2899+
Labels: {
2900+
'com.docker.compose.project.working_dir': '/opt/global-stack',
2901+
'com.docker.compose.project.config_files': 'compose.yml',
2902+
},
2903+
},
2904+
});
2905+
2906+
const result = await docker.addImageDetailsToContainer(container);
2907+
2908+
expect(registry.ensureDockercomposeTriggerForContainer).toHaveBeenCalledWith(
2909+
'test-container-native-global',
2910+
'/opt/global-stack/compose.yml',
2911+
{},
2912+
);
2913+
expect(result.triggerInclude).toBe('dockercompose.global-stack-test-container-native-global');
2914+
});
2915+
2916+
test('should not auto-include dockercompose trigger from compose-native labels when dd.compose.native is false', async () => {
2917+
const container = await setupContainerDetailTest(docker, {
2918+
registerConfig: {
2919+
composenative: true,
2920+
},
2921+
container: {
2922+
Image: 'nginx:1.0.0',
2923+
Names: ['/test-container-native-disabled'],
2924+
Labels: {
2925+
'dd.compose.native': 'false',
2926+
'com.docker.compose.project.working_dir': '/opt/disabled-stack',
2927+
'com.docker.compose.project.config_files': 'compose.yml',
2928+
},
2929+
},
2930+
});
2931+
2932+
const result = await docker.addImageDetailsToContainer(container);
2933+
2934+
expect(registry.ensureDockercomposeTriggerForContainer).not.toHaveBeenCalled();
2935+
expect(result.triggerInclude).toBeUndefined();
2936+
});
2937+
28672938
test('should pass compose trigger options from labels', async () => {
28682939
const container = await setupContainerDetailTest(docker, {
28692940
container: {
@@ -4688,6 +4759,42 @@ describe('Docker Watcher', () => {
46884759
expect(testable_getLabel({}, 'dd.display.name')).toBeUndefined();
46894760
});
46904761

4762+
test('getComposeFilePathFromLabels should prefer dd.compose.file over compose-native labels', () => {
4763+
const composeFile = testable_getComposeFilePathFromLabels(
4764+
{
4765+
'dd.compose.file': '/opt/explicit/docker-compose.yml',
4766+
'dd.compose.native': 'true',
4767+
'com.docker.compose.project.working_dir': '/opt/native',
4768+
'com.docker.compose.project.config_files': 'compose.yml',
4769+
},
4770+
false,
4771+
);
4772+
expect(composeFile).toBe('/opt/explicit/docker-compose.yml');
4773+
});
4774+
4775+
test('getComposeFilePathFromLabels should resolve compose-native labels when enabled globally', () => {
4776+
const composeFile = testable_getComposeFilePathFromLabels(
4777+
{
4778+
'com.docker.compose.project.working_dir': '/opt/native',
4779+
'com.docker.compose.project.config_files': 'compose.yml',
4780+
},
4781+
true,
4782+
);
4783+
expect(composeFile).toBe('/opt/native/compose.yml');
4784+
});
4785+
4786+
test('getComposeFilePathFromLabels should return undefined when compose-native labels are disabled', () => {
4787+
const composeFile = testable_getComposeFilePathFromLabels(
4788+
{
4789+
'dd.compose.native': 'false',
4790+
'com.docker.compose.project.working_dir': '/opt/native',
4791+
'com.docker.compose.project.config_files': 'compose.yml',
4792+
},
4793+
true,
4794+
);
4795+
expect(composeFile).toBeUndefined();
4796+
});
4797+
46914798
test('appendTriggerId should return triggerInclude when triggerId is undefined', () => {
46924799
expect(testable_appendTriggerId('ntfy.default:major', undefined)).toBe('ntfy.default:major');
46934800
});

app/watchers/providers/docker/Docker.ts

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import fs from 'node:fs';
2+
import path from 'node:path';
23
import axios from 'axios';
34
import Dockerode from 'dockerode';
45
import Joi from 'joi';
@@ -38,6 +39,7 @@ import {
3839
ddComposeBackup,
3940
ddComposeDryrun,
4041
ddComposeFile,
42+
ddComposeNative,
4143
ddComposePrune,
4244
ddComposeThreshold,
4345
ddDisplayIcon,
@@ -59,6 +61,7 @@ import {
5961
wudDisplayIcon,
6062
wudDisplayName,
6163
wudComposeFile,
64+
wudComposeNative,
6265
wudComposePrune,
6366
wudComposeThreshold,
6467
wudInspectTagPath,
@@ -97,6 +100,7 @@ export interface DockerWatcherConfiguration extends ComponentConfiguration {
97100
watchdigest?: any;
98101
watchevents: boolean;
99102
watchatstart: boolean;
103+
composenative: boolean;
100104
maintenancewindow?: string;
101105
maintenancewindowtz: string;
102106
imgset?: Record<string, any>;
@@ -135,6 +139,8 @@ const START_WATCHER_DELAY_MS = 1000;
135139
const DEBOUNCED_WATCH_CRON_MS = 5000;
136140
const MAINTENANCE_WINDOW_QUEUE_POLL_MS = 60 * 1000;
137141
const SWARM_SERVICE_ID_LABEL = 'com.docker.swarm.service.id';
142+
const COMPOSE_PROJECT_CONFIG_FILES_LABEL = 'com.docker.compose.project.config_files';
143+
const COMPOSE_PROJECT_WORKING_DIR_LABEL = 'com.docker.compose.project.working_dir';
138144
const OIDC_ACCESS_TOKEN_REFRESH_WINDOW_MS = 30 * 1000;
139145
const OIDC_DEFAULT_ACCESS_TOKEN_TTL_MS = 5 * 60 * 1000;
140146
const OIDC_DEFAULT_TIMEOUT_MS = 5000;
@@ -224,6 +230,58 @@ function getDockercomposeTriggerConfigurationFromLabels(labels: Record<string, s
224230
return dockercomposeConfig;
225231
}
226232

233+
function isAutoComposeEnabled(
234+
labels: Record<string, string>,
235+
composeNativeEnabledByWatcher: boolean,
236+
): boolean {
237+
const autoComposeLabelValue = getLabel(labels, ddComposeNative, wudComposeNative);
238+
if (autoComposeLabelValue !== undefined && autoComposeLabelValue.trim() !== '') {
239+
return autoComposeLabelValue.toLowerCase() === 'true';
240+
}
241+
return composeNativeEnabledByWatcher;
242+
}
243+
244+
function getComposeNativeFilePathFromLabels(labels: Record<string, string>) {
245+
const composeConfigFiles = labels[COMPOSE_PROJECT_CONFIG_FILES_LABEL];
246+
if (!composeConfigFiles || composeConfigFiles.trim() === '') {
247+
return undefined;
248+
}
249+
250+
const composeProjectWorkingDir = labels[COMPOSE_PROJECT_WORKING_DIR_LABEL];
251+
const configFiles = composeConfigFiles
252+
.split(',')
253+
.map((configFile) => configFile.trim())
254+
.filter((configFile) => configFile !== '');
255+
256+
for (const configFile of configFiles) {
257+
if (path.isAbsolute(configFile)) {
258+
return configFile;
259+
}
260+
if (composeProjectWorkingDir && composeProjectWorkingDir.trim() !== '') {
261+
return path.join(composeProjectWorkingDir, configFile);
262+
}
263+
return configFile;
264+
}
265+
266+
return undefined;
267+
}
268+
269+
function getComposeFilePathFromLabels(
270+
labels: Record<string, string>,
271+
composeNativeEnabledByWatcher: boolean,
272+
) {
273+
const composeFilePathFromLabel = getLabel(labels, ddComposeFile, wudComposeFile);
274+
if (composeFilePathFromLabel) {
275+
return composeFilePathFromLabel;
276+
}
277+
278+
if (!isAutoComposeEnabled(labels, composeNativeEnabledByWatcher)) {
279+
return undefined;
280+
}
281+
282+
return getComposeNativeFilePathFromLabels(labels);
283+
}
284+
227285
interface ResolvedImgset {
228286
name: string;
229287
includeTags?: string;
@@ -1066,6 +1124,7 @@ class Docker extends Watcher {
10661124
watchdigest: this.joi.any(),
10671125
watchevents: this.joi.boolean().default(true),
10681126
watchatstart: this.joi.boolean().default(true),
1127+
composenative: this.joi.boolean().default(false),
10691128
maintenancewindow: joi.string().cron().optional(),
10701129
maintenancewindowtz: this.joi.string().default('UTC'),
10711130
imgset: this.joi
@@ -1266,7 +1325,10 @@ class Docker extends Watcher {
12661325

12671326
for (const containerInStore of containersInStore) {
12681327
const containerLabels = containerInStore.labels || {};
1269-
const composeFilePath = getLabel(containerLabels, ddComposeFile, wudComposeFile);
1328+
const composeFilePath = getComposeFilePathFromLabels(
1329+
containerLabels,
1330+
this.configuration.composenative,
1331+
);
12701332
if (!composeFilePath) {
12711333
continue;
12721334
}
@@ -2045,7 +2107,10 @@ class Docker extends Watcher {
20452107
}
20462108

20472109
const containerId = containerFound.id;
2048-
const composeFilePath = getLabel(labelsToApply, ddComposeFile, wudComposeFile);
2110+
const composeFilePath = getComposeFilePathFromLabels(
2111+
labelsToApply,
2112+
this.configuration.composenative,
2113+
);
20492114
if (composeFilePath) {
20502115
let dockercomposeTriggerId = this.composeTriggersByContainer[containerId];
20512116
if (!dockercomposeTriggerId) {
@@ -2514,7 +2579,10 @@ class Docker extends Watcher {
25142579
async addImageDetailsToContainer(container: any, labelOverrides: ContainerLabelOverrides = {}) {
25152580
const containerId = container.Id;
25162581
const containerLabels = container.Labels || {};
2517-
const composeFilePath = containerLabels[ddComposeFile] || containerLabels[wudComposeFile];
2582+
const composeFilePath = getComposeFilePathFromLabels(
2583+
containerLabels,
2584+
this.configuration.composenative,
2585+
);
25182586
const needsComposeTriggerCreation =
25192587
!!composeFilePath && !this.composeTriggersByContainer[containerId];
25202588

@@ -2736,4 +2804,5 @@ export {
27362804
getImageReferenceCandidatesFromPattern as testable_getImageReferenceCandidatesFromPattern,
27372805
getImgsetSpecificity as testable_getImgsetSpecificity,
27382806
getInspectValueByPath as testable_getInspectValueByPath,
2807+
getComposeFilePathFromLabels as testable_getComposeFilePathFromLabels,
27392808
};

app/watchers/providers/docker/label.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,13 @@ export const wudComposeDryrun = 'wud.compose.dryrun';
105105
export const ddComposeAuto = 'dd.compose.auto';
106106
export const wudComposeAuto = 'wud.compose.auto';
107107

108+
/**
109+
* Optional auto-compose discovery mode (true | false).
110+
* When enabled, use compose-native labels to derive compose file path.
111+
*/
112+
export const ddComposeNative = 'dd.compose.native';
113+
export const wudComposeNative = 'wud.compose.native';
114+
108115
/**
109116
* Optional dockercompose trigger threshold setting.
110117
*/

0 commit comments

Comments
 (0)