import * as plugins from './mod.plugins.js'; import * as paths from '../npmci.paths.js'; import { logger } from '../npmci.logging.js'; import { bash } from '../npmci.bash.js'; import { DockerRegistry } from './mod.classes.dockerregistry.js'; import * as helpers from './mod.helpers.js'; import { NpmciDockerManager } from './index.js'; import { Npmci } from '../npmci.classes.npmci.js'; /** * class Dockerfile represents a Dockerfile on disk in npmci */ export class Dockerfile { // STATIC /** * creates instance of class Dockerfile for all Dockerfiles in cwd * @returns Promise */ public static async readDockerfiles( npmciDockerManagerRefArg: NpmciDockerManager ): Promise { const fileTree = await plugins.smartfile.fs.listFileTree(paths.cwd, 'Dockerfile*'); // create the Dockerfile array const readDockerfilesArray: Dockerfile[] = []; logger.log('info', `found ${fileTree.length} Dockerfiles:`); console.log(fileTree); for (const dockerfilePath of fileTree) { const myDockerfile = new Dockerfile(npmciDockerManagerRefArg, { filePath: dockerfilePath, read: true, }); readDockerfilesArray.push(myDockerfile); } return readDockerfilesArray; } /** * Sorts Dockerfiles into a build order based on dependencies. * @param dockerfiles An array of Dockerfile instances. * @returns A Promise that resolves to a sorted array of Dockerfiles. */ public static async sortDockerfiles(dockerfiles: Dockerfile[]): Promise { logger.log('info', 'Sorting Dockerfiles based on dependencies...'); // Map from cleanTag to Dockerfile instance for quick lookup const tagToDockerfile = new Map(); dockerfiles.forEach((dockerfile) => { tagToDockerfile.set(dockerfile.cleanTag, dockerfile); }); // Build the dependency graph const graph = new Map(); dockerfiles.forEach((dockerfile) => { const dependencies: Dockerfile[] = []; const baseImage = dockerfile.baseImage; // Check if the baseImage is among the local Dockerfiles if (tagToDockerfile.has(baseImage)) { const baseDockerfile = tagToDockerfile.get(baseImage); dependencies.push(baseDockerfile); dockerfile.localBaseImageDependent = true; dockerfile.localBaseDockerfile = baseDockerfile; } graph.set(dockerfile, dependencies); }); // Perform topological sort const sortedDockerfiles: Dockerfile[] = []; const visited = new Set(); const tempMarked = new Set(); const visit = (dockerfile: Dockerfile) => { if (tempMarked.has(dockerfile)) { throw new Error(`Circular dependency detected involving ${dockerfile.cleanTag}`); } if (!visited.has(dockerfile)) { tempMarked.add(dockerfile); const dependencies = graph.get(dockerfile) || []; dependencies.forEach((dep) => visit(dep)); tempMarked.delete(dockerfile); visited.add(dockerfile); sortedDockerfiles.push(dockerfile); } }; try { dockerfiles.forEach((dockerfile) => { if (!visited.has(dockerfile)) { visit(dockerfile); } }); } catch (error) { logger.log('error', error.message); throw error; } // Log the sorted order sortedDockerfiles.forEach((dockerfile, index) => { logger.log('info', `Build order ${index + 1}: ${dockerfile.cleanTag} with base image ${dockerfile.baseImage}`); }); return sortedDockerfiles; } /** * maps local Dockerfiles dependencies to the correspoding Dockerfile class instances */ public static async mapDockerfiles(sortedDockerfileArray: Dockerfile[]): Promise { sortedDockerfileArray.forEach((dockerfileArg) => { if (dockerfileArg.localBaseImageDependent) { sortedDockerfileArray.forEach((dockfile2: Dockerfile) => { if (dockfile2.cleanTag === dockerfileArg.baseImage) { dockerfileArg.localBaseDockerfile = dockfile2; } }); } }); return sortedDockerfileArray; } /** * builds the correspoding real docker image for each Dockerfile class instance */ public static async buildDockerfiles(sortedArrayArg: Dockerfile[]) { for (const dockerfileArg of sortedArrayArg) { await dockerfileArg.build(); } return sortedArrayArg; } /** * tests all Dockerfiles in by calling class Dockerfile.test(); * @param sortedArrayArg Dockerfile[] that contains all Dockerfiles in cwd */ public static async testDockerfiles(sortedArrayArg: Dockerfile[]) { for (const dockerfileArg of sortedArrayArg) { await dockerfileArg.test(); } return sortedArrayArg; } /** * returns a version for a docker file * @execution SYNC */ public static dockerFileVersion( dockerfileInstanceArg: Dockerfile, dockerfileNameArg: string ): string { let versionString: string; const versionRegex = /Dockerfile_(.+)$/; const regexResultArray = versionRegex.exec(dockerfileNameArg); if (regexResultArray && regexResultArray.length === 2) { versionString = regexResultArray[1]; } else { versionString = 'latest'; } versionString = versionString.replace( '##version##', dockerfileInstanceArg.npmciDockerManagerRef.npmciRef.npmciConfig.getConfig().projectInfo.npm .version ); return versionString; } /** * returns the docker base image for a Dockerfile */ public static dockerBaseImage(dockerfileContentArg: string): string { const baseImageRegex = /FROM\s([a-zA-z0-9\/\-\:]*)\n?/; const regexResultArray = baseImageRegex.exec(dockerfileContentArg); return regexResultArray[1]; } /** * returns the docker tag */ public static getDockerTagString( npmciDockerManagerRef: NpmciDockerManager, registryArg: string, repoArg: string, versionArg: string, suffixArg?: string ): string { // determine wether the repo should be mapped accordingly to the registry const mappedRepo = npmciDockerManagerRef.npmciRef.npmciConfig.getConfig().dockerRegistryRepoMap[registryArg]; const repo = (() => { if (mappedRepo) { return mappedRepo; } else { return repoArg; } })(); // determine wether the version contais a suffix let version = versionArg; if (suffixArg) { version = versionArg + '_' + suffixArg; } const tagString = `${registryArg}/${repo}:${version}`; return tagString; } public static async getDockerBuildArgs( npmciDockerManagerRef: NpmciDockerManager ): Promise { logger.log('info', 'checking for env vars to be supplied to the docker build'); let buildArgsString: string = ''; for (const dockerArgKey of Object.keys( npmciDockerManagerRef.npmciRef.npmciConfig.getConfig().dockerBuildargEnvMap )) { const dockerArgOuterEnvVar = npmciDockerManagerRef.npmciRef.npmciConfig.getConfig().dockerBuildargEnvMap[dockerArgKey]; logger.log( 'note', `docker ARG "${dockerArgKey}" maps to outer env var "${dockerArgOuterEnvVar}"` ); const targetValue = process.env[dockerArgOuterEnvVar]; buildArgsString = `${buildArgsString} --build-arg ${dockerArgKey}="${targetValue}"`; } return buildArgsString; } // INSTANCE public npmciDockerManagerRef: NpmciDockerManager; public filePath: string; public repo: string; public version: string; public cleanTag: string; public buildTag: string; public pushTag: string; public containerName: string; public content: string; public baseImage: string; public localBaseImageDependent: boolean; public localBaseDockerfile: Dockerfile; constructor( dockerManagerRefArg: NpmciDockerManager, options: { filePath?: string; fileContents?: string | Buffer; read?: boolean } ) { this.npmciDockerManagerRef = dockerManagerRefArg; this.filePath = options.filePath; this.repo = this.npmciDockerManagerRef.npmciRef.npmciEnv.repo.user + '/' + this.npmciDockerManagerRef.npmciRef.npmciEnv.repo.repo; this.version = Dockerfile.dockerFileVersion(this, plugins.path.parse(options.filePath).base); this.cleanTag = this.repo + ':' + this.version; this.buildTag = this.cleanTag; this.containerName = 'dockerfile-' + this.version; if (options.filePath && options.read) { this.content = plugins.smartfile.fs.toStringSync(plugins.path.resolve(options.filePath)); } this.baseImage = Dockerfile.dockerBaseImage(this.content); this.localBaseImageDependent = false; } /** * builds the Dockerfile */ public async build() { logger.log('info', 'now building Dockerfile for ' + this.cleanTag); const buildArgsString = await Dockerfile.getDockerBuildArgs(this.npmciDockerManagerRef); const buildCommand = `docker build --label="version=${ this.npmciDockerManagerRef.npmciRef.npmciConfig.getConfig().projectInfo.npm.version }" -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`; await bash(buildCommand); return; } /** * pushes the Dockerfile to a registry */ public async push(dockerRegistryArg: DockerRegistry, versionSuffix: string = null) { this.pushTag = Dockerfile.getDockerTagString( this.npmciDockerManagerRef, dockerRegistryArg.registryUrl, this.repo, this.version, versionSuffix ); await bash(`docker tag ${this.buildTag} ${this.pushTag}`); await bash(`docker push ${this.pushTag}`); const imageDigest = ( await bash(`docker inspect --format="{{index .RepoDigests 0}}" ${this.pushTag}`) ).split('@')[1]; console.log(`The image ${this.pushTag} has digest ${imageDigest}`); await this.npmciDockerManagerRef.npmciRef.cloudlyConnector.announceDockerContainer({ registryUrl: this.pushTag, tag: this.buildTag, labels: [], version: this.npmciDockerManagerRef.npmciRef.npmciConfig.getConfig().projectInfo.npm.version, }); await this.npmciDockerManagerRef.npmciRef.npmciConfig.kvStorage.writeKey( 'latestPushedDockerTag', this.pushTag ); } /** * pulls the Dockerfile from a registry */ public async pull(registryArg: DockerRegistry, versionSuffixArg: string = null) { const pullTag = Dockerfile.getDockerTagString( this.npmciDockerManagerRef, registryArg.registryUrl, this.repo, this.version, versionSuffixArg ); await bash(`docker pull ${pullTag}`); await bash(`docker tag ${pullTag} ${this.buildTag}`); } /** * tests the Dockerfile; */ public async test() { const testFile: string = plugins.path.join(paths.NpmciTestDir, 'test_' + this.version + '.sh'); const testFileExists: boolean = plugins.smartfile.fs.fileExistsSync(testFile); if (testFileExists) { // run tests await bash( `docker run --name npmci_test_container --entrypoint="bash" ${this.buildTag} -c "mkdir /npmci_test"` ); await bash(`docker cp ${testFile} npmci_test_container:/npmci_test/test.sh`); await bash(`docker commit npmci_test_container npmci_test_image`); await bash(`docker run --entrypoint="bash" npmci_test_image -x /npmci_test/test.sh`); await bash(`docker rm npmci_test_container`); await bash(`docker rmi --force npmci_test_image`); } else { logger.log('warn', 'skipping tests for ' + this.cleanTag + ' because no testfile was found!'); } } /** * gets the id of a Dockerfile */ public async getId() { const containerId = await bash( 'docker inspect --type=image --format="{{.Id}}" ' + this.buildTag ); return containerId; } }