Skip to content

Commit c6e6238

Browse files
committed
add automatic compose file location fetching
1 parent 4f68321 commit c6e6238

File tree

5 files changed

+192
-3
lines changed

5 files changed

+192
-3
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
- **Compose label-driven docker-compose trigger configuration** — Added support for container labels to create and scope compose triggers from discovered containers, including `dd.compose.file` / `wud.compose.file` and compose trigger options (`backup`, `prune`, `dryrun`, `auto`, `threshold`).
1616
- **Compose-file digest update support** — Docker-compose trigger now supports digest-pinned image references in compose files (`image@sha256:...` and `image:tag@sha256:...`) so digest-based services can be updated without dropping pinning.
1717

18+
- **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.
19+
- **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.
20+
1821
### Fixed
1922

2023
- **TrueForge registry default behavior** — Fixed TrueForge registry integration so it works out of the box with default configuration.

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,
@@ -2863,6 +2864,76 @@ describe('Docker Watcher', () => {
28632864
expect(result.triggerInclude).toBe('dockercompose.tmp-test-container-wud');
28642865
});
28652866

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

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

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)