mirror of https://github.com/coder/coder.git
Merge branch 'main' into node-20
This commit is contained in:
commit
b256fda35b
|
@ -232,20 +232,21 @@ func findAgent(agentName string, haystack []codersdk.WorkspaceResource) (*coders
|
|||
func writeBundle(src *support.Bundle, dest *zip.Writer) error {
|
||||
// We JSON-encode the following:
|
||||
for k, v := range map[string]any{
|
||||
"deployment/buildinfo.json": src.Deployment.BuildInfo,
|
||||
"deployment/config.json": src.Deployment.Config,
|
||||
"deployment/experiments.json": src.Deployment.Experiments,
|
||||
"deployment/health.json": src.Deployment.HealthReport,
|
||||
"network/netcheck.json": src.Network.Netcheck,
|
||||
"workspace/workspace.json": src.Workspace.Workspace,
|
||||
"agent/agent.json": src.Agent.Agent,
|
||||
"agent/listening_ports.json": src.Agent.ListeningPorts,
|
||||
"agent/manifest.json": src.Agent.Manifest,
|
||||
"agent/peer_diagnostics.json": src.Agent.PeerDiagnostics,
|
||||
"agent/ping_result.json": src.Agent.PingResult,
|
||||
"deployment/buildinfo.json": src.Deployment.BuildInfo,
|
||||
"deployment/config.json": src.Deployment.Config,
|
||||
"deployment/experiments.json": src.Deployment.Experiments,
|
||||
"deployment/health.json": src.Deployment.HealthReport,
|
||||
"network/connection_info.json": src.Network.ConnectionInfo,
|
||||
"network/netcheck.json": src.Network.Netcheck,
|
||||
"workspace/template.json": src.Workspace.Template,
|
||||
"workspace/template_version.json": src.Workspace.TemplateVersion,
|
||||
"workspace/parameters.json": src.Workspace.Parameters,
|
||||
"workspace/workspace.json": src.Workspace.Workspace,
|
||||
} {
|
||||
f, err := dest.Create(k)
|
||||
if err != nil {
|
||||
|
@ -265,17 +266,17 @@ func writeBundle(src *support.Bundle, dest *zip.Writer) error {
|
|||
|
||||
// The below we just write as we have them:
|
||||
for k, v := range map[string]string{
|
||||
"network/coordinator_debug.html": src.Network.CoordinatorDebug,
|
||||
"network/tailnet_debug.html": src.Network.TailnetDebug,
|
||||
"workspace/build_logs.txt": humanizeBuildLogs(src.Workspace.BuildLogs),
|
||||
"agent/logs.txt": string(src.Agent.Logs),
|
||||
"agent/agent_magicsock.html": string(src.Agent.AgentMagicsockHTML),
|
||||
"agent/client_magicsock.html": string(src.Agent.ClientMagicsockHTML),
|
||||
"agent/startup_logs.txt": humanizeAgentLogs(src.Agent.StartupLogs),
|
||||
"agent/prometheus.txt": string(src.Agent.Prometheus),
|
||||
"workspace/template_file.zip": string(templateVersionBytes),
|
||||
"logs.txt": strings.Join(src.Logs, "\n"),
|
||||
"cli_logs.txt": string(src.CLILogs),
|
||||
"logs.txt": strings.Join(src.Logs, "\n"),
|
||||
"network/coordinator_debug.html": src.Network.CoordinatorDebug,
|
||||
"network/tailnet_debug.html": src.Network.TailnetDebug,
|
||||
"workspace/build_logs.txt": humanizeBuildLogs(src.Workspace.BuildLogs),
|
||||
"workspace/template_file.zip": string(templateVersionBytes),
|
||||
} {
|
||||
f, err := dest.Create(k)
|
||||
if err != nil {
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbfake"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
"github.com/coder/coder/v2/coderd/healthcheck/derphealth"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/codersdk/agentsdk"
|
||||
"github.com/coder/coder/v2/codersdk/healthsdk"
|
||||
|
@ -182,6 +183,10 @@ func assertBundleContents(t *testing.T, path string, wantWorkspace bool, wantAge
|
|||
var v healthsdk.HealthcheckReport
|
||||
decodeJSONFromZip(t, f, &v)
|
||||
require.NotEmpty(t, v, "health report should not be empty")
|
||||
case "network/connection_info.json":
|
||||
var v workspacesdk.AgentConnectionInfo
|
||||
decodeJSONFromZip(t, f, &v)
|
||||
require.NotEmpty(t, v, "agent connection info should not be empty")
|
||||
case "network/coordinator_debug.html":
|
||||
bs := readBytesFromZip(t, f)
|
||||
require.NotEmpty(t, bs, "coordinator debug should not be empty")
|
||||
|
@ -189,13 +194,9 @@ func assertBundleContents(t *testing.T, path string, wantWorkspace bool, wantAge
|
|||
bs := readBytesFromZip(t, f)
|
||||
require.NotEmpty(t, bs, "tailnet debug should not be empty")
|
||||
case "network/netcheck.json":
|
||||
var v workspacesdk.AgentConnectionInfo
|
||||
var v derphealth.Report
|
||||
decodeJSONFromZip(t, f, &v)
|
||||
if !wantAgent || !wantWorkspace {
|
||||
require.Empty(t, v, "expected connection info to be empty")
|
||||
continue
|
||||
}
|
||||
require.NotEmpty(t, v, "connection info should not be empty")
|
||||
require.NotEmpty(t, v, "netcheck should not be empty")
|
||||
case "workspace/workspace.json":
|
||||
var v codersdk.Workspace
|
||||
decodeJSONFromZip(t, f, &v)
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
# Generate and upload a Support Bundle to Coder Support
|
||||
|
||||
When you engage with Coder support to diagnose an issue with your deployment,
|
||||
you may be asked to generate and upload a "Support Bundle" for offline analysis.
|
||||
This document explains the contents of a support bundle and the steps to submit
|
||||
a support bundle to Coder staff.
|
||||
|
||||
## What is a Support Bundle?
|
||||
|
||||
A support bundle is an archive containing a snapshot of information about your
|
||||
Coder deployment.
|
||||
|
||||
It contains information about the workspace, the template it uses, running
|
||||
agents in the workspace, and other detailed information useful for
|
||||
troubleshooting.
|
||||
|
||||
It is primarily intended for troubleshooting connectivity issues to workspaces,
|
||||
but can be useful for diagnosing other issues as well.
|
||||
|
||||
**While we attempt to redact sensitive information from support bundles, they
|
||||
may contain information deemed sensitive by your organization and should be
|
||||
treated as such.**
|
||||
|
||||
A brief overview of all files contained in the bundle is provided below:
|
||||
|
||||
> Note: detailed descriptions of all the information available in the bundle is
|
||||
> out of scope, as support bundles are primarily intended for internal use.
|
||||
|
||||
| Filename | Description |
|
||||
| --------------------------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| `agent/agent.json` | The agent used to connect to the workspace with environment variables stripped. |
|
||||
| `agent/agent_magicsock.html` | The contents of the HTTP debug endpoint of the agent's Tailscale connection. |
|
||||
| `agent/client_magicsock.html` | The contents of the HTTP debug endpoint of the client's Tailscale connection. |
|
||||
| `agent/listening_ports.json` | The listening ports detected by the selected agent running in the workspace. |
|
||||
| `agent/logs.txt` | The logs of the selected agent running in the workspace. |
|
||||
| `agent/manifest.json` | The manifest of the selected agent with environment variables stripped. |
|
||||
| `agent/startup_logs.txt` | Startup logs of the workspace agent. |
|
||||
| `agent/prometheus.txt` | The contents of the agent's Prometheus endpoint. |
|
||||
| `cli_logs.txt` | Logs from running the `coder support bundle` command. |
|
||||
| `deployment/buildinfo.json` | Coder version and build information. |
|
||||
| `deployment/config.json` | Deployment [configuration](../api/general.md#get-deployment-config), with secret values removed. |
|
||||
| `deployment/experiments.json` | Any [experiments](../cli/server.md#experiments) currently enabled for the deployment. |
|
||||
| `deployment/health.json` | A snapshot of the [health status](../admin/healthcheck.md) of the deployment. |
|
||||
| `logs.txt` | Logs from the `codersdk.Client` used to generate the bundle. |
|
||||
| `network/connection_info.json` | Information used by workspace agents used to connect to Coder (DERP map etc.) |
|
||||
| `network/coordinator_debug.html` | Peers currently connected to each Coder instance and the tunnels established between peers. |
|
||||
| `network/netcheck.json` | Results of running `coder netcheck` locally. |
|
||||
| `network/tailnet_debug.html` | Tailnet coordinators, their heartbeat ages, connected peers, and tunnels. |
|
||||
| `workspace/build_logs.txt` | Build logs of the selected workspace. |
|
||||
| `workspace/workspace.json` | Details of the selected workspace. |
|
||||
| `workspace/parameters.json` | Build parameters of the selected workspace. |
|
||||
| `workspace/template.json` | The template currently in use by the selected workspace. |
|
||||
| `workspace/template_file.zip` | The source code of the template currently in use by the selected workspace. |
|
||||
| `workspace/template_version.json` | The template version currently in use by the selected workspace. |
|
||||
|
||||
## How do I generate a Support Bundle?
|
||||
|
||||
1. Ensure your deployment is up and running. Generating a support bundle
|
||||
requires the Coder deployment to be available.
|
||||
|
||||
2. Ensure you have the Coder CLI installed on a local machine. See
|
||||
(installation)[../install/index.md] for steps on how to do this.
|
||||
|
||||
> Note: It is recommended to generate a support bundle from a location
|
||||
> experiencing workspace connectivity issues.
|
||||
|
||||
3. Ensure you are [logged in](../cli/login.md#login) to your Coder deployment as
|
||||
a user with the Owner privilege.
|
||||
|
||||
4. Run `coder support bundle [owner/workspace]`, and respond `yes` to the
|
||||
prompt. The support bundle will be generated in the current directory with
|
||||
the filename `coder-support-$TIMESTAMP.zip`.
|
||||
|
||||
> While support bundles can be generated without a running workspace, it is
|
||||
> recommended to specify one to maximize troubleshooting information.
|
||||
|
||||
5. (Recommended) Extract the support bundle and review its contents, redacting
|
||||
any information you deem necessary.
|
||||
|
||||
6. Coder staff will provide you a link where you can upload the bundle along
|
||||
with any other necessary supporting files.
|
||||
|
||||
> Note: It is helpful to leave an informative message regarding the nature of
|
||||
> supporting files.
|
||||
|
||||
Coder support will then review the information you provided and respond to you
|
||||
with next steps.
|
|
@ -1075,6 +1075,11 @@
|
|||
"path": "./guides/index.md",
|
||||
"icon_path": "./images/icons/notes.svg",
|
||||
"children": [
|
||||
{
|
||||
"title": "Generate a Support Bundle",
|
||||
"description": "Generate and upload a Support Bundle to Coder Support",
|
||||
"path": "./guides/support-bundle.md"
|
||||
},
|
||||
{
|
||||
"title": "Configuring Okta",
|
||||
"description": "Custom claims/scopes with Okta for group/role sync",
|
||||
|
|
|
@ -190,6 +190,8 @@ if [[ "${CODER_LIBSH_NO_CHECK_DEPENDENCIES:-}" != *t* ]]; then
|
|||
if isdarwin; then
|
||||
log "On darwin:"
|
||||
log "- brew install bash"
|
||||
# shellcheck disable=SC2016
|
||||
log '- Add "$(brew --prefix bash)/bin" to your PATH'
|
||||
log "- Restart your terminal"
|
||||
fi
|
||||
log
|
||||
|
@ -203,7 +205,7 @@ if [[ "${CODER_LIBSH_NO_CHECK_DEPENDENCIES:-}" != *t* ]]; then
|
|||
log "On darwin:"
|
||||
log "- brew install gnu-getopt"
|
||||
# shellcheck disable=SC2016
|
||||
log '- Add "$(brew --prefix)/opt/gnu-getopt/bin" to your PATH'
|
||||
log '- Add "$(brew --prefix gnu-getopt)/bin" to your PATH'
|
||||
log "- Restart your terminal"
|
||||
fi
|
||||
log
|
||||
|
@ -226,7 +228,7 @@ if [[ "${CODER_LIBSH_NO_CHECK_DEPENDENCIES:-}" != *t* ]]; then
|
|||
log "On darwin:"
|
||||
log "- brew install make"
|
||||
# shellcheck disable=SC2016
|
||||
log '- Add "$(brew --prefix)/opt/make/libexec/gnubin" to your PATH (you should Google this first)'
|
||||
log '- Add "$(brew --prefix make)/libexec/gnubin" to your PATH'
|
||||
log "- Restart your terminal"
|
||||
fi
|
||||
log
|
||||
|
|
|
@ -12,6 +12,8 @@ cd site
|
|||
# Build the frontend assets. If you are actively changing
|
||||
# the site to debug an issue, add `--watch`.
|
||||
pnpm build
|
||||
# Alternatively, build with debug info and source maps:
|
||||
NODE_ENV=development pnpm vite build --mode=development
|
||||
# Install the browsers to `~/.cache/ms-playwright`.
|
||||
pnpm playwright:install
|
||||
# Run E2E tests. You can see the configuration of the server
|
||||
|
|
|
@ -37,3 +37,7 @@ export const requireEnterpriseTests = Boolean(
|
|||
process.env.CODER_E2E_REQUIRE_ENTERPRISE_TESTS,
|
||||
);
|
||||
export const enterpriseLicense = process.env.CODER_E2E_ENTERPRISE_LICENSE ?? "";
|
||||
|
||||
// Fake experiments to verify that site presents them as enabled.
|
||||
export const e2eFakeExperiment1 = "e2e-fake-experiment-1";
|
||||
export const e2eFakeExperiment2 = "e2e-fake-experiment-2";
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
import { defineConfig } from "@playwright/test";
|
||||
import * as path from "path";
|
||||
import { coderMain, coderPort, coderdPProfPort, gitAuth } from "./constants";
|
||||
import {
|
||||
coderMain,
|
||||
coderPort,
|
||||
coderdPProfPort,
|
||||
e2eFakeExperiment1,
|
||||
e2eFakeExperiment2,
|
||||
gitAuth,
|
||||
} from "./constants";
|
||||
|
||||
export const wsEndpoint = process.env.CODER_E2E_WS_ENDPOINT;
|
||||
|
||||
|
@ -22,7 +29,7 @@ export default defineConfig({
|
|||
testMatch: /.*\.spec\.ts/,
|
||||
dependencies: ["testsSetup"],
|
||||
use: { storageState },
|
||||
timeout: 20_000,
|
||||
timeout: 50_000,
|
||||
},
|
||||
],
|
||||
reporter: [["./reporter.ts"]],
|
||||
|
@ -60,6 +67,8 @@ export default defineConfig({
|
|||
.join(" "),
|
||||
env: {
|
||||
...process.env,
|
||||
// Otherwise, the runner fails on Mac with: could not determine kind of name for C.uuid_string_t
|
||||
CGO_ENABLED: "0",
|
||||
|
||||
// This is the test provider for git auth with devices!
|
||||
CODER_GITAUTH_0_ID: gitAuth.deviceProvider,
|
||||
|
@ -101,6 +110,7 @@ export default defineConfig({
|
|||
gitAuth.validatePath,
|
||||
),
|
||||
CODER_PPROF_ADDRESS: "127.0.0.1:" + coderdPProfPort,
|
||||
CODER_EXPERIMENTS: e2eFakeExperiment1 + "," + e2eFakeExperiment2,
|
||||
},
|
||||
reuseExistingServer: false,
|
||||
},
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
} from "../helpers";
|
||||
import { beforeCoderTest } from "../hooks";
|
||||
|
||||
test.beforeEach(async ({ page }) => await beforeCoderTest(page));
|
||||
test.beforeEach(({ page }) => beforeCoderTest(page));
|
||||
|
||||
test("app", async ({ context, page }) => {
|
||||
const appContent = "Hello World";
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
import { expect, test } from "@playwright/test";
|
||||
import {
|
||||
createTemplate,
|
||||
createWorkspace,
|
||||
requiresEnterpriseLicense,
|
||||
} from "../helpers";
|
||||
import { beforeCoderTest } from "../hooks";
|
||||
|
||||
test.beforeEach(({ page }) => beforeCoderTest(page));
|
||||
|
||||
test("inspecting and filtering audit logs", async ({ page }) => {
|
||||
requiresEnterpriseLicense();
|
||||
|
||||
const userName = "admin";
|
||||
// Do some stuff that should show up in the audit logs
|
||||
const templateName = await createTemplate(page);
|
||||
const workspaceName = await createWorkspace(page, templateName);
|
||||
|
||||
// Go to the audit history
|
||||
await page.goto("/audit");
|
||||
|
||||
// Make sure those things we did all actually show up
|
||||
await expect(page.getByText(`${userName} logged in`)).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(`${userName} created template ${templateName}`),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(`${userName} created workspace ${workspaceName}`),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(`${userName} started workspace ${workspaceName}`),
|
||||
).toBeVisible();
|
||||
|
||||
// Make sure we can inspect the details of the log item
|
||||
const createdWorkspace = page.locator(".MuiTableRow-root", {
|
||||
hasText: `${userName} created workspace ${workspaceName}`,
|
||||
});
|
||||
await createdWorkspace.getByLabel("open-dropdown").click();
|
||||
await expect(
|
||||
createdWorkspace.getByText(`automatic_updates: "never"`),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
createdWorkspace.getByText(`name: "${workspaceName}"`),
|
||||
).toBeVisible();
|
||||
|
||||
const startedWorkspaceMessage = `${userName} started workspace ${workspaceName}`;
|
||||
const loginMessage = `${userName} logged in`;
|
||||
|
||||
// Filter by resource type
|
||||
await page.getByText("All resource types").click();
|
||||
await page.getByRole("menu").getByText("Workspace Build").click();
|
||||
// Our workspace build should be visible
|
||||
await expect(page.getByText(startedWorkspaceMessage)).toBeVisible();
|
||||
// Logins should no longer be visible
|
||||
await expect(page.getByText(loginMessage)).not.toBeVisible();
|
||||
|
||||
// Clear filters, everything should be visible again
|
||||
await page.getByLabel("Clear filter").click();
|
||||
await expect(page.getByText(startedWorkspaceMessage)).toBeVisible();
|
||||
await expect(page.getByText(loginMessage)).toBeVisible();
|
||||
|
||||
// Filter by action type
|
||||
await page.getByText("All actions").click();
|
||||
await page.getByRole("menu").getByText("Login").click();
|
||||
// Logins should be visible
|
||||
await expect(page.getByText(loginMessage)).toBeVisible();
|
||||
// Our workspace build should no longer be visible
|
||||
await expect(page.getByText(startedWorkspaceMessage)).not.toBeVisible();
|
||||
});
|
|
@ -18,7 +18,7 @@ import {
|
|||
} from "../parameters";
|
||||
import type { RichParameter } from "../provisionerGenerated";
|
||||
|
||||
test.beforeEach(async ({ page }) => await beforeCoderTest(page));
|
||||
test.beforeEach(({ page }) => beforeCoderTest(page));
|
||||
|
||||
test("create workspace", async ({ page }) => {
|
||||
const template = await createTemplate(page, {
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
import { chromium, expect, test } from "@playwright/test";
|
||||
import { expectUrl } from "../../expectUrl";
|
||||
import { randomName, requiresEnterpriseLicense } from "../../helpers";
|
||||
|
||||
test("set application name", async ({ page }) => {
|
||||
requiresEnterpriseLicense();
|
||||
|
||||
await page.goto("/deployment/appearance", { waitUntil: "domcontentloaded" });
|
||||
|
||||
const applicationName = randomName();
|
||||
|
||||
// Fill out the form
|
||||
const form = page.locator("form", { hasText: "Application name" });
|
||||
await form
|
||||
.getByLabel("Application name", { exact: true })
|
||||
.fill(applicationName);
|
||||
await form.getByRole("button", { name: "Submit" }).click();
|
||||
|
||||
// Open a new session without cookies to see the login page
|
||||
const browser = await chromium.launch();
|
||||
const incognitoContext = await browser.newContext();
|
||||
await incognitoContext.clearCookies();
|
||||
const incognitoPage = await incognitoContext.newPage();
|
||||
await incognitoPage.goto("/", { waitUntil: "domcontentloaded" });
|
||||
|
||||
// Verify the application name
|
||||
const name = incognitoPage.locator("h1", { hasText: applicationName });
|
||||
await expect(name).toBeVisible();
|
||||
|
||||
// Shut down browser
|
||||
await incognitoPage.close();
|
||||
await browser.close();
|
||||
});
|
||||
|
||||
test("set application logo", async ({ page }) => {
|
||||
requiresEnterpriseLicense();
|
||||
|
||||
await page.goto("/deployment/appearance", { waitUntil: "domcontentloaded" });
|
||||
|
||||
const imageLink = "/icon/azure.png";
|
||||
|
||||
// Fill out the form
|
||||
const form = page.locator("form", { hasText: "Logo URL" });
|
||||
await form.getByLabel("Logo URL", { exact: true }).fill(imageLink);
|
||||
await form.getByRole("button", { name: "Submit" }).click();
|
||||
|
||||
// Open a new session without cookies to see the login page
|
||||
const browser = await chromium.launch();
|
||||
const incognitoContext = await browser.newContext();
|
||||
await incognitoContext.clearCookies();
|
||||
const incognitoPage = await incognitoContext.newPage();
|
||||
await incognitoPage.goto("/", { waitUntil: "domcontentloaded" });
|
||||
|
||||
// Verify banner
|
||||
const logo = incognitoPage.locator("img");
|
||||
await expect(logo).toHaveAttribute("src", imageLink);
|
||||
|
||||
// Shut down browser
|
||||
await incognitoPage.close();
|
||||
await browser.close();
|
||||
});
|
||||
|
||||
test("set service banner", async ({ page }) => {
|
||||
requiresEnterpriseLicense();
|
||||
|
||||
await page.goto("/deployment/appearance", { waitUntil: "domcontentloaded" });
|
||||
|
||||
const message = "Mary has a little lamb.";
|
||||
|
||||
// Fill out the form
|
||||
const form = page.locator("form", { hasText: "Service Banner" });
|
||||
await form.getByLabel("Enabled", { exact: true }).check();
|
||||
await form.getByLabel("Message", { exact: true }).fill(message);
|
||||
await form.getByRole("button", { name: "Submit" }).click();
|
||||
|
||||
// Verify service banner
|
||||
await page.goto("/workspaces", { waitUntil: "domcontentloaded" });
|
||||
await expectUrl(page).toHavePathName("/workspaces");
|
||||
|
||||
const bar = page.locator("div.service-banner", { hasText: message });
|
||||
await expect(bar).toBeVisible();
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
import { expect, test } from "@playwright/test";
|
||||
import * as API from "api/api";
|
||||
import { setupApiCalls } from "../../api";
|
||||
import { e2eFakeExperiment1, e2eFakeExperiment2 } from "../../constants";
|
||||
|
||||
test("experiments", async ({ page }) => {
|
||||
await setupApiCalls(page);
|
||||
|
||||
// Load experiments from backend API
|
||||
const availableExperiments = await API.getAvailableExperiments();
|
||||
|
||||
// Verify if the site lists the same experiments
|
||||
await page.goto("/deployment/general", { waitUntil: "networkidle" });
|
||||
|
||||
const experimentsLocator = page.locator(
|
||||
"div.options-table tr.option-experiments ul.option-array",
|
||||
);
|
||||
await expect(experimentsLocator).toBeVisible();
|
||||
|
||||
// Firstly, check if all enabled experiments are listed
|
||||
expect(
|
||||
experimentsLocator.locator(
|
||||
`li.option-array-item-${e2eFakeExperiment1}.option-enabled`,
|
||||
),
|
||||
).toBeVisible;
|
||||
expect(
|
||||
experimentsLocator.locator(
|
||||
`li.option-array-item-${e2eFakeExperiment2}.option-enabled`,
|
||||
),
|
||||
).toBeVisible;
|
||||
|
||||
// Secondly, check if available experiments are listed
|
||||
for (const experiment of availableExperiments.safe) {
|
||||
const experimentLocator = experimentsLocator.locator(
|
||||
`li.option-array-item-${experiment}`,
|
||||
);
|
||||
await expect(experimentLocator).toBeVisible();
|
||||
}
|
||||
});
|
|
@ -5,7 +5,7 @@ import { gitAuth } from "../constants";
|
|||
import { Awaiter, createServer } from "../helpers";
|
||||
import { beforeCoderTest } from "../hooks";
|
||||
|
||||
test.beforeEach(async ({ page }) => await beforeCoderTest(page));
|
||||
test.beforeEach(({ page }) => beforeCoderTest(page));
|
||||
|
||||
// Ensures that a Git auth provider with the device flow functions and completes!
|
||||
test("external auth device", async ({ page }) => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { beforeCoderTest } from "../hooks";
|
||||
|
||||
test.beforeEach(async ({ page }) => await beforeCoderTest(page));
|
||||
test.beforeEach(({ page }) => beforeCoderTest(page));
|
||||
|
||||
test("list templates", async ({ page, baseURL }) => {
|
||||
await page.goto(`${baseURL}/templates`, { waitUntil: "domcontentloaded" });
|
||||
|
|
|
@ -14,7 +14,7 @@ import { beforeCoderTest } from "../hooks";
|
|||
// we no longer support versions prior to single tailnet: https://github.com/coder/coder/commit/d7cbdbd9c64ad26821e6b35834c59ecf85dcd9d4
|
||||
const agentVersion = "v0.27.0";
|
||||
|
||||
test.beforeEach(async ({ page }) => await beforeCoderTest(page));
|
||||
test.beforeEach(({ page }) => beforeCoderTest(page));
|
||||
|
||||
test("ssh with agent " + agentVersion, async ({ page }) => {
|
||||
test.setTimeout(40_000); // This is a slow test, 20s may not be enough on Mac.
|
||||
|
|
|
@ -14,7 +14,7 @@ import { beforeCoderTest } from "../hooks";
|
|||
// we no longer support versions prior to single tailnet: https://github.com/coder/coder/commit/d7cbdbd9c64ad26821e6b35834c59ecf85dcd9d4
|
||||
const clientVersion = "v0.27.0";
|
||||
|
||||
test.beforeEach(async ({ page }) => await beforeCoderTest(page));
|
||||
test.beforeEach(({ page }) => beforeCoderTest(page));
|
||||
|
||||
test("ssh with client " + clientVersion, async ({ page }) => {
|
||||
const token = randomUUID();
|
||||
|
|
|
@ -10,7 +10,7 @@ import { beforeCoderTest } from "../hooks";
|
|||
import { firstBuildOption, secondBuildOption } from "../parameters";
|
||||
import type { RichParameter } from "../provisionerGenerated";
|
||||
|
||||
test.beforeEach(async ({ page }) => await beforeCoderTest(page));
|
||||
test.beforeEach(({ page }) => beforeCoderTest(page));
|
||||
|
||||
test("restart workspace with ephemeral parameters", async ({ page }) => {
|
||||
const richParameters: RichParameter[] = [firstBuildOption, secondBuildOption];
|
||||
|
|
|
@ -7,9 +7,12 @@ import {
|
|||
stopWorkspace,
|
||||
verifyParameters,
|
||||
} from "../helpers";
|
||||
import { beforeCoderTest } from "../hooks";
|
||||
import { firstBuildOption, secondBuildOption } from "../parameters";
|
||||
import type { RichParameter } from "../provisionerGenerated";
|
||||
|
||||
test.beforeEach(({ page }) => beforeCoderTest(page));
|
||||
|
||||
test("start workspace with ephemeral parameters", async ({ page }) => {
|
||||
const richParameters: RichParameter[] = [firstBuildOption, secondBuildOption];
|
||||
const template = await createTemplate(
|
||||
|
|
|
@ -6,6 +6,9 @@ import {
|
|||
requiresEnterpriseLicense,
|
||||
updateTemplateSettings,
|
||||
} from "../helpers";
|
||||
import { beforeCoderTest } from "../hooks";
|
||||
|
||||
test.beforeEach(({ page }) => beforeCoderTest(page));
|
||||
|
||||
test("template update with new name redirects on successful submit", async ({
|
||||
page,
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
} from "../parameters";
|
||||
import type { RichParameter } from "../provisionerGenerated";
|
||||
|
||||
test.beforeEach(async ({ page }) => await beforeCoderTest(page));
|
||||
test.beforeEach(({ page }) => beforeCoderTest(page));
|
||||
|
||||
test("update workspace, new optional, immutable parameter added", async ({
|
||||
page,
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
} from "../helpers";
|
||||
import { beforeCoderTest } from "../hooks";
|
||||
|
||||
test.beforeEach(async ({ page }) => await beforeCoderTest(page));
|
||||
test.beforeEach(({ page }) => beforeCoderTest(page));
|
||||
|
||||
test("web terminal", async ({ context, page }) => {
|
||||
const token = randomUUID();
|
||||
|
|
|
@ -16,7 +16,7 @@ export const ServiceBannerView: FC<ServiceBannerViewProps> = ({
|
|||
isPreview,
|
||||
}) => {
|
||||
return (
|
||||
<div css={[styles.banner, { backgroundColor }]}>
|
||||
<div css={[styles.banner, { backgroundColor }]} className="service-banner">
|
||||
{isPreview && <Pill type="info">Preview</Pill>}
|
||||
<div
|
||||
css={[
|
||||
|
|
|
@ -105,6 +105,9 @@ export const AppearanceSettingsPageView: FC<
|
|||
fullWidth
|
||||
placeholder='Leave empty to display "Coder".'
|
||||
disabled={!isEntitled}
|
||||
inputProps={{
|
||||
"aria-label": "Application name",
|
||||
}}
|
||||
/>
|
||||
</Fieldset>
|
||||
|
||||
|
@ -150,6 +153,9 @@ export const AppearanceSettingsPageView: FC<
|
|||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
inputProps={{
|
||||
"aria-label": "Logo URL",
|
||||
}}
|
||||
/>
|
||||
</Fieldset>
|
||||
|
||||
|
@ -208,6 +214,7 @@ export const AppearanceSettingsPageView: FC<
|
|||
);
|
||||
await serviceBannerForm.setFieldValue("enabled", newState);
|
||||
}}
|
||||
data-testid="switch-service-banner"
|
||||
/>
|
||||
}
|
||||
label="Enabled"
|
||||
|
@ -221,6 +228,9 @@ export const AppearanceSettingsPageView: FC<
|
|||
fullWidth
|
||||
label="Message"
|
||||
multiline
|
||||
inputProps={{
|
||||
"aria-label": "Message",
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ export const OptionValue: FC<OptionValueProps> = (props) => {
|
|||
|
||||
if (typeof value === "object" && !Array.isArray(value)) {
|
||||
return (
|
||||
<ul css={{ listStyle: "none" }}>
|
||||
<ul css={{ listStyle: "none" }} className="option-array">
|
||||
{Object.entries(value)
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(([option, isEnabled]) => (
|
||||
|
@ -64,6 +64,9 @@ export const OptionValue: FC<OptionValueProps> = (props) => {
|
|||
color: theme.palette.text.disabled,
|
||||
},
|
||||
]}
|
||||
className={`option-array-item-${option} ${
|
||||
isEnabled ? "option-enabled" : "option-disabled"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
css={{
|
||||
|
|
|
@ -27,7 +27,7 @@ const OptionsTable: FC<OptionsTableProps> = ({ options, additionalValues }) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<TableContainer className="options-table">
|
||||
<Table
|
||||
css={css`
|
||||
& td {
|
||||
|
@ -57,7 +57,7 @@ const OptionsTable: FC<OptionsTableProps> = ({ options, additionalValues }) => {
|
|||
return null;
|
||||
}
|
||||
return (
|
||||
<TableRow key={option.flag}>
|
||||
<TableRow key={option.flag} className={"option-" + option.flag}>
|
||||
<TableCell>
|
||||
<OptionName>{option.name}</OptionName>
|
||||
<OptionDescription>{option.description}</OptionDescription>
|
||||
|
|
|
@ -13,6 +13,9 @@ import (
|
|||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/xerrors"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/netcheck"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/healthcheck/derphealth"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
|
@ -46,9 +49,16 @@ type Deployment struct {
|
|||
}
|
||||
|
||||
type Network struct {
|
||||
CoordinatorDebug string `json:"coordinator_debug"`
|
||||
TailnetDebug string `json:"tailnet_debug"`
|
||||
Netcheck *workspacesdk.AgentConnectionInfo `json:"netcheck"`
|
||||
ConnectionInfo workspacesdk.AgentConnectionInfo
|
||||
CoordinatorDebug string `json:"coordinator_debug"`
|
||||
Netcheck *derphealth.Report `json:"netcheck"`
|
||||
TailnetDebug string `json:"tailnet_debug"`
|
||||
}
|
||||
|
||||
type Netcheck struct {
|
||||
Report *netcheck.Report `json:"report"`
|
||||
Error string `json:"error"`
|
||||
Logs []string `json:"logs"`
|
||||
}
|
||||
|
||||
type Workspace struct {
|
||||
|
@ -62,6 +72,7 @@ type Workspace struct {
|
|||
|
||||
type Agent struct {
|
||||
Agent *codersdk.WorkspaceAgent `json:"agent"`
|
||||
ConnectionInfo *workspacesdk.AgentConnectionInfo `json:"connection_info"`
|
||||
ListeningPorts *codersdk.WorkspaceAgentListeningPortsResponse `json:"listening_ports"`
|
||||
Logs []byte `json:"logs"`
|
||||
ClientMagicsockHTML []byte `json:"client_magicsock_html"`
|
||||
|
@ -136,7 +147,7 @@ func DeploymentInfo(ctx context.Context, client *codersdk.Client, log slog.Logge
|
|||
return d
|
||||
}
|
||||
|
||||
func NetworkInfo(ctx context.Context, client *codersdk.Client, log slog.Logger, agentID uuid.UUID) Network {
|
||||
func NetworkInfo(ctx context.Context, client *codersdk.Client, log slog.Logger) Network {
|
||||
var (
|
||||
n Network
|
||||
eg errgroup.Group
|
||||
|
@ -171,15 +182,18 @@ func NetworkInfo(ctx context.Context, client *codersdk.Client, log slog.Logger,
|
|||
})
|
||||
|
||||
eg.Go(func() error {
|
||||
if agentID == uuid.Nil {
|
||||
log.Warn(ctx, "agent id required for agent connection info")
|
||||
// Need connection info to get DERP map for netcheck
|
||||
connInfo, err := workspacesdk.New(client).AgentConnectionInfoGeneric(ctx)
|
||||
if err != nil {
|
||||
log.Warn(ctx, "unable to fetch generic agent connection info")
|
||||
return nil
|
||||
}
|
||||
connInfo, err := workspacesdk.New(client).AgentConnectionInfo(ctx, agentID)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("fetch agent conn info: %w", err)
|
||||
}
|
||||
n.Netcheck = &connInfo
|
||||
n.ConnectionInfo = connInfo
|
||||
var rpt derphealth.Report
|
||||
rpt.Run(ctx, &derphealth.ReportOptions{
|
||||
DERPMap: connInfo.DERPMap,
|
||||
})
|
||||
n.Netcheck = &rpt
|
||||
return nil
|
||||
})
|
||||
|
||||
|
@ -482,7 +496,7 @@ func Run(ctx context.Context, d *Deps) (*Bundle, error) {
|
|||
return nil
|
||||
})
|
||||
eg.Go(func() error {
|
||||
ni := NetworkInfo(ctx, d.Client, d.Log, d.AgentID)
|
||||
ni := NetworkInfo(ctx, d.Client, d.Log)
|
||||
b.Network = ni
|
||||
return nil
|
||||
})
|
||||
|
|
|
@ -62,9 +62,10 @@ func TestRun(t *testing.T) {
|
|||
assertSanitizedDeploymentConfig(t, bun.Deployment.Config)
|
||||
assertNotNilNotEmpty(t, bun.Deployment.HealthReport, "deployment health report should be present")
|
||||
assertNotNilNotEmpty(t, bun.Deployment.Experiments, "deployment experiments should be present")
|
||||
assertNotNilNotEmpty(t, bun.Network.ConnectionInfo, "agent connection info should be present")
|
||||
assertNotNilNotEmpty(t, bun.Network.CoordinatorDebug, "network coordinator debug should be present")
|
||||
assertNotNilNotEmpty(t, bun.Network.TailnetDebug, "network tailnet debug should be present")
|
||||
assertNotNilNotEmpty(t, bun.Network.Netcheck, "network netcheck should be present")
|
||||
assertNotNilNotEmpty(t, bun.Network.TailnetDebug, "network tailnet debug should be present")
|
||||
assertNotNilNotEmpty(t, bun.Workspace.Workspace, "workspace should be present")
|
||||
assertSanitizedWorkspace(t, bun.Workspace.Workspace)
|
||||
assertNotNilNotEmpty(t, bun.Workspace.BuildLogs, "workspace build logs should be present")
|
||||
|
@ -109,9 +110,10 @@ func TestRun(t *testing.T) {
|
|||
assertSanitizedDeploymentConfig(t, bun.Deployment.Config)
|
||||
assertNotNilNotEmpty(t, bun.Deployment.HealthReport, "deployment health report should be present")
|
||||
assertNotNilNotEmpty(t, bun.Deployment.Experiments, "deployment experiments should be present")
|
||||
assertNotNilNotEmpty(t, bun.Network.ConnectionInfo, "agent connection info should be present")
|
||||
assertNotNilNotEmpty(t, bun.Network.CoordinatorDebug, "network coordinator debug should be present")
|
||||
assertNotNilNotEmpty(t, bun.Network.Netcheck, "network netcheck should be present")
|
||||
assertNotNilNotEmpty(t, bun.Network.TailnetDebug, "network tailnet debug should be present")
|
||||
assert.Empty(t, bun.Network.Netcheck, "did not expect netcheck to be present")
|
||||
assert.Empty(t, bun.Workspace.Workspace, "did not expect workspace to be present")
|
||||
assert.Empty(t, bun.Agent, "did not expect agent to be present")
|
||||
assertNotNilNotEmpty(t, bun.Logs, "bundle logs should be present")
|
||||
|
|
Loading…
Reference in New Issue