Refactoring to do on-the-fly processing of layers

This will allow the layers.d/ directory to be full of tarballs and Ahab can
figure out whatever intermediate checksums it might need at runtime.

With this in place, the request dance with the upstream manifest API can begin!
This commit is contained in:
R Tyler Croy 2019-10-20 12:25:53 -07:00
parent fe173e0e6c
commit 888fee1be2
No known key found for this signature in database
GPG Key ID: E5C92681BEF6CEA2
3 changed files with 166 additions and 146 deletions

241
index.js
View File

@ -1,24 +1,67 @@
const crypto = require('crypto');
const express = require('express');
const fs = require('fs');
const path = require('path');
const request = require('request');
/**
* This file is the main server for Ahab and contains all the basic request
* routing logic for proxying and mutating results from a Docker registry
*/
const logger = require('./src/logger');
const express = require('express');
const fs = require('fs');
const path = require('path');
const request = require('request');
const rp = require('request-promise');
const { LAYERS_CHECKSUM_CACHE, Layer } = require('./src/layers');
const logger = require('./src/logger');
const UPSTREAM_REGISTRY = process.env.UPSTREAM_REGISTRY || 'https://registry-1.docker.io';
const LAYERS_DIR = path.resolve('./layers.d/');
const LAYERS_DIR = path.resolve(process.env.LAYERS_DIR || './layers.d/');
logger.info(`Using the layers directory: ${LAYERS_DIR}`);
/**
* Return the computed override path for the given image:tag
*/
function overridePathFor(org, image, tag) {
return path.join(LAYERS_DIR, org, image, tag);
}
/**
* Return true if we have an override directory for the image
*/
function shouldOverrideImage(org, name, tag) {
const computedPath = path.join(LAYERS_DIR, org, name, tag);
function shouldOverrideImage(org, image, tag) {
const computedPath = overridePathFor(org, image, tag);
logger.debug(`Checking to presence of override dir: ${computedPath}`);
return fs.existsSync(computedPath);
}
/**
* Collect the layers to use for override
*
* @return Array of strings with full file paths
*/
function collectLayersFor(org, image, tag) {
// Just to make sure we're never called with bad data
if (!shouldOverrideImage(org, image, tag)) {
return [];
}
const computedPath = overridePathFor(org, image, tag);
return fs.readdirSync(computedPath)
.filter((filename) => {
return filename.endsWith('.tar.gz');
})
.sort()
.map((filename) => {
const key = Layer.keyFor(org, image, tag, filename);
if (!LAYERS_CHECKSUM_CACHE[key]) {
logger.debug(`Computing a new layer for key ${key}`);
const layer = new Layer(org, image, tag, path.join(computedPath, filename));
layer.process();
LAYERS_CHECKSUM_CACHE[key] = layer;
}
return LAYERS_CHECKSUM_CACHE[key];
});
}
/**
* Proxy the given request directly to the upstream
*
@ -40,133 +83,8 @@ function proxyToUpstream(req, res) {
}).pipe(res);
}
/**
* Create a SHA256 checksum (hex encoded) of the given object
*
* The object will be JSON encoded before checksumming
*/
function sha256Sum(obj) {
return crypto.createHash('sha256')
.update(JSON.stringify(obj), 'utf8')
.digest('hex');
}
const app = express();
const port = 9090;
const imageConfig = 'sha256:b0b17eacf99071d7178bbf12d31a556bb3f05b429564c7fc4db33899d3eda85b';
let alpineResponse = {
"manifests": [
{
"digest": "sha256:acd3ca9941a85e8ed16515bfc5328e4e2f8c128caa72959a58a127b7801ee01f",
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"platform": {
"architecture": "amd64",
"os": "linux"
},
"size": 528
},
{
"digest": "sha256:0489474da8ea22426ece86ace6c1c0026ab2fd3cdfbbd62b7e94650266c37d9a",
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"platform": {
"architecture": "arm",
"os": "linux",
"variant": "v6"
},
"size": 528
},
{
"digest": "sha256:1316a4e4b2361242457f4136cc409c38e8a48ffda76e4e1be31896006e5fc4a2",
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"platform": {
"architecture": "arm",
"os": "linux",
"variant": "v7"
},
"size": 528
},
{
"digest": "sha256:db7f3dcef3d586f7dd123f107c93d7911515a5991c4b9e51fa2a43e46335a43e",
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"platform": {
"architecture": "arm64",
"os": "linux",
"variant": "v8"
},
"size": 528
},
{
"digest": "sha256:499416c8a5bcb311d75e12fb4667886e32b43aaf11003be2a334cbe265e81ce4",
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"platform": {
"architecture": "386",
"os": "linux"
},
"size": 528
},
{
"digest": "sha256:5abbfe2915ad8c466bf6c9f33d03622cde0298c36cd88f55d16a3aa3d9c2c35e",
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"platform": {
"architecture": "ppc64le",
"os": "linux"
},
"size": 528
},
{
"digest": "sha256:43955d6857268cc948ae9b370b221091057de83c4962da0826f9a2bdc9bd6b44",
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"platform": {
"architecture": "s390x",
"os": "linux"
},
"size": 528
}
],
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
"schemaVersion": 2
}
let alpineImage = {
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 1512,
"digest": "sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 2789669,
"digest": "sha256:9d48c3bd43c520dc2784e868a780e976b207cbf493eaff8c6596eb871cbd9609"
}
]
}
let response = {
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 1585,
"digest": imageConfig,
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 977,
"digest": "sha256:1b930d010525941c1d56ec53b97bd057a67ae1865eebf042686d2a2d18271ced"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 5808040,
"digest": "sha256:cf98438b1781c2be1fde41967959140379c715b75a85723501d0bca82f215a76"
}
]
};
/*
* The manifests API is defined in the "Pulling an Image" part of the HTTP
@ -183,26 +101,57 @@ let response = {
app.get('/v2/:org/:image/manifests/:digest', (req, res) => {
const { org, image, digest } = req.params;
if (shouldOverrideImage(org, image, digest)) {
logger.info(`Overriding the pull for ${org}/${image}:${digest}`);
if (!shouldOverrideImage(org, image, digest)) {
return proxyToUpstream(req, res);
}
res.status(500);
res.send('Fail');
return
console.log(req.originalUrl);
const checksum = crypto.createHash('sha256').update(JSON.stringify(response), 'utf8').digest('hex');
res.set('Docker-Content-Digest', `sha256:${checksum}`);
/*
* If we don't explicitly set the content type here, the client will think
* that we're sending back a v1 manifest schema and complain about a "missing
* signature key"
*/
res.set('Content-Type', 'application/vnd.docker.distribution.manifest.v2+json');
res.send(response);
logger.info(`Overriding the pull for ${org}/${image}:${digest}`);
const layers = collectLayersFor(org, image, digest);
logger.info(layers);
res.status(500);
res.send('Fail');
return
rp({
url: `${UPSTREAM_REGISTRY}/v2/${org}/${image}/blobs/${digest}`,
headers: {
/*
* We need to send the Authorization header along as well, otherwise
* the upstream repository might complain that we're not authorized
*/
'Authorization' : req.get('Authorization'),
},
})
.then((response) => {
image = JSON.parse(response);
//image = JSON.parse(image);
//image.rootfs.diff_ids.push(
// 'sha256:5f9da7cc9d8d83c96245ac27854466f6ed89fbfade5dd8a4f307861bfb72d1b8',
//);
//console.log(image.rootfs);
//const checksum = crypto.createHash('sha256').update(JSON.stringify(image), 'utf8').digest('hex');
//console.log(JSON.stringify(image));
//console.log(`Sending content with a checksum of ${checksum}`);
//console.log(`Client is expecting ${req.params.sha}`);
//res.send(image);
//console.log(req.originalUrl);
//const checksum = crypto.createHash('sha256').update(JSON.stringify(response), 'utf8').digest('hex');
//res.set('Docker-Content-Digest', `sha256:${checksum}`);
})
.catch((error) => {
logger.error(`Failed to hit the blob API for ${org}/${image}:${digest} - ${error}`);
res.status(500);
res.send(error);
});
});
/*

18
src/checksum.js Normal file
View File

@ -0,0 +1,18 @@
const crypto = require('crypto');
module.exports = {};
/**
* Create a SHA256 checksum (hex encoded) of the given object
*
* Any non-string objects will be JSON encoded before checksumming
*/
module.exports.sha256sum = (obj) => {
let buffer = obj;
if (!obj instanceof String) {
buffer = JSON.stringify(obj);
}
return crypto.createHash('sha256')
.update(buffer, 'utf8')
.digest('hex');
}

