Merge branch 'main' into node-20

This commit is contained in:
Muhammad Atif Ali 2024-04-13 18:37:31 +03:00 committed by GitHub
commit b256fda35b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 385 additions and 48 deletions

View File

@ -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 {

View File

@ -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)

View File

@ -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.

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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";

View File

@ -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,
},

View File

@ -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";

View File

@ -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();
});

View File

@ -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, {

View File

@ -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();
});

View File

@ -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();
}
});

View File

@ -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 }) => {

View File

@ -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" });

View File

@ -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.

View File

@ -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();

View File

@ -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];

View File

@ -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(

View File

@ -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,

View File

@ -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,

View File

@ -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();

View File

@ -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={[

View File

@ -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>

View File

@ -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={{

View File

@ -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>

View File

@ -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
})

View File

@ -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")