From d6515aea917ea33e26648c0ba9573deae9872151 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 26 Feb 2025 14:55:48 +0200 Subject: [PATCH] Add integration test for creating and using presets from scratch Signed-off-by: Danny Kopping --- coderd/prebuilds/api.go | 7 +- site/e2e/playwright.config.ts | 4 +- site/e2e/setup/preflight.ts | 2 +- site/e2e/tests/presets/createPreset.spec.ts | 76 ++++++++++ site/e2e/tests/presets/template.zip | Bin 0 -> 4184 bytes site/e2e/tests/presets/template/main.tf | 152 ++++++++++++++++++++ 6 files changed, 235 insertions(+), 6 deletions(-) create mode 100644 site/e2e/tests/presets/createPreset.spec.ts create mode 100644 site/e2e/tests/presets/template.zip create mode 100644 site/e2e/tests/presets/template/main.tf diff --git a/coderd/prebuilds/api.go b/coderd/prebuilds/api.go index 3dc1c97344..7d2276ecc7 100644 --- a/coderd/prebuilds/api.go +++ b/coderd/prebuilds/api.go @@ -3,10 +3,8 @@ package prebuilds import ( "context" - "github.com/google/uuid" - "golang.org/x/xerrors" - "github.com/coder/coder/v2/coderd/database" + "github.com/google/uuid" ) type Claimer interface { @@ -17,7 +15,8 @@ type Claimer interface { type AGPLPrebuildClaimer struct{} func (c AGPLPrebuildClaimer) Claim(context.Context, database.Store, uuid.UUID, string, uuid.UUID) (*uuid.UUID, error) { - return nil, xerrors.Errorf("not entitled to claim prebuilds") + // Not entitled to claim prebuilds in AGPL version. + return nil, nil } func (c AGPLPrebuildClaimer) Initiator() uuid.UUID { diff --git a/site/e2e/playwright.config.ts b/site/e2e/playwright.config.ts index 762b7f0158..c5f6cefc29 100644 --- a/site/e2e/playwright.config.ts +++ b/site/e2e/playwright.config.ts @@ -122,6 +122,8 @@ export default defineConfig({ CODER_OIDC_SIGN_IN_TEXT: "Hello", CODER_OIDC_ICON_URL: "/icon/google.svg", }, - reuseExistingServer: false, + reuseExistingServer: process.env.CODER_E2E_REUSE_EXISTING_SERVER + ? Boolean(process.env.CODER_E2E_REUSE_EXISTING_SERVER) + : false, }, }); diff --git a/site/e2e/setup/preflight.ts b/site/e2e/setup/preflight.ts index dedcc195db..0a5eefc68c 100644 --- a/site/e2e/setup/preflight.ts +++ b/site/e2e/setup/preflight.ts @@ -36,7 +36,7 @@ export default function () { throw new Error(msg); } - if (!process.env.CI) { + if (!process.env.CI && !process.env.CODER_E2E_REUSE_EXISTING_SERVER) { console.info("==> make site/e2e/bin/coder"); execSync("make site/e2e/bin/coder", { cwd: path.join(__dirname, "../../../"), diff --git a/site/e2e/tests/presets/createPreset.spec.ts b/site/e2e/tests/presets/createPreset.spec.ts new file mode 100644 index 0000000000..a3b58d572e --- /dev/null +++ b/site/e2e/tests/presets/createPreset.spec.ts @@ -0,0 +1,76 @@ +import {expect, test} from "@playwright/test"; +import {currentUser, login} from "../../helpers"; +import {beforeCoderTest} from "../../hooks"; +import path from "node:path"; + +test.beforeEach(async ({page}) => { + beforeCoderTest(page); + await login(page); +}); + +test("create template with preset and use in workspace", async ({page, baseURL}) => { + test.setTimeout(120_000); + + // Create new template. + await page.goto('/templates/new', {waitUntil: 'domcontentloaded'}); + await page.getByTestId('drop-zone').click(); + + // Select the template file. + const [fileChooser] = await Promise.all([ + page.waitForEvent('filechooser'), + page.getByTestId('drop-zone').click() + ]); + await fileChooser.setFiles(path.join(__dirname, 'template.zip')); + + // Set name and submit. + const templateName = generateRandomName(); + await page.locator("input[name=name]").fill(templateName); + await page.getByRole('button', {name: 'Save'}).click(); + + await page.waitForURL(`/templates/${templateName}/files`, { + timeout: 120_000, + }); + + // Visit workspace creation page for new template. + await page.goto(`/templates/default/${templateName}/workspace`, {waitUntil: 'domcontentloaded'}); + + await page.locator('button[aria-label="Preset"]').click(); + + const preset1 = page.getByText('I Like GoLand'); + const preset2 = page.getByText('Some Like PyCharm'); + + await expect(preset1).toBeVisible(); + await expect(preset2).toBeVisible(); + + // Choose the GoLand preset. + await preset1.click(); + + // Validate the preset was applied correctly. + await expect(page.locator('input[value="GO"]')).toBeChecked(); + + // Create a workspace. + const workspaceName = generateRandomName(); + await page.locator("input[name=name]").fill(workspaceName); + await page.getByRole('button', {name: 'Create workspace'}).click(); + + // Wait for the workspace build display to be navigated to. + const user = currentUser(page); + await page.waitForURL(`/@${user.username}/${workspaceName}`, { + timeout: 120_000, // Account for workspace build time. + }); + + // Visit workspace settings page. + await page.goto(`/@${user.username}/${workspaceName}/settings/parameters`); + + // Validate the preset was applied correctly. + await expect(page.locator('input[value="GO"]')).toBeChecked(); +}); + +function generateRandomName() { + const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + let name = ''; + for (let i = 0; i < 10; i++) { + name += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return name; +} diff --git a/site/e2e/tests/presets/template.zip b/site/e2e/tests/presets/template.zip new file mode 100644 index 0000000000000000000000000000000000000000..0cf58ba1b89a2aecefb6fa07976a6b03284b2492 GIT binary patch literal 4184 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 AGXMYp literal 0 HcmV?d00001 diff --git a/site/e2e/tests/presets/template/main.tf b/site/e2e/tests/presets/template/main.tf new file mode 100644 index 0000000000..c2cf20afe0 --- /dev/null +++ b/site/e2e/tests/presets/template/main.tf @@ -0,0 +1,152 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "2.1.3" + } + docker = { + source = "kreuzwerker/docker" + version = "3.0.2" + } + } +} + +variable "docker_socket" { + default = "" + description = "(Optional) Docker socket URI" + type = string +} + +provider "docker" { + # Defaulting to null if the variable is an empty string lets us have an optional variable without having to set our own default + host = var.docker_socket != "" ? var.docker_socket : null +} + +data "coder_provisioner" "me" {} +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +data "coder_workspace_preset" "goland" { + name = "I Like GoLand" + parameters = { + "jetbrains_ide" = "GO" + } + prebuilds { + instances = 1 + } +} + +data "coder_workspace_preset" "python" { + name = "Some Like PyCharm" + parameters = { + "jetbrains_ide" = "PY" + } + prebuilds { + instances = 2 + } +} + +resource "coder_agent" "main" { + arch = data.coder_provisioner.me.arch + os = "linux" + startup_script = <<-EOT + set -e + + # Prepare user home with default files on first start! + if [ ! -f ~/.init_done ]; then + cp -rT /etc/skel ~ + touch ~/.init_done + fi + + # Add any commands that should be executed at workspace startup (e.g install requirements, start a program, etc) here + EOT + + # These environment variables allow you to make Git commits right away after creating a + # workspace. Note that they take precedence over configuration defined in ~/.gitconfig! + # You can remove this block if you'd prefer to configure Git manually or using + # dotfiles. (see docs/dotfiles.md) + env = { + GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name) + GIT_AUTHOR_EMAIL = "${data.coder_workspace_owner.me.email}" + GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name) + GIT_COMMITTER_EMAIL = "${data.coder_workspace_owner.me.email}" + } + + # The following metadata blocks are optional. They are used to display + # information about your workspace in the dashboard. You can remove them + # if you don't want to display any information. + # For basic resources, you can use the `coder stat` command. + # If you need more control, you can write your own script. + metadata { + display_name = "Is Prebuild" + key = "prebuild" + script = "echo ${data.coder_workspace.me.prebuild_count}" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Hostname" + key = "hostname" + script = "hostname" + interval = 10 + timeout = 1 + } +} + +# See https://registry.coder.com/modules/jetbrains-gateway +module "jetbrains_gateway" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/jetbrains-gateway/coder" + + # JetBrains IDEs to make available for the user to select + jetbrains_ides = ["IU", "PY", "WS", "PS", "RD", "CL", "GO", "RM"] + default = "IU" + + # Default folder to open when starting a JetBrains IDE + folder = "/home/coder" + + # This ensures that the latest version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production. + version = ">= 1.0.0" + + agent_id = coder_agent.main.id + agent_name = "main" + order = 2 +} + +resource "docker_volume" "home_volume" { + name = "coder-${data.coder_workspace.me.id}-home" + # Protect the volume from being deleted due to changes in attributes. + lifecycle { + ignore_changes = all + } +} + +resource "docker_container" "workspace" { + lifecycle { + ignore_changes = all + } + + network_mode = "host" + + count = data.coder_workspace.me.start_count + image = "codercom/enterprise-base:ubuntu" + # Uses lower() to avoid Docker restriction on container names. + name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}" + # Hostname makes the shell more user friendly: coder@my-workspace:~$ + hostname = data.coder_workspace.me.name + # Use the docker gateway if the access URL is 127.0.0.1 + entrypoint = [ + "sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal") + ] + env = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"] + host { + host = "host.docker.internal" + ip = "host-gateway" + } + volumes { + container_path = "/home/coder" + volume_name = docker_volume.home_volume.name + read_only = false + } +}