Skip to content

Commit

Permalink
Merge pull request #512 from getwud/feature/#502_specific_triggers
Browse files Browse the repository at this point in the history
⭐ [TRIGGER] - Add support for associating specific triggers to s…
  • Loading branch information
fmartinou authored Dec 19, 2024
2 parents bde9dc6 + 6e11b46 commit 05b3ea0
Show file tree
Hide file tree
Showing 22 changed files with 2,776 additions and 6,024 deletions.
8 changes: 4 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ script:

# Pull vaultwarden
- docker pull vaultwarden/server
- docker pull vaultwarden/server:1.32.5-alpine
- docker pull vaultwarden/server:1.32.6-alpine

# Pull youtubedl
- docker pull jeeaaasustest/youtube-dl
Expand All @@ -117,7 +117,7 @@ script:
# - docker run -d --name gcr_sub_sub_test --label "wud.watch=true" gcr.io/wud-test/sub/sub/test:1.0.0

# GHCR
- docker run -d --name ghcr_radarr --label 'wud.watch=true' --label 'wud.tag.include=^\d+\.\d+\.\d+\.\d+-ls\d+$' ghcr.io/linuxserver/radarr:3.2.1.5070-ls105
- docker run -d --name ghcr_radarr --label 'wud.watch=true' --label 'wud.tag.include=^\d+\.\d+\.\d+\.\d+-ls\d+$' ghcr.io/linuxserver/radarr:5.14.0.9383-ls245

# GITLAB
- docker run -d --name gitlab_test --label 'wud.watch=true' --label 'wud.tag.include=^\d+\.\d+\.\d+$' registry.gitlab.com/manfred-martin/docker-registry-test:1.0.0
Expand All @@ -141,13 +141,13 @@ script:
- docker run -d --name hub_traefik_245 --label 'wud.watch=true' --label 'wud.tag.include=^\d+\.\d+.\d+$' traefik:2.4.5
- docker run -d --name hub_traefik_latest --label 'wud.watch=true' --label 'wud.watch.digest=true' --label 'wud.tag.include=^latest$' traefik

- docker run -d --name hub_vaultwarden_1222 --label 'wud.watch=true' --label 'wud.tag.include=^\d+\.\d+.\d+-alpine$' -e I_REALLY_WANT_VOLATILE_STORAGE=true vaultwarden/server:1.32.5-alpine
- docker run -d --name hub_vaultwarden_1222 --label 'wud.watch=true' --label 'wud.tag.include=^\d+\.\d+.\d+-alpine$' -e I_REALLY_WANT_VOLATILE_STORAGE=true vaultwarden/server:1.32.6-alpine
- docker run -d --name hub_vaultwarden_latest --label 'wud.watch=true' --label 'wud.watch.digest=true' --label 'wud.tag.include=^latest$' -e I_REALLY_WANT_VOLATILE_STORAGE=true vaultwarden/server

- docker run -d --name hub_youtubedb_latest --label 'wud.watch=true' --label 'wud.watch.digest=true' --label 'wud.tag.include=^latest$' jeeaaasustest/youtube-dl

# LSCR
- docker run -d --name lscr_radarr --label 'wud.watch=true' --label 'wud.tag.include=^\d+\.\d+\.\d+\.\d+-ls\d+$' lscr.io/linuxserver/radarr:3.2.1.5070-ls105
- docker run -d --name lscr_radarr --label 'wud.watch=true' --label 'wud.tag.include=^\d+\.\d+\.\d+\.\d+-ls\d+$' lscr.io/linuxserver/radarr:5.14.0.9383-ls245

# QUAY
- docker run -d --name quay_prometheus --label 'wud.watch=true' --label 'wud.tag.include=^v\d+\.\d+\.\d+$' quay.io/prometheus/prometheus:v2.52.0
Expand Down
45 changes: 45 additions & 0 deletions app/api/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ const nocache = require('nocache');
const storeContainer = require('../store/container');
const registry = require('../registry');
const { getServerConfiguration } = require('../configuration');
const { mapComponentsToList } = require('./component');
const Trigger = require('../triggers/providers/Trigger');

const router = express.Router();

Expand All @@ -16,6 +18,14 @@ function getWatchers() {
return registry.getState().watcher;
}

/**
* Return registered triggers.
* @returns {{id: string}[]}
*/
function getTriggers() {
return registry.getState().trigger;
}

