Skip to content

Commit a4bd50c

Browse files
authored
chore: enable terraform provisioners in e2e by default (coder#13134)
* skip docker test for now, it leaks containers
1 parent 1832a75 commit a4bd50c

File tree

9 files changed

+150
-23
lines changed

9 files changed

+150
-23
lines changed

.vscode/settings.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,5 +222,7 @@
222222
"go.testFlags": ["-short", "-coverpkg=./..."],
223223
// We often use a version of TypeScript that's ahead of the version shipped
224224
// with VS Code.
225-
"typescript.tsdk": "./site/node_modules/typescript/lib"
225+
"typescript.tsdk": "./site/node_modules/typescript/lib",
226+
// Playwright tests in VSCode will open a browser to live "view" the test.
227+
"playwright.reuseBrowser": true
226228
}

site/e2e/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ export const requireEnterpriseTests = Boolean(
3939
);
4040
export const enterpriseLicense = process.env.CODER_E2E_ENTERPRISE_LICENSE ?? "";
4141

42+
// Disabling terraform tests is optional for environments without Docker + Terraform.
43+
// By default, we opt into these tests.
44+
export const requireTerraformTests = !process.env.CODER_E2E_DISABLE_TERRAFORM;
45+
4246
// Fake experiments to verify that site presents them as enabled.
4347
export const e2eFakeExperiment1 = "e2e-fake-experiment-1";
4448
export const e2eFakeExperiment2 = "e2e-fake-experiment-2";

