Skip to content

Commit 131ba38

Browse files
authored
calculate content digest
1 parent ea0b98d commit 131ba38

File tree

2 files changed

+83
-21
lines changed

2 files changed

+83
-21
lines changed

src/spec-configuration/containerFeaturesOCI.ts

+56-18
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,25 @@ export interface OCIFeatureRef {
1616
registry: string;
1717
}
1818

19+
export interface OCILayer {
20+
mediaType: string;
21+
digest: string;
22+
size: number;
23+
annotations: {
24+
// 'org.opencontainers.image.ref.name': string;
25+
'org.opencontainers.image.title': string;
26+
};
27+
}
1928
export interface OCIManifest {
29+
digest?: string;
2030
schemaVersion: number;
2131
mediaType: string;
2232
config: {
23-
mediaType: string;
2433
digest: string;
34+
mediaType: string;
2535
size: number;
2636
};
27-
layers: [
28-
{
29-
mediaType: string;
30-
digest: string;
31-
size: number;
32-
annotations: {
33-
'org.opencontainers.image.ref.name': string;
34-
'org.opencontainers.image.title': string;
35-
};
36-
}
37-
];
37+
layers: OCILayer[];
3838
}
3939

4040
export function getOCIFeatureSet(output: Log, identifier: string, options: boolean | string | Record<string, boolean | string | undefined>, manifest: OCIManifest): FeatureSet {
@@ -223,13 +223,21 @@ export async function getGHCRtoken(output: Log, id: string) {
223223

224224
// -- Push
225225

226-
export async function createManifest(output: Log, pathToTgz: string): Promise<OCIManifest | undefined> {
226+
export async function generateManifest(output: Log, pathToTgz: string): Promise<OCIManifest | undefined> {
227227

228-
/*const tgzLayer = */calculateTgzLayer(output, pathToTgz);
229-
return undefined;
228+
const tgzLayer = await calculateTgzLayer(output, pathToTgz);
229+
if (!tgzLayer) {
230+
output.write(`Failed to calculate tgz layer.`, LogLevel.Error);
231+
return undefined;
232+
}
233+
234+
const { manifestObj, hash } = await calculateContentDigest(output, tgzLayer);
235+
manifestObj.digest = `sha256:${hash}`;
236+
237+
return manifestObj;
230238
}
231239

232-
export async function calculateTgzLayer(output: Log, pathToTgz: string): Promise<{ digest: string; size: number; mediaType: string } | undefined> {
240+
export async function calculateTgzLayer(output: Log, pathToTgz: string): Promise<OCILayer | undefined> {
233241
output.write(`Creating manifest from ${pathToTgz}`, LogLevel.Trace);
234242
if (!(await isLocalFile(pathToTgz))) {
235243
output.write(`${pathToTgz} does not exist.`, LogLevel.Error);
@@ -238,13 +246,43 @@ export async function calculateTgzLayer(output: Log, pathToTgz: string): Promise
238246

239247
const tarBytes = fs.readFileSync(pathToTgz);
240248

241-
242249
const tarSha256 = crypto.createHash('sha256').update(tarBytes).digest('hex');
243250
output.write(`${pathToTgz}: sha256:${tarSha256} (size: ${tarBytes.byteLength})`, LogLevel.Trace);
244251

245252
return {
253+
mediaType: 'application/vnd.devcontainers.layer.v1+tar',
246254
digest: `sha256:${tarSha256}`,
247255
size: tarBytes.byteLength,
248-
mediaType: 'application/octet-stream'
256+
annotations: {
257+
'org.opencontainers.image.title': path.basename(pathToTgz),
258+
}
259+
};
260+
}
261+
262+
export async function calculateContentDigest(output: Log, tgzLayer: OCILayer) {
263+
// {"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.devcontainers","digest":"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","size":0},"layers":[{"mediaType":"application/vnd.devcontainers.layer.v1+tar","digest":"sha256:b2006e7647191f7b47222ae48df049c6e21a4c5a04acfad0c4ef614d819de4c5","size":15872,"annotations":{"org.opencontainers.image.title":"go.tgz"}}]}
264+
265+
let manifest: OCIManifest = {
266+
schemaVersion: 2,
267+
mediaType: 'application/vnd.oci.image.manifest.v1+json',
268+
config: {
269+
mediaType: 'application/vnd.devcontainers',
270+
digest: 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
271+
size: 0
272+
},
273+
layers: [
274+
tgzLayer
275+
]
249276
};
277+
278+
const manifestStringified = JSON.stringify(manifest);
279+
const manifestHash = crypto.createHash('sha256').update(manifestStringified).digest('hex');
280+
output.write(`manifest: sha256:${manifestHash} (size: ${manifestHash.length})`, LogLevel.Trace);
281+
282+
return {
283+
manifestStr: manifestStringified,
284+
manifestObj: manifest,
285+
hash: manifestHash,
286+
};
287+
250288
}

src/test/container-features/containerFeaturesOCI.offline.test.ts

+27-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// import { assert } from 'chai';
22
// import { getFeatureManifest, getFeatureBlob, getFeatureRef, createManifest } from '../../spec-configuration/containerFeaturesOCI';
33
import { assert } from 'chai';
4-
import { calculateTgzLayer } from '../../spec-configuration/containerFeaturesOCI';
4+
import { calculateContentDigest, calculateTgzLayer } from '../../spec-configuration/containerFeaturesOCI';
55
import { createPlainLog, LogLevel, makeLog } from '../../spec-utils/log';
66

77
export const output = makeLog(createPlainLog(text => process.stdout.write(text), () => LogLevel.Trace));
@@ -52,13 +52,37 @@ const testAssetsDir = `${__dirname}/assets`;
5252

5353
describe('Test OCI Push', () => {
5454

55+
// Example:
56+
// https://github.com/codspace/features/pkgs/container/features%2Fgo/29819216?tag=1
57+
// NOTE: This was pushed via the oras reference impl.
5558
it('Generates the correct tgz manifest layer', async () => {
59+
60+
// Calculate the tgz layer
5661
const res = await calculateTgzLayer(output, `${testAssetsDir}/go.tgz`);
5762
const expected = {
5863
digest: 'sha256:b2006e7647191f7b47222ae48df049c6e21a4c5a04acfad0c4ef614d819de4c5',
59-
mediaType: 'application/octet-stream',
60-
size: 15872
64+
mediaType: 'application/vnd.devcontainers.layer.v1+tar',
65+
size: 15872,
66+
annotations: {
67+
'org.opencontainers.image.title': 'go.tgz'
68+
}
6169
};
70+
71+
if (!res) {
72+
assert.fail();
73+
}
6274
assert.deepEqual(res, expected);
75+
76+
// Generate entire manifest to be able to calculate content digest
77+
const { manifestStr, hash } = await calculateContentDigest(output, res);
78+
79+
// 'Expected' is taken from intermediate value in oras reference implementation, before hash calculation
80+
assert.strictEqual('{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.devcontainers","digest":"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","size":0},"layers":[{"mediaType":"application/vnd.devcontainers.layer.v1+tar","digest":"sha256:b2006e7647191f7b47222ae48df049c6e21a4c5a04acfad0c4ef614d819de4c5","size":15872,"annotations":{"org.opencontainers.image.title":"go.tgz"}}]}', manifestStr);
81+
82+
assert.strictEqual('9726054859c13377c4c3c3c73d15065de59d0c25d61d5652576c0125f2ea8ed3', hash);
83+
84+
85+
86+
6387
});
6488
});

0 commit comments

Comments
 (0)