Skip to content

Commit 6a794f7

Browse files
authored
feat: add support for more configuration via composefile labels (#5)
* feat: add support for more configuration via composefile labels * improve test coverage
1 parent 3ac0be1 commit 6a794f7

File tree

6 files changed

+230
-5
lines changed

6 files changed

+230
-5
lines changed

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,24 @@ All triggers support **threshold filtering** (`all`, `major`, `minor`, `patch`)
390390

391391
</details>
392392

393+
<details>
394+
<summary><strong>Docker Compose per-container labels</strong></summary>
395+
396+
When using the Docker Compose trigger, container labels can override trigger settings per service.
397+
398+
| Label (preferred) | Legacy label | Trigger env equivalent | Expected value |
399+
| --- | --- | --- | --- |
400+
| `dd.compose.file` | `wud.compose.file` | `DD_TRIGGER_DOCKERCOMPOSE_xxx_FILE` | Compose file path |
401+
| `dd.compose.backup` | `wud.compose.backup` | `DD_TRIGGER_DOCKERCOMPOSE_xxx_BACKUP` | `true` / `false` |
402+
| `dd.compose.prune` | `wud.compose.prune` | `DD_TRIGGER_DOCKERCOMPOSE_xxx_PRUNE` | `true` / `false` |
403+
| `dd.compose.dryrun` | `wud.compose.dryrun` | `DD_TRIGGER_DOCKERCOMPOSE_xxx_DRYRUN` | `true` / `false` |
404+
| `dd.compose.auto` | `wud.compose.auto` | `DD_TRIGGER_DOCKERCOMPOSE_xxx_AUTO` | `true` / `false` |
405+
| `dd.compose.threshold` | `wud.compose.threshold` | `DD_TRIGGER_DOCKERCOMPOSE_xxx_THRESHOLD` | `all` / `major` / `minor` / `patch` |
406+
407+
`dd.*` labels take precedence when both `dd.*` and `wud.*` are present.
408+
409+
</details>
410+
393411
<h2 align="center" id="authentication">Authentication</h2>
394412

395413
---

app/registry/index.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,29 @@ test('ensureDockercomposeTriggerForContainer should handle paths without parent
337337
expect(Object.keys(registry.getState().trigger)).toContain(triggerId);
338338
});
339339

