Skip to content

Commit 1748064

Browse files
feat: add automatic compose file location fetching (#9)
* more fixes * add automatic compose file location fetching * improve tests * make docs more clear * Update app/watchers/providers/docker/Docker.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update app/watchers/providers/docker/Docker.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix tests --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 7a3817f commit 1748064

5 files changed

Lines changed: 274 additions & 5 deletions

File tree

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. This requires the resolved compose path to exist inside the drydock container (same path context used by `docker compose`).
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: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,14 +402,37 @@ 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+
- Compose-native/automatic detection requires the resolved compose file path to be valid inside the drydock container (same path that `docker compose` uses); if Compose was run from a host-only path, bind-mount that path into drydock at the same location or set `dd.compose.file` explicitly.
414+
- `DD_WATCHER_DOCKER_xxx_COMPOSENATIVE=true` enables compose-native lookup by default for all containers in that watcher (container label can still override).
411415
- If `dd.compose.auto` is omitted, normal trigger default applies (`auto=true`).
412416

417+
Troubleshooting path mismatch:
418+
419+
- Symptom: compose-native is enabled, but DryDock cannot resolve/update the compose file.
420+
- Cause: the path from Compose labels exists on the host, but not inside the DryDock container at the same absolute path.
421+
- Fix: bind-mount the host compose project path into DryDock using the same container path, or set `dd.compose.file` per container.
422+
423+
Example (host path and container path are identical):
424+
425+
```yaml
426+
services:
427+
drydock:
428+
image: codeswhat/drydock:latest
429+
volumes:
430+
- /var/run/docker.sock:/var/run/docker.sock
431+
- /opt/stacks:/opt/stacks
432+
```
433+
434+
If your stack was started from `/opt/stacks/myapp/docker-compose.yml`, DryDock must also see that file at `/opt/stacks/myapp/docker-compose.yml`.
435+
413436
`dd.*` labels take precedence when both `dd.*` and `wud.*` are present.
414437

415438
</details>

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

Lines changed: 161 additions & 2 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,
@@ -1514,7 +1515,6 @@ describe('Docker Watcher', () => {
15141515
expect(docker.composeTriggersByContainer.container123).toBeUndefined();
15151516
expect(storeContainer.updateContainer).not.toHaveBeenCalled();
15161517
});
1517-
15181518
test('should skip store update when inspect payload does not change tracked fields', async () => {
15191519
await docker.register('watcher', 'docker', 'test', {});
15201520
docker.log = createMockLogWithChild(['info']);
@@ -2864,6 +2864,76 @@ describe('Docker Watcher', () => {
28642864
expect(result.triggerInclude).toBe('dockercompose.tmp-test-container-wud');
28652865
});
28662866

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+
28672937
test('should pass compose trigger options from labels', async () => {
28682938
const container = await setupContainerDetailTest(docker, {
28692939
container: {
@@ -4688,6 +4758,96 @@ describe('Docker Watcher', () => {
46884758
expect(testable_getLabel({}, 'dd.display.name')).toBeUndefined();
46894759
});
46904760

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+
4797+
test('getComposeFilePathFromLabels should fallback to watcher setting when compose-native label is blank', () => {
4798+
const composeFile = testable_getComposeFilePathFromLabels(
4799+
{
4800+
'dd.compose.native': ' ',
4801+
'com.docker.compose.project.working_dir': '/opt/native',
4802+
'com.docker.compose.project.config_files': 'compose.yml',
4803+
},
4804+
true,
4805+
);
4806+
expect(composeFile).toBe('/opt/native/compose.yml');
4807+
});
4808+
4809+
test('getComposeFilePathFromLabels should return undefined when compose-native config files label is missing', () => {
4810+
const composeFile = testable_getComposeFilePathFromLabels(
4811+
{
4812+
'com.docker.compose.project.working_dir': '/opt/native',
4813+
},
4814+
true,
4815+
);
4816+
expect(composeFile).toBeUndefined();
4817+
});
4818+
4819+
test('getComposeFilePathFromLabels should keep absolute compose-native config file path', () => {
4820+
const composeFile = testable_getComposeFilePathFromLabels(
4821+
{
4822+
'com.docker.compose.project.working_dir': '/opt/native',
4823+
'com.docker.compose.project.config_files': '/etc/compose/docker-compose.yml',
4824+
},
4825+
true,
4826+
);
4827+
expect(composeFile).toBe('/etc/compose/docker-compose.yml');
4828+
});
4829+
4830+
test('getComposeFilePathFromLabels should return relative compose-native config file when working dir is absent', () => {
4831+
const composeFile = testable_getComposeFilePathFromLabels(
4832+
{
4833+
'com.docker.compose.project.config_files': 'compose.yml',
4834+
},
4835+
true,
4836+
);
4837+
expect(composeFile).toBe('compose.yml');
4838+
});
4839+
4840+
test('getComposeFilePathFromLabels should return undefined when compose-native config files are only empty entries', () => {
4841+
const composeFile = testable_getComposeFilePathFromLabels(
4842+
{
4843+
'com.docker.compose.project.config_files': ' , , ',
4844+
'dd.compose.native': 'true',
4845+
},
4846+
false,
4847+
);
4848+
expect(composeFile).toBeUndefined();
4849+
});
4850+
46914851
test('appendTriggerId should return triggerInclude when triggerId is undefined', () => {
46924852
expect(testable_appendTriggerId('ntfy.default:major', undefined)).toBe('ntfy.default:major');
46934853
});
@@ -4722,7 +4882,6 @@ describe('Docker Watcher', () => {
47224882
expect(testable_removeTriggerId('dockercompose.test', 'dockercompose.test')).toBeUndefined();
47234883
});
47244884

4725-
47264885
test('getCurrentPrefix should return the non-numeric prefix before the first digit', () => {
47274886
expect(testable_getCurrentPrefix('v2026.2.1')).toBe('v');
47284887
});

0 commit comments

Comments
 (0)