-
Notifications
You must be signed in to change notification settings - Fork 31
/
Copy pathopa.ts
365 lines (314 loc) · 11.3 KB
/
opa.ts
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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
"use strict";
import cp = require("child_process");
import { sync as commandExistsSync } from "command-exists";
import { existsSync } from "fs";
import { dirname } from "path";
import * as vscode from "vscode";
import { promptForInstall } from "./github-installer";
import { advertiseLanguageServers } from "./ls/advertise";
import { getImports, getPackage, replaceWorkspaceFolderPathVariable } from "./util";
const regoVarPattern = new RegExp("^[a-zA-Z_][a-zA-Z0-9_]*$");
// TODO: link to instructions that describe compiling OPA from source
const windowsArm64NotSupported =
"OPA binaries are not supported for windows/arm architecture. To use the features of this plugin, compile OPA from source.";
export function getDataDir(uri: vscode.Uri): string {
// NOTE(tsandall): we don't have a precise version for 3be55ed6 so
// do our best and rely on the -dev tag.
if (!installedOPASameOrNewerThan("0.14.0-dev")) {
return uri.fsPath;
}
if (uri.scheme === "file") {
return uri.toString();
}
return decodeURIComponent(uri.toString());
}
export function canUseBundleFlags(): boolean {
const bundleMode = vscode.workspace.getConfiguration("opa").get<boolean>("bundleMode", true);
return installedOPASameOrNewerThan("0.14.0-dev") && bundleMode;
}
export function canUseStrictFlag(): boolean {
const strictMode = vscode.workspace.getConfiguration("opa").get<boolean>("strictMode", true);
return strictMode && installedOPASameOrNewerThan("0.37.0");
}
function dataFlag(): string {
if (canUseBundleFlags()) {
return "--bundle";
}
return "--data";
}
// returns true if installed OPA is same or newer than OPA version x.
function installedOPASameOrNewerThan(x: string): boolean {
const s = getOPAVersionString(undefined);
return opaVersionSameOrNewerThan(s, x);
}
function replacePathVariables(path: string): string {
let result = replaceWorkspaceFolderPathVariable(path);
if (vscode.window.activeTextEditor !== undefined) {
result = result.replace("${fileDirname}", dirname(vscode.window.activeTextEditor!.document.fileName));
} else if (path.indexOf("${fileDirname}") >= 0) {
// Report on the original path
vscode.window.showWarningMessage("${fileDirname} variable configured in settings, but no document is active");
}
return result;
}
// Returns a list of root data path URIs based on the plugin configuration.
export function getRoots(): string[] {
const roots = vscode.workspace.getConfiguration("opa").get<string[]>("roots", []);
return roots.map((root: string) => getDataDir(vscode.Uri.parse(replacePathVariables(root))));
}
// Returns a list of root data parameters in an array
// like ["--bundle=file:///a/b/x/", "--bundle=file:///a/b/y"] in bundle mode
// or ["--data=file:///a/b/x", "--data=file://a/b/y"] otherwise.
export function getRootParams(): string[] {
const flag = dataFlag();
const roots = getRoots();
return roots.map((root) => `${flag}=${root}`);
}
// Returns a list of schema parameters in an array.
// The returned array either contains a single entry; or is empty, if the 'opa.schema' setting isn't set.
export function getSchemaParams(): string[] {
let schemaPath = vscode.workspace.getConfiguration("opa").get<string>("schema");
if (schemaPath === undefined || schemaPath === null) {
return [];
}
schemaPath = replacePathVariables(schemaPath);
// At this stage, we don't care if the path is valid; let the OPA command return an error.
return [`--schema=${schemaPath}`];
}
// returns true if OPA version a is same or newer than OPA version b. If either
// version is not in the expected format (i.e.,
// <major>.<minor>.<point>[-<patch>]) this function returns true. Major, minor,
// and point versions are compared numerically. Patch versions are compared
// lexicographically however an empty patch version is considered newer than a
// non-empty patch version.
function opaVersionSameOrNewerThan(a: string, b: string): boolean {
const aVersion = parseOPAVersion(a);
const bVersion = parseOPAVersion(b);
if (aVersion.length !== 4 || bVersion.length !== 4) {
return true;
}
for (let i = 0; i < 3; i++) {
if (aVersion[i] > bVersion[i]) {
return true;
} else if (bVersion[i] > aVersion[i]) {
return false;
}
}
if (aVersion[3] === "" && bVersion[3] !== "") {
return true;
} else if (aVersion[3] !== "" && bVersion[3] === "") {
return false;
}
return aVersion[3] >= bVersion[3];
}
// returns array of numbers and strings representing an OPA semantic version.
function parseOPAVersion(s: string): any[] {
const parts = s.split(".", 3);
if (parts.length < 3) {
return [];
}
const major = Number(parts[0]);
const minor = Number(parts[1]);
const pointParts = parts[2].split("-", 2);
const point = Number(pointParts[0]);
let patch = "";
if (pointParts.length >= 2) {
patch = pointParts[1];
}
return [major, minor, point, patch];
}
// returns the installed OPA version as a string.
function getOPAVersionString(context?: vscode.ExtensionContext): string {
const opaPath = getOpaPath(context, "opa", false);
if (opaPath === undefined) {
return "";
}
const result = cp.spawnSync(opaPath, ["version"]);
if (result.status !== 0) {
return "";
}
const lines = result.stdout.toString().split("\n");
for (let i = 0; i < lines.length; i++) {
const parts = lines[i].trim().split(": ", 2);
if (parts.length < 2) {
continue;
}
if (parts[0] === "Version") {
return parts[1];
}
}
return "";
}
// refToString formats a ref as a string. Strings are special-cased for
// dot-style lookup. Note: this function is currently only used for populating
// picklists based on dependencies. As such it doesn't handle all term types
// properly.
export function refToString(ref: any[]): string {
let result = ref[0].value;
for (let i = 1; i < ref.length; i++) {
if (ref[i].type === "string") {
if (regoVarPattern.test(ref[i].value)) {
result += "." + ref[i].value;
continue;
}
}
result += "[" + JSON.stringify(ref[i].value) + "]";
}
return result;
}
/**
* Helpers for executing OPA as a subprocess.
*/
export function parse(
context: vscode.ExtensionContext,
opaPath: string,
path: string,
cb: (pkg: string, imports: string[]) => void,
onerror: (output: string) => void,
) {
run(context, opaPath, ["parse", path, "--format", "json"], "", (_, result) => {
const pkg = getPackage(result);
const imports = getImports(result);
cb(pkg, imports);
}, onerror);
}
export function run(
context: vscode.ExtensionContext,
path: string,
args: string[],
stdin: string,
onSuccess: (stderr: string, result: any) => void,
onFailure: (msg: string) => void,
) {
runWithStatus(context, path, args, stdin, (code: number, stderr: string, stdout: string) => {
if (code === 0) {
onSuccess(stderr, JSON.parse(stdout));
} else if (stdout !== "") {
onFailure(stdout);
} else {
onFailure(stderr);
}
});
}
export function opaIsInstalled(context: vscode.ExtensionContext): boolean {
return getOpaPath(context, "opa", false) !== undefined;
}
function getOpaPath(
context: vscode.ExtensionContext | undefined,
path: string,
shouldPromptForInstall: boolean,
): string | undefined {
let opaPath = vscode.workspace.getConfiguration("opa.dependency_paths").get<string>("opa");
// if not set, check the deprecated setting location
if (opaPath === undefined || opaPath === null) {
opaPath = vscode.workspace.getConfiguration("opa").get<string>("path");
}
if (opaPath !== undefined && opaPath !== null) {
opaPath = replaceWorkspaceFolderPathVariable(opaPath);
}
if (opaPath !== undefined && opaPath !== null && opaPath.length > 0) {
if (opaPath.startsWith("file://")) {
opaPath = opaPath.substring(7);
}
if (existsSync(opaPath)) {
return opaPath;
}
console.warn("'opa.path' setting configured with invalid path:", opaPath);
}
if (commandExistsSync(path)) {
return path;
}
if (shouldPromptForInstall) {
// this is used to track if the user has been offered to install OPA,
// this can later be used to allow other prompts to be shown
if (context) {
const globalStateKey = "opa.prompts.install.opa";
context.globalState.update(globalStateKey, true);
}
promptForInstall(
"opa",
"open-policy-agent/opa",
"OPA is either not installed or is missing from your path, would you like to install it?",
(release: any) => {
// release.assets.name contains {'darwin', 'linux', 'windows'} and {'arm64', 'amd64'}
// in the format `opa_$OS_$ARCH...` so we can use that to search for the appropriate asset
const assets = release.assets || [];
const os = process.platform;
// node can return many values for process.arch but OPA only supports arm64 and amd64
const arch = process.arch.indexOf("arm") !== -1 ? "arm64" : "amd64";
let targetAsset: { browser_download_url: string };
switch (os) {
case "darwin":
targetAsset = assets.filter((asset: { name: string }) => asset.name.indexOf(`darwin_${arch}`) !== -1)[0];
break;
case "linux":
targetAsset = assets.filter((asset: { name: string }) => asset.name.indexOf(`linux_${arch}`) !== -1)[0];
break;
case "win32":
// arm is not supported officially for windows, so prompt the user
// to build from source and provide an empty url from the default case
if (arch === "arm64") {
throw windowsArm64NotSupported;
} else {
targetAsset = assets.filter((asset: { name: string }) => asset.name.indexOf(`windows`) !== -1)[0];
break;
}
default:
targetAsset = { browser_download_url: "" };
}
return targetAsset.browser_download_url;
},
() => {
const os = process.platform;
switch (os) {
case "darwin":
case "linux":
return "opa";
case "win32":
return "opa.exe";
default:
return "opa";
}
},
() => {
context && advertiseLanguageServers(context);
},
);
}
}
function getOpaEnv(): NodeJS.ProcessEnv {
const env = vscode.workspace.getConfiguration("opa").get<NodeJS.ProcessEnv>("env", {});
return Object.fromEntries(Object.entries(env).map(([k, v]) => [k, replacePathVariables(v as string)]));
}
// runWithStatus executes the OPA binary at path with args and stdin. The
// callback is invoked with the exit status, stderr, and stdout buffers.
export function runWithStatus(
context: vscode.ExtensionContext | undefined,
path: string,
args: string[],
stdin: string,
cb: (code: number, stderr: string, stdout: string) => void,
) {
const opaPath = getOpaPath(context, path, true);
if (opaPath === undefined) {
return;
}
console.log("spawn:", opaPath, "args:", args.toString());
const proc = cp.spawn(opaPath, args, { env: { ...process.env, ...getOpaEnv() } });
proc.stdin.write(stdin);
proc.stdin.end();
let stdout = "";
let stderr = "";
proc.stdout.on("data", (data) => {
stdout += data;
});
proc.stderr.on("data", (data) => {
stderr += data;
});
proc.on("exit", (code) => {
console.log("code:", code);
console.log("stdout:", stdout);
console.log("stderr:", stderr);
cb(code || 0, stderr, stdout);
});
}