Created
March 11, 2024 23:49
-
-
Save lionello/1729b152d840207afb42ee5534ec44ea to your computer and use it in GitHub Desktop.
TS file to extract a file from a container image
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as tar from "tar-stream"; | |
import gunzip from "gunzip-maybe"; | |
import { pipeline } from "stream"; | |
import { promisify } from "util"; | |
const pipelineAsync = promisify(pipeline); | |
const dockerRegistryUrl = "https://registry-1.docker.io"; | |
const imageName = "library/ubuntu"; // Replace with your image name | |
const tag = "latest"; // or the specific tag you want to download | |
const fileToExtract = "etc/shadow"; // The specific file you want to extract | |
interface TokenResponse { | |
token: string; | |
} | |
interface DockerResponse { | |
schemaVersion: number; | |
mediaType: string; | |
} | |
interface ImageManifestResponse extends DockerResponse { | |
config: { | |
mediaType: string; | |
size: number; | |
digest: string; | |
}; | |
layers: { | |
mediaType: string; | |
size: number; | |
digest: string; | |
urls: string[]; | |
}[]; | |
} | |
interface ManifestResponse extends DockerResponse { | |
manifests: { | |
digest: string; | |
mediaType: string; | |
size: number; | |
annotations: { | |
"org.opencontainers.image.ref.name": string; | |
}; | |
platform: { | |
architecture: string; | |
os: string; | |
variant?: string; | |
}; | |
}[]; | |
} | |
async function extractFileFromDockerImage( | |
image: string, | |
tag: string, | |
filePath: string | |
): Promise<Buffer | null> { | |
const fetch = (await import("node-fetch")).default; | |
let Authorization: string | null = null; | |
const manifestResponse = await fetch( | |
`${dockerRegistryUrl}/v2/${image}/manifests/${tag}`, | |
{ | |
headers: { | |
Accept: "application/vnd.docker.distribution.manifest.v2+json", | |
}, | |
} | |
); | |
let manifests = (await manifestResponse.json()) as ManifestResponse; | |
if (manifestResponse.status === 401) { | |
console.log("Received a 401 response. Authenticating..."); | |
const wwwAuthenticate = manifestResponse.headers.get("www-authenticate"); | |
console.debug("www-authenticate:", wwwAuthenticate); | |
if (wwwAuthenticate && wwwAuthenticate.startsWith("Bearer")) { | |
const realm = wwwAuthenticate.match(/realm="([^"]+)"/)?.[1]; | |
const service = wwwAuthenticate.match(/service="([^"]+)"/)?.[1]; | |
const scope = | |
wwwAuthenticate.match(/scope="([^"]+)"/)?.[1] || | |
`repository:${image}:pull`; | |
const tokenResponse = await fetch( | |
`${realm}?service=${service}&scope=${scope}` | |
); | |
const token = ((await tokenResponse.json()) as TokenResponse).token; | |
Authorization = `Bearer ${token}`; | |
console.debug(Authorization); | |
// Retry the manifest request with the token | |
const manifestResponseWithToken = await fetch( | |
`${dockerRegistryUrl}/v2/${image}/manifests/${tag}`, | |
{ | |
headers: { | |
Accept: "application/vnd.docker.distribution.manifest.v2+json", | |
Authorization, | |
}, | |
} | |
); | |
if (manifestResponseWithToken.status !== 200) { | |
throw new Error( | |
`Failed to fetch manifest with token: ${manifestResponseWithToken.statusText}` | |
); | |
} | |
manifests = (await manifestResponseWithToken.json()) as ManifestResponse; | |
console.debug(manifests); | |
} | |
} | |
const manifest = manifests.manifests[0]!; // TODO: find the right manifest for the current platform | |
const configResponse = await fetch( | |
`${dockerRegistryUrl}/v2/${image}/manifests/${manifest.digest}`, | |
{ | |
headers: { | |
Accept: manifest.mediaType, | |
...(Authorization ? { Authorization } : {}), | |
}, | |
} | |
); | |
const config = (await configResponse.json()) as ImageManifestResponse; | |
console.debug(config) | |
for (const layer of config.layers) { | |
const layerResponse = await fetch( | |
`${dockerRegistryUrl}/v2/${image}/blobs/${layer.digest}`, | |
{ | |
headers: { | |
Accept: layer.mediaType, | |
...(Authorization ? { Authorization } : {}), | |
}, | |
} | |
); | |
const layerStream = layerResponse.body!.pipe(gunzip()); | |
const extract = tar.extract(); | |
let fileContent: Buffer | null = null; | |
extract.on("entry", (header, stream, next) => { | |
if (header.name === filePath) { | |
const chunks: Buffer[] = []; | |
stream.on("data", (chunk) => chunks.push(Buffer.from(chunk))); | |
stream.on("end", () => { | |
fileContent = Buffer.concat(chunks); | |
next(); | |
}); | |
stream.resume(); | |
} else { | |
stream.on("end", () => next()); | |
stream.resume(); | |
} | |
}); | |
try { | |
await pipelineAsync(layerStream, extract); | |
if (fileContent) return fileContent; | |
} catch (error) { | |
console.error("Error processing layer:", error); | |
// Continue to the next layer if there's an error in the current one | |
} | |
} | |
return null; // File not found in any layer | |
} | |
async function main() { | |
try { | |
const fileContent = await extractFileFromDockerImage( | |
imageName, | |
tag, | |
fileToExtract | |
); | |
if (fileContent) { | |
console.log("File content:", fileContent.toString()); | |
// Do something with the file content | |
} else { | |
console.log("File not found."); | |
} | |
} catch (error) { | |
console.error("An error occurred:", error); | |
} | |
} | |
main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment