@elastic.io/component-build-helper
Advanced tools
Comparing version 0.1.0 to 1.0.0-dev.1
@@ -28,2 +28,4 @@ "use strict"; | ||
'gradlew', | ||
// Java > 8 | ||
'jdeps.info', | ||
// Common | ||
@@ -30,0 +32,0 @@ 'component.json', |
@@ -27,5 +27,8 @@ import { ComponentContextLoader } from './ComponentContextLoader'; | ||
export declare class GradleDockerfileGenerator extends DockerfileGenerator { | ||
readonly defaultGradleVersion = "7.4.2"; | ||
readonly SUPPORTED_JAVA_VERSIONS: string[]; | ||
readonly DEFAULT_GRADLE_VERSION = "7.4.2"; | ||
readonly JAVA_8 = "1.8"; | ||
private javaVersion; | ||
private sailorBuildGralde; | ||
private sailorBuildGradle; | ||
private predefinedJdeps; | ||
constructor(componentContextLoader: ComponentContextLoader); | ||
@@ -37,2 +40,3 @@ static detect(componentContextLoader: ComponentContextLoader): Promise<boolean>; | ||
getComponentRuntimeImage(): Promise<string> | string; | ||
private prepareJreModules; | ||
/** | ||
@@ -39,0 +43,0 @@ * Load needed information from build.gradle and calculate needed java version |
@@ -57,5 +57,8 @@ "use strict"; | ||
getLabels() { | ||
// need to stringify 2 times to get escaped json string | ||
const componentJsonLabel = JSON.stringify(JSON.stringify(this.componentJson)) | ||
// Escape substitution in the dockerfile | ||
.split('$').join('\\$'); | ||
return [ | ||
// need to stringify 2 times to get escaped json string | ||
`LABEL elastic.io.component=${this.prepareLabel(JSON.stringify(JSON.stringify(this.componentJson)))}`, | ||
`LABEL elastic.io.component=${this.prepareLabel(componentJsonLabel)}`, | ||
`LABEL elastic.io.logo="${this.prepareLabel(this._icon)}"` | ||
@@ -88,3 +91,5 @@ ]; | ||
super(componentContextLoader); | ||
this.defaultGradleVersion = '7.4.2'; | ||
this.SUPPORTED_JAVA_VERSIONS = ['1.8', '11', '17', '18']; | ||
this.DEFAULT_GRADLE_VERSION = '7.4.2'; | ||
this.JAVA_8 = '1.8'; | ||
this._componentLanguage = index_1.COMPONENT_LANGUAGE.JAVA; | ||
@@ -108,15 +113,15 @@ } | ||
// installing gradle, because wrapper is not exists in the component | ||
gradle.push(`FROM amazoncorretto:${this.getComponentBuildImage()} as gradle`, `ARG GRADLE_VERSION=${this.defaultGradleVersion}`, 'ENV GRADLE_ARCHIVE gradle-$GRADLE_VERSION-bin.zip', 'WORKDIR /opt', 'RUN apk update && apk add --no-cache wget libstdc++ && rm -rf /var/cache/apk/*', 'RUN wget --no-verbose https://services.gradle.org/distributions/$GRADLE_ARCHIVE', 'RUN unzip -q $GRADLE_ARCHIVE && rm $GRADLE_ARCHIVE', 'ENV PATH "$PATH:/opt/gradle-$GRADLE_VERSION/bin/"'); | ||
gradle.push(`FROM ${this.getComponentBuildImage()} as gradle`, `ARG GRADLE_VERSION=${this.DEFAULT_GRADLE_VERSION}`, 'ENV GRADLE_ARCHIVE gradle-$GRADLE_VERSION-bin.zip', 'WORKDIR /opt', 'RUN apk update && apk add --no-cache wget libstdc++ && rm -rf /var/cache/apk/*', 'RUN wget --no-verbose https://services.gradle.org/distributions/$GRADLE_ARCHIVE', 'RUN unzip -q $GRADLE_ARCHIVE && rm $GRADLE_ARCHIVE', 'ENV PATH "$PATH:/opt/gradle-$GRADLE_VERSION/bin/"'); | ||
build.push('FROM gradle as build'); | ||
} | ||
else { | ||
build.push(`FROM amazoncorretto:${this.getComponentBuildImage()} as build`); | ||
build.push(`FROM ${this.getComponentBuildImage()} as build`); | ||
} | ||
build.push('ARG CLASSES_MAIN_DIR=./build/classes/main', 'RUN apk add --update tini python3 make g++ && rm -rf /var/cache/apk/*', 'WORKDIR /home/apprunner', 'COPY ./ ./', | ||
build.push('ARG CLASSES_MAIN_DIR=./build/classes/main', 'RUN apk add --update git tini python3 make g++ && rm -rf /var/cache/apk/*', 'WORKDIR /home/apprunner', 'COPY ./ ./', | ||
// https://stackoverflow.com/questions/33439230/how-to-write-commands-with-multiple-lines-in-dockerfile-while-preserving-the-new | ||
`RUN echo $'${this.sailorBuildGralde.replace(/\n/g, '\\n\\\n')}' > sailor_jvm_build.gradle`, `RUN ${this.isWrapperExists() ? './gradlew' : 'gradle'} clean prepareSailorWithDependencies -b ./sailor_jvm_build.gradle`, 'COPY ./component.json $CLASSES_MAIN_DIR'); | ||
`RUN echo $'${this.sailorBuildGradle.replace(/\n/g, '\\n\\\n')}' > sailor_jvm_build.gradle`, `RUN ${this.isWrapperExists() ? './gradlew' : 'gradle'} clean prepareSailorWithDependencies -b ./sailor_jvm_build.gradle`, 'COPY ./component.json $CLASSES_MAIN_DIR', this.prepareJreModules()); | ||
return [ | ||
...gradle, | ||
...build, | ||
`FROM amazoncorretto:${this.getComponentRuntimeImage()}`, | ||
`FROM ${this.getComponentRuntimeImage()}`, | ||
'ARG CLASSES_MAIN_DIR=./build/classes/main', | ||
@@ -126,2 +131,4 @@ 'ARG RESOURCES_MAIN_DIR=./build/resources/main', | ||
'ENV CLASSPATH $CLASSES_MAIN_DIR:$RESOURCES_MAIN_DIR:$DEPENDENCIES_DIR/*', | ||
// add generated jre to the path | ||
this.javaVersion === this.JAVA_8 ? '' : 'ENV PATH="/home/apprunner/jre/bin:$PATH"', | ||
...this.getLabels(), | ||
@@ -131,2 +138,3 @@ 'RUN addgroup -S apprunner && adduser -S apprunner -G apprunner -h /home/apprunner', | ||
'USER apprunner', | ||
'COPY --from=build --chown=apprunner /home/apprunner/jre /home/apprunner/jre', | ||
'COPY --from=build --chown=apprunner /home/apprunner/build /home/apprunner/build', | ||
@@ -138,13 +146,29 @@ 'COPY --from=build --chown=apprunner /sbin/tini /sbin/tini', | ||
getComponentBuildImage() { | ||
if (this.javaVersion === '1.8') { | ||
return `8-alpine-jdk`; | ||
if (this.javaVersion === this.JAVA_8) { | ||
return `amazoncorretto:8-alpine-jdk`; | ||
} | ||
return `${this.javaVersion}-alpine-jdk`; | ||
return `amazoncorretto:${this.javaVersion}-alpine-jdk`; | ||
} | ||
getComponentRuntimeImage() { | ||
if (this.javaVersion === '1.8') { | ||
return `8-alpine-jre`; | ||
if (this.javaVersion === this.JAVA_8) { | ||
return `amazoncorretto:8-alpine-jre`; | ||
} | ||
return `${this.javaVersion}-alpine-jdk`; | ||
return `alpine:latest`; | ||
} | ||
prepareJreModules() { | ||
// do not calc modules for java 8 | ||
if (this.javaVersion === this.JAVA_8) { | ||
return ''; | ||
} | ||
const calculatedModules = []; | ||
// in case jdeps.info file is found in the sources - use it | ||
if (this.predefinedJdeps) { | ||
calculatedModules.push(`RUN echo "${this.predefinedJdeps.replace(/\n/g, '\\n\\\n')}" > jdeps.info`); | ||
} | ||
else { | ||
calculatedModules.push(`RUN jdeps --ignore-missing-deps --print-module-deps -q --multi-release ${this.javaVersion} --class-path ./build/dependencies/* ./build/classes/main > jdeps.info`); | ||
} | ||
calculatedModules.push(`RUN jlink --verbose --compress 2 --strip-debug --no-header-files --no-man-pages --output jre --add-modules $(cat jdeps.info)`); | ||
return calculatedModules.join('\n'); | ||
} | ||
/** | ||
@@ -158,9 +182,10 @@ * Load needed information from build.gradle and calculate needed java version | ||
// build/elasticio/dependencies/ folder | ||
this.sailorBuildGralde = (await fs_1.promises.readFile(`${path_1.default.resolve(__dirname, '../component/sailor_jvm_build.gradle')}`)) | ||
this.sailorBuildGradle = (await fs_1.promises.readFile(`${path_1.default.resolve(__dirname, '../component/sailor_jvm_build.gradle').toString()}`)) | ||
.toString(); | ||
// build.gradle is provided by component developer | ||
const buildGradle = await parser_1.default.parseText(context['build.gradle']); | ||
// TODO allow read version from buildGradle.targetCompatibility || buildGradle.sourceCompatibility | ||
// after generateDockerfile will support java versions >=11 | ||
this.javaVersion = '1.8'; | ||
this.javaVersion = buildGradle.targetCompatibility || '17'; | ||
if (!this.SUPPORTED_JAVA_VERSIONS.includes(this.javaVersion)) { | ||
throw new Error(`Unsupported Java version. Allowed values: ${JSON.stringify(this.SUPPORTED_JAVA_VERSIONS)}`); | ||
} | ||
this._sailorVersion = buildGradle.dependencies.find(item => item.name === 'sailor-jvm' && item.group === 'io.elastic').version; | ||
@@ -170,2 +195,4 @@ if (!this._sailorVersion) { | ||
} | ||
// jdeps.info can be used to specify the list of needed for component runtime env java modules | ||
this.predefinedJdeps = this.componentContextLoader.context['jdeps.info']; | ||
this.checkIsDockerfilePredefined(); | ||
@@ -201,3 +228,3 @@ this.initialized = true; | ||
`FROM ${this.getComponentBuildImage()} AS base`, | ||
'RUN apk add --update tini python3 make g++ && rm -rf /var/cache/apk/*', | ||
'RUN apk add --update git tini python3 make g++ && rm -rf /var/cache/apk/*', | ||
'WORKDIR /home/node', | ||
@@ -204,0 +231,0 @@ `COPY --chown=node:node . ./`, |
{ | ||
"name": "@elastic.io/component-build-helper", | ||
"version": "0.1.0", | ||
"version": "1.0.0-dev.1", | ||
"description": "Helpers for the component build process", | ||
@@ -5,0 +5,0 @@ "main": "dist/src/index.js", |
@@ -18,2 +18,3 @@ { | ||
"title": "Executes custom code", | ||
"placeholder": "INSERT INTO films (code,title,kind) VALUES (${code},${title},${kind})", | ||
"metadata" : { | ||
@@ -20,0 +21,0 @@ "in" : "./in/in-metadata.json" |
@@ -14,3 +14,3 @@ import sinon, { SinonSandbox } from 'sinon'; | ||
gradleDockerfile, | ||
gradleWrapperDockerfile, labelsWithSplittedLogo, | ||
gradleWrapperDockerfile, java11Dockerfile, java11PredefinedJdepsInfo, labelsWithSplittedLogo, | ||
predefinedDockerfile | ||
@@ -87,3 +87,3 @@ } from '../fixtures/DockerfileGenerator.data'; | ||
'FROM node:16-alpine AS base\n' + | ||
'RUN apk add --update tini python3 make g++ && rm -rf /var/cache/apk/*\n' + | ||
'RUN apk add --update git tini python3 make g++ && rm -rf /var/cache/apk/*\n' + | ||
'WORKDIR /home/node\n' + | ||
@@ -100,3 +100,3 @@ 'COPY --chown=node:node . ./\n' + | ||
'FROM node:16-alpine AS release\n' + | ||
`LABEL elastic.io.component=${JSON.stringify(JSON.stringify(componentJsonLabel))}\n` + | ||
`LABEL elastic.io.component=${componentJsonLabel}\n` + | ||
`LABEL elastic.io.logo="${logo}"\n` + | ||
@@ -117,3 +117,3 @@ 'WORKDIR /home/node\n' + | ||
await expect(dockerfileGenerator.genDockerfile()).to.eventually.equal(predefinedDockerfile + | ||
`\nLABEL elastic.io.component=${JSON.stringify(JSON.stringify(componentJsonLabel))}\n` + | ||
`\nLABEL elastic.io.component=${componentJsonLabel}\n` + | ||
`LABEL elastic.io.logo="${logo}"`); | ||
@@ -138,3 +138,3 @@ }); | ||
'FROM node:15-alpine AS base\n' + | ||
'RUN apk add --update tini python3 make g++ && rm -rf /var/cache/apk/*\n' + | ||
'RUN apk add --update git tini python3 make g++ && rm -rf /var/cache/apk/*\n' + | ||
'WORKDIR /home/node\n' + | ||
@@ -249,4 +249,34 @@ 'COPY --chown=node:node . ./\n' + | ||
}); | ||
it('genDockerfile should return dockerfile with jdeps/jlink dockerfile for java version >=11', async () => { | ||
const path = './spec/fixtures/component/gradle'; | ||
const readFileStub = sandbox.stub(fsPromise, 'readFile'); | ||
readFileStub.withArgs(`${path}/build.Gradle`).resolves(await fsPromise.readFile('./spec/fixtures/java11/build.gradle')); | ||
readFileStub.callThrough(); | ||
componentContextLoader = await ComponentContextLoader.getInstance(path); | ||
dockerfileGenerator = await DockerfileGenerator.getInstance(componentContextLoader); | ||
await expect(dockerfileGenerator.genDockerfile()).to.be.equal(java11Dockerfile); | ||
}); | ||
it('genDockerfile should return dockerfile with jdeps/jlink dockerfile with predefined jdeps.info', async () => { | ||
const path = './spec/fixtures/component/gradle'; | ||
const buildGradle = await fsPromise.readFile('./spec/fixtures/java11/build.gradle'); | ||
const jdepsInfo = await fsPromise.readFile('./spec/fixtures/java11/jdeps.info'); | ||
const readFileStub = sandbox.stub(fsPromise, 'readFile'); | ||
readFileStub.withArgs(`${path}/build.gradle`).resolves(buildGradle); | ||
readFileStub.withArgs(`${path}/jdeps.info`).resolves(jdepsInfo); | ||
readFileStub.callThrough(); | ||
componentContextLoader = await ComponentContextLoader.getInstance(path); | ||
dockerfileGenerator = await DockerfileGenerator.getInstance(componentContextLoader); | ||
await expect(dockerfileGenerator.genDockerfile()).to.be.equal(java11PredefinedJdepsInfo); | ||
}); | ||
it('genDockerfile should get an error in case unsupported java version specified', async () => { | ||
const path = './spec/fixtures/component/gradle'; | ||
const buildGradle = await fsPromise.readFile('./spec/fixtures/invalid-java-version.gradle'); | ||
const readFileStub = sandbox.stub(fsPromise, 'readFile'); | ||
readFileStub.withArgs(`${path}/build.gradle`).resolves(buildGradle); | ||
readFileStub.callThrough(); | ||
componentContextLoader = await ComponentContextLoader.getInstance(path); | ||
await expect(DockerfileGenerator.getInstance(componentContextLoader)).to.be.rejectedWith('Unsupported Java version. Allowed values: ["1.8","11","17","18"]'); | ||
}); | ||
}); | ||
}); | ||
}); |
@@ -19,2 +19,4 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ | ||
'gradlew', | ||
// Java > 8 | ||
'jdeps.info', | ||
// Common | ||
@@ -21,0 +23,0 @@ 'component.json', |
@@ -10,2 +10,3 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ | ||
import path from 'path'; | ||
export abstract class DockerfileGenerator { | ||
@@ -76,5 +77,8 @@ protected _componentLanguage: COMPONENT_LANGUAGE; | ||
protected getLabels() { | ||
// need to stringify 2 times to get escaped json string | ||
const componentJsonLabel = JSON.stringify(JSON.stringify(this.componentJson)) | ||
// Escape substitution in the dockerfile | ||
.split('$').join('\\$'); | ||
return [ | ||
// need to stringify 2 times to get escaped json string | ||
`LABEL elastic.io.component=${this.prepareLabel(JSON.stringify(JSON.stringify(this.componentJson)))}`, | ||
`LABEL elastic.io.component=${this.prepareLabel(componentJsonLabel)}`, | ||
`LABEL elastic.io.logo="${this.prepareLabel(this._icon)}"` | ||
@@ -107,5 +111,8 @@ ]; | ||
export class GradleDockerfileGenerator extends DockerfileGenerator { | ||
readonly defaultGradleVersion = '7.4.2'; | ||
private javaVersion: '1.8' | '11' | '17'; | ||
private sailorBuildGralde: string; | ||
readonly SUPPORTED_JAVA_VERSIONS = ['1.8', '11', '17', '18']; | ||
readonly DEFAULT_GRADLE_VERSION = '7.4.2'; | ||
readonly JAVA_8 = '1.8'; | ||
private javaVersion: '1.8' | '11' | '17' | '18'; | ||
private sailorBuildGradle: string; | ||
private predefinedJdeps: string; | ||
@@ -137,4 +144,4 @@ constructor(componentContextLoader: ComponentContextLoader) { | ||
gradle.push( | ||
`FROM amazoncorretto:${this.getComponentBuildImage()} as gradle`, | ||
`ARG GRADLE_VERSION=${this.defaultGradleVersion}`, | ||
`FROM ${this.getComponentBuildImage()} as gradle`, | ||
`ARG GRADLE_VERSION=${this.DEFAULT_GRADLE_VERSION}`, | ||
'ENV GRADLE_ARCHIVE gradle-$GRADLE_VERSION-bin.zip', | ||
@@ -152,3 +159,3 @@ 'WORKDIR /opt', | ||
build.push( | ||
`FROM amazoncorretto:${this.getComponentBuildImage()} as build` | ||
`FROM ${this.getComponentBuildImage()} as build` | ||
); | ||
@@ -158,9 +165,10 @@ } | ||
'ARG CLASSES_MAIN_DIR=./build/classes/main', | ||
'RUN apk add --update tini python3 make g++ && rm -rf /var/cache/apk/*', | ||
'RUN apk add --update git tini python3 make g++ && rm -rf /var/cache/apk/*', | ||
'WORKDIR /home/apprunner', | ||
'COPY ./ ./', | ||
// https://stackoverflow.com/questions/33439230/how-to-write-commands-with-multiple-lines-in-dockerfile-while-preserving-the-new | ||
`RUN echo $'${this.sailorBuildGralde.replace(/\n/g, '\\n\\\n')}' > sailor_jvm_build.gradle`, | ||
`RUN echo $'${this.sailorBuildGradle.replace(/\n/g, '\\n\\\n')}' > sailor_jvm_build.gradle`, | ||
`RUN ${this.isWrapperExists() ? './gradlew' : 'gradle'} clean prepareSailorWithDependencies -b ./sailor_jvm_build.gradle`, | ||
'COPY ./component.json $CLASSES_MAIN_DIR' | ||
'COPY ./component.json $CLASSES_MAIN_DIR', | ||
this.prepareJreModules() | ||
); | ||
@@ -171,3 +179,3 @@ | ||
...build, | ||
`FROM amazoncorretto:${this.getComponentRuntimeImage()}`, | ||
`FROM ${this.getComponentRuntimeImage()}`, | ||
'ARG CLASSES_MAIN_DIR=./build/classes/main', | ||
@@ -177,2 +185,4 @@ 'ARG RESOURCES_MAIN_DIR=./build/resources/main', | ||
'ENV CLASSPATH $CLASSES_MAIN_DIR:$RESOURCES_MAIN_DIR:$DEPENDENCIES_DIR/*', | ||
// add generated jre to the path | ||
this.javaVersion === this.JAVA_8 ? '' : 'ENV PATH="/home/apprunner/jre/bin:$PATH"', | ||
...this.getLabels(), | ||
@@ -182,2 +192,3 @@ 'RUN addgroup -S apprunner && adduser -S apprunner -G apprunner -h /home/apprunner', | ||
'USER apprunner', | ||
'COPY --from=build --chown=apprunner /home/apprunner/jre /home/apprunner/jre', | ||
'COPY --from=build --chown=apprunner /home/apprunner/build /home/apprunner/build', | ||
@@ -190,15 +201,32 @@ 'COPY --from=build --chown=apprunner /sbin/tini /sbin/tini', | ||
getComponentBuildImage(): Promise<string> | string { | ||
if (this.javaVersion === '1.8') { | ||
return `8-alpine-jdk`; | ||
if (this.javaVersion === this.JAVA_8) { | ||
return `amazoncorretto:8-alpine-jdk`; | ||
} | ||
return `${this.javaVersion}-alpine-jdk`; | ||
return `amazoncorretto:${this.javaVersion}-alpine-jdk`; | ||
} | ||
getComponentRuntimeImage(): Promise<string> | string { | ||
if (this.javaVersion === '1.8') { | ||
return `8-alpine-jre`; | ||
if (this.javaVersion === this.JAVA_8) { | ||
return `amazoncorretto:8-alpine-jre`; | ||
} | ||
return `${this.javaVersion}-alpine-jdk`; | ||
return `alpine:latest`; | ||
} | ||
private prepareJreModules() { | ||
// do not calc modules for java 8 | ||
if (this.javaVersion === this.JAVA_8) { | ||
return ''; | ||
} | ||
const calculatedModules = []; | ||
// in case jdeps.info file is found in the sources - use it | ||
if (this.predefinedJdeps) { | ||
calculatedModules.push(`RUN echo "${this.predefinedJdeps.replace(/\n/g, '\\n\\\n')}" > jdeps.info`); | ||
} else { | ||
calculatedModules.push(`RUN jdeps --ignore-missing-deps --print-module-deps -q --multi-release ${this.javaVersion} --class-path ./build/dependencies/* ./build/classes/main > jdeps.info`); | ||
} | ||
calculatedModules.push(`RUN jlink --verbose --compress 2 --strip-debug --no-header-files --no-man-pages --output jre --add-modules $(cat jdeps.info)`); | ||
return calculatedModules.join('\n'); | ||
} | ||
/** | ||
@@ -212,9 +240,10 @@ * Load needed information from build.gradle and calculate needed java version | ||
// build/elasticio/dependencies/ folder | ||
this.sailorBuildGralde = (await fs.readFile(`${path.resolve(__dirname, '../component/sailor_jvm_build.gradle')}`)) | ||
this.sailorBuildGradle = (await fs.readFile(`${path.resolve(__dirname, '../component/sailor_jvm_build.gradle').toString()}`)) | ||
.toString(); | ||
// build.gradle is provided by component developer | ||
const buildGradle = await g2js.parseText(context['build.gradle']); | ||
// TODO allow read version from buildGradle.targetCompatibility || buildGradle.sourceCompatibility | ||
// after generateDockerfile will support java versions >=11 | ||
this.javaVersion = '1.8'; | ||
this.javaVersion = buildGradle.targetCompatibility || '17'; | ||
if (!this.SUPPORTED_JAVA_VERSIONS.includes(this.javaVersion)) { | ||
throw new Error(`Unsupported Java version. Allowed values: ${JSON.stringify(this.SUPPORTED_JAVA_VERSIONS)}`); | ||
} | ||
this._sailorVersion = buildGradle.dependencies.find(item => | ||
@@ -225,2 +254,4 @@ item.name === 'sailor-jvm' && item.group === 'io.elastic').version; | ||
} | ||
// jdeps.info can be used to specify the list of needed for component runtime env java modules | ||
this.predefinedJdeps = this.componentContextLoader.context['jdeps.info']; | ||
this.checkIsDockerfilePredefined(); | ||
@@ -262,3 +293,3 @@ this.initialized = true; | ||
`FROM ${this.getComponentBuildImage()} AS base`, | ||
'RUN apk add --update tini python3 make g++ && rm -rf /var/cache/apk/*', | ||
'RUN apk add --update git tini python3 make g++ && rm -rf /var/cache/apk/*', | ||
'WORKDIR /home/node', | ||
@@ -265,0 +296,0 @@ `COPY --chown=node:node . ./`, |
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
776472
77
3461