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 0000000000..0cf58ba1b8 Binary files /dev/null and b/site/e2e/tests/presets/template.zip differ 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 + } +}