Skip to content

Commit 889ff6a

Browse files
committed
structured
1 parent af32daa commit 889ff6a

File tree

3 files changed

+336
-4
lines changed

3 files changed

+336
-4
lines changed

registry/mavrickrishi/.images/avatar.svg

Lines changed: 0 additions & 4 deletions
This file was deleted.

registry/mavrickrishi/avatar.jpeg

7.64 KB
Loading

test/test.ts

Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
import { readableStreamToText, spawn } from "bun";
2+
import { expect, it } from "bun:test";
3+
import { readFile, unlink } from "node:fs/promises";
4+
5+
export const runContainer = async (
6+
image: string,
7+
init = "sleep infinity",
8+
): Promise<string> => {
9+
const proc = spawn([
10+
"docker",
11+
"run",
12+
"--rm",
13+
"-d",
14+
"--label",
15+
"modules-test=true",
16+
"--network",
17+
"host",
18+
"--entrypoint",
19+
"sh",
20+
image,
21+
"-c",
22+
init,
23+
]);
24+
25+
const containerID = await readableStreamToText(proc.stdout);
26+
const exitCode = await proc.exited;
27+
if (exitCode !== 0) {
28+
throw new Error(containerID);
29+
}
30+
return containerID.trim();
31+
};
32+
33+
export const removeContainer = async (id: string) => {
34+
const proc = spawn(["docker", "rm", "-f", id], {
35+
stderr: "pipe",
36+
stdout: "pipe",
37+
});
38+
const exitCode = await proc.exited;
39+
const [stderr, stdout] = await Promise.all([
40+
readableStreamToText(proc.stderr ?? new ReadableStream()),
41+
readableStreamToText(proc.stdout ?? new ReadableStream()),
42+
]);
43+
if (exitCode !== 0) {
44+
throw new Error(`${stderr}\n${stdout}`);
45+
}
46+
};
47+
48+
export interface scriptOutput {
49+
exitCode: number;
50+
stdout: string[];
51+
stderr: string[];
52+
}
53+
54+
/**
55+
* Finds the only "coder_script" resource in the given state and runs it in a
56+
* container.
57+
*/
58+
export const executeScriptInContainer = async (
59+
state: TerraformState,
60+
image: string,
61+
shell = "sh",
62+
before?: string,
63+
): Promise<scriptOutput> => {
64+
const instance = findResourceInstance(state, "coder_script");
65+
const id = await runContainer(image);
66+
67+
if (before) {
68+
await execContainer(id, [shell, "-c", before]);
69+
}
70+
71+
const resp = await execContainer(id, [shell, "-c", instance.script]);
72+
const stdout = resp.stdout.trim().split("\n");
73+
const stderr = resp.stderr.trim().split("\n");
74+
return {
75+
exitCode: resp.exitCode,
76+
stdout,
77+
stderr,
78+
};
79+
};
80+
81+
export const execContainer = async (
82+
id: string,
83+
cmd: string[],
84+
args?: string[],
85+
): Promise<{
86+
exitCode: number;
87+
stderr: string;
88+
stdout: string;
89+
}> => {
90+
const proc = spawn(["docker", "exec", ...(args ?? []), id, ...cmd], {
91+
stderr: "pipe",
92+
stdout: "pipe",
93+
});
94+
const [stderr, stdout] = await Promise.all([
95+
readableStreamToText(proc.stderr),
96+
readableStreamToText(proc.stdout),
97+
]);
98+
const exitCode = await proc.exited;
99+
return {
100+
exitCode,
101+
stderr,
102+
stdout,
103+
};
104+
};
105+
106+
type JsonValue =
107+
| string
108+
| number
109+
| boolean
110+
| null
111+
| JsonValue[]
112+
| { [key: string]: JsonValue };
113+
114+
type TerraformStateResource = {
115+
type: string;
116+
name: string;
117+
provider: string;
118+
119+
instances: [
120+
{
121+
attributes: Record<string, JsonValue>;
122+
},
123+
];
124+
};
125+
126+
type TerraformOutput = {
127+
type: string;
128+
value: JsonValue;
129+
};
130+
131+
export interface TerraformState {
132+
outputs: Record<string, TerraformOutput>;
133+
resources: [TerraformStateResource, ...TerraformStateResource[]];
134+
}
135+
136+
type TerraformVariables = Record<string, JsonValue>;
137+
138+
export interface CoderScriptAttributes {
139+
script: string;
140+
agent_id: string;
141+
url: string;
142+
}
143+
144+
export type ResourceInstance<T extends string = string> =
145+
T extends "coder_script" ? CoderScriptAttributes : Record<string, string>;
146+
147+
/**
148+
* finds the first instance of the given resource type in the given state. If
149+
* name is specified, it will only find the instance with the given name.
150+
*/
151+
export const findResourceInstance = <T extends string>(
152+
state: TerraformState,
153+
type: T,
154+
name?: string,
155+
): ResourceInstance<T> => {
156+
const resource = state.resources.find(
157+
(resource) =>
158+
resource.type === type && (name ? resource.name === name : true),
159+
);
160+
if (!resource) {
161+
throw new Error(`Resource ${type} not found`);
162+
}
163+
if (resource.instances.length !== 1) {
164+
throw new Error(
165+
`Resource ${type} has ${resource.instances.length} instances`,
166+
);
167+
}
168+
169+
return resource.instances[0].attributes as ResourceInstance<T>;
170+
};
171+
172+
/**
173+
* Creates a test-case for each variable provided and ensures that the apply
174+
* fails without it.
175+
*/
176+
export const testRequiredVariables = <TVars extends TerraformVariables>(
177+
dir: string,
178+
vars: Readonly<TVars>,
179+
) => {
180+
// Ensures that all required variables are provided.
181+
it("required variables", async () => {
182+
await runTerraformApply(dir, vars);
183+
});
184+
185+
const varNames = Object.keys(vars);
186+
for (const varName of varNames) {
187+
// Ensures that every variable provided is required!
188+
it(`missing variable: ${varName}`, async () => {
189+
const localVars: TerraformVariables = {};
190+
for (const otherVarName of varNames) {
191+
if (otherVarName !== varName) {
192+
localVars[otherVarName] = vars[otherVarName];
193+
}
194+
}
195+
196+
try {
197+
await runTerraformApply(dir, localVars);
198+
} catch (ex) {
199+
if (!(ex instanceof Error)) {
200+
throw new Error("Unknown error generated");
201+
}
202+
203+
expect(ex.message).toContain(
204+
`input variable \"${varName}\" is not set`,
205+
);
206+
return;
207+
}
208+
throw new Error(`${varName} is not a required variable!`);
209+
});
210+
}
211+
};
212+
213+
/**
214+
* Runs terraform apply in the given directory with the given variables. It is
215+
* fine to run in parallel with other instances of this function, as it uses a
216+
* random state file.
217+
*/
218+
export const runTerraformApply = async <TVars extends TerraformVariables>(
219+
dir: string,
220+
vars: Readonly<TVars>,
221+
customEnv?: Record<string, string>,
222+
): Promise<TerraformState> => {
223+
const stateFile = `${dir}/${crypto.randomUUID()}.tfstate`;
224+
225+
const childEnv: Record<string, string | undefined> = {
226+
...process.env,
227+
...(customEnv ?? {}),
228+
};
229+
230+
// This is a fix for when you try to run the tests from a Coder workspace.
231+
// When process.env is destructured into the object, it can sometimes have
232+
// workspace-specific values, which causes the resulting URL to be different
233+
// from what the tests have classically expected.
234+
childEnv.CODER_AGENT_URL = undefined;
235+
childEnv.CODER_WORKSPACE_NAME = undefined;
236+
237+
for (const [key, value] of Object.entries(vars) as [string, JsonValue][]) {
238+
if (value !== null) {
239+
childEnv[`TF_VAR_${key}`] = String(value);
240+
}
241+
}
242+
243+
const proc = spawn(
244+
[
245+
"terraform",
246+
"apply",
247+
"-compact-warnings",
248+
"-input=false",
249+
"-auto-approve",
250+
"-state",
251+
"-no-color",
252+
stateFile,
253+
],
254+
{
255+
cwd: dir,
256+
env: childEnv,
257+
stderr: "pipe",
258+
stdout: "pipe",
259+
},
260+
);
261+
262+
const text = await readableStreamToText(proc.stderr);
263+
const exitCode = await proc.exited;
264+
if (exitCode !== 0) {
265+
throw new Error(text);
266+
}
267+
268+
const content = await readFile(stateFile, "utf8");
269+
await unlink(stateFile);
270+
return JSON.parse(content);
271+
};
272+
273+
/**
274+
* Runs terraform init in the given directory.
275+
*/
276+
export const runTerraformInit = async (dir: string) => {
277+
const proc = spawn(["terraform", "init"], {
278+
cwd: dir,
279+
});
280+
const text = await readableStreamToText(proc.stdout);
281+
const exitCode = await proc.exited;
282+
if (exitCode !== 0) {
283+
throw new Error(text);
284+
}
285+
};
286+
287+
export const createJSONResponse = (obj: object, statusCode = 200): Response => {
288+
return new Response(JSON.stringify(obj), {
289+
headers: {
290+
"Content-Type": "application/json",
291+
},
292+
status: statusCode,
293+
});
294+
};
295+
296+
export const writeCoder = async (id: string, script: string) => {
297+
await writeFileContainer(id, "/usr/bin/coder", script, {
298+
user: "root",
299+
});
300+
const execResult = await execContainer(
301+
id,
302+
["chmod", "755", "/usr/bin/coder"],
303+
["--user", "root"],
304+
);
305+
expect(execResult.exitCode).toBe(0);
306+
};
307+
308+
export const writeFileContainer = async (
309+
id: string,
310+
path: string,
311+
content: string,
312+
options?: {
313+
user?: string;
314+
},
315+
) => {
316+
const contentBase64 = Buffer.from(content).toString("base64");
317+
const proc = await execContainer(
318+
id,
319+
["sh", "-c", `echo '${contentBase64}' | base64 -d > '${path}'`],
320+
options?.user ? ["--user", options.user] : undefined,
321+
);
322+
if (proc.exitCode !== 0) {
323+
throw new Error(`Failed to write file: ${proc.stderr}`);
324+
}
325+
expect(proc.exitCode).toBe(0);
326+
};
327+
328+
export const readFileContainer = async (id: string, path: string) => {
329+
const proc = await execContainer(id, ["cat", path], ["--user", "root"]);
330+
if (proc.exitCode !== 0) {
331+
console.log(proc.stderr);
332+
console.log(proc.stdout);
333+
}
334+
expect(proc.exitCode).toBe(0);
335+
return proc.stdout;
336+
};

0 commit comments

Comments
 (0)