From 9ef76c5a47d14fb68f2b885b8ec6a508b5b57ae3 Mon Sep 17 00:00:00 2001 From: Robert Munteanu <robert.munteanu@gmail.com> Date: Mon, 19 Dec 2022 21:00:13 +0100 Subject: [PATCH] feat: add osgi dependency manager (#19282) --- lib/modules/manager/api.ts | 2 + lib/modules/manager/osgi/extract.spec.ts | 279 +++++++++++++++++++++++ lib/modules/manager/osgi/extract.ts | 135 +++++++++++ lib/modules/manager/osgi/index.ts | 12 + lib/modules/manager/osgi/readme.md | 10 + lib/modules/manager/osgi/types.ts | 14 ++ 6 files changed, 452 insertions(+) create mode 100644 lib/modules/manager/osgi/extract.spec.ts create mode 100644 lib/modules/manager/osgi/extract.ts create mode 100644 lib/modules/manager/osgi/index.ts create mode 100644 lib/modules/manager/osgi/readme.md create mode 100644 lib/modules/manager/osgi/types.ts diff --git a/lib/modules/manager/api.ts b/lib/modules/manager/api.ts index 810bc94024..06939c884c 100644 --- a/lib/modules/manager/api.ts +++ b/lib/modules/manager/api.ts @@ -55,6 +55,7 @@ import * as nodenv from './nodenv'; import * as npm from './npm'; import * as nuget from './nuget'; import * as nvm from './nvm'; +import * as osgi from './osgi'; import * as pipCompile from './pip-compile'; import * as pip_requirements from './pip_requirements'; import * as pip_setup from './pip_setup'; @@ -140,6 +141,7 @@ api.set('nodenv', nodenv); api.set('npm', npm); api.set('nuget', nuget); api.set('nvm', nvm); +api.set('osgi', osgi); api.set('pip-compile', pipCompile); api.set('pip_requirements', pip_requirements); api.set('pip_setup', pip_setup); diff --git a/lib/modules/manager/osgi/extract.spec.ts b/lib/modules/manager/osgi/extract.spec.ts new file mode 100644 index 0000000000..c77c15151d --- /dev/null +++ b/lib/modules/manager/osgi/extract.spec.ts @@ -0,0 +1,279 @@ +import { extractPackageFile } from './extract'; + +const noArtifacts = `{ + "configurations": { + "org.apache.sling.jcr.davex.impl.servlets.SlingDavExServlet":{ + "alias":"/server" + } + } +}`; +const unsupportedFeatureVersion = `{ + "feature-resource-version": "2.0", + "bundles":[ + { + "id":"commons-codec:commons-codec:1.15", + "start-order":"5" + } + ] +}`; +const featureWithBundlesAsObjects = `{ + "feature-resource-version": "1.0", + "bundles":[ + { + "id":"commons-codec:commons-codec:1.15", + "start-order":"5" + }, + { + "id":"commons-collections:commons-collections:3.2.2", + "start-order":"15" + } + ] +}`; +const featureWithBundlesAsStrings = `{ + "bundles": [ + "org.apache.felix/org.apache.felix.scr/2.1.26", + "org.apache.felix/org.apache.felix.log/1.2.4" + ] +}`; +const featureWithComment = `{ + // comments are permitted + "bundles": [ "org.apache.aries:org.apache.aries.util:1.1.3" ] +}`; +const artifactsExtension = `{ + "content-packages:ARTIFACTS|true": [ + "com.day.cq:core.wcm.components.all:zip:2.21.0" + ] +}`; +const doubleSlashNotComment = `{ + "bundles":[ + { + "id":"com.h2database:h2-mvstore:2.1.214", + "start-order":"15" + }, + { + "id":"org.mongodb:mongo-java-driver:3.12.11", + "start-order":"15" + } + ], + "configurations":{ + "org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreService":{ + "db":"sling", + "mongouri":"mongodb://$[env:MONGODB_HOST;default=localhost]:$[env:MONGODB_PORT;type=Integer;default=27017]" + } + } +}`; +const frameworkArtifact = `{ + "execution-environment:JSON|false":{ + "framework":{ + "id":"org.apache.felix:org.apache.felix.framework:7.0.5" + } + } +}`; +const versionWithVariable = `{ + "bundles":[ + { + "id":"com.fasterxml.jackson.core:jackson-annotations:$\{jackson.version}", + "start-order":"20" + } + ] +}`; +const malformedDefinitions = `{ + "bundles":[ + { + "#": "missing the 'id' attribute", + "not-id":"commons-codec:commons-codec:1.15" + }, + { + "#": "too few parts in the GAV definition", + "id":"commons-codec:1.15" + }, + { + "#": "valid definition, should be extracted", + "id":"commons-codec:commons-codec:1.15" + } + ] +}`; +const invalidFeatureVersion = `{ + "feature-resource-version": "unknown", + "bundles":[ + { + "id":"commons-codec:commons-codec:1.15", + "start-order":"5" + } + ] +}`; + +describe('modules/manager/osgi/extract', () => { + describe('extractPackageFile()', () => { + it('returns null for empty file', () => { + expect(extractPackageFile('', '', undefined)).toBeNull(); + }); + + it('returns null for invalid file', () => { + expect(extractPackageFile('this-is-not-json', '', undefined)).toBeNull(); + }); + + it('returns null for unsupported version of feature model definition', () => { + expect( + extractPackageFile(unsupportedFeatureVersion, '', undefined) + ).toBeNull(); + }); + + it('returns null for an invalid version of feature model definition', () => { + expect( + extractPackageFile(invalidFeatureVersion, '', undefined) + ).toBeNull(); + }); + + it('returns null for a null string passed in as a feature model definition', () => { + expect(extractPackageFile('null', '', undefined)).toBeNull(); + }); + + it('returns null for a valid file with no artifact definitions', () => { + expect(extractPackageFile(noArtifacts, '', undefined)).toBeNull(); + }); + + it('extracts the bundles from a file with object bundles definitions', () => { + const packageFile = extractPackageFile( + featureWithBundlesAsObjects, + '', + undefined + ); + expect(packageFile).toEqual({ + deps: [ + { + datasource: 'maven', + depName: 'commons-codec:commons-codec', + currentValue: '1.15', + }, + { + datasource: 'maven', + depName: 'commons-collections:commons-collections', + currentValue: '3.2.2', + }, + ], + }); + }); + + it('extracts the bundles from a file with string bundles defintions', () => { + const packageFile = extractPackageFile( + featureWithBundlesAsStrings, + '', + undefined + ); + expect(packageFile).toEqual({ + deps: [ + { + datasource: 'maven', + depName: 'org.apache.felix:org.apache.felix.scr', + currentValue: '2.1.26', + }, + { + datasource: 'maven', + depName: 'org.apache.felix:org.apache.felix.log', + currentValue: '1.2.4', + }, + ], + }); + }); + + it('extracts the bundles from a file with comments', () => { + const packageFile = extractPackageFile(featureWithComment, '', undefined); + expect(packageFile).toEqual({ + deps: [ + { + datasource: 'maven', + depName: 'org.apache.aries:org.apache.aries.util', + currentValue: '1.1.3', + }, + ], + }); + }); + + it('extracts the artifacts from an extension section', () => { + const packageFile = extractPackageFile(artifactsExtension, '', undefined); + expect(packageFile).toEqual({ + deps: [ + { + datasource: 'maven', + depName: 'com.day.cq:core.wcm.components.all', + currentValue: '2.21.0', + }, + ], + }); + }); + + it('extracts the artifacts a file with a double slash', () => { + const packageFile = extractPackageFile( + doubleSlashNotComment, + '', + undefined + ); + expect(packageFile).toEqual({ + deps: [ + { + datasource: 'maven', + depName: 'com.h2database:h2-mvstore', + currentValue: '2.1.214', + }, + { + datasource: 'maven', + depName: 'org.mongodb:mongo-java-driver', + currentValue: '3.12.11', + }, + ], + }); + }); + + it('extracts the artifacts from the framework artifact section', () => { + const packageFile = extractPackageFile(frameworkArtifact, '', undefined); + expect(packageFile).toEqual({ + deps: [ + { + datasource: 'maven', + depName: 'org.apache.felix:org.apache.felix.framework', + currentValue: '7.0.5', + }, + ], + }); + }); + + it('skips depedencies with with malformed definitions', () => { + const packageFile = extractPackageFile( + malformedDefinitions, + '', + undefined + ); + expect(packageFile).toEqual({ + deps: [ + { + depName: 'commons-codec:1.15', + skipReason: 'invalid-value', + }, + { + datasource: 'maven', + depName: 'commons-codec:commons-codec', + currentValue: '1.15', + }, + ], + }); + }); + + it('skips artifacts with variables in version', () => { + const packageFile = extractPackageFile( + versionWithVariable, + '', + undefined + ); + expect(packageFile).toEqual({ + deps: [ + { + datasource: 'maven', + depName: 'com.fasterxml.jackson.core:jackson-annotations', + skipReason: 'contains-variable', + }, + ], + }); + }); + }); +}); diff --git a/lib/modules/manager/osgi/extract.ts b/lib/modules/manager/osgi/extract.ts new file mode 100644 index 0000000000..b38670b7ca --- /dev/null +++ b/lib/modules/manager/osgi/extract.ts @@ -0,0 +1,135 @@ +import is from '@sindresorhus/is'; +import * as json5 from 'json5'; +import { coerce, satisfies } from 'semver'; +import { logger } from '../../../logger'; +import { MavenDatasource } from '../../datasource/maven'; +import type { ExtractConfig, PackageDependency, PackageFile } from '../types'; +import type { Bundle, FeatureModel } from './types'; + +export function extractPackageFile( + content: string, + fileName: string, + config?: ExtractConfig +): PackageFile | null { + // References: + // - OSGi compendium release 8 ( https://docs.osgi.org/specification/osgi.cmpn/8.0.0/service.feature.html ) + // - The Sling implementation of the feature model ( https://sling.apache.org/documentation/development/feature-model.html ) + logger.trace({ fileName }, 'osgi.extractPackageFile'); + + const deps: PackageDependency[] = []; + let featureModel: FeatureModel; + try { + // Compendium R8 159.3: JS comments are supported + featureModel = json5.parse<FeatureModel>(content); + } catch (err) { + logger.warn({ fileName, err }, 'Failed to parse osgi file'); + return null; + } + + if ( + // for empty an empty result + is.nullOrUndefined(featureModel) || + // Compendium R8 159.9: resource versioning + !isSupportedFeatureResourceVersion(featureModel, fileName) + ) { + return null; + } + + // OSGi Compendium R8 159.4: bundles entry + const allBundles = featureModel.bundles ?? []; + + // The 'execution-environment' key is supported by the Sling/OSGi feature model implementation + const execEnvFramework = + featureModel['execution-environment:JSON|false']?.['framework']; + if (!is.nullOrUndefined(execEnvFramework)) { + allBundles.push(execEnvFramework); + } + + // parse custom sections + // + // Note: we do not support artifact list extensions as defined in + // section 159.7.3 yet. As of 05-12-2022, there is no implementation that + // supports this + for (const [section, value] of Object.entries(featureModel)) { + logger.trace({ fileName, section }, 'Parsing section'); + const customSectionEntries = extractArtifactList(section, value); + allBundles.push(...customSectionEntries); + } + + // convert bundles to dependencies + for (const entry of allBundles) { + const rawGav = typeof entry === 'string' ? entry : entry.id; + // skip invalid definitions, such as objects without an id set + if (!rawGav) { + continue; + } + + // both '/' and ':' are valid separators, but the Maven datasource + // expects the separator to be ':' + const gav = rawGav.replace(/\//g, ':'); + + // identifiers support 3-5 parts, see OSGi R8 - 159.2.1 Identifiers + // groupId ':' artifactId ( ':' type ( ':' classifier )? )? ':' version + const parts = gav.split(':'); + if (parts.length < 3 || parts.length > 5) { + deps.push({ + depName: gav, + skipReason: 'invalid-value', + }); + continue; + } + // parsing should use the last entry for the version + const currentValue = parts[parts.length - 1]; + const result: PackageDependency = { + datasource: MavenDatasource.id, + depName: `${parts[0]}:${parts[1]}`, + }; + if (currentValue.includes('${')) { + result.skipReason = 'contains-variable'; + } else { + result.currentValue = currentValue; + } + + deps.push(result); + } + + return deps.length ? { deps } : null; +} + +function isSupportedFeatureResourceVersion( + featureModel: FeatureModel, + fileName: string +): boolean { + const resourceVersion = featureModel['feature-resource-version']; + if (resourceVersion) { + const resourceSemVer = coerce(resourceVersion); + if (!resourceSemVer) { + logger.debug( + `Skipping file ${fileName} due to invalid feature-resource-version '${resourceVersion}'` + ); + return false; + } + + // we only support 1.x, although no over version has been defined + if (!satisfies(resourceSemVer, '^1')) { + logger.debug( + `Skipping file ${fileName} due to unsupported feature-resource-version '${resourceVersion}'` + ); + return false; + } + } + + return true; +} + +function extractArtifactList( + sectionName: string, + sectionValue: unknown +): Bundle[] { + // The 'ARTIFACTS' key is supported by the Sling/OSGi feature model implementation + if (sectionName.includes(':ARTIFACTS|') && is.array(sectionValue)) { + return sectionValue as Bundle[]; + } + + return []; +} diff --git a/lib/modules/manager/osgi/index.ts b/lib/modules/manager/osgi/index.ts new file mode 100644 index 0000000000..3fc0a553f3 --- /dev/null +++ b/lib/modules/manager/osgi/index.ts @@ -0,0 +1,12 @@ +import type { ProgrammingLanguage } from '../../../constants'; +import { MavenDatasource } from '../../datasource/maven'; + +export { extractPackageFile } from './extract'; + +export const language: ProgrammingLanguage = 'java'; + +export const defaultConfig = { + fileMatch: ['(^|/)src/main/features/.+\\.json$'], +}; + +export const supportedDatasources = [MavenDatasource.id]; diff --git a/lib/modules/manager/osgi/readme.md b/lib/modules/manager/osgi/readme.md new file mode 100644 index 0000000000..a3044df828 --- /dev/null +++ b/lib/modules/manager/osgi/readme.md @@ -0,0 +1,10 @@ +The `osgi` manager extracts dependencies from feature model definition files, typically located under `src/main/features`. +It uses the `maven` datasource to find dependency updates. + +Artifact list extensions are not supported. +For the definition of artifact list extensions, read [section 159.7.3 of the OSGi R8 spec](https://docs.osgi.org/specification/osgi.cmpn/8.0.0/service.feature.html#d0e156801). + +References: + +- [OSGi compendium release 8, Feature Service Specification](https://docs.osgi.org/specification/osgi.cmpn/8.0.0/service.feature.html) +- [The Sling implementation of the feature model](https://sling.apache.org/documentation/development/feature-model.html) diff --git a/lib/modules/manager/osgi/types.ts b/lib/modules/manager/osgi/types.ts new file mode 100644 index 0000000000..944369edce --- /dev/null +++ b/lib/modules/manager/osgi/types.ts @@ -0,0 +1,14 @@ +export interface FeatureModel { + 'feature-resource-version'?: string; + bundles?: Bundle[]; + 'execution-environment:JSON|false'?: { + framework?: Bundle; + }; + [x: string]: unknown; +} + +export type Bundle = string | BundleObject; + +export interface BundleObject { + id: string; +} -- GitLab