53
src/layers.js Normal file
View File

@ -0,0 +1,53 @@
const fs = require('fs');
const path = require('path');
const zlib = require('zlib');
const { sha256sum } = require('./checksum');
const logger = require('./logger');
module.exports = {};
/**
* This cache is to ensure that we're not processing files found in LAYERS_DIR
* and is intended to hold a key-value mapping from a layer `key` to a `Layer`
* class
*
*/
module.exports.LAYERS_CHECKSUM_CACHE = {};
class Layer {
constructor(org, image, tag, filePath) {
this.filePath = filePath;
this.key = Layer.keyFor(org, image, tag, path.basename(filePath));
this.digest = null;
this.tarDigest = null;
}
static keyFor(org, image, tag, filename) {
return `${org}/${image}:${tag}|${filename}`;
}
/**
* The process function will ensure that the file has been considered and
* checksums have been created
*/
process() {
logger.info(`Processing ${this} with ${this.filePath}`);
if ((this.digest == null) || (this.tarDigest == null)) {
// We'll need the contents of the layer for checksumming and gunziping
const layerContents = fs.readFileSync(this.filePath);
logger.debug(`Computing digest for ${this}`);
this.digest = sha256sum(layerContents);
logger.debug(`Computing tarDigest for ${this}`);
this.tarDigest = sha256sum(zlib.gunzipSync(layerContents));
}
}
toString() {
return `Layer(${this.key})`;
}
}
module.exports.Layer = Layer;