Skip to content

Commit 494291d

Browse files
feat: add defaults envs for auto compose (#10)
* feat: add defaults by env for auto compose * fix: consistent empty string handling for compose labels (#11) * Initial plan * fix: use consistent empty string handling for compose labels Co-authored-by: Crow-Control <7613738+Crow-Control@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Crow-Control <7613738+Crow-Control@users.noreply.github.com> --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
1 parent 1748064 commit 494291d

File tree

3 files changed

+203
-10
lines changed

3 files changed

+203
-10
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,13 @@ Behavior notes:
413413
- 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.
414414
- `DD_WATCHER_DOCKER_xxx_COMPOSENATIVE=true` enables compose-native lookup by default for all containers in that watcher (container label can still override).
415415
- If `dd.compose.auto` is omitted, normal trigger default applies (`auto=true`).
416+
- You can set watcher-level defaults via env vars (per watcher):
417+
- `DD_WATCHER_<WATCHER_NAME>_COMPOSE_BACKUP`
418+
- `DD_WATCHER_<WATCHER_NAME>_COMPOSE_PRUNE`
419+
- `DD_WATCHER_<WATCHER_NAME>_COMPOSE_DRYRUN`
420+
- `DD_WATCHER_<WATCHER_NAME>_COMPOSE_AUTO`
421+
- `DD_WATCHER_<WATCHER_NAME>_COMPOSE_THRESHOLD`
422+
These defaults apply when corresponding compose labels are not present.
416423

417424
Troubleshooting path mismatch:
418425

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

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,39 @@ describe('Docker Watcher', () => {
635635
);
636636
});
637637

638+
test('should apply watcher compose defaults from configuration during init', async () => {
639+
storeContainer.getContainers.mockReturnValue([
640+
{
641+
id: 'existing-compose-defaults',
642+
name: 'web-defaults',
643+
watcher: 'test',
644+
labels: {
645+
'dd.compose.file': '/tmp/my-stack/docker-compose.yml',
646+
},
647+
triggerInclude: 'ntfy.default:major',
648+
},
649+
]);
650+
651+
await docker.register('watcher', 'docker', 'test', {
652+
watchatstart: true,
653+
compose: {
654+
threshold: 'minor',
655+
auto: false,
656+
},
657+
});
658+
659+
await docker.init();
660+
661+
expect(registry.ensureDockercomposeTriggerForContainer).toHaveBeenCalledWith(
662+
'web-defaults',
663+
'/tmp/my-stack/docker-compose.yml',
664+
{
665+
auto: 'false',
666+
threshold: 'minor',
667+
},
668+
);
669+
});
670+
638671
test('should keep watchatstart disabled when explicitly set to false', async () => {
639672
storeContainer.getContainers.mockReturnValue([]);
640673
await docker.register('watcher', 'docker', 'test', {
@@ -2965,6 +2998,115 @@ describe('Docker Watcher', () => {
29652998
);
29662999
});
29673000

3001+
test('should use watcher compose defaults when compose labels are missing', async () => {
3002+
const container = await setupContainerDetailTest(docker, {
3003+
registerConfig: {
3004+
compose: {
3005+
backup: true,
3006+
prune: false,
3007+
dryrun: true,
3008+
auto: false,
3009+
threshold: 'minor',
3010+
},
3011+
},
3012+
container: {
3013+
Image: 'nginx:1.0.0',
3014+
Names: ['/test-container-default-options'],
3015+
Labels: {
3016+
'dd.compose.file': '/tmp/docker-compose.yml',
3017+
},
3018+
},
3019+
});
3020+
3021+
await docker.addImageDetailsToContainer(container);
3022+
3023+
expect(registry.ensureDockercomposeTriggerForContainer).toHaveBeenCalledWith(
3024+
'test-container-default-options',
3025+
'/tmp/docker-compose.yml',
3026+
{
3027+
backup: 'true',
3028+
prune: 'false',
3029+
dryrun: 'true',
3030+
auto: 'false',
3031+
threshold: 'minor',
3032+
},
3033+
);
3034+
});
3035+
3036+
test('should let compose labels override watcher compose defaults', async () => {
3037+
const container = await setupContainerDetailTest(docker, {
3038+
registerConfig: {
3039+
compose: {
3040+
backup: true,
3041+
prune: true,
3042+
dryrun: true,
3043+
auto: true,
3044+
threshold: 'major',
3045+
},
3046+
},
3047+
container: {
3048+
Image: 'nginx:1.0.0',
3049+
Names: ['/test-container-default-override'],
3050+
Labels: {
3051+
'dd.compose.file': '/tmp/docker-compose.yml',
3052+
'dd.compose.backup': 'false',
3053+
'dd.compose.threshold': 'patch',
3054+
},
3055+
},
3056+
});
3057+
3058+
await docker.addImageDetailsToContainer(container);
3059+
3060+
expect(registry.ensureDockercomposeTriggerForContainer).toHaveBeenCalledWith(
3061+
'test-container-default-override',
3062+
'/tmp/docker-compose.yml',
3063+
{
3064+
backup: 'false',
3065+
prune: 'true',
3066+
dryrun: 'true',
3067+
auto: 'true',
3068+
threshold: 'patch',
3069+
},
3070+
);
3071+
});
3072+
3073+
test('should treat empty string compose labels as unset and fall back to watcher defaults', async () => {
3074+
const container = await setupContainerDetailTest(docker, {
3075+
registerConfig: {
3076+
compose: {
3077+
backup: true,
3078+
prune: false,
3079+
dryrun: true,
3080+
auto: false,
3081+
threshold: 'minor',
3082+
},
3083+
},
3084+
container: {
3085+
Image: 'nginx:1.0.0',
3086+
Names: ['/test-container-empty-labels'],
3087+
Labels: {
3088+
'dd.compose.file': '/tmp/docker-compose.yml',
3089+
'dd.compose.backup': '',
3090+
'dd.compose.threshold': '',
3091+
},
3092+
},
3093+
});
3094+
3095+
await docker.addImageDetailsToContainer(container);
3096+
3097+
expect(registry.ensureDockercomposeTriggerForContainer).toHaveBeenCalledWith(
3098+
'test-container-empty-labels',
3099+
'/tmp/docker-compose.yml',
3100+
{
3101+
backup: 'true',
3102+
prune: 'false',
3103+
dryrun: 'true',
3104+
auto: 'false',
3105+
threshold: 'minor',
3106+
},
3107+
);
3108+
});
3109+
29683110
test('should pass compose trigger options from wud labels as fallback', async () => {
29693111
const container = await setupContainerDetailTest(docker, {
29703112
container: {

app/watchers/providers/docker/Docker.ts

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,13 @@ export interface DockerWatcherConfiguration extends ComponentConfiguration {
103103
composenative: boolean;
104104
maintenancewindow?: string;
105105
maintenancewindowtz: string;
106+
compose?: {
107+
backup?: boolean;
108+
prune?: boolean;
109+
dryrun?: boolean;
110+
auto?: boolean;
111+
threshold?: string;
112+
};
106113
imgset?: Record<string, any>;
107114
}
108115

@@ -199,30 +206,50 @@ function removeTriggerId(triggerInclude: string | undefined, triggerId: string |
199206
return triggersIncluded.length > 0 ? triggersIncluded.join(',') : undefined;
200207
}
201208

202-
function getDockercomposeTriggerConfigurationFromLabels(labels: Record<string, string>) {
209+
function normalizeComposeDefaultValue(value: string | boolean | undefined) {
210+
if (value === undefined) {
211+
return undefined;
212+
}
213+
return `${value}`;
214+
}
215+
216+
function getDockercomposeTriggerConfigurationFromLabels(
217+
labels: Record<string, string>,
218+
composeDefaults: DockerWatcherConfiguration['compose'] = {},
219+
) {
203220
const dockercomposeConfig: Record<string, string> = {};
204221

205-
const backup = getLabel(labels, ddComposeBackup, wudComposeBackup);
222+
const backup =
223+
getLabel(labels, ddComposeBackup, wudComposeBackup) ||
224+
normalizeComposeDefaultValue(composeDefaults.backup);
206225
if (backup !== undefined) {
207226
dockercomposeConfig.backup = backup;
208227
}
209228

210-
const prune = getLabel(labels, ddComposePrune, wudComposePrune);
229+
const prune =
230+
getLabel(labels, ddComposePrune, wudComposePrune) ||
231+
normalizeComposeDefaultValue(composeDefaults.prune);
211232
if (prune !== undefined) {
212233
dockercomposeConfig.prune = prune;
213234
}
214235

215-
const dryrun = getLabel(labels, ddComposeDryrun, wudComposeDryrun);
236+
const dryrun =
237+
getLabel(labels, ddComposeDryrun, wudComposeDryrun) ||
238+
normalizeComposeDefaultValue(composeDefaults.dryrun);
216239
if (dryrun !== undefined) {
217240
dockercomposeConfig.dryrun = dryrun;
218241
}
219242

220-
const auto = getLabel(labels, ddComposeAuto, wudComposeAuto);
243+
const auto =
244+
getLabel(labels, ddComposeAuto, wudComposeAuto) ||
245+
normalizeComposeDefaultValue(composeDefaults.auto);
221246
if (auto !== undefined) {
222247
dockercomposeConfig.auto = auto;
223248
}
224249

225-
const threshold = getLabel(labels, ddComposeThreshold, wudComposeThreshold);
250+
const threshold =
251+
getLabel(labels, ddComposeThreshold, wudComposeThreshold) ||
252+
normalizeConfigStringValue(composeDefaults.threshold);
226253
if (threshold !== undefined) {
227254
dockercomposeConfig.threshold = threshold;
228255
}
@@ -1135,6 +1162,15 @@ class Docker extends Watcher {
11351162
composenative: this.joi.boolean().default(false),
11361163
maintenancewindow: joi.string().cron().optional(),
11371164
maintenancewindowtz: this.joi.string().default('UTC'),
1165+
compose: this.joi
1166+
.object({
1167+
backup: this.joi.boolean(),
1168+
prune: this.joi.boolean(),
1169+
dryrun: this.joi.boolean(),
1170+
auto: this.joi.boolean(),
1171+
threshold: this.joi.string(),
1172+
})
1173+
.default({}),
11381174
imgset: this.joi
11391175
.object()
11401176
.pattern(
@@ -1347,7 +1383,10 @@ class Docker extends Watcher {
13471383
dockercomposeTriggerId = await registry.ensureDockercomposeTriggerForContainer(
13481384
containerInStore.name,
13491385
composeFilePath,
1350-
getDockercomposeTriggerConfigurationFromLabels(containerLabels),
1386+
getDockercomposeTriggerConfigurationFromLabels(
1387+
containerLabels,
1388+
this.configuration.compose,
1389+
),
13511390
);
13521391
this.composeTriggersByContainer[containerInStore.id] = dockercomposeTriggerId;
13531392
} catch (e: any) {
@@ -2126,7 +2165,10 @@ class Docker extends Watcher {
21262165
dockercomposeTriggerId = await registry.ensureDockercomposeTriggerForContainer(
21272166
newName || oldName,
21282167
composeFilePath,
2129-
getDockercomposeTriggerConfigurationFromLabels(labelsToApply),
2168+
getDockercomposeTriggerConfigurationFromLabels(
2169+
labelsToApply,
2170+
this.configuration.compose,
2171+
),
21302172
);
21312173
this.composeTriggersByContainer[containerId] = dockercomposeTriggerId;
21322174
} catch (e: any) {
@@ -2652,8 +2694,10 @@ class Docker extends Watcher {
26522694
}
26532695
const containerName = getContainerName(container);
26542696
let triggerIncludeUpdated = resolvedConfig.triggerInclude;
2655-
const dockercomposeTriggerConfiguration =
2656-
getDockercomposeTriggerConfigurationFromLabels(containerLabels);
2697+
const dockercomposeTriggerConfiguration = getDockercomposeTriggerConfigurationFromLabels(
2698+
containerLabels,
2699+
this.configuration.compose,
2700+
);
26572701
if (composeFilePath) {
26582702
let dockercomposeTriggerId = this.composeTriggersByContainer[containerId];
26592703
if (!dockercomposeTriggerId) {

0 commit comments

Comments
 (0)