From 6ce3eaeae2badd7f7e70f156010b1103a66cbe07 Mon Sep 17 00:00:00 2001 From: jlssmt Date: Sun, 30 Mar 2025 11:16:44 +0200 Subject: [PATCH] semver of container with latest tag can now sometimes be extracted --- .editorconfig | 4 + Dockerfile | 6 +- app/package.json | 1 + app/tag/index.js | 18 ++-- app/watchers/providers/docker/Docker.js | 128 ++++++++++++++++++------ app/watchers/providers/docker/label.js | 5 + 6 files changed, 118 insertions(+), 44 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..921dcc9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +root = true + +[*] +end_of_line = lf \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 2471e662..db481838 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,13 @@ # Common Stage FROM node:23-slim as base +ARG WUD_VERSION=unknown + LABEL maintainer="fmartinou" +LABEL org.opencontainers.image.authors="fmartinou" +LABEL org.opencontainers.image.version="$WUD_VERSION" EXPOSE 3000 -ARG WUD_VERSION=unknown - ENV WORKDIR=/home/node/app ENV WUD_LOG_FORMAT=text ENV WUD_VERSION=$WUD_VERSION diff --git a/app/package.json b/app/package.json index c66ac89c..f39031b1 100644 --- a/app/package.json +++ b/app/package.json @@ -5,6 +5,7 @@ "main": "index.js", "scripts": { "start": "nodemon index.js | bunyan -L -o short", + "debug": "nodemon --inspect index.js | bunyan -L -o long", "doc": ".docsify serve ./docs", "format": "prettier --write .", "lint:fix": "prettier --write . && eslint '**/*.js'", diff --git a/app/tag/index.js b/app/tag/index.js index 2b7add8f..3b9239d0 100644 --- a/app/tag/index.js +++ b/app/tag/index.js @@ -4,23 +4,22 @@ const semver = require('semver'); const log = require('../log'); +function coerce(rawVersion) { + return semver.coerce(rawVersion); +} + /** * Parse a string to a semver (return null is it cannot be parsed as a valid semver). * @param rawVersion * @returns {*|SemVer} */ function parse(rawVersion) { - const rawVersionCleaned = semver.clean(rawVersion, { loose: true }); const rawVersionSemver = semver.parse( - rawVersionCleaned !== null ? rawVersionCleaned : rawVersion, + semver.clean(rawVersion, { loose: true }) || rawVersion, ); - // Hurrah! - if (rawVersionSemver !== null) { - return rawVersionSemver; - } - // Last chance; try to coerce (all data behind patch digit will be lost). - return semver.coerce(rawVersion); + // Coerce means that all data behind patch digit will be lost + return rawVersionSemver || semver.coerce(rawVersion); } /** @@ -36,7 +35,7 @@ function isGreater(version1, version2) { if (version1Semver === null || version2Semver === null) { return false; } - return semver.gte(version1Semver, version2Semver); + return semver.gt(version1Semver, version2Semver); } /** @@ -96,6 +95,7 @@ function transform(transformFormula, originalTag) { } module.exports = { + coerce, parse, isGreater, diff, diff --git a/app/watchers/providers/docker/Docker.js b/app/watchers/providers/docker/Docker.js index ae4ecb71..80dc487a 100644 --- a/app/watchers/providers/docker/Docker.js +++ b/app/watchers/providers/docker/Docker.js @@ -5,6 +5,7 @@ const cron = require('node-cron'); const parse = require('parse-docker-image-name'); const debounce = require('just-debounce'); const { + coerce: coerceSemver, parse: parseSemver, isGreater: isGreaterSemver, transform: transformTag, @@ -21,6 +22,7 @@ const { wudDisplayIcon, wudTriggerInclude, wudTriggerExclude, + wudInspectTagPath, } = require('./label'); const storeContainer = require('../../../store/container'); const log = require('../../../log'); @@ -240,6 +242,46 @@ function isDigestToWatch(wudWatchDigestLabelValue, isSemver) { return result; } +/** + * Extract image semver from locally installed image. + * @param containerData + * @param logger + * @param inspectPath + * @param inspectRegex + * @returns {string} + */ +function getSemverOfInstalledImage( + containerData, + logger, + inspectPath = 'Config/Labels/org.opencontainers.image.version', +) { + try { + const rawValue = getContainerValueByPath(containerData, inspectPath); + return coerceSemver(rawValue)?.version; + } catch (err) { + logger.warn(err); + } +} + +function getContainerValueByPath(obj, path) { + const parts = path.split('/'); + let currentValue = obj; + + for (let part of parts) { + if (Array.isArray(currentValue)) { + const found = currentValue.find((e) => e.startsWith(part + '=')); + if (found) return found.split('=')[1]; + return undefined; + } else if (currentValue && part in currentValue) { + currentValue = currentValue[part]; + } else { + return undefined; + } + } + + return currentValue; +} + /** * Docker Watcher Component. */ @@ -377,40 +419,47 @@ class Docker extends Component { * @return {Promise} */ async onDockerEvent(dockerEventChunk) { - const dockerEvent = JSON.parse(dockerEventChunk.toString()); - const action = dockerEvent.Action; - const containerId = dockerEvent.id; + try { + const dockerEvent = JSON.parse(dockerEventChunk.toString()); + const action = dockerEvent.Action; + const containerId = dockerEvent.id; - // If the container was created or destroyed => perform a watch - if (action === 'destroy' || action === 'create') { - await this.watchCronDebounced(); - } else { - // Update container state in db if so - try { - const container = - await this.dockerApi.getContainer(containerId); - const containerInspect = await container.inspect(); - const newStatus = containerInspect.State.Status; - const containerFound = storeContainer.getContainer(containerId); - if (containerFound) { - // Child logger for the container to process - const logContainer = this.log.child({ - container: fullName(containerFound), - }); - const oldStatus = containerFound.status; - containerFound.status = newStatus; - if (oldStatus !== newStatus) { - storeContainer.updateContainer(containerFound); - logContainer.info( - `Status changed from ${oldStatus} to ${newStatus}`, - ); + // If the container was created or destroyed => perform a watch + if (action === 'destroy' || action === 'create') { + await this.watchCronDebounced(); + } else { + // Update container state in db if so + try { + const container = + await this.dockerApi.getContainer(containerId); + const containerInspect = await container.inspect(); + const newStatus = containerInspect.State.Status; + const containerFound = + storeContainer.getContainer(containerId); + if (containerFound) { + // Child logger for the container to process + const logContainer = this.log.child({ + container: fullName(containerFound), + }); + const oldStatus = containerFound.status; + containerFound.status = newStatus; + if (oldStatus !== newStatus) { + storeContainer.updateContainer(containerFound); + logContainer.info( + `Status changed from ${oldStatus} to ${newStatus}`, + ); + } } + } catch (e) { + this.log.debug( + `Unable to get container details for container id=[${containerId}] (${e.message})`, + ); } - } catch (e) { - this.log.debug( - `Unable to get container details for container id=[${containerId}] (${e.message})`, - ); } + } catch (e) { + this.log.debug( + `Unable to parse docker event chunk=[${dockerEventChunk}] (${e.message})`, + ); } } @@ -576,7 +625,6 @@ class Docker extends Component { /** * Find new version for a Container. */ - async findNewVersion(container, logContainer) { const registryProvider = getRegistry(container.image.registry.name); const result = { tag: container.image.tag.value }; @@ -681,7 +729,13 @@ class Docker extends Component { } // Get container image details - const image = await this.dockerApi.getImage(container.Image).inspect(); + let image; + try { + image = await this.dockerApi.getImage(container.Image).inspect(); + } catch (e) { + this.log.warn('Cannot get image inspect', e); + return Promise.reject(); + } // Get useful properties const containerName = getContainerName(container); @@ -706,7 +760,15 @@ class Docker extends Component { [imageNameToParse] = image.RepoTags; } const parsedImage = parse(imageNameToParse); - const tagName = parsedImage.tag || 'latest'; + const tagName = container.Labels[wudInspectTagPath] + ? getSemverOfInstalledImage( + await this.dockerApi.getContainer(containerId).inspect(), + this.log, + container.Labels[wudInspectTagPath], + ) || + parsedImage.tag || + 'latest' + : parsedImage.tag || 'latest'; const parsedTag = parseSemver(transformTag(transformTags, tagName)); const isSemver = parsedTag !== null && parsedTag !== undefined; const watchDigest = isDigestToWatch( diff --git a/app/watchers/providers/docker/label.js b/app/watchers/providers/docker/label.js index 77745450..4786b0fe 100644 --- a/app/watchers/providers/docker/label.js +++ b/app/watchers/providers/docker/label.js @@ -22,6 +22,11 @@ module.exports = { */ wudTagTransform: 'wud.tag.transform', + /** + * Optional path of docker inspect object where to find current semver. + */ + wudInspectTagPath: 'wud.inspect.tag.path', + /** * Should container digest be tracked? (true | false). */