-
Notifications
You must be signed in to change notification settings - Fork 397
Expand file tree
/
Copy pathcontrolManifest.ts
More file actions
111 lines (97 loc) · 3.99 KB
/
controlManifest.ts
File metadata and controls
111 lines (97 loc) · 3.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/
import { promises as fs } from 'fs';
import * as path from 'path';
import * as jsonc from 'jsonc-parser';
import { request } from '../spec-utils/httpRequest';
import * as crypto from 'crypto';
import { Log, LogLevel } from '../spec-utils/log';
export interface DisallowedFeature {
featureIdPrefix: string;
documentationURL?: string;
}
export interface FeatureAdvisory {
featureId: string;
introducedInVersion: string;
fixedInVersion: string;
description: string;
documentationURL?: string;
}
export interface DevContainerControlManifest {
disallowedFeatures: DisallowedFeature[];
featureAdvisories: FeatureAdvisory[];
}
const controlManifestFilename = 'control-manifest.json';
const emptyControlManifest: DevContainerControlManifest = {
disallowedFeatures: [],
featureAdvisories: [],
};
const cacheTimeoutMillis = 5 * 60 * 1000; // 5 minutes
export async function getControlManifest(cacheFolder: string, output: Log): Promise<DevContainerControlManifest> {
const controlManifestPath = path.join(cacheFolder, controlManifestFilename);
const cacheStat = await fs.stat(controlManifestPath)
.catch(err => {
if (err?.code !== 'ENOENT') {
throw err;
}
});
const cacheBuffer = cacheStat?.isFile() ? await fs.readFile(controlManifestPath)
.catch(err => {
if (err?.code !== 'ENOENT') {
throw err;
}
}) : undefined;
const cachedManifest = cacheBuffer ? sanitizeControlManifest(jsonc.parse(cacheBuffer.toString())) : undefined;
if (cacheStat && cachedManifest && cacheStat.mtimeMs + cacheTimeoutMillis > Date.now()) {
return cachedManifest;
}
return updateControlManifest(controlManifestPath, cachedManifest, output);
}
async function updateControlManifest(controlManifestPath: string, oldManifest: DevContainerControlManifest | undefined, output: Log): Promise<DevContainerControlManifest> {
let manifestBuffer: Buffer;
try {
manifestBuffer = await fetchControlManifest(output);
} catch (error) {
output.write(`Failed to fetch control manifest: ${error.message}`, LogLevel.Error);
if (oldManifest) {
// Keep old manifest to not lose existing information and update timestamp to avoid flooding the server.
const now = new Date();
await fs.utimes(controlManifestPath, now, now);
return oldManifest;
}
manifestBuffer = Buffer.from(JSON.stringify(emptyControlManifest, undefined, 2));
}
const controlManifestTmpPath = `${controlManifestPath}-${crypto.randomUUID()}`;
await fs.mkdir(path.dirname(controlManifestPath), { recursive: true });
await fs.writeFile(controlManifestTmpPath, manifestBuffer);
await fs.rename(controlManifestTmpPath, controlManifestPath);
return sanitizeControlManifest(jsonc.parse(manifestBuffer.toString()));
}
async function fetchControlManifest(output: Log) {
const controlManifestURL = process.env.DEVCONTAINERS_CONTROL_MANIFEST_URL ?? 'https://containers.dev/static/devcontainer-control-manifest.json';
return request({
type: 'GET',
url: controlManifestURL,
headers: {
'user-agent': 'devcontainers-vscode',
'accept': 'application/json',
},
}, output);
}
function sanitizeControlManifest(manifest: any): DevContainerControlManifest {
if (!manifest || typeof manifest !== 'object') {
return emptyControlManifest;
}
const disallowedFeatures = manifest.disallowedFeatures as DisallowedFeature[] | undefined;
const featureAdvisories = manifest.featureAdvisories as FeatureAdvisory[] | undefined;
return {
disallowedFeatures: Array.isArray(disallowedFeatures) ? disallowedFeatures.filter(f => typeof f.featureIdPrefix === 'string') : [],
featureAdvisories: Array.isArray(featureAdvisories) ? featureAdvisories.filter(f =>
typeof f.featureId === 'string' &&
typeof f.introducedInVersion === 'string' &&
typeof f.fixedInVersion === 'string' &&
typeof f.description === 'string'
) : [],
};
}