UNPKG

snyk-docker-plugin

Version:
252 lines 10 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.pullIfNotLocal = exports.extractImageDetails = exports.getImageArchive = void 0; const Debug = require("debug"); const fs = require("fs"); const mkdirp = require("mkdirp"); const path = require("path"); const docker_1 = require("../docker"); const image_1 = require("../extractor/image"); const debug = Debug("snyk"); async function getInspectResult(docker, targetImage) { const info = await docker.inspectImage(targetImage); return JSON.parse(info.stdout)[0]; } function cleanupCallback(imageFolderPath, imageName) { return () => { const fullImagePath = path.join(imageFolderPath, imageName); if (fs.existsSync(fullImagePath)) { fs.unlinkSync(fullImagePath); } try { fs.rmdirSync(imageFolderPath); } catch (err) { debug(`Can't remove folder ${imageFolderPath}, got error ${err.message}`); } }; } async function pullWithDockerBinary(docker, targetImage, saveLocation, username, password, platform) { let pullAndSaveSuccessful = false; try { if (username || password) { debug("using local docker binary credentials. the credentials you provided will be ignored"); } await docker.pullCli(targetImage, { platform }); await docker.save(targetImage, saveLocation); return (pullAndSaveSuccessful = true); } catch (err) { debug(`couldn't pull ${targetImage} using docker binary: ${err.message}`); handleDockerPullError(err.stderr, platform); return pullAndSaveSuccessful; } } function handleDockerPullError(err, platform) { if (err && err.includes("unknown operating system or architecture")) { throw new Error("Unknown operating system or architecture"); } if (err.includes("operating system is not supported")) { throw new Error(`Operating system is not supported`); } const unknownManifestConditions = [ "no matching manifest for", "manifest unknown", ]; if (unknownManifestConditions.some((value) => err.includes(value))) { if (platform) { throw new Error(`The image does not exist for ${platform}`); } throw new Error(`The image does not exist for the current platform`); } if (err.includes("invalid reference format")) { throw new Error(`invalid image format`); } if (err.includes("unknown flag: --platform")) { throw new Error('"--platform" is only supported on a Docker daemon with version later than 17.09'); } if (err === '"--platform" is only supported on a Docker daemon with experimental features enabled') { throw new Error(err); } } async function pullFromContainerRegistry(docker, targetImage, imageSavePath, username, password, platform) { const { hostname, imageName, tag } = extractImageDetails(targetImage); debug(`Attempting to pull: registry: ${hostname}, image: ${imageName}, tag: ${tag}`); try { return await docker.pull(hostname, imageName, tag, imageSavePath, username, password, platform); } catch (err) { handleDockerPullError(err.message); throw err; } } async function pullImage(docker, targetImage, saveLocation, imageSavePath, username, password, platform) { if (await docker_1.Docker.binaryExists()) { const pullAndSaveSuccessful = await pullWithDockerBinary(docker, targetImage, saveLocation, username, password, platform); if (pullAndSaveSuccessful) { return new image_1.ImageName(targetImage); } } const { indexDigest, manifestDigest } = await pullFromContainerRegistry(docker, targetImage, imageSavePath, username, password, platform); const imageName = new image_1.ImageName(targetImage, { manifest: manifestDigest, index: indexDigest, }); return imageName; } /** * In the case that an `ImageType.Identifier` is detected we need to produce * an image archive, either by saving the image if it's already loaded into * the local docker daemon, or by pulling the image from a remote registry and * saving it to the filesystem directly. * * Users may also provide us with a URL to an image in a Docker compatible * remote registry. * * @param {string} targetImage - The image to test, this could be in one of * the following forms: * * [registry/]<repo>/<image>[:tag] * * <repo>/<image>[:tag] * * <image>[:tag] * In the case that a registry is not provided, the plugin will default * this to Docker Hub. If a tag is not provided this will default to * `latest`. * @param {string} [username] - Optional username for private repo auth. * @param {string} [password] - Optional password for private repo auth. * @param {string} [platform] - Optional platform parameter to pull specific image arch. */ async function getImageArchive(targetImage, imageSavePath, username, password, platform) { const docker = new docker_1.Docker(); mkdirp.sync(imageSavePath); const destination = { name: imageSavePath, removeCallback: cleanupCallback(imageSavePath, "image.tar"), }; const saveLocation = path.join(destination.name, "image.tar"); let inspectResult; try { inspectResult = await getInspectResult(docker, targetImage); } catch (_a) { debug(`${targetImage} does not exist locally, proceeding to pull image.`); } if (inspectResult === undefined) { const imageName = await pullImage(docker, targetImage, saveLocation, imageSavePath, username, password, platform); return { imageName, path: saveLocation, removeArchive: destination.removeCallback, }; } if (platform !== undefined && inspectResult && !isLocalImageSameArchitecture(platform, inspectResult.Architecture)) { const imageName = await pullImage(docker, targetImage, saveLocation, imageSavePath, username, password, platform); return { imageName, path: saveLocation, removeArchive: destination.removeCallback, }; } else { await docker.save(targetImage, saveLocation); const imageName = new image_1.ImageName(targetImage); return { imageName, path: saveLocation, removeArchive: destination.removeCallback, }; } } exports.getImageArchive = getImageArchive; function isImagePartOfURL(targetImage) { // Based on the Docker spec, if the image contains a hostname, then the hostname should contain // a `.` or `:` before the first instance of a `/`. ref: https://stackoverflow.com/a/37867949 if (!targetImage.includes("/")) { return false; } const partBeforeFirstForwardSlash = targetImage.split("/")[0]; return (partBeforeFirstForwardSlash.includes(".") || partBeforeFirstForwardSlash.includes(":") || partBeforeFirstForwardSlash === "localhost"); } function extractHostnameFromTargetImage(targetImage) { // We need to detect if the `targetImage` is part of a URL. If not, the default hostname will be // used (registry-1.docker.io). ref: https://stackoverflow.com/a/37867949 const defaultHostname = "registry-1.docker.io"; if (!isImagePartOfURL(targetImage)) { return { hostname: defaultHostname, remainder: targetImage }; } const dockerFriendlyRegistryHostname = "docker.io/"; if (targetImage.startsWith(dockerFriendlyRegistryHostname)) { return { hostname: defaultHostname, remainder: targetImage.substring(dockerFriendlyRegistryHostname.length), }; } const i = targetImage.indexOf("/"); return { hostname: targetImage.substring(0, i), remainder: targetImage.substring(i + 1), }; } function extractImageNameAndTag(remainder, targetImage) { const defaultTag = "latest"; if (!remainder.includes("@")) { const [imageName, tag] = remainder.split(":"); return { imageName: appendDefaultRepoPrefixIfRequired(imageName, targetImage), tag: tag || defaultTag, }; } const [imageName, tag] = remainder.split("@"); return { imageName: appendDefaultRepoPrefixIfRequired(dropTagIfSHAIsPresent(imageName), targetImage), tag: tag || defaultTag, }; } function appendDefaultRepoPrefixIfRequired(imageName, targetImage) { const defaultRepoPrefix = "library/"; if (isImagePartOfURL(targetImage) || imageName.includes("/")) { return imageName; } return defaultRepoPrefix + imageName; } function dropTagIfSHAIsPresent(imageName) { if (!imageName.includes(":")) { return imageName; } return imageName.split(":")[0]; } function extractImageDetails(targetImage) { const { hostname, remainder } = extractHostnameFromTargetImage(targetImage); const { imageName, tag } = extractImageNameAndTag(remainder, targetImage); return { hostname, imageName, tag }; } exports.extractImageDetails = extractImageDetails; function isLocalImageSameArchitecture(platformOption, inspectResultArchitecture) { let platformArchitecture; try { // Note: this is using the same flag/input pattern as the new Docker buildx: eg. linux/arm64/v8 platformArchitecture = platformOption.split("/")[1]; } catch (error) { debug(`Error parsing platform flag: '${error.message}'`); return false; } return platformArchitecture === inspectResultArchitecture; } async function pullIfNotLocal(targetImage, options) { const docker = new docker_1.Docker(); try { await docker.inspectImage(targetImage); return; } catch (err) { // image doesn't exist locally } await docker.pullCli(targetImage); } exports.pullIfNotLocal = pullIfNotLocal; //# sourceMappingURL=image-inspector.js.map