/**
* Get containers from store.
* @param query
Expand Down Expand Up @@ -87,6 +97,40 @@ async function watchContainers(req, res) {
}
}

async function getContainerTriggers(req, res) {
const { id } = req.params;

const container = storeContainer.getContainer(id);
if (container) {
const allTriggers = mapComponentsToList(getTriggers());
const includedTriggers = container.triggerInclude ? container.triggerInclude.split(/\s*,\s*/).map((includedTrigger) => Trigger.parseIncludeOrIncludeTriggerString(includedTrigger)) : undefined;
const excludedTriggers = container.triggerExclude ? container.triggerExclude.split(/\s*,\s*/).map((excludedTrigger) => Trigger.parseIncludeOrIncludeTriggerString(excludedTrigger)) : undefined;
const associatedTriggers = [];
allTriggers.forEach((trigger) => {
const triggerToAssociate = { ...trigger };
let associated = true;
if (includedTriggers) {
const includedTrigger = includedTriggers.find((tr) => tr.id === trigger.id);
if (includedTrigger) {
triggerToAssociate.configuration.threshold = includedTrigger.threshold;
} else {
associated = false;
}
}
if (excludedTriggers && excludedTriggers
.map((excludedTrigger) => excludedTrigger.id).includes(trigger.id)) {
associated = false;
}
if (associated) {
associatedTriggers.push(triggerToAssociate);
}
});
res.status(200).json(associatedTriggers);
} else {
res.sendStatus(404);
}
}

/**
* Watch an image.
* @param req
Expand Down Expand Up @@ -139,6 +183,7 @@ function init() {
router.post('/watch', watchContainers);
router.get('/:id', getContainer);
router.delete('/:id', deleteContainer);
router.get('/:id/triggers', getContainerTriggers);
router.post('/:id/watch', watchContainer);
return router;
}
Expand Down
2 changes: 2 additions & 0 deletions app/model/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const schema = joi.object({
transformTags: joi.string(),
linkTemplate: joi.string(),
link: joi.string(),
triggerInclude: joi.string(),
triggerExclude: joi.string(),
image: joi.object({
id: joi.string().min(1).required(),
registry: joi.object({
Expand Down
9 changes: 5 additions & 4 deletions app/nodemon.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
"env": {
"WUD_VERSION": "dev",
"WUD_STORE_PATH": ".store",
"WUD_WATCHER_LOCAL_WATCHBYDEFAULT": "true",
"WUD_LOG_LEVEL": "debug",
"WUD_WATCHER_LOCAL_WATCHBYDEFAULT": "false",
"WUD_AUTH_BASIC_JOHN_USER": "john",
"WUD_AUTH_BASIC_JOHN_HASH": "$apr1$aefKbZEa$ZSA5Y3zv9vDQOxr283NGx/",
"WUD_TRIGGER_NTFY_ONE_TOPIC": "NnlZCpSXEKRvLSfw",
"WUD_TRIGGER_NTFY_TWO_TOPIC": "NnlZCpSXEKRvLSfw",
"WUD_TRIGGER_NTFY_THREE_TOPIC": "NnlZCpSXEKRvLSfw"
"WUD_TRIGGER_NTFY_ONE_TOPIC": "235ef38e-f1db-414a-964f-ce3f2cc8094d",
"WUD_TRIGGER_NTFY_TWO_TOPIC": "6abd7f9d-f140-4543-af3e-749252660d89",
"WUD_TRIGGER_NTFY_THREE_TOPIC": "9f5c9961-adb2-4ab7-a078-0671856948c4"
}
}
25 changes: 11 additions & 14 deletions app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions app/prometheus/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ function init() {
'transform_tags',
'link_template',
'link',
'trigger_include',
'trigger_exclude',
'image_id',
'image_registry_name',
'image_registry_url',
Expand Down
125 changes: 96 additions & 29 deletions app/triggers/providers/Trigger.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,58 @@ function renderSimple(template, container) {
* Trigger base component.
*/
class Trigger extends Component {
/**
* Return true if update reaches trigger threshold.
* @param containerResult
* @param threshold
* @returns {boolean}
*/
static isThresholdReached(containerResult, threshold) {
let thresholdPassing = true;
if (
threshold.toLowerCase() !== 'all'
&& containerResult.updateKind
&& containerResult.updateKind.kind === 'tag'
&& containerResult.updateKind.semverDiff
&& containerResult.updateKind.semverDiff !== 'unknown'
) {
switch (threshold) {
case 'minor':
thresholdPassing = containerResult.updateKind.semverDiff !== 'major';
break;
case 'patch':
thresholdPassing = containerResult.updateKind.semverDiff !== 'major'
&& containerResult.updateKind.semverDiff !== 'minor';
break;
default:
thresholdPassing = true;
}
}
return thresholdPassing;
}

/**
* Parse $type.$name:$threshold string.
* @param {*} includeOrExcludeTriggerString
* @returns
*/
static parseIncludeOrIncludeTriggerString(includeOrExcludeTriggerString) {
const includeOrExcludeTriggerSplit = includeOrExcludeTriggerString.split(/\s*:\s*/);
const includeOrExcludeTrigger = {
id: `trigger.${includeOrExcludeTriggerSplit[0]}`,
threshold: 'all',
};
if (includeOrExcludeTriggerSplit.length === 2) {
switch (includeOrExcludeTriggerSplit[1]) {
case 'major': includeOrExcludeTrigger.threshold = 'major'; break;
case 'minor': includeOrExcludeTrigger.threshold = 'minor'; break;
case 'patch': includeOrExcludeTrigger.threshold = 'patch'; break;
default: includeOrExcludeTrigger.threshold = 'all';
}
}
return includeOrExcludeTrigger;
}

/**
* Handle container report (simple mode).
* @param containerReport
Expand All @@ -41,10 +93,15 @@ class Trigger extends Component {
.log.child({ container: fullName(containerReport.container) }) || this.log;
let status = 'error';
try {
if (!this.isThresholdReached(containerReport.container)) {
logContainer.debug('Threshold not reached => do not trigger');
if (!Trigger.isThresholdReached(
containerReport.container,
this.configuration.threshold.toLowerCase(),
)) {
logContainer.debug('Threshold not reached => ignore');
} else if (!this.mustTrigger(containerReport.container)) {
logContainer.debug('Trigger conditions not met => ignore');
} else {
logContainer.debug('Run trigger');
logContainer.debug('Run');
await this.trigger(containerReport.container);
}
status = 'success';
Expand All @@ -68,11 +125,15 @@ class Trigger extends Component {
const containerReportsFiltered = containerReports
.filter((containerReport) => containerReport.changed || !this.configuration.once)
.filter((containerReport) => containerReport.container.updateAvailable)
.filter((containerReport) => this.isThresholdReached(containerReport.container));
.filter((containerReport) => this.mustTrigger(containerReport.container))
.filter((containerReport) => Trigger.isThresholdReached(
containerReport.container,
this.configuration.threshold.toLowerCase(),
));
const containersFiltered = containerReportsFiltered
.map((containerReport) => containerReport.container);
if (containersFiltered.length > 0) {
this.log.debug('Run trigger batch');
this.log.debug('Run batch');
await this.triggerBatch(containersFiltered);
}
} catch (e) {
Expand All @@ -81,34 +142,40 @@ class Trigger extends Component {
}
}

isTriggerIncludedOrExcluded(containerResult, trigger) {
const triggers = trigger.split(/\s*,\s*/).map((triggerToMatch) => Trigger.parseIncludeOrIncludeTriggerString(triggerToMatch));
const triggerMatched = triggers.find(
(triggerToMatch) => triggerToMatch.id.toLowerCase() === this.getId(),
);
if (!triggerMatched) {
return false;
}
return Trigger.isThresholdReached(containerResult, triggerMatched.threshold.toLowerCase());
}