site/e2e/helpers.ts

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
enterpriseLicense,
2020
prometheusPort,
2121
requireEnterpriseTests,
22+
requireTerraformTests,
2223
} from "./constants";
2324
import { expectUrl } from "./expectUrl";
2425
import {
@@ -43,6 +44,11 @@ export function requiresEnterpriseLicense() {
4344
test.skip(!enterpriseLicense);
4445
}
4546

47+
// requireTerraformProvisioner by default is enabled.
48+
export function requireTerraformProvisioner() {
49+
test.skip(!requireTerraformTests);
50+
}
51+
4652
// createWorkspace creates a workspace for a template.
4753
// It does not wait for it to be running, but it does navigate to the page.
4854
export const createWorkspace = async (
@@ -149,25 +155,46 @@ export const verifyParameters = async (
149155
}
150156
};
151157

158+
// StarterTemplates are ids of starter templates that can be used in place of
159+
// the responses payload. These starter templates will require real provisioners.
160+
export enum StarterTemplates {
161+
STARTER_DOCKER = "docker",
162+
}
163+
164+
function isStarterTemplate(
165+
input: EchoProvisionerResponses | StarterTemplates | undefined,
166+
): input is StarterTemplates {
167+
if (!input) {
168+
return false;
169+
}
170+
return typeof input === "string";
171+
}
172+
152173
// createTemplate navigates to the /templates/new page and uploads a template
153174
// with the resources provided in the responses argument.
154175
export const createTemplate = async (
155176
page: Page,
156-
responses?: EchoProvisionerResponses,
177+
responses?: EchoProvisionerResponses | StarterTemplates,
157178
): Promise<string> => {
158-
// Required to have templates submit their provisioner type as echo!
159-
await page.addInitScript({
160-
content: "window.playwright = true",
161-
});
179+
let path = "/templates/new";
180+
if (isStarterTemplate(responses)) {
181+
path += `?exampleId=${responses}`;
182+
} else {
183+
// The form page will read this value and use it as the default type.
184+
path += "?provisioner_type=echo";
185+
}
162186

163-
await page.goto("/templates/new", { waitUntil: "domcontentloaded" });
187+
await page.goto(path, { waitUntil: "domcontentloaded" });
164188
await expectUrl(page).toHavePathName("/templates/new");
165189

166-
await page.getByTestId("file-upload").setInputFiles({
167-
buffer: await createTemplateVersionTar(responses),
168-
mimeType: "application/x-tar",
169-
name: "template.tar",
170-
});
190+
if (!isStarterTemplate(responses)) {
191+
await page.getByTestId("file-upload").setInputFiles({
192+
buffer: await createTemplateVersionTar(responses),
193+
mimeType: "application/x-tar",
194+
name: "template.tar",
195+
});
196+
}
197+
171198
const name = randomName();
172199
await page.getByLabel("Name *").fill(name);
173200
await page.getByTestId("form-submit").click();
@@ -868,6 +895,7 @@ export async function openTerminalWindow(
868895
page: Page,
869896
context: BrowserContext,
870897
workspaceName: string,
898+
agentName: string = "dev",
871899
): Promise<Page> {
872900
// Wait for the web terminal to open in a new tab
873901
const pagePromise = context.waitForEvent("page");
@@ -879,7 +907,7 @@ export async function openTerminalWindow(
879907
// isn't POSIX compatible, such as Fish.
880908
const commandQuery = `?command=${encodeURIComponent("/usr/bin/env bash")}`;
881909
await expectUrl(terminal).toHavePathName(
882-
`/@admin/${workspaceName}.dev/terminal`,
910+
`/@admin/${workspaceName}.${agentName}/terminal`,
883911
);
884912
await terminal.goto(`/@admin/${workspaceName}.dev/terminal${commandQuery}`);
885913

site/e2e/playwright.config.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { defineConfig } from "@playwright/test";
2+
import { execSync } from "child_process";
23
import * as path from "path";
34
import {
45
coderMain,
@@ -7,13 +8,47 @@ import {
78
e2eFakeExperiment1,
89
e2eFakeExperiment2,
910
gitAuth,
11+
requireTerraformTests,
1012
} from "./constants";
1113

1214
export const wsEndpoint = process.env.CODER_E2E_WS_ENDPOINT;
1315

1416
// This is where auth cookies are stored!
1517
export const storageState = path.join(__dirname, ".auth.json");
1618

19+
// If running terraform tests, verify the requirements exist in the
20+
// environment.
21+
//
22+
// These execs will throw an error if the status code is non-zero.
23+
// So if both these work, then we can launch terraform provisioners.
24+
let hasTerraform = false;
25+
let hasDocker = false;
26+
try {
27+
execSync("terraform --version");
28+
hasTerraform = true;
29+
} catch {
30+
/* empty */
31+
}
32+
33+
try {
34+
execSync("docker --version");
35+
hasDocker = true;
36+
} catch {
37+
/* empty */
38+
}
39+
40+
if (!hasTerraform || !hasDocker) {
41+
const msg =
42+
"Terraform provisioners require docker & terraform binaries to function. \n" +
43+
(hasTerraform
44+
? ""
45+
: "\tThe `terraform` executable is not present in the runtime environment.\n") +
46+
(hasDocker
47+
? ""
48+
: "\tThe `docker` executable is not present in the runtime environment.\n");
49+
throw new Error(msg);
50+
}
51+
1752
const localURL = (port: number, path: string): string => {
1853
return `http://localhost:${port}${path}`;
1954
};
@@ -54,13 +89,14 @@ export default defineConfig({
5489
`go run -tags embed ${coderMain} server`,
5590
"--global-config $(mktemp -d -t e2e-XXXXXXXXXX)",
5691
`--access-url=http://localhost:${coderPort}`,
57-
`--http-address=localhost:${coderPort}`,
92+
`--http-address=0.0.0.0:${coderPort}`,
5893
"--in-memory",
5994
"--telemetry=false",
6095
"--dangerous-disable-rate-limits",
6196
"--provisioner-daemons 10",
6297
// TODO: Enable some terraform provisioners
63-
"--provisioner-types=echo",
98+
`--provisioner-types=echo${requireTerraformTests ? ",terraform" : ""}`,
99+
`--provisioner-daemons=10`,
64100
"--web-terminal-renderer=dom",
65101
"--pprof-enable",
66102
]

site/e2e/tests/createWorkspace.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { test, expect } from "@playwright/test";
22
import {
3+
StarterTemplates,
34
createTemplate,
45
createWorkspace,
56
echoResponsesWithParameters,
7+
openTerminalWindow,
8+
requireTerraformProvisioner,
69
verifyParameters,
710
} from "../helpers";
811
import { beforeCoderTest } from "../hooks";
@@ -147,3 +150,42 @@ test("create workspace with disable_param search params", async ({ page }) => {
147150
await expect(page.getByLabel(/First parameter/i)).toBeDisabled();
148151
await expect(page.getByLabel(/Second parameter/i)).toBeDisabled();
149152
});
153+
154+
test("create docker workspace", async ({ context, page }) => {
155+
test.skip(
156+
true,
157+
"creating docker containers is currently leaky. They are not cleaned up when the tests are over.",
158+
);
159+
requireTerraformProvisioner();
160+
const template = await createTemplate(page, StarterTemplates.STARTER_DOCKER);
161+
162+
const workspaceName = await createWorkspace(page, template);
163+
164+
// The workspace agents must be ready before we try to interact with the workspace.
165+
await page.waitForSelector(
166+
`//div[@role="status"][@data-testid="agent-status-ready"]`,
167+
{
168+
state: "visible",
169+
},
170+
);
171+
172+
// Wait for the terminal button to be visible, and click it.
173+
const terminalButton =
174+
"//a[@data-testid='terminal'][normalize-space()='Terminal']";
175+
await page.waitForSelector(terminalButton, {
176+
state: "visible",
177+
});
178+
179+
const terminal = await openTerminalWindow(
180+
page,
181+
context,
182+
workspaceName,
183+
"main",
184+
);
185+
await terminal.waitForSelector(
186+
`//textarea[contains(@class,"xterm-helper-textarea")]`,
187+
{
188+
state: "visible",
189+
},
190+
);
191+
});

site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import { useFormik } from "formik";
33
import camelCase from "lodash/camelCase";
44
import capitalize from "lodash/capitalize";
55
import type { FC } from "react";
6+
import { useSearchParams } from "react-router-dom";
67
import * as Yup from "yup";
78
import type {
89
ProvisionerJobLog,
10+
ProvisionerType,
911
Template,
1012
TemplateExample,
1113
TemplateVersionVariable,
@@ -50,6 +52,7 @@ export interface CreateTemplateData {
5052
parameter_values_by_name?: Record<string, string>;
5153
user_variable_values?: VariableValue[];
5254
allow_everyone_group_access: boolean;
55+
provisioner_type: ProvisionerType;
5356
}
5457

5558
const validationSchema = Yup.object({
@@ -81,23 +84,31 @@ const defaultInitialValues: CreateTemplateData = {
8184
allow_user_autostart: false,
8285
allow_user_autostop: false,
8386
allow_everyone_group_access: true,
87+
provisioner_type: "terraform",
8488
};
8589

8690
type GetInitialValuesParams = {
8791
fromExample?: TemplateExample;
8892
fromCopy?: Template;
8993
variables?: TemplateVersionVariable[];
9094
allowAdvancedScheduling: boolean;
95+
searchParams: URLSearchParams;
9196
};
9297

9398
const getInitialValues = ({
9499
fromExample,
95100
fromCopy,
96101
allowAdvancedScheduling,
97102
variables,
103+
searchParams,
98104
}: GetInitialValuesParams) => {
99105
let initialValues = defaultInitialValues;
100106

107+
// Will assume the query param has a valid ProvisionerType, as this query param is only used
108+
// in testing.
109+
defaultInitialValues.provisioner_type =
110+
(searchParams.get("provisioner_type") as ProvisionerType) || "terraform";
111+
101112
if (!allowAdvancedScheduling) {
102113
initialValues = {
103114
...initialValues,
@@ -164,6 +175,7 @@ export type CreateTemplateFormProps = (
164175
};
165176

166177
export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
178+
const [searchParams] = useSearchParams();
167179
const {
168180
onCancel,
169181
onSubmit,
@@ -176,13 +188,15 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
176188
allowAdvancedScheduling,
177189
variablesSectionRef,
178190
} = props;
191+
179192
const form = useFormik<CreateTemplateData>({
180193
initialValues: getInitialValues({
181194
allowAdvancedScheduling,
182195
fromExample:
183196
"starterTemplate" in props ? props.starterTemplate : undefined,
184197
fromCopy: "copiedTemplate" in props ? props.copiedTemplate : undefined,
185198
variables,
199+
searchParams,
186200
}),
187201
validationSchema,
188202
onSubmit,

site/src/pages/CreateTemplatePage/DuplicateTemplateView.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export const DuplicateTemplateView: FC<CreateTemplatePageViewProps> = ({
8282
version: firstVersionFromFile(
8383
templateVersionQuery.data!.job.file_id,
8484
formData.user_variable_values,
85+
formData.provisioner_type,
8586
),
8687
template: newTemplate(formData),
8788
});

site/src/pages/CreateTemplatePage/UploadTemplateView.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export const UploadTemplateView: FC<CreateTemplatePageViewProps> = ({
6464
version: firstVersionFromFile(
6565
uploadedFile!.hash,
6666
formData.user_variable_values,
67+
formData.provisioner_type,
6768
),
6869
template: newTemplate(formData),
6970
});

site/src/pages/CreateTemplatePage/utils.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type {
2+
CreateTemplateVersionRequest,
23
Entitlements,
34
ProvisionerType,
45
TemplateExample,
@@ -7,10 +8,6 @@ import type {
78
import { calculateAutostopRequirementDaysValue } from "utils/schedule";
89
import type { CreateTemplateData } from "./CreateTemplateForm";
910

10-
const provisioner: ProvisionerType =
11-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Playwright needs to use a different provisioner type!
12-
typeof (window as any).playwright !== "undefined" ? "echo" : "terraform";
13-
1411
export const newTemplate = (formData: CreateTemplateData) => {
1512
const { autostop_requirement_days_of_week, autostop_requirement_weeks } =
1613
formData;
@@ -56,10 +53,11 @@ export const getFormPermissions = (entitlements: Entitlements) => {
5653
export const firstVersionFromFile = (
5754
fileId: string,
5855
variables: VariableValue[] | undefined,
59-
) => {
56+
provisionerType: ProvisionerType,
57+
): CreateTemplateVersionRequest => {
6058
return {
6159
storage_method: "file" as const,
62-
provisioner: provisioner,
60+
provisioner: provisionerType,
6361
user_variable_values: variables,
6462
file_id: fileId,
6563
tags: {},
@@ -69,10 +67,11 @@ export const firstVersionFromFile = (
6967
export const firstVersionFromExample = (
7068
example: TemplateExample,
7169
variables: VariableValue[] | undefined,
72-
) => {
70+
): CreateTemplateVersionRequest => {
7371
return {
7472
storage_method: "file" as const,
75-
provisioner: provisioner,
73+
// All starter templates are for the terraform provisioner type.
74+
provisioner: "terraform",
7675
user_variable_values: variables,
7776
example_id: example.id,
7877
tags: {},

0 commit comments

Comments
 (0)