340+
test('ensureDockercomposeTriggerForContainer should set trigger configuration when provided', async () => {
341+
const triggerId = await registry.ensureDockercomposeTriggerForContainer(
342+
'my-service',
343+
'/home/user/myapp/docker-compose.yml',
344+
{
345+
backup: 'true',
346+
prune: 'false',
347+
dryrun: 'true',
348+
auto: 'false',
349+
threshold: 'minor',
350+
},
351+
);
352+
353+
expect(triggerId).toBe('dockercompose.myapp-my-service');
354+
expect(registry.getState().trigger[triggerId].configuration).toMatchObject({
355+
backup: true,
356+
prune: false,
357+
dryrun: true,
358+
auto: false,
359+
threshold: 'minor',
360+
});
361+
});
362+
340363
test('sanitizeComponentName should handle empty string', () => {
341364
const result = registry.testable_sanitizeComponentName('');
342365
expect(result).toBe('container');

app/registry/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ function sanitizeComponentName(name: string): string {
286286
export async function ensureDockercomposeTriggerForContainer(
287287
containerName: string,
288288
composeFilePath?: string,
289+
triggerConfiguration: Record<string, any> = {},
289290
): Promise<string> {
290291
let triggerBaseName: string;
291292

@@ -325,7 +326,7 @@ export async function ensureDockercomposeTriggerForContainer(
325326
kind: 'trigger',
326327
provider: 'dockercompose',
327328
name: triggerName,
328-
configuration: {},
329+
configuration: triggerConfiguration,
329330
componentPath: 'triggers/providers',
330331
});
331332

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

Lines changed: 109 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2504,7 +2504,11 @@ describe('Docker Watcher', () => {
25042504

25052505
const result = await docker.addImageDetailsToContainer(container);
25062506

2507-
expect(registry.ensureDockercomposeTriggerForContainer).toHaveBeenCalledWith('test-container', '/tmp/docker-compose.yml');
2507+
expect(registry.ensureDockercomposeTriggerForContainer).toHaveBeenCalledWith(
2508+
'test-container',
2509+
'/tmp/docker-compose.yml',
2510+
{},
2511+
);
25082512
expect(result.triggerInclude).toBe('dockercompose.tmp-test-container');
25092513
});
25102514

@@ -2539,10 +2543,112 @@ describe('Docker Watcher', () => {
25392543

25402544
const result = await docker.addImageDetailsToContainer(container);
25412545

2542-
expect(registry.ensureDockercomposeTriggerForContainer).toHaveBeenCalledWith('test-container-wud', '/tmp/docker-compose.yml');
2546+
expect(registry.ensureDockercomposeTriggerForContainer).toHaveBeenCalledWith(
2547+
'test-container-wud',
2548+
'/tmp/docker-compose.yml',
2549+
{},
2550+
);
25432551
expect(result.triggerInclude).toBe('dockercompose.tmp-test-container-wud');
25442552
});
25452553

2554+
test('should pass compose trigger options from labels', async () => {
2555+
const container = await setupContainerDetailTest(docker, {
2556+
container: {
2557+
Image: 'nginx:1.0.0',
2558+
Names: ['/test-container-options'],
2559+
Labels: {
2560+
'dd.compose.file': '/tmp/docker-compose.yml',
2561+
'dd.compose.backup': 'true',
2562+
'dd.compose.prune': 'false',
2563+
'dd.compose.dryrun': 'true',
2564+
'dd.compose.auto': 'false',
2565+
'dd.compose.threshold': 'minor',
2566+
},
2567+
},
2568+
});
2569+
2570+
await docker.addImageDetailsToContainer(container);
2571+
2572+
expect(registry.ensureDockercomposeTriggerForContainer).toHaveBeenCalledWith(
2573+
'test-container-options',
2574+
'/tmp/docker-compose.yml',
2575+
{
2576+
backup: 'true',
2577+
prune: 'false',
2578+
dryrun: 'true',
2579+
auto: 'false',
2580+
threshold: 'minor',
2581+
},
2582+
);
2583+
});
2584+
2585+
test('should pass compose trigger options from wud labels as fallback', async () => {
2586+
const container = await setupContainerDetailTest(docker, {
2587+
container: {
2588+
Image: 'nginx:1.0.0',
2589+
Names: ['/test-container-options-wud'],
2590+
Labels: {
2591+
'wud.compose.file': '/tmp/docker-compose.yml',
2592+
'wud.compose.backup': 'false',
2593+
'wud.compose.prune': 'true',
2594+
'wud.compose.dryrun': 'false',
2595+
'wud.compose.auto': 'true',
2596+
'wud.compose.threshold': 'patch',
2597+
},
2598+
},
2599+
});
2600+
2601+
await docker.addImageDetailsToContainer(container);
2602+
2603+
expect(registry.ensureDockercomposeTriggerForContainer).toHaveBeenCalledWith(
2604+
'test-container-options-wud',
2605+
'/tmp/docker-compose.yml',
2606+
{
2607+
backup: 'false',
2608+
prune: 'true',
2609+
dryrun: 'false',
2610+
auto: 'true',
2611+
threshold: 'patch',
2612+
},
2613+
);
2614+
});
2615+
2616+
test('should prefer dd compose trigger options over wud when both are set', async () => {
2617+
const container = await setupContainerDetailTest(docker, {
2618+
container: {
2619+
Image: 'nginx:1.0.0',
2620+
Names: ['/test-container-options-precedence'],
2621+
Labels: {
2622+
'dd.compose.file': '/tmp/docker-compose.yml',
2623+
'dd.compose.backup': 'true',
2624+
'dd.compose.prune': 'false',
2625+
'dd.compose.dryrun': 'true',
2626+
'dd.compose.auto': 'false',
2627+
'dd.compose.threshold': 'minor',
2628+
'wud.compose.backup': 'false',
2629+
'wud.compose.prune': 'true',
2630+
'wud.compose.dryrun': 'false',
2631+
'wud.compose.auto': 'true',
2632+
'wud.compose.threshold': 'patch',
2633+
},
2634+
},
2635+
});
2636+
2637+
await docker.addImageDetailsToContainer(container);
2638+
2639+
expect(registry.ensureDockercomposeTriggerForContainer).toHaveBeenCalledWith(
2640+
'test-container-options-precedence',
2641+
'/tmp/docker-compose.yml',
2642+
{
2643+
backup: 'true',
2644+
prune: 'false',
2645+
dryrun: 'true',
2646+
auto: 'false',
2647+
threshold: 'minor',
2648+
},
2649+
);
2650+
});
2651+
25462652
test('should continue when dockercompose trigger creation fails', async () => {
25472653
const ensureTriggerSpy = vi
25482654
.spyOn(registry, 'ensureDockercomposeTriggerForContainer')
@@ -2564,7 +2670,7 @@ describe('Docker Watcher', () => {
25642670
triggerInclude: 'ntfy.default:major',
25652671
});
25662672

2567-
expect(ensureTriggerSpy).toHaveBeenCalledWith('test-container', '/tmp/docker-compose.yml');
2673+
expect(ensureTriggerSpy).toHaveBeenCalledWith('test-container', '/tmp/docker-compose.yml', {});
25682674
// On failure, processing should continue and the original triggerInclude should be preserved.
25692675
expect(result.triggerInclude).toBe('ntfy.default:major');
25702676
});

app/watchers/providers/docker/Docker.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,12 @@ import {
3434
} from '../../../tag/index.js';
3535
import Watcher from '../../Watcher.js';
3636
import {
37+
ddComposeAuto,
38+
ddComposeBackup,
39+
ddComposeDryrun,
3740
ddComposeFile,
41+
ddComposePrune,
42+
ddComposeThreshold,
3843
ddDisplayIcon,
3944
ddDisplayName,
4045
ddInspectTagPath,
@@ -48,9 +53,14 @@ import {
4853
ddTriggerInclude,
4954
ddWatch,
5055
ddWatchDigest,
56+
wudComposeAuto,
57+
wudComposeBackup,
58+
wudComposeDryrun,
5159
wudDisplayIcon,
5260
wudDisplayName,
5361
wudComposeFile,
62+
wudComposePrune,
63+
wudComposeThreshold,
5464
wudInspectTagPath,
5565
wudLinkTemplate,
5666
wudRegistryLookupImage,
@@ -172,6 +182,37 @@ function appendTriggerId(triggerInclude: string | undefined, triggerId: string |
172182
return `${triggerInclude},${triggerId}`;
173183
}
174184

185+
function getDockercomposeTriggerConfigurationFromLabels(labels: Record<string, string>) {
186+
const dockercomposeConfig: Record<string, string> = {};
187+
188+
const backup = getLabel(labels, ddComposeBackup, wudComposeBackup);
189+
if (backup !== undefined) {
190+
dockercomposeConfig.backup = backup;
191+
}
192+
193+
const prune = getLabel(labels, ddComposePrune, wudComposePrune);
194+
if (prune !== undefined) {
195+
dockercomposeConfig.prune = prune;
196+
}
197+
198+
const dryrun = getLabel(labels, ddComposeDryrun, wudComposeDryrun);
199+
if (dryrun !== undefined) {
200+
dockercomposeConfig.dryrun = dryrun;
201+
}
202+
203+
const auto = getLabel(labels, ddComposeAuto, wudComposeAuto);
204+
if (auto !== undefined) {
205+
dockercomposeConfig.auto = auto;
206+
}
207+
208+
const threshold = getLabel(labels, ddComposeThreshold, wudComposeThreshold);
209+
if (threshold !== undefined) {
210+
dockercomposeConfig.threshold = threshold;
211+
}
212+
213+
return dockercomposeConfig;
214+
}
215+
175216
interface ResolvedImgset {
176217
name: string;
177218
includeTags?: string;
@@ -2432,12 +2473,18 @@ class Docker extends Watcher {
24322473
const containerName = getContainerName(container);
24332474
let triggerIncludeUpdated = resolvedConfig.triggerInclude;
24342475
const composeFilePath = containerLabels[ddComposeFile] || containerLabels[wudComposeFile];
2476+
const dockercomposeTriggerConfiguration =
2477+
getDockercomposeTriggerConfigurationFromLabels(containerLabels);
24352478
if (composeFilePath) {
24362479
let dockercomposeTriggerId = this.composeTriggersByContainer[containerId];
24372480
if (!dockercomposeTriggerId) {
24382481
try {
24392482
dockercomposeTriggerId =
2440-
await registry.ensureDockercomposeTriggerForContainer(containerName, composeFilePath);
2483+
await registry.ensureDockercomposeTriggerForContainer(
2484+
containerName,
2485+
composeFilePath,
2486+
dockercomposeTriggerConfiguration,
2487+
);
24412488
this.composeTriggersByContainer[containerId] = dockercomposeTriggerId;
24422489
} catch (e: any) {
24432490
this.ensureLogger();

app/watchers/providers/docker/label.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,36 @@ export const wudTriggerInclude = 'wud.trigger.include';
8181
export const ddComposeFile = 'dd.compose.file';
8282
export const wudComposeFile = 'wud.compose.file';
8383

84+
/**
85+
* Optional dockercompose trigger backup mode (true | false).
86+
*/
87+
export const ddComposeBackup = 'dd.compose.backup';
88+
export const wudComposeBackup = 'wud.compose.backup';
89+
90+
/**
91+
* Optional dockercompose trigger prune mode (true | false).
92+
*/
93+
export const ddComposePrune = 'dd.compose.prune';
94+
export const wudComposePrune = 'wud.compose.prune';
95+
96+
/**
97+
* Optional dockercompose trigger dry-run mode (true | false).
98+
*/
99+
export const ddComposeDryrun = 'dd.compose.dryrun';
100+
export const wudComposeDryrun = 'wud.compose.dryrun';
101+
102+
/**
103+
* Optional dockercompose trigger auto mode (true | false).
104+
*/
105+
export const ddComposeAuto = 'dd.compose.auto';
106+
export const wudComposeAuto = 'wud.compose.auto';
107+
108+
/**
109+
* Optional dockercompose trigger threshold setting.
110+
*/
111+
export const ddComposeThreshold = 'dd.compose.threshold';
112+
export const wudComposeThreshold = 'wud.compose.threshold';
113+
84114
/**
85115
* Optional list of triggers to exclude
86116
*/

0 commit comments

Comments
 (0)