isTriggerIncluded(containerResult, triggerInclude) {
if (!triggerInclude) {
return true;
}
return this.isTriggerIncludedOrExcluded(containerResult, triggerInclude);
}

isTriggerExcluded(containerResult, triggerExclude) {
if (!triggerExclude) {
return false;
}
return this.isTriggerIncludedOrExcluded(containerResult, triggerExclude);
}

/**
* Return true if update reaches trigger threshold.
* Return true if must trigger on this container.
* @param containerResult
* @returns {boolean}
*/
isThresholdReached(containerResult) {
let thresholdPassing = true;
if (
this.configuration.threshold.toLowerCase() !== 'all'
&& containerResult.updateKind
&& containerResult.updateKind.kind === 'tag'
&& containerResult.updateKind.semverDiff
&& containerResult.updateKind.semverDiff !== 'unknown'
) {
const threshold = this.configuration.threshold.toLowerCase();
switch (threshold) {
case 'minor':
thresholdPassing = containerResult.updateKind.semverDiff !== 'major';
break;
case 'patch':
thresholdPassing = containerResult.updateKind.semverDiff !== 'major'
&& containerResult.updateKind.semverDiff !== 'minor';
break;
default:
thresholdPassing = true;
}
}
return thresholdPassing;
mustTrigger(containerResult) {
const { triggerInclude, triggerExclude } = containerResult;
return this.isTriggerIncluded(containerResult, triggerInclude)
&& !this.isTriggerExcluded(containerResult, triggerExclude);
}

/**
Expand Down
8 changes: 4 additions & 4 deletions app/triggers/providers/Trigger.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -305,22 +305,22 @@ test.each(isThresholdReachedTestCases)(
trigger.configuration = {
threshold: item.threshold,
};
expect(trigger.isThresholdReached({
expect(Trigger.isThresholdReached({
updateKind: {
kind: item.kind,
semverDiff: item.change,
},
})).toEqual(item.result);
}, trigger.configuration.threshold)).toEqual(item.result);
},
);

test('isThresholdReached should return true when there is no semverDiff regardless of the threshold', async () => {
trigger.configuration = {
threshold: 'all',
};
expect(trigger.isThresholdReached({
expect(Trigger.isThresholdReached({
updateKind: { kind: 'digest' },
})).toBeTruthy();
}, trigger.configuration.threshold)).toBeTruthy();
});

test('renderSimpleTitle should replace placeholders when called', async () => {
Expand Down
Loading

0 comments on commit 05b3ea0

Please sign in to comment.