chore: enable terraform provisioners in e2e by default (#13134)

* skip docker test for now, it leaks containers
This commit is contained in:
Steven Masley
2024-05-08 13:34:22 -05:00
committed by GitHub
parent 1832a755e1
commit a4bd50c985
9 changed files with 150 additions and 23 deletions
+3 -1
View File
@@ -222,5 +222,7 @@
"go.testFlags": ["-short", "-coverpkg=./..."],
// We often use a version of TypeScript that's ahead of the version shipped
// with VS Code.
"typescript.tsdk": "./site/node_modules/typescript/lib"
"typescript.tsdk": "./site/node_modules/typescript/lib",
// Playwright tests in VSCode will open a browser to live "view" the test.
"playwright.reuseBrowser": true
}
+4
View File
@@ -39,6 +39,10 @@ export const requireEnterpriseTests = Boolean(
);
export const enterpriseLicense = process.env.CODER_E2E_ENTERPRISE_LICENSE ?? "";
// Disabling terraform tests is optional for environments without Docker + Terraform.
// By default, we opt into these tests.
export const requireTerraformTests = !process.env.CODER_E2E_DISABLE_TERRAFORM;
// Fake experiments to verify that site presents them as enabled.
export const e2eFakeExperiment1 = "e2e-fake-experiment-1";
export const e2eFakeExperiment2 = "e2e-fake-experiment-2";
+40 -12
View File
@@ -19,6 +19,7 @@ import {
enterpriseLicense,
prometheusPort,
requireEnterpriseTests,
requireTerraformTests,
} from "./constants";
import { expectUrl } from "./expectUrl";
import {
@@ -43,6 +44,11 @@ export function requiresEnterpriseLicense() {
test.skip(!enterpriseLicense);
}
// requireTerraformProvisioner by default is enabled.
export function requireTerraformProvisioner() {
test.skip(!requireTerraformTests);
}
// createWorkspace creates a workspace for a template.
// It does not wait for it to be running, but it does navigate to the page.
export const createWorkspace = async (
@@ -149,25 +155,46 @@ export const verifyParameters = async (
}
};
// StarterTemplates are ids of starter templates that can be used in place of
// the responses payload. These starter templates will require real provisioners.
export enum StarterTemplates {
STARTER_DOCKER = "docker",
}
function isStarterTemplate(
input: EchoProvisionerResponses | StarterTemplates | undefined,
): input is StarterTemplates {
if (!input) {
return false;
}
return typeof input === "string";
}
// createTemplate navigates to the /templates/new page and uploads a template
// with the resources provided in the responses argument.
export const createTemplate = async (
page: Page,
responses?: EchoProvisionerResponses,
responses?: EchoProvisionerResponses | StarterTemplates,
): Promise<string> => {
// Required to have templates submit their provisioner type as echo!
await page.addInitScript({
content: "window.playwright = true",
});
let path = "/templates/new";
if (isStarterTemplate(responses)) {
path += `?exampleId=${responses}`;
} else {
// The form page will read this value and use it as the default type.
path += "?provisioner_type=echo";
}
await page.goto("/templates/new", { waitUntil: "domcontentloaded" });
await page.goto(path, { waitUntil: "domcontentloaded" });
await expectUrl(page).toHavePathName("/templates/new");
await page.getByTestId("file-upload").setInputFiles({
buffer: await createTemplateVersionTar(responses),
mimeType: "application/x-tar",
name: "template.tar",
});
if (!isStarterTemplate(responses)) {
await page.getByTestId("file-upload").setInputFiles({
buffer: await createTemplateVersionTar(responses),
mimeType: "application/x-tar",
name: "template.tar",
});
}
const name = randomName();
await page.getByLabel("Name *").fill(name);
await page.getByTestId("form-submit").click();
@@ -868,6 +895,7 @@ export async function openTerminalWindow(
page: Page,
context: BrowserContext,
workspaceName: string,
agentName: string = "dev",
): Promise<Page> {
// Wait for the web terminal to open in a new tab
const pagePromise = context.waitForEvent("page");
@@ -879,7 +907,7 @@ export async function openTerminalWindow(
// isn't POSIX compatible, such as Fish.
const commandQuery = `?command=${encodeURIComponent("/usr/bin/env bash")}`;
await expectUrl(terminal).toHavePathName(
`/@admin/${workspaceName}.dev/terminal`,
`/@admin/${workspaceName}.${agentName}/terminal`,
);
await terminal.goto(`/@admin/${workspaceName}.dev/terminal${commandQuery}`);
+38 -2
View File
@@ -1,4 +1,5 @@
import { defineConfig } from "@playwright/test";
import { execSync } from "child_process";
import * as path from "path";
import {
coderMain,
@@ -7,6 +8,7 @@ import {
e2eFakeExperiment1,
e2eFakeExperiment2,
gitAuth,
requireTerraformTests,
} from "./constants";
export const wsEndpoint = process.env.CODER_E2E_WS_ENDPOINT;
@@ -14,6 +16,39 @@ export const wsEndpoint = process.env.CODER_E2E_WS_ENDPOINT;
// This is where auth cookies are stored!
export const storageState = path.join(__dirname, ".auth.json");
// If running terraform tests, verify the requirements exist in the
// environment.
//
// These execs will throw an error if the status code is non-zero.
// So if both these work, then we can launch terraform provisioners.
let hasTerraform = false;
let hasDocker = false;
try {
execSync("terraform --version");
hasTerraform = true;
} catch {
/* empty */
}
try {
execSync("docker --version");
hasDocker = true;
} catch {
/* empty */
}
if (!hasTerraform || !hasDocker) {
const msg =
"Terraform provisioners require docker & terraform binaries to function. \n" +
(hasTerraform
? ""
: "\tThe `terraform` executable is not present in the runtime environment.\n") +
(hasDocker
? ""
: "\tThe `docker` executable is not present in the runtime environment.\n");
throw new Error(msg);
}
const localURL = (port: number, path: string): string => {
return `http://localhost:${port}${path}`;
};
@@ -54,13 +89,14 @@ export default defineConfig({
`go run -tags embed ${coderMain} server`,
"--global-config $(mktemp -d -t e2e-XXXXXXXXXX)",
`--access-url=http://localhost:${coderPort}`,
`--http-address=localhost:${coderPort}`,
`--http-address=0.0.0.0:${coderPort}`,
"--in-memory",
"--telemetry=false",
"--dangerous-disable-rate-limits",
"--provisioner-daemons 10",
// TODO: Enable some terraform provisioners
"--provisioner-types=echo",
`--provisioner-types=echo${requireTerraformTests ? ",terraform" : ""}`,
`--provisioner-daemons=10`,
"--web-terminal-renderer=dom",
"--pprof-enable",
]
+42
View File
@@ -1,8 +1,11 @@
import { test, expect } from "@playwright/test";
import {
StarterTemplates,
createTemplate,
createWorkspace,
echoResponsesWithParameters,
openTerminalWindow,
requireTerraformProvisioner,
verifyParameters,
} from "../helpers";
import { beforeCoderTest } from "../hooks";
@@ -147,3 +150,42 @@ test("create workspace with disable_param search params", async ({ page }) => {
await expect(page.getByLabel(/First parameter/i)).toBeDisabled();
await expect(page.getByLabel(/Second parameter/i)).toBeDisabled();
});
test("create docker workspace", async ({ context, page }) => {
test.skip(
true,
"creating docker containers is currently leaky. They are not cleaned up when the tests are over.",
);
requireTerraformProvisioner();
const template = await createTemplate(page, StarterTemplates.STARTER_DOCKER);
const workspaceName = await createWorkspace(page, template);
// The workspace agents must be ready before we try to interact with the workspace.
await page.waitForSelector(
`//div[@role="status"][@data-testid="agent-status-ready"]`,
{
state: "visible",
},
);
// Wait for the terminal button to be visible, and click it.
const terminalButton =
"//a[@data-testid='terminal'][normalize-space()='Terminal']";
await page.waitForSelector(terminalButton, {
state: "visible",
});
const terminal = await openTerminalWindow(
page,
context,
workspaceName,
"main",
);
await terminal.waitForSelector(
`//textarea[contains(@class,"xterm-helper-textarea")]`,
{
state: "visible",
},
);
});
@@ -3,9 +3,11 @@ import { useFormik } from "formik";
import camelCase from "lodash/camelCase";
import capitalize from "lodash/capitalize";
import type { FC } from "react";
import { useSearchParams } from "react-router-dom";
import * as Yup from "yup";
import type {
ProvisionerJobLog,
ProvisionerType,
Template,
TemplateExample,
TemplateVersionVariable,
@@ -50,6 +52,7 @@ export interface CreateTemplateData {
parameter_values_by_name?: Record<string, string>;
user_variable_values?: VariableValue[];
allow_everyone_group_access: boolean;
provisioner_type: ProvisionerType;
}
const validationSchema = Yup.object({
@@ -81,6 +84,7 @@ const defaultInitialValues: CreateTemplateData = {
allow_user_autostart: false,
allow_user_autostop: false,
allow_everyone_group_access: true,
provisioner_type: "terraform",
};
type GetInitialValuesParams = {
@@ -88,6 +92,7 @@ type GetInitialValuesParams = {
fromCopy?: Template;
variables?: TemplateVersionVariable[];
allowAdvancedScheduling: boolean;
searchParams: URLSearchParams;
};
const getInitialValues = ({
@@ -95,9 +100,15 @@ const getInitialValues = ({
fromCopy,
allowAdvancedScheduling,
variables,
searchParams,
}: GetInitialValuesParams) => {
let initialValues = defaultInitialValues;
// Will assume the query param has a valid ProvisionerType, as this query param is only used
// in testing.
defaultInitialValues.provisioner_type =
(searchParams.get("provisioner_type") as ProvisionerType) || "terraform";
if (!allowAdvancedScheduling) {
initialValues = {
...initialValues,
@@ -164,6 +175,7 @@ export type CreateTemplateFormProps = (
};
export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
const [searchParams] = useSearchParams();
const {
onCancel,
onSubmit,
@@ -176,6 +188,7 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
allowAdvancedScheduling,
variablesSectionRef,
} = props;
const form = useFormik<CreateTemplateData>({
initialValues: getInitialValues({
allowAdvancedScheduling,
@@ -183,6 +196,7 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
"starterTemplate" in props ? props.starterTemplate : undefined,
fromCopy: "copiedTemplate" in props ? props.copiedTemplate : undefined,
variables,
searchParams,
}),
validationSchema,
onSubmit,
@@ -82,6 +82,7 @@ export const DuplicateTemplateView: FC<CreateTemplatePageViewProps> = ({
version: firstVersionFromFile(
templateVersionQuery.data!.job.file_id,
formData.user_variable_values,
formData.provisioner_type,
),
template: newTemplate(formData),
});
@@ -64,6 +64,7 @@ export const UploadTemplateView: FC<CreateTemplatePageViewProps> = ({
version: firstVersionFromFile(
uploadedFile!.hash,
formData.user_variable_values,
formData.provisioner_type,
),
template: newTemplate(formData),
});
+7 -8
View File
@@ -1,4 +1,5 @@
import type {
CreateTemplateVersionRequest,
Entitlements,
ProvisionerType,
TemplateExample,
@@ -7,10 +8,6 @@ import type {
import { calculateAutostopRequirementDaysValue } from "utils/schedule";
import type { CreateTemplateData } from "./CreateTemplateForm";
const provisioner: ProvisionerType =
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Playwright needs to use a different provisioner type!
typeof (window as any).playwright !== "undefined" ? "echo" : "terraform";
export const newTemplate = (formData: CreateTemplateData) => {
const { autostop_requirement_days_of_week, autostop_requirement_weeks } =
formData;
@@ -56,10 +53,11 @@ export const getFormPermissions = (entitlements: Entitlements) => {
export const firstVersionFromFile = (
fileId: string,
variables: VariableValue[] | undefined,
) => {
provisionerType: ProvisionerType,
): CreateTemplateVersionRequest => {
return {
storage_method: "file" as const,
provisioner: provisioner,
provisioner: provisionerType,
user_variable_values: variables,
file_id: fileId,
tags: {},
@@ -69,10 +67,11 @@ export const firstVersionFromFile = (
export const firstVersionFromExample = (
example: TemplateExample,
variables: VariableValue[] | undefined,
) => {
): CreateTemplateVersionRequest => {
return {
storage_method: "file" as const,
provisioner: provisioner,
// All starter templates are for the terraform provisioner type.
provisioner: "terraform",
user_variable_values: variables,
example_id: example.id,
tags: {},