Skip to content

Instantly share code, notes, and snippets.

@lionello
Created March 11, 2024 23:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lionello/1729b152d840207afb42ee5534ec44ea to your computer and use it in GitHub Desktop.
Save lionello/1729b152d840207afb42ee5534ec44ea to your computer and use it in GitHub Desktop.
TS file to extract a file from a container image
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