chore: use tabs for prettier and biome (#14283)

This commit is contained in:
Kayla Washburn-Love
2024-08-15 14:53:53 -06:00
committed by GitHub
parent db2d0596d4
commit 95a7c0c4f0
961 changed files with 112866 additions and 112874 deletions
+10 -10
View File
@@ -1,13 +1,13 @@
{ {
"name": "Development environments on your infrastructure", "name": "Development environments on your infrastructure",
"image": "codercom/oss-dogfood:latest", "image": "codercom/oss-dogfood:latest",
"features": { "features": {
// See all possible options here https://github.com/devcontainers/features/tree/main/src/docker-in-docker // See all possible options here https://github.com/devcontainers/features/tree/main/src/docker-in-docker
"ghcr.io/devcontainers/features/docker-in-docker:2": { "ghcr.io/devcontainers/features/docker-in-docker:2": {
"moby": "false" "moby": "false"
} }
}, },
// SYS_PTRACE to enable go debugging // SYS_PTRACE to enable go debugging
"runArgs": ["--cap-add=SYS_PTRACE"] "runArgs": ["--cap-add=SYS_PTRACE"]
} }
-4
View File
@@ -95,10 +95,6 @@ updates:
- "@emotion*" - "@emotion*"
exclude-patterns: exclude-patterns:
- "jest-runner-eslint" - "jest-runner-eslint"
eslint:
patterns:
- "eslint*"
- "@typescript-eslint*"
jest: jest:
patterns: patterns:
- "jest" - "jest"
+24 -24
View File
@@ -1,26 +1,26 @@
{ {
"ignorePatterns": [ "ignorePatterns": [
{ {
"pattern": "://localhost" "pattern": "://localhost"
}, },
{ {
"pattern": "://.*.?example\\.com" "pattern": "://.*.?example\\.com"
}, },
{ {
"pattern": "developer.github.com" "pattern": "developer.github.com"
}, },
{ {
"pattern": "docs.github.com" "pattern": "docs.github.com"
}, },
{ {
"pattern": "support.google.com" "pattern": "support.google.com"
}, },
{ {
"pattern": "tailscale.com" "pattern": "tailscale.com"
}, },
{ {
"pattern": "wireguard.com" "pattern": "wireguard.com"
} }
], ],
"aliveStatusCodes": [200, 0] "aliveStatusCodes": [200, 0]
} }
+1 -1
View File
@@ -4,7 +4,7 @@
printWidth: 80 printWidth: 80
proseWrap: always proseWrap: always
trailingComma: all trailingComma: all
useTabs: false useTabs: true
tabWidth: 2 tabWidth: 2
overrides: overrides:
- files: - files:
+13 -13
View File
@@ -1,15 +1,15 @@
{ {
"recommendations": [ "recommendations": [
"github.vscode-codeql", "github.vscode-codeql",
"golang.go", "golang.go",
"hashicorp.terraform", "hashicorp.terraform",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"foxundermoon.shell-format", "foxundermoon.shell-format",
"emeraldwalk.runonsave", "emeraldwalk.runonsave",
"zxh404.vscode-proto3", "zxh404.vscode-proto3",
"redhat.vscode-yaml", "redhat.vscode-yaml",
"streetsidesoftware.code-spell-checker", "streetsidesoftware.code-spell-checker",
"EditorConfig.EditorConfig", "EditorConfig.EditorConfig",
"biomejs.biome" "biomejs.biome"
] ]
} }
+234 -234
View File
@@ -1,238 +1,238 @@
{ {
"cSpell.words": [ "cSpell.words": [
"afero", "afero",
"agentsdk", "agentsdk",
"apps", "apps",
"ASKPASS", "ASKPASS",
"authcheck", "authcheck",
"autostop", "autostop",
"awsidentity", "awsidentity",
"bodyclose", "bodyclose",
"buildinfo", "buildinfo",
"buildname", "buildname",
"circbuf", "circbuf",
"cliflag", "cliflag",
"cliui", "cliui",
"codecov", "codecov",
"coderd", "coderd",
"coderdenttest", "coderdenttest",
"coderdtest", "coderdtest",
"codersdk", "codersdk",
"contravariance", "contravariance",
"cronstrue", "cronstrue",
"databasefake", "databasefake",
"dbgen", "dbgen",
"dbmem", "dbmem",
"dbtype", "dbtype",
"DERP", "DERP",
"derphttp", "derphttp",
"derpmap", "derpmap",
"devel", "devel",
"devtunnel", "devtunnel",
"dflags", "dflags",
"drpc", "drpc",
"drpcconn", "drpcconn",
"drpcmux", "drpcmux",
"drpcserver", "drpcserver",
"Dsts", "Dsts",
"embeddedpostgres", "embeddedpostgres",
"enablements", "enablements",
"enterprisemeta", "enterprisemeta",
"errgroup", "errgroup",
"eventsourcemock", "eventsourcemock",
"externalauth", "externalauth",
"Failf", "Failf",
"fatih", "fatih",
"Formik", "Formik",
"gitauth", "gitauth",
"gitsshkey", "gitsshkey",
"goarch", "goarch",
"gographviz", "gographviz",
"goleak", "goleak",
"gonet", "gonet",
"gossh", "gossh",
"gsyslog", "gsyslog",
"GTTY", "GTTY",
"hashicorp", "hashicorp",
"hclsyntax", "hclsyntax",
"httpapi", "httpapi",
"httpmw", "httpmw",
"idtoken", "idtoken",
"Iflag", "Iflag",
"incpatch", "incpatch",
"initialisms", "initialisms",
"ipnstate", "ipnstate",
"isatty", "isatty",
"Jobf", "Jobf",
"Keygen", "Keygen",
"kirsle", "kirsle",
"Kubernetes", "Kubernetes",
"ldflags", "ldflags",
"magicsock", "magicsock",
"manifoldco", "manifoldco",
"mapstructure", "mapstructure",
"mattn", "mattn",
"mitchellh", "mitchellh",
"moby", "moby",
"namesgenerator", "namesgenerator",
"namespacing", "namespacing",
"netaddr", "netaddr",
"netip", "netip",
"netmap", "netmap",
"netns", "netns",
"netstack", "netstack",
"nettype", "nettype",
"nfpms", "nfpms",
"nhooyr", "nhooyr",
"nmcfg", "nmcfg",
"nolint", "nolint",
"nosec", "nosec",
"ntqry", "ntqry",
"OIDC", "OIDC",
"oneof", "oneof",
"opty", "opty",
"paralleltest", "paralleltest",
"parameterscopeid", "parameterscopeid",
"pqtype", "pqtype",
"prometheusmetrics", "prometheusmetrics",
"promhttp", "promhttp",
"protobuf", "protobuf",
"provisionerd", "provisionerd",
"provisionerdserver", "provisionerdserver",
"provisionersdk", "provisionersdk",
"ptty", "ptty",
"ptys", "ptys",
"ptytest", "ptytest",
"quickstart", "quickstart",
"reconfig", "reconfig",
"replicasync", "replicasync",
"retrier", "retrier",
"rpty", "rpty",
"SCIM", "SCIM",
"sdkproto", "sdkproto",
"sdktrace", "sdktrace",
"Signup", "Signup",
"slogtest", "slogtest",
"sourcemapped", "sourcemapped",
"spinbutton", "spinbutton",
"Srcs", "Srcs",
"stdbuf", "stdbuf",
"stretchr", "stretchr",
"STTY", "STTY",
"stuntest", "stuntest",
"tailbroker", "tailbroker",
"tailcfg", "tailcfg",
"tailexchange", "tailexchange",
"tailnet", "tailnet",
"tailnettest", "tailnettest",
"Tailscale", "Tailscale",
"tanstack", "tanstack",
"tbody", "tbody",
"TCGETS", "TCGETS",
"tcpip", "tcpip",
"TCSETS", "TCSETS",
"templateversions", "templateversions",
"testdata", "testdata",
"testid", "testid",
"testutil", "testutil",
"tfexec", "tfexec",
"tfjson", "tfjson",
"tfplan", "tfplan",
"tfstate", "tfstate",
"thead", "thead",
"tios", "tios",
"tmpdir", "tmpdir",
"tokenconfig", "tokenconfig",
"Topbar", "Topbar",
"tparallel", "tparallel",
"trialer", "trialer",
"trimprefix", "trimprefix",
"tsdial", "tsdial",
"tslogger", "tslogger",
"tstun", "tstun",
"turnconn", "turnconn",
"typegen", "typegen",
"typesafe", "typesafe",
"unconvert", "unconvert",
"Untar", "Untar",
"Userspace", "Userspace",
"VMID", "VMID",
"walkthrough", "walkthrough",
"weblinks", "weblinks",
"webrtc", "webrtc",
"wgcfg", "wgcfg",
"wgconfig", "wgconfig",
"wgengine", "wgengine",
"wgmonitor", "wgmonitor",
"wgnet", "wgnet",
"workspaceagent", "workspaceagent",
"workspaceagents", "workspaceagents",
"workspaceapp", "workspaceapp",
"workspaceapps", "workspaceapps",
"workspacebuilds", "workspacebuilds",
"workspacename", "workspacename",
"wsjson", "wsjson",
"xerrors", "xerrors",
"xlarge", "xlarge",
"xsmall", "xsmall",
"yamux" "yamux"
], ],
"cSpell.ignorePaths": ["site/package.json", ".vscode/settings.json"], "cSpell.ignorePaths": ["site/package.json", ".vscode/settings.json"],
"emeraldwalk.runonsave": { "emeraldwalk.runonsave": {
"commands": [ "commands": [
{ {
"match": "database/queries/*.sql", "match": "database/queries/*.sql",
"cmd": "make gen" "cmd": "make gen"
}, },
{ {
"match": "provisionerd/proto/provisionerd.proto", "match": "provisionerd/proto/provisionerd.proto",
"cmd": "make provisionerd/proto/provisionerd.pb.go" "cmd": "make provisionerd/proto/provisionerd.pb.go"
} }
] ]
}, },
"search.exclude": { "search.exclude": {
"**.pb.go": true, "**.pb.go": true,
"**/*.gen.json": true, "**/*.gen.json": true,
"**/testdata/*": true, "**/testdata/*": true,
"coderd/apidoc/**": true, "coderd/apidoc/**": true,
"docs/reference/api/*.md": true, "docs/reference/api/*.md": true,
"docs/reference/cli/*.md": true, "docs/reference/cli/*.md": true,
"docs/templates/*.md": true, "docs/templates/*.md": true,
"LICENSE": true, "LICENSE": true,
"scripts/metricsdocgen/metrics": true, "scripts/metricsdocgen/metrics": true,
"site/out/**": true, "site/out/**": true,
"site/storybook-static/**": true, "site/storybook-static/**": true,
"**.map": true, "**.map": true,
"pnpm-lock.yaml": true "pnpm-lock.yaml": true
}, },
// Ensure files always have a newline. // Ensure files always have a newline.
"files.insertFinalNewline": true, "files.insertFinalNewline": true,
"go.lintTool": "golangci-lint", "go.lintTool": "golangci-lint",
"go.lintFlags": ["--fast"], "go.lintFlags": ["--fast"],
"go.coverageDecorator": { "go.coverageDecorator": {
"type": "gutter", "type": "gutter",
"coveredGutterStyle": "blockgreen", "coveredGutterStyle": "blockgreen",
"uncoveredGutterStyle": "blockred" "uncoveredGutterStyle": "blockred"
}, },
// The codersdk is used by coderd another other packages extensively. // The codersdk is used by coderd another other packages extensively.
// To reduce redundancy in tests, it's covered by other packages. // To reduce redundancy in tests, it's covered by other packages.
// Since package coverage pairing can't be defined, all packages cover // Since package coverage pairing can't be defined, all packages cover
// all other packages. // all other packages.
"go.testFlags": ["-short", "-coverpkg=./..."], "go.testFlags": ["-short", "-coverpkg=./..."],
// We often use a version of TypeScript that's ahead of the version shipped // We often use a version of TypeScript that's ahead of the version shipped
// with VS Code. // 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 tests in VSCode will open a browser to live "view" the test.
"playwright.reuseBrowser": true, "playwright.reuseBrowser": true,
"[javascript][javascriptreact][json][jsonc][typescript][typescriptreact]": { "[javascript][javascriptreact][json][jsonc][typescript][typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome" "editor.defaultFormatter": "biomejs.biome"
// "editor.codeActionsOnSave": { // "editor.codeActionsOnSave": {
// "source.organizeImports.biome": "explicit" // "source.organizeImports.biome": "explicit"
// } // }
}, },
"[css][html][markdown][yaml]": { "[css][html][markdown][yaml]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
} }
} }
+4 -4
View File
@@ -491,7 +491,7 @@ gen: \
site/src/api/typesGenerated.ts \ site/src/api/typesGenerated.ts \
coderd/rbac/object_gen.go \ coderd/rbac/object_gen.go \
codersdk/rbacresources_gen.go \ codersdk/rbacresources_gen.go \
site/src/api/rbacresources_gen.ts \ site/src/api/rbacresourcesGenerated.ts \
docs/admin/prometheus.md \ docs/admin/prometheus.md \
docs/reference/cli/README.md \ docs/reference/cli/README.md \
docs/admin/audit-logs.md \ docs/admin/audit-logs.md \
@@ -520,7 +520,7 @@ gen/mark-fresh:
site/src/api/typesGenerated.ts \ site/src/api/typesGenerated.ts \
coderd/rbac/object_gen.go \ coderd/rbac/object_gen.go \
codersdk/rbacresources_gen.go \ codersdk/rbacresources_gen.go \
site/src/api/rbacresources_gen.ts \ site/src/api/rbacresourcesGenerated.ts \
docs/admin/prometheus.md \ docs/admin/prometheus.md \
docs/reference/cli/README.md \ docs/reference/cli/README.md \
docs/admin/audit-logs.md \ docs/admin/audit-logs.md \
@@ -621,8 +621,8 @@ coderd/rbac/object_gen.go: scripts/rbacgen/rbacobject.gotmpl scripts/rbacgen/mai
codersdk/rbacresources_gen.go: scripts/rbacgen/codersdk.gotmpl scripts/rbacgen/main.go coderd/rbac/object.go coderd/rbac/policy/policy.go codersdk/rbacresources_gen.go: scripts/rbacgen/codersdk.gotmpl scripts/rbacgen/main.go coderd/rbac/object.go coderd/rbac/policy/policy.go
go run scripts/rbacgen/main.go codersdk > codersdk/rbacresources_gen.go go run scripts/rbacgen/main.go codersdk > codersdk/rbacresources_gen.go
site/src/api/rbacresources_gen.ts: scripts/rbacgen/codersdk.gotmpl scripts/rbacgen/main.go coderd/rbac/object.go coderd/rbac/policy/policy.go site/src/api/rbacresourcesGenerated.ts: scripts/rbacgen/codersdk.gotmpl scripts/rbacgen/main.go coderd/rbac/object.go coderd/rbac/policy/policy.go
go run scripts/rbacgen/main.go typescript > site/src/api/rbacresources_gen.ts go run scripts/rbacgen/main.go typescript > "$@"
docs/admin/prometheus.md: scripts/metricsdocgen/main.go scripts/metricsdocgen/metrics docs/admin/prometheus.md: scripts/metricsdocgen/main.go scripts/metricsdocgen/metrics
+14297 -14297
View File
File diff suppressed because it is too large Load Diff
+44 -44
View File
@@ -1,46 +1,46 @@
{ {
"action": "never-match-action", "action": "never-match-action",
"object": { "object": {
"id": "9046b041-58ed-47a3-9c3a-de302577875a", "id": "9046b041-58ed-47a3-9c3a-de302577875a",
"owner": "00000000-0000-0000-0000-000000000000", "owner": "00000000-0000-0000-0000-000000000000",
"org_owner": "bf7b72bd-a2b1-4ef2-962c-1d698e0483f6", "org_owner": "bf7b72bd-a2b1-4ef2-962c-1d698e0483f6",
"type": "workspace", "type": "workspace",
"acl_user_list": { "acl_user_list": {
"f041847d-711b-40da-a89a-ede39f70dc7f": ["create"] "f041847d-711b-40da-a89a-ede39f70dc7f": ["create"]
}, },
"acl_group_list": {} "acl_group_list": {}
}, },
"subject": { "subject": {
"id": "10d03e62-7703-4df5-a358-4f76577d4e2f", "id": "10d03e62-7703-4df5-a358-4f76577d4e2f",
"roles": [ "roles": [
{ {
"name": "owner", "name": "owner",
"display_name": "Owner", "display_name": "Owner",
"site": [ "site": [
{ {
"negate": false, "negate": false,
"resource_type": "*", "resource_type": "*",
"action": "*" "action": "*"
} }
], ],
"org": {}, "org": {},
"user": [] "user": []
} }
], ],
"groups": ["b617a647-b5d0-4cbe-9e40-26f89710bf18"], "groups": ["b617a647-b5d0-4cbe-9e40-26f89710bf18"],
"scope": { "scope": {
"name": "Scope_all", "name": "Scope_all",
"display_name": "All operations", "display_name": "All operations",
"site": [ "site": [
{ {
"negate": false, "negate": false,
"resource_type": "*", "resource_type": "*",
"action": "*" "action": "*"
} }
], ],
"org": {}, "org": {},
"user": [], "user": [],
"allow_list": ["*"] "allow_list": ["*"]
} }
} }
} }
+28 -28
View File
@@ -82,34 +82,34 @@ entry:
```json ```json
{ {
"ts": "2023-06-13T03:45:37.294730279Z", "ts": "2023-06-13T03:45:37.294730279Z",
"level": "INFO", "level": "INFO",
"msg": "audit_log", "msg": "audit_log",
"caller": "/home/runner/work/coder/coder/enterprise/audit/backends/slog.go:36", "caller": "/home/runner/work/coder/coder/enterprise/audit/backends/slog.go:36",
"func": "github.com/coder/coder/enterprise/audit/backends.slogBackend.Export", "func": "github.com/coder/coder/enterprise/audit/backends.slogBackend.Export",
"logger_names": ["coderd"], "logger_names": ["coderd"],
"fields": { "fields": {
"ID": "033a9ffa-b54d-4c10-8ec3-2aaf9e6d741a", "ID": "033a9ffa-b54d-4c10-8ec3-2aaf9e6d741a",
"Time": "2023-06-13T03:45:37.288506Z", "Time": "2023-06-13T03:45:37.288506Z",
"UserID": "6c405053-27e3-484a-9ad7-bcb64e7bfde6", "UserID": "6c405053-27e3-484a-9ad7-bcb64e7bfde6",
"OrganizationID": "00000000-0000-0000-0000-000000000000", "OrganizationID": "00000000-0000-0000-0000-000000000000",
"Ip": "{IPNet:{IP:\u003cnil\u003e Mask:\u003cnil\u003e} Valid:false}", "Ip": "{IPNet:{IP:\u003cnil\u003e Mask:\u003cnil\u003e} Valid:false}",
"UserAgent": "{String: Valid:false}", "UserAgent": "{String: Valid:false}",
"ResourceType": "workspace_build", "ResourceType": "workspace_build",
"ResourceID": "ca5647e0-ef50-4202-a246-717e04447380", "ResourceID": "ca5647e0-ef50-4202-a246-717e04447380",
"ResourceTarget": "", "ResourceTarget": "",
"Action": "start", "Action": "start",
"Diff": {}, "Diff": {},
"StatusCode": 200, "StatusCode": 200,
"AdditionalFields": { "AdditionalFields": {
"workspace_name": "linux-container", "workspace_name": "linux-container",
"build_number": "9", "build_number": "9",
"build_reason": "initiator", "build_reason": "initiator",
"workspace_owner": "" "workspace_owner": ""
}, },
"RequestID": "bb791ac3-f6ee-4da8-8ec2-f54e87013e93", "RequestID": "bb791ac3-f6ee-4da8-8ec2-f54e87013e93",
"ResourceIcon": "" "ResourceIcon": ""
} }
} }
``` ```
+1 -1
View File
@@ -316,7 +316,7 @@ OIDC provider will be added to the `myCoderGroupName` group in Coder.
> **Note:** Groups are only updated on login. > **Note:** Groups are only updated on login.
[azure-gids]: [azure-gids]:
https://github.com/MicrosoftDocs/azure-docs/issues/59766#issuecomment-664387195 https://github.com/MicrosoftDocs/azure-docs/issues/59766#issuecomment-664387195
### Group allowlist ### Group allowlist
+23 -23
View File
@@ -125,17 +125,17 @@ within the component's story.
```tsx ```tsx
export const WithQuota: Story = { export const WithQuota: Story = {
parameters: { parameters: {
queries: [ queries: [
{ {
key: getWorkspaceQuotaQueryKey(MockUser.username), key: getWorkspaceQuotaQueryKey(MockUser.username),
data: { data: {
credits_consumed: 2, credits_consumed: 2,
budget: 40, budget: 40,
}, },
}, },
], ],
}, },
}; };
``` ```
@@ -150,12 +150,12 @@ example below:
```ts ```ts
export const getAgentListeningPorts = async ( export const getAgentListeningPorts = async (
agentID: string, agentID: string,
): Promise<TypesGen.ListeningPortsResponse> => { ): Promise<TypesGen.ListeningPortsResponse> => {
const response = await axiosInstance.get( const response = await axiosInstance.get(
`/api/v2/workspaceagents/${agentID}/listening-ports`, `/api/v2/workspaceagents/${agentID}/listening-ports`,
); );
return response.data; return response.data;
}; };
``` ```
@@ -164,10 +164,10 @@ wrap it as a single function.
```ts ```ts
export const updateWorkspaceVersion = async ( export const updateWorkspaceVersion = async (
workspace: TypesGen.Workspace, workspace: TypesGen.Workspace,
): Promise<TypesGen.WorkspaceBuild> => { ): Promise<TypesGen.WorkspaceBuild> => {
const template = await getTemplate(workspace.template_id); const template = await getTemplate(workspace.template_id);
return startWorkspace(workspace.id, template.active_version_id); return startWorkspace(workspace.id, template.active_version_id);
}; };
``` ```
@@ -214,10 +214,10 @@ inside the component itself using MUI's `visuallyHidden` utility function.
import { visuallyHidden } from "@mui/utils"; import { visuallyHidden } from "@mui/utils";
<Button> <Button>
<GearIcon /> <GearIcon />
<Box component="span" sx={visuallyHidden}> <Box component="span" sx={visuallyHidden}>
Settings Settings
</Box> </Box>
</Button>; </Button>;
``` ```
+59 -59
View File
@@ -39,21 +39,21 @@ following:
```json ```json
{ {
"Version": "2012-10-17", "Version": "2012-10-17",
"Statement": [ "Statement": [
{ {
"Effect": "Allow", "Effect": "Allow",
"Principal": { "Principal": {
"Federated": "accounts.google.com" "Federated": "accounts.google.com"
}, },
"Action": "sts:AssumeRoleWithWebIdentity", "Action": "sts:AssumeRoleWithWebIdentity",
"Condition": { "Condition": {
"StringEquals": { "StringEquals": {
"accounts.google.com:aud": "<enter-OAuth-client-ID-here" "accounts.google.com:aud": "<enter-OAuth-client-ID-here"
} }
} }
} }
] ]
} }
``` ```
@@ -64,50 +64,50 @@ following policy to the role:
```json ```json
{ {
"Version": "2012-10-17", "Version": "2012-10-17",
"Statement": [ "Statement": [
{ {
"Sid": "VisualEditor0", "Sid": "VisualEditor0",
"Effect": "Allow", "Effect": "Allow",
"Action": [ "Action": [
"ec2:GetDefaultCreditSpecification", "ec2:GetDefaultCreditSpecification",
"ec2:DescribeIamInstanceProfileAssociations", "ec2:DescribeIamInstanceProfileAssociations",
"ec2:DescribeTags", "ec2:DescribeTags",
"ec2:DescribeInstances", "ec2:DescribeInstances",
"ec2:DescribeInstanceTypes", "ec2:DescribeInstanceTypes",
"ec2:CreateTags", "ec2:CreateTags",
"ec2:RunInstances", "ec2:RunInstances",
"ec2:DescribeInstanceCreditSpecifications", "ec2:DescribeInstanceCreditSpecifications",
"ec2:DescribeImages", "ec2:DescribeImages",
"ec2:ModifyDefaultCreditSpecification", "ec2:ModifyDefaultCreditSpecification",
"ec2:DescribeVolumes" "ec2:DescribeVolumes"
], ],
"Resource": "*" "Resource": "*"
}, },
{ {
"Sid": "CoderResources", "Sid": "CoderResources",
"Effect": "Allow", "Effect": "Allow",
"Action": [ "Action": [
"ec2:DescribeInstanceAttribute", "ec2:DescribeInstanceAttribute",
"ec2:UnmonitorInstances", "ec2:UnmonitorInstances",
"ec2:TerminateInstances", "ec2:TerminateInstances",
"ec2:StartInstances", "ec2:StartInstances",
"ec2:StopInstances", "ec2:StopInstances",
"ec2:DeleteTags", "ec2:DeleteTags",
"ec2:MonitorInstances", "ec2:MonitorInstances",
"ec2:CreateTags", "ec2:CreateTags",
"ec2:RunInstances", "ec2:RunInstances",
"ec2:ModifyInstanceAttribute", "ec2:ModifyInstanceAttribute",
"ec2:ModifyInstanceCreditSpecification" "ec2:ModifyInstanceCreditSpecification"
], ],
"Resource": "arn:aws:ec2:*:*:instance/*", "Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": { "Condition": {
"StringEquals": { "StringEquals": {
"aws:ResourceTag/Coder_Provisioned": "true" "aws:ResourceTag/Coder_Provisioned": "true"
} }
} }
} }
] ]
} }
``` ```
+13 -13
View File
@@ -23,12 +23,12 @@ actual Docker registry URL, username, and password.
```json ```json
{ {
"auths": { "auths": {
"<your-registry>": { "<your-registry>": {
"username": "<your-username>", "username": "<your-username>",
"password": "<your-password>" "password": "<your-password>"
} }
} }
} }
``` ```
@@ -54,13 +54,13 @@ The output should look similar to this:
```json ```json
{ {
"auths": { "auths": {
"your.private.registry.com": { "your.private.registry.com": {
"username": "ericpaulsen", "username": "ericpaulsen",
"password": "xxxx", "password": "xxxx",
"auth": "c3R...zE2" "auth": "c3R...zE2"
} }
} }
} }
``` ```
+1182 -1182
View File
File diff suppressed because it is too large Load Diff
+213 -213
View File
@@ -38,8 +38,8 @@ curl -X POST http://coder-server:8080/api/v2/workspaceagents/aws-instance-identi
```json ```json
{ {
"document": "string", "document": "string",
"signature": "string" "signature": "string"
} }
``` ```
@@ -55,7 +55,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaceagents/aws-instance-identi
```json ```json
{ {
"session_token": "string" "session_token": "string"
} }
``` ```
@@ -85,8 +85,8 @@ curl -X POST http://coder-server:8080/api/v2/workspaceagents/azure-instance-iden
```json ```json
{ {
"encoding": "string", "encoding": "string",
"signature": "string" "signature": "string"
} }
``` ```
@@ -102,7 +102,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaceagents/azure-instance-iden
```json ```json
{ {
"session_token": "string" "session_token": "string"
} }
``` ```
@@ -132,7 +132,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaceagents/google-instance-ide
```json ```json
{ {
"json_web_token": "string" "json_web_token": "string"
} }
``` ```
@@ -148,7 +148,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaceagents/google-instance-ide
```json ```json
{ {
"session_token": "string" "session_token": "string"
} }
``` ```
@@ -187,12 +187,12 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/external-auth?mat
```json ```json
{ {
"access_token": "string", "access_token": "string",
"password": "string", "password": "string",
"token_extra": {}, "token_extra": {},
"type": "string", "type": "string",
"url": "string", "url": "string",
"username": "string" "username": "string"
} }
``` ```
@@ -231,12 +231,12 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/gitauth?match=str
```json ```json
{ {
"access_token": "string", "access_token": "string",
"password": "string", "password": "string",
"token_extra": {}, "token_extra": {},
"type": "string", "type": "string",
"url": "string", "url": "string",
"username": "string" "username": "string"
} }
``` ```
@@ -267,8 +267,8 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/gitsshkey \
```json ```json
{ {
"private_key": "string", "private_key": "string",
"public_key": "string" "public_key": "string"
} }
``` ```
@@ -298,9 +298,9 @@ curl -X POST http://coder-server:8080/api/v2/workspaceagents/me/log-source \
```json ```json
{ {
"display_name": "string", "display_name": "string",
"icon": "string", "icon": "string",
"id": "string" "id": "string"
} }
``` ```
@@ -316,11 +316,11 @@ curl -X POST http://coder-server:8080/api/v2/workspaceagents/me/log-source \
```json ```json
{ {
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"display_name": "string", "display_name": "string",
"icon": "string", "icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"workspace_agent_id": "7ad2e618-fea7-4c1a-b70a-f501566a72f1" "workspace_agent_id": "7ad2e618-fea7-4c1a-b70a-f501566a72f1"
} }
``` ```
@@ -350,14 +350,14 @@ curl -X PATCH http://coder-server:8080/api/v2/workspaceagents/me/logs \
```json ```json
{ {
"log_source_id": "string", "log_source_id": "string",
"logs": [ "logs": [
{ {
"created_at": "string", "created_at": "string",
"level": "trace", "level": "trace",
"output": "string" "output": "string"
} }
] ]
} }
``` ```
@@ -373,14 +373,14 @@ curl -X PATCH http://coder-server:8080/api/v2/workspaceagents/me/logs \
```json ```json
{ {
"detail": "string", "detail": "string",
"message": "string", "message": "string",
"validations": [ "validations": [
{ {
"detail": "string", "detail": "string",
"field": "string" "field": "string"
} }
] ]
} }
``` ```
@@ -417,91 +417,91 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent} \
```json ```json
{ {
"api_version": "string", "api_version": "string",
"apps": [ "apps": [
{ {
"command": "string", "command": "string",
"display_name": "string", "display_name": "string",
"external": true, "external": true,
"health": "disabled", "health": "disabled",
"healthcheck": { "healthcheck": {
"interval": 0, "interval": 0,
"threshold": 0, "threshold": 0,
"url": "string" "url": "string"
}, },
"icon": "string", "icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"sharing_level": "owner", "sharing_level": "owner",
"slug": "string", "slug": "string",
"subdomain": true, "subdomain": true,
"subdomain_name": "string", "subdomain_name": "string",
"url": "string" "url": "string"
} }
], ],
"architecture": "string", "architecture": "string",
"connection_timeout_seconds": 0, "connection_timeout_seconds": 0,
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"directory": "string", "directory": "string",
"disconnected_at": "2019-08-24T14:15:22Z", "disconnected_at": "2019-08-24T14:15:22Z",
"display_apps": ["vscode"], "display_apps": ["vscode"],
"environment_variables": { "environment_variables": {
"property1": "string", "property1": "string",
"property2": "string" "property2": "string"
}, },
"expanded_directory": "string", "expanded_directory": "string",
"first_connected_at": "2019-08-24T14:15:22Z", "first_connected_at": "2019-08-24T14:15:22Z",
"health": { "health": {
"healthy": false, "healthy": false,
"reason": "agent has lost connection" "reason": "agent has lost connection"
}, },
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"instance_id": "string", "instance_id": "string",
"last_connected_at": "2019-08-24T14:15:22Z", "last_connected_at": "2019-08-24T14:15:22Z",
"latency": { "latency": {
"property1": { "property1": {
"latency_ms": 0, "latency_ms": 0,
"preferred": true "preferred": true
}, },
"property2": { "property2": {
"latency_ms": 0, "latency_ms": 0,
"preferred": true "preferred": true
} }
}, },
"lifecycle_state": "created", "lifecycle_state": "created",
"log_sources": [ "log_sources": [
{ {
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"display_name": "string", "display_name": "string",
"icon": "string", "icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"workspace_agent_id": "7ad2e618-fea7-4c1a-b70a-f501566a72f1" "workspace_agent_id": "7ad2e618-fea7-4c1a-b70a-f501566a72f1"
} }
], ],
"logs_length": 0, "logs_length": 0,
"logs_overflowed": true, "logs_overflowed": true,
"name": "string", "name": "string",
"operating_system": "string", "operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z", "ready_at": "2019-08-24T14:15:22Z",
"resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f",
"scripts": [ "scripts": [
{ {
"cron": "string", "cron": "string",
"log_path": "string", "log_path": "string",
"log_source_id": "4197ab25-95cf-4b91-9c78-f7f2af5d353a", "log_source_id": "4197ab25-95cf-4b91-9c78-f7f2af5d353a",
"run_on_start": true, "run_on_start": true,
"run_on_stop": true, "run_on_stop": true,
"script": "string", "script": "string",
"start_blocks_login": true, "start_blocks_login": true,
"timeout": 0 "timeout": 0
} }
], ],
"started_at": "2019-08-24T14:15:22Z", "started_at": "2019-08-24T14:15:22Z",
"startup_script_behavior": "blocking", "startup_script_behavior": "blocking",
"status": "connecting", "status": "connecting",
"subsystems": ["envbox"], "subsystems": ["envbox"],
"troubleshooting_url": "string", "troubleshooting_url": "string",
"updated_at": "2019-08-24T14:15:22Z", "updated_at": "2019-08-24T14:15:22Z",
"version": "string" "version": "string"
} }
``` ```
@@ -538,67 +538,67 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/con
```json ```json
{ {
"derp_force_websockets": true, "derp_force_websockets": true,
"derp_map": { "derp_map": {
"homeParams": { "homeParams": {
"regionScore": { "regionScore": {
"property1": 0, "property1": 0,
"property2": 0 "property2": 0
} }
}, },
"omitDefaultRegions": true, "omitDefaultRegions": true,
"regions": { "regions": {
"property1": { "property1": {
"avoid": true, "avoid": true,
"embeddedRelay": true, "embeddedRelay": true,
"nodes": [ "nodes": [
{ {
"canPort80": true, "canPort80": true,
"certName": "string", "certName": "string",
"derpport": 0, "derpport": 0,
"forceHTTP": true, "forceHTTP": true,
"hostName": "string", "hostName": "string",
"insecureForTests": true, "insecureForTests": true,
"ipv4": "string", "ipv4": "string",
"ipv6": "string", "ipv6": "string",
"name": "string", "name": "string",
"regionID": 0, "regionID": 0,
"stunonly": true, "stunonly": true,
"stunport": 0, "stunport": 0,
"stuntestIP": "string" "stuntestIP": "string"
} }
], ],
"regionCode": "string", "regionCode": "string",
"regionID": 0, "regionID": 0,
"regionName": "string" "regionName": "string"
}, },
"property2": { "property2": {
"avoid": true, "avoid": true,
"embeddedRelay": true, "embeddedRelay": true,
"nodes": [ "nodes": [
{ {
"canPort80": true, "canPort80": true,
"certName": "string", "certName": "string",
"derpport": 0, "derpport": 0,
"forceHTTP": true, "forceHTTP": true,
"hostName": "string", "hostName": "string",
"insecureForTests": true, "insecureForTests": true,
"ipv4": "string", "ipv4": "string",
"ipv6": "string", "ipv6": "string",
"name": "string", "name": "string",
"regionID": 0, "regionID": 0,
"stunonly": true, "stunonly": true,
"stunport": 0, "stunport": 0,
"stuntestIP": "string" "stuntestIP": "string"
} }
], ],
"regionCode": "string", "regionCode": "string",
"regionID": 0, "regionID": 0,
"regionName": "string" "regionName": "string"
} }
} }
}, },
"disable_direct_connections": true "disable_direct_connections": true
} }
``` ```
@@ -661,13 +661,13 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/lis
```json ```json
{ {
"ports": [ "ports": [
{ {
"network": "string", "network": "string",
"port": 0, "port": 0,
"process_name": "string" "process_name": "string"
} }
] ]
} }
``` ```
@@ -708,13 +708,13 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/log
```json ```json
[ [
{ {
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"id": 0, "id": 0,
"level": "trace", "level": "trace",
"output": "string", "output": "string",
"source_id": "ae50a35c-df42-4eff-ba26-f8bc28d2af81" "source_id": "ae50a35c-df42-4eff-ba26-f8bc28d2af81"
} }
] ]
``` ```
@@ -804,13 +804,13 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/sta
```json ```json
[ [
{ {
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"id": 0, "id": 0,
"level": "trace", "level": "trace",
"output": "string", "output": "string",
"source_id": "ae50a35c-df42-4eff-ba26-f8bc28d2af81" "source_id": "ae50a35c-df42-4eff-ba26-f8bc28d2af81"
} }
] ]
``` ```
+1 -1
View File
@@ -45,7 +45,7 @@ curl -X GET http://coder-server:8080/api/v2/applications/host \
```json ```json
{ {
"host": "string" "host": "string"
} }
``` ```
+60 -60
View File
@@ -27,66 +27,66 @@ curl -X GET http://coder-server:8080/api/v2/audit?limit=0 \
```json ```json
{ {
"audit_logs": [ "audit_logs": [
{ {
"action": "create", "action": "create",
"additional_fields": [0], "additional_fields": [0],
"description": "string", "description": "string",
"diff": { "diff": {
"property1": { "property1": {
"new": null, "new": null,
"old": null, "old": null,
"secret": true "secret": true
}, },
"property2": { "property2": {
"new": null, "new": null,
"old": null, "old": null,
"secret": true "secret": true
} }
}, },
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"ip": "string", "ip": "string",
"is_deleted": true, "is_deleted": true,
"organization": { "organization": {
"display_name": "string", "display_name": "string",
"icon": "string", "icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"name": "string" "name": "string"
}, },
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"request_id": "266ea41d-adf5-480b-af50-15b940c2b846", "request_id": "266ea41d-adf5-480b-af50-15b940c2b846",
"resource_icon": "string", "resource_icon": "string",
"resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f",
"resource_link": "string", "resource_link": "string",
"resource_target": "string", "resource_target": "string",
"resource_type": "template", "resource_type": "template",
"status_code": 0, "status_code": 0,
"time": "2019-08-24T14:15:22Z", "time": "2019-08-24T14:15:22Z",
"user": { "user": {
"avatar_url": "http://example.com", "avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "", "login_type": "",
"name": "string", "name": "string",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
"display_name": "string", "display_name": "string",
"name": "string", "name": "string",
"organization_id": "string" "organization_id": "string"
} }
], ],
"status": "active", "status": "active",
"theme_preference": "string", "theme_preference": "string",
"updated_at": "2019-08-24T14:15:22Z", "updated_at": "2019-08-24T14:15:22Z",
"username": "string" "username": "string"
}, },
"user_agent": "string" "user_agent": "string"
} }
], ],
"count": 0 "count": 0
} }
``` ```
+33 -33
View File
@@ -18,28 +18,28 @@ curl -X POST http://coder-server:8080/api/v2/authcheck \
```json ```json
{ {
"checks": { "checks": {
"property1": { "property1": {
"action": "create", "action": "create",
"object": { "object": {
"any_org": true, "any_org": true,
"organization_id": "string", "organization_id": "string",
"owner_id": "string", "owner_id": "string",
"resource_id": "string", "resource_id": "string",
"resource_type": "*" "resource_type": "*"
} }
}, },
"property2": { "property2": {
"action": "create", "action": "create",
"object": { "object": {
"any_org": true, "any_org": true,
"organization_id": "string", "organization_id": "string",
"owner_id": "string", "owner_id": "string",
"resource_id": "string", "resource_id": "string",
"resource_type": "*" "resource_type": "*"
} }
} }
} }
} }
``` ```
@@ -55,8 +55,8 @@ curl -X POST http://coder-server:8080/api/v2/authcheck \
```json ```json
{ {
"property1": true, "property1": true,
"property2": true "property2": true
} }
``` ```
@@ -85,8 +85,8 @@ curl -X POST http://coder-server:8080/api/v2/users/login \
```json ```json
{ {
"email": "user@example.com", "email": "user@example.com",
"password": "string" "password": "string"
} }
``` ```
@@ -102,7 +102,7 @@ curl -X POST http://coder-server:8080/api/v2/users/login \
```json ```json
{ {
"session_token": "string" "session_token": "string"
} }
``` ```
@@ -130,8 +130,8 @@ curl -X POST http://coder-server:8080/api/v2/users/{user}/convert-login \
```json ```json
{ {
"password": "string", "password": "string",
"to_type": "" "to_type": ""
} }
``` ```
@@ -148,10 +148,10 @@ curl -X POST http://coder-server:8080/api/v2/users/{user}/convert-login \
```json ```json
{ {
"expires_at": "2019-08-24T14:15:22Z", "expires_at": "2019-08-24T14:15:22Z",
"state_string": "string", "state_string": "string",
"to_type": "", "to_type": "",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5" "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
} }
``` ```
+871 -871
View File
File diff suppressed because it is too large Load Diff
+329 -329
View File
@@ -45,332 +45,332 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \
```json ```json
{ {
"access_url": { "access_url": {
"access_url": "string", "access_url": "string",
"dismissed": true, "dismissed": true,
"error": "string", "error": "string",
"healthy": true, "healthy": true,
"healthz_response": "string", "healthz_response": "string",
"reachable": true, "reachable": true,
"severity": "ok", "severity": "ok",
"status_code": 0, "status_code": 0,
"warnings": [ "warnings": [
{ {
"code": "EUNKNOWN", "code": "EUNKNOWN",
"message": "string" "message": "string"
} }
] ]
}, },
"coder_version": "string", "coder_version": "string",
"database": { "database": {
"dismissed": true, "dismissed": true,
"error": "string", "error": "string",
"healthy": true, "healthy": true,
"latency": "string", "latency": "string",
"latency_ms": 0, "latency_ms": 0,
"reachable": true, "reachable": true,
"severity": "ok", "severity": "ok",
"threshold_ms": 0, "threshold_ms": 0,
"warnings": [ "warnings": [
{ {
"code": "EUNKNOWN", "code": "EUNKNOWN",
"message": "string" "message": "string"
} }
] ]
}, },
"derp": { "derp": {
"dismissed": true, "dismissed": true,
"error": "string", "error": "string",
"healthy": true, "healthy": true,
"netcheck": { "netcheck": {
"captivePortal": "string", "captivePortal": "string",
"globalV4": "string", "globalV4": "string",
"globalV6": "string", "globalV6": "string",
"hairPinning": "string", "hairPinning": "string",
"icmpv4": true, "icmpv4": true,
"ipv4": true, "ipv4": true,
"ipv4CanSend": true, "ipv4CanSend": true,
"ipv6": true, "ipv6": true,
"ipv6CanSend": true, "ipv6CanSend": true,
"mappingVariesByDestIP": "string", "mappingVariesByDestIP": "string",
"oshasIPv6": true, "oshasIPv6": true,
"pcp": "string", "pcp": "string",
"pmp": "string", "pmp": "string",
"preferredDERP": 0, "preferredDERP": 0,
"regionLatency": { "regionLatency": {
"property1": 0, "property1": 0,
"property2": 0 "property2": 0
}, },
"regionV4Latency": { "regionV4Latency": {
"property1": 0, "property1": 0,
"property2": 0 "property2": 0
}, },
"regionV6Latency": { "regionV6Latency": {
"property1": 0, "property1": 0,
"property2": 0 "property2": 0
}, },
"udp": true, "udp": true,
"upnP": "string" "upnP": "string"
}, },
"netcheck_err": "string", "netcheck_err": "string",
"netcheck_logs": ["string"], "netcheck_logs": ["string"],
"regions": { "regions": {
"property1": { "property1": {
"error": "string", "error": "string",
"healthy": true, "healthy": true,
"node_reports": [ "node_reports": [
{ {
"can_exchange_messages": true, "can_exchange_messages": true,
"client_errs": [["string"]], "client_errs": [["string"]],
"client_logs": [["string"]], "client_logs": [["string"]],
"error": "string", "error": "string",
"healthy": true, "healthy": true,
"node": { "node": {
"canPort80": true, "canPort80": true,
"certName": "string", "certName": "string",
"derpport": 0, "derpport": 0,
"forceHTTP": true, "forceHTTP": true,
"hostName": "string", "hostName": "string",
"insecureForTests": true, "insecureForTests": true,
"ipv4": "string", "ipv4": "string",
"ipv6": "string", "ipv6": "string",
"name": "string", "name": "string",
"regionID": 0, "regionID": 0,
"stunonly": true, "stunonly": true,
"stunport": 0, "stunport": 0,
"stuntestIP": "string" "stuntestIP": "string"
}, },
"node_info": { "node_info": {
"tokenBucketBytesBurst": 0, "tokenBucketBytesBurst": 0,
"tokenBucketBytesPerSecond": 0 "tokenBucketBytesPerSecond": 0
}, },
"round_trip_ping": "string", "round_trip_ping": "string",
"round_trip_ping_ms": 0, "round_trip_ping_ms": 0,
"severity": "ok", "severity": "ok",
"stun": { "stun": {
"canSTUN": true, "canSTUN": true,
"enabled": true, "enabled": true,
"error": "string" "error": "string"
}, },
"uses_websocket": true, "uses_websocket": true,
"warnings": [ "warnings": [
{ {
"code": "EUNKNOWN", "code": "EUNKNOWN",
"message": "string" "message": "string"
} }
] ]
} }
], ],
"region": { "region": {
"avoid": true, "avoid": true,
"embeddedRelay": true, "embeddedRelay": true,
"nodes": [ "nodes": [
{ {
"canPort80": true, "canPort80": true,
"certName": "string", "certName": "string",
"derpport": 0, "derpport": 0,
"forceHTTP": true, "forceHTTP": true,
"hostName": "string", "hostName": "string",
"insecureForTests": true, "insecureForTests": true,
"ipv4": "string", "ipv4": "string",
"ipv6": "string", "ipv6": "string",
"name": "string", "name": "string",
"regionID": 0, "regionID": 0,
"stunonly": true, "stunonly": true,
"stunport": 0, "stunport": 0,
"stuntestIP": "string" "stuntestIP": "string"
} }
], ],
"regionCode": "string", "regionCode": "string",
"regionID": 0, "regionID": 0,
"regionName": "string" "regionName": "string"
}, },
"severity": "ok", "severity": "ok",
"warnings": [ "warnings": [
{ {
"code": "EUNKNOWN", "code": "EUNKNOWN",
"message": "string" "message": "string"
} }
] ]
}, },
"property2": { "property2": {
"error": "string", "error": "string",
"healthy": true, "healthy": true,
"node_reports": [ "node_reports": [
{ {
"can_exchange_messages": true, "can_exchange_messages": true,
"client_errs": [["string"]], "client_errs": [["string"]],
"client_logs": [["string"]], "client_logs": [["string"]],
"error": "string", "error": "string",
"healthy": true, "healthy": true,
"node": { "node": {
"canPort80": true, "canPort80": true,
"certName": "string", "certName": "string",
"derpport": 0, "derpport": 0,
"forceHTTP": true, "forceHTTP": true,
"hostName": "string", "hostName": "string",
"insecureForTests": true, "insecureForTests": true,
"ipv4": "string", "ipv4": "string",
"ipv6": "string", "ipv6": "string",
"name": "string", "name": "string",
"regionID": 0, "regionID": 0,
"stunonly": true, "stunonly": true,
"stunport": 0, "stunport": 0,
"stuntestIP": "string" "stuntestIP": "string"
}, },
"node_info": { "node_info": {
"tokenBucketBytesBurst": 0, "tokenBucketBytesBurst": 0,
"tokenBucketBytesPerSecond": 0 "tokenBucketBytesPerSecond": 0
}, },
"round_trip_ping": "string", "round_trip_ping": "string",
"round_trip_ping_ms": 0, "round_trip_ping_ms": 0,
"severity": "ok", "severity": "ok",
"stun": { "stun": {
"canSTUN": true, "canSTUN": true,
"enabled": true, "enabled": true,
"error": "string" "error": "string"
}, },
"uses_websocket": true, "uses_websocket": true,
"warnings": [ "warnings": [
{ {
"code": "EUNKNOWN", "code": "EUNKNOWN",
"message": "string" "message": "string"
} }
] ]
} }
], ],
"region": { "region": {
"avoid": true, "avoid": true,
"embeddedRelay": true, "embeddedRelay": true,
"nodes": [ "nodes": [
{ {
"canPort80": true, "canPort80": true,
"certName": "string", "certName": "string",
"derpport": 0, "derpport": 0,
"forceHTTP": true, "forceHTTP": true,
"hostName": "string", "hostName": "string",
"insecureForTests": true, "insecureForTests": true,
"ipv4": "string", "ipv4": "string",
"ipv6": "string", "ipv6": "string",
"name": "string", "name": "string",
"regionID": 0, "regionID": 0,
"stunonly": true, "stunonly": true,
"stunport": 0, "stunport": 0,
"stuntestIP": "string" "stuntestIP": "string"
} }
], ],
"regionCode": "string", "regionCode": "string",
"regionID": 0, "regionID": 0,
"regionName": "string" "regionName": "string"
}, },
"severity": "ok", "severity": "ok",
"warnings": [ "warnings": [
{ {
"code": "EUNKNOWN", "code": "EUNKNOWN",
"message": "string" "message": "string"
} }
] ]
} }
}, },
"severity": "ok", "severity": "ok",
"warnings": [ "warnings": [
{ {
"code": "EUNKNOWN", "code": "EUNKNOWN",
"message": "string" "message": "string"
} }
] ]
}, },
"healthy": true, "healthy": true,
"provisioner_daemons": { "provisioner_daemons": {
"dismissed": true, "dismissed": true,
"error": "string", "error": "string",
"items": [ "items": [
{ {
"provisioner_daemon": { "provisioner_daemon": {
"api_version": "string", "api_version": "string",
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"name": "string", "name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"provisioners": ["string"], "provisioners": ["string"],
"tags": { "tags": {
"property1": "string", "property1": "string",
"property2": "string" "property2": "string"
}, },
"version": "string" "version": "string"
}, },
"warnings": [ "warnings": [
{ {
"code": "EUNKNOWN", "code": "EUNKNOWN",
"message": "string" "message": "string"
} }
] ]
} }
], ],
"severity": "ok", "severity": "ok",
"warnings": [ "warnings": [
{ {
"code": "EUNKNOWN", "code": "EUNKNOWN",
"message": "string" "message": "string"
} }
] ]
}, },
"severity": "ok", "severity": "ok",
"time": "2019-08-24T14:15:22Z", "time": "2019-08-24T14:15:22Z",
"websocket": { "websocket": {
"body": "string", "body": "string",
"code": 0, "code": 0,
"dismissed": true, "dismissed": true,
"error": "string", "error": "string",
"healthy": true, "healthy": true,
"severity": "ok", "severity": "ok",
"warnings": [ "warnings": [
{ {
"code": "EUNKNOWN", "code": "EUNKNOWN",
"message": "string" "message": "string"
} }
] ]
}, },
"workspace_proxy": { "workspace_proxy": {
"dismissed": true, "dismissed": true,
"error": "string", "error": "string",
"healthy": true, "healthy": true,
"severity": "ok", "severity": "ok",
"warnings": [ "warnings": [
{ {
"code": "EUNKNOWN", "code": "EUNKNOWN",
"message": "string" "message": "string"
} }
], ],
"workspace_proxies": { "workspace_proxies": {
"regions": [ "regions": [
{ {
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"deleted": true, "deleted": true,
"derp_enabled": true, "derp_enabled": true,
"derp_only": true, "derp_only": true,
"display_name": "string", "display_name": "string",
"healthy": true, "healthy": true,
"icon_url": "string", "icon_url": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"name": "string", "name": "string",
"path_app_url": "string", "path_app_url": "string",
"status": { "status": {
"checked_at": "2019-08-24T14:15:22Z", "checked_at": "2019-08-24T14:15:22Z",
"report": { "report": {
"errors": ["string"], "errors": ["string"],
"warnings": ["string"] "warnings": ["string"]
}, },
"status": "ok" "status": "ok"
}, },
"updated_at": "2019-08-24T14:15:22Z", "updated_at": "2019-08-24T14:15:22Z",
"version": "string", "version": "string",
"wildcard_hostname": "string" "wildcard_hostname": "string"
} }
] ]
} }
} }
} }
``` ```
@@ -401,7 +401,7 @@ curl -X GET http://coder-server:8080/api/v2/debug/health/settings \
```json ```json
{ {
"dismissed_healthchecks": ["DERP"] "dismissed_healthchecks": ["DERP"]
} }
``` ```
@@ -431,7 +431,7 @@ curl -X PUT http://coder-server:8080/api/v2/debug/health/settings \
```json ```json
{ {
"dismissed_healthchecks": ["DERP"] "dismissed_healthchecks": ["DERP"]
} }
``` ```
@@ -447,7 +447,7 @@ curl -X PUT http://coder-server:8080/api/v2/debug/health/settings \
```json ```json
{ {
"dismissed_healthchecks": ["DERP"] "dismissed_healthchecks": ["DERP"]
} }
``` ```
+647 -647
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -34,7 +34,7 @@ file: string
```json ```json
{ {
"hash": "19686d84-b10d-4f90-b18e-84fd3fa038fd" "hash": "19686d84-b10d-4f90-b18e-84fd3fa038fd"
} }
``` ```
+419 -419
View File
@@ -18,14 +18,14 @@ curl -X GET http://coder-server:8080/api/v2/ \
```json ```json
{ {
"detail": "string", "detail": "string",
"message": "string", "message": "string",
"validations": [ "validations": [
{ {
"detail": "string", "detail": "string",
"field": "string" "field": "string"
} }
] ]
} }
``` ```
@@ -53,14 +53,14 @@ curl -X GET http://coder-server:8080/api/v2/buildinfo \
```json ```json
{ {
"agent_api_version": "string", "agent_api_version": "string",
"dashboard_url": "string", "dashboard_url": "string",
"deployment_id": "string", "deployment_id": "string",
"external_url": "string", "external_url": "string",
"telemetry": true, "telemetry": true,
"upgrade_message": "string", "upgrade_message": "string",
"version": "string", "version": "string",
"workspace_proxy": true "workspace_proxy": true
} }
``` ```
@@ -87,7 +87,7 @@ curl -X POST http://coder-server:8080/api/v2/csp/reports \
```json ```json
{ {
"csp-report": {} "csp-report": {}
} }
``` ```
@@ -124,377 +124,377 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \
```json ```json
{ {
"config": { "config": {
"access_url": { "access_url": {
"forceQuery": true, "forceQuery": true,
"fragment": "string", "fragment": "string",
"host": "string", "host": "string",
"omitHost": true, "omitHost": true,
"opaque": "string", "opaque": "string",
"path": "string", "path": "string",
"rawFragment": "string", "rawFragment": "string",
"rawPath": "string", "rawPath": "string",
"rawQuery": "string", "rawQuery": "string",
"scheme": "string", "scheme": "string",
"user": {} "user": {}
}, },
"address": { "address": {
"host": "string", "host": "string",
"port": "string" "port": "string"
}, },
"agent_fallback_troubleshooting_url": { "agent_fallback_troubleshooting_url": {
"forceQuery": true, "forceQuery": true,
"fragment": "string", "fragment": "string",
"host": "string", "host": "string",
"omitHost": true, "omitHost": true,
"opaque": "string", "opaque": "string",
"path": "string", "path": "string",
"rawFragment": "string", "rawFragment": "string",
"rawPath": "string", "rawPath": "string",
"rawQuery": "string", "rawQuery": "string",
"scheme": "string", "scheme": "string",
"user": {} "user": {}
}, },
"agent_stat_refresh_interval": 0, "agent_stat_refresh_interval": 0,
"allow_workspace_renames": true, "allow_workspace_renames": true,
"autobuild_poll_interval": 0, "autobuild_poll_interval": 0,
"browser_only": true, "browser_only": true,
"cache_directory": "string", "cache_directory": "string",
"cli_upgrade_message": "string", "cli_upgrade_message": "string",
"config": "string", "config": "string",
"config_ssh": { "config_ssh": {
"deploymentName": "string", "deploymentName": "string",
"sshconfigOptions": ["string"] "sshconfigOptions": ["string"]
}, },
"dangerous": { "dangerous": {
"allow_all_cors": true, "allow_all_cors": true,
"allow_path_app_sharing": true, "allow_path_app_sharing": true,
"allow_path_app_site_owner_access": true "allow_path_app_site_owner_access": true
}, },
"derp": { "derp": {
"config": { "config": {
"block_direct": true, "block_direct": true,
"force_websockets": true, "force_websockets": true,
"path": "string", "path": "string",
"url": "string" "url": "string"
}, },
"server": { "server": {
"enable": true, "enable": true,
"region_code": "string", "region_code": "string",
"region_id": 0, "region_id": 0,
"region_name": "string", "region_name": "string",
"relay_url": { "relay_url": {
"forceQuery": true, "forceQuery": true,
"fragment": "string", "fragment": "string",
"host": "string", "host": "string",
"omitHost": true, "omitHost": true,
"opaque": "string", "opaque": "string",
"path": "string", "path": "string",
"rawFragment": "string", "rawFragment": "string",
"rawPath": "string", "rawPath": "string",
"rawQuery": "string", "rawQuery": "string",
"scheme": "string", "scheme": "string",
"user": {} "user": {}
}, },
"stun_addresses": ["string"] "stun_addresses": ["string"]
} }
}, },
"disable_owner_workspace_exec": true, "disable_owner_workspace_exec": true,
"disable_password_auth": true, "disable_password_auth": true,
"disable_path_apps": true, "disable_path_apps": true,
"docs_url": { "docs_url": {
"forceQuery": true, "forceQuery": true,
"fragment": "string", "fragment": "string",
"host": "string", "host": "string",
"omitHost": true, "omitHost": true,
"opaque": "string", "opaque": "string",
"path": "string", "path": "string",
"rawFragment": "string", "rawFragment": "string",
"rawPath": "string", "rawPath": "string",
"rawQuery": "string", "rawQuery": "string",
"scheme": "string", "scheme": "string",
"user": {} "user": {}
}, },
"enable_terraform_debug_mode": true, "enable_terraform_debug_mode": true,
"experiments": ["string"], "experiments": ["string"],
"external_auth": { "external_auth": {
"value": [ "value": [
{ {
"app_install_url": "string", "app_install_url": "string",
"app_installations_url": "string", "app_installations_url": "string",
"auth_url": "string", "auth_url": "string",
"client_id": "string", "client_id": "string",
"device_code_url": "string", "device_code_url": "string",
"device_flow": true, "device_flow": true,
"display_icon": "string", "display_icon": "string",
"display_name": "string", "display_name": "string",
"id": "string", "id": "string",
"no_refresh": true, "no_refresh": true,
"regex": "string", "regex": "string",
"scopes": ["string"], "scopes": ["string"],
"token_url": "string", "token_url": "string",
"type": "string", "type": "string",
"validate_url": "string" "validate_url": "string"
} }
] ]
}, },
"external_token_encryption_keys": ["string"], "external_token_encryption_keys": ["string"],
"healthcheck": { "healthcheck": {
"refresh": 0, "refresh": 0,
"threshold_database": 0 "threshold_database": 0
}, },
"http_address": "string", "http_address": "string",
"in_memory_database": true, "in_memory_database": true,
"job_hang_detector_interval": 0, "job_hang_detector_interval": 0,
"logging": { "logging": {
"human": "string", "human": "string",
"json": "string", "json": "string",
"log_filter": ["string"], "log_filter": ["string"],
"stackdriver": "string" "stackdriver": "string"
}, },
"metrics_cache_refresh_interval": 0, "metrics_cache_refresh_interval": 0,
"notifications": { "notifications": {
"dispatch_timeout": 0, "dispatch_timeout": 0,
"email": { "email": {
"auth": { "auth": {
"identity": "string", "identity": "string",
"password": "string", "password": "string",
"password_file": "string", "password_file": "string",
"username": "string" "username": "string"
}, },
"force_tls": true, "force_tls": true,
"from": "string", "from": "string",
"hello": "string", "hello": "string",
"smarthost": { "smarthost": {
"host": "string", "host": "string",
"port": "string" "port": "string"
}, },
"tls": { "tls": {
"ca_file": "string", "ca_file": "string",
"cert_file": "string", "cert_file": "string",
"insecure_skip_verify": true, "insecure_skip_verify": true,
"key_file": "string", "key_file": "string",
"server_name": "string", "server_name": "string",
"start_tls": true "start_tls": true
} }
}, },
"fetch_interval": 0, "fetch_interval": 0,
"lease_count": 0, "lease_count": 0,
"lease_period": 0, "lease_period": 0,
"max_send_attempts": 0, "max_send_attempts": 0,
"method": "string", "method": "string",
"retry_interval": 0, "retry_interval": 0,
"sync_buffer_size": 0, "sync_buffer_size": 0,
"sync_interval": 0, "sync_interval": 0,
"webhook": { "webhook": {
"endpoint": { "endpoint": {
"forceQuery": true, "forceQuery": true,
"fragment": "string", "fragment": "string",
"host": "string", "host": "string",
"omitHost": true, "omitHost": true,
"opaque": "string", "opaque": "string",
"path": "string", "path": "string",
"rawFragment": "string", "rawFragment": "string",
"rawPath": "string", "rawPath": "string",
"rawQuery": "string", "rawQuery": "string",
"scheme": "string", "scheme": "string",
"user": {} "user": {}
} }
} }
}, },
"oauth2": { "oauth2": {
"github": { "github": {
"allow_everyone": true, "allow_everyone": true,
"allow_signups": true, "allow_signups": true,
"allowed_orgs": ["string"], "allowed_orgs": ["string"],
"allowed_teams": ["string"], "allowed_teams": ["string"],
"client_id": "string", "client_id": "string",
"client_secret": "string", "client_secret": "string",
"enterprise_base_url": "string" "enterprise_base_url": "string"
} }
}, },
"oidc": { "oidc": {
"allow_signups": true, "allow_signups": true,
"auth_url_params": {}, "auth_url_params": {},
"client_cert_file": "string", "client_cert_file": "string",
"client_id": "string", "client_id": "string",
"client_key_file": "string", "client_key_file": "string",
"client_secret": "string", "client_secret": "string",
"email_domain": ["string"], "email_domain": ["string"],
"email_field": "string", "email_field": "string",
"group_allow_list": ["string"], "group_allow_list": ["string"],
"group_auto_create": true, "group_auto_create": true,
"group_mapping": {}, "group_mapping": {},
"group_regex_filter": {}, "group_regex_filter": {},
"groups_field": "string", "groups_field": "string",
"icon_url": { "icon_url": {
"forceQuery": true, "forceQuery": true,
"fragment": "string", "fragment": "string",
"host": "string", "host": "string",
"omitHost": true, "omitHost": true,
"opaque": "string", "opaque": "string",
"path": "string", "path": "string",
"rawFragment": "string", "rawFragment": "string",
"rawPath": "string", "rawPath": "string",
"rawQuery": "string", "rawQuery": "string",
"scheme": "string", "scheme": "string",
"user": {} "user": {}
}, },
"ignore_email_verified": true, "ignore_email_verified": true,
"ignore_user_info": true, "ignore_user_info": true,
"issuer_url": "string", "issuer_url": "string",
"name_field": "string", "name_field": "string",
"scopes": ["string"], "scopes": ["string"],
"sign_in_text": "string", "sign_in_text": "string",
"signups_disabled_text": "string", "signups_disabled_text": "string",
"skip_issuer_checks": true, "skip_issuer_checks": true,
"user_role_field": "string", "user_role_field": "string",
"user_role_mapping": {}, "user_role_mapping": {},
"user_roles_default": ["string"], "user_roles_default": ["string"],
"username_field": "string" "username_field": "string"
}, },
"pg_auth": "string", "pg_auth": "string",
"pg_connection_url": "string", "pg_connection_url": "string",
"pprof": { "pprof": {
"address": { "address": {
"host": "string", "host": "string",
"port": "string" "port": "string"
}, },
"enable": true "enable": true
}, },
"prometheus": { "prometheus": {
"address": { "address": {
"host": "string", "host": "string",
"port": "string" "port": "string"
}, },
"aggregate_agent_stats_by": ["string"], "aggregate_agent_stats_by": ["string"],
"collect_agent_stats": true, "collect_agent_stats": true,
"collect_db_metrics": true, "collect_db_metrics": true,
"enable": true "enable": true
}, },
"provisioner": { "provisioner": {
"daemon_poll_interval": 0, "daemon_poll_interval": 0,
"daemon_poll_jitter": 0, "daemon_poll_jitter": 0,
"daemon_psk": "string", "daemon_psk": "string",
"daemon_types": ["string"], "daemon_types": ["string"],
"daemons": 0, "daemons": 0,
"force_cancel_interval": 0 "force_cancel_interval": 0
}, },
"proxy_health_status_interval": 0, "proxy_health_status_interval": 0,
"proxy_trusted_headers": ["string"], "proxy_trusted_headers": ["string"],
"proxy_trusted_origins": ["string"], "proxy_trusted_origins": ["string"],
"rate_limit": { "rate_limit": {
"api": 0, "api": 0,
"disable_all": true "disable_all": true
}, },
"redirect_to_access_url": true, "redirect_to_access_url": true,
"scim_api_key": "string", "scim_api_key": "string",
"secure_auth_cookie": true, "secure_auth_cookie": true,
"session_lifetime": { "session_lifetime": {
"default_duration": 0, "default_duration": 0,
"disable_expiry_refresh": true, "disable_expiry_refresh": true,
"max_token_lifetime": 0 "max_token_lifetime": 0
}, },
"ssh_keygen_algorithm": "string", "ssh_keygen_algorithm": "string",
"strict_transport_security": 0, "strict_transport_security": 0,
"strict_transport_security_options": ["string"], "strict_transport_security_options": ["string"],
"support": { "support": {
"links": { "links": {
"value": [ "value": [
{ {
"icon": "bug", "icon": "bug",
"name": "string", "name": "string",
"target": "string" "target": "string"
} }
] ]
} }
}, },
"swagger": { "swagger": {
"enable": true "enable": true
}, },
"telemetry": { "telemetry": {
"enable": true, "enable": true,
"trace": true, "trace": true,
"url": { "url": {
"forceQuery": true, "forceQuery": true,
"fragment": "string", "fragment": "string",
"host": "string", "host": "string",
"omitHost": true, "omitHost": true,
"opaque": "string", "opaque": "string",
"path": "string", "path": "string",
"rawFragment": "string", "rawFragment": "string",
"rawPath": "string", "rawPath": "string",
"rawQuery": "string", "rawQuery": "string",
"scheme": "string", "scheme": "string",
"user": {} "user": {}
} }
}, },
"terms_of_service_url": "string", "terms_of_service_url": "string",
"tls": { "tls": {
"address": { "address": {
"host": "string", "host": "string",
"port": "string" "port": "string"
}, },
"allow_insecure_ciphers": true, "allow_insecure_ciphers": true,
"cert_file": ["string"], "cert_file": ["string"],
"client_auth": "string", "client_auth": "string",
"client_ca_file": "string", "client_ca_file": "string",
"client_cert_file": "string", "client_cert_file": "string",
"client_key_file": "string", "client_key_file": "string",
"enable": true, "enable": true,
"key_file": ["string"], "key_file": ["string"],
"min_version": "string", "min_version": "string",
"redirect_http": true, "redirect_http": true,
"supported_ciphers": ["string"] "supported_ciphers": ["string"]
}, },
"trace": { "trace": {
"capture_logs": true, "capture_logs": true,
"data_dog": true, "data_dog": true,
"enable": true, "enable": true,
"honeycomb_api_key": "string" "honeycomb_api_key": "string"
}, },
"update_check": true, "update_check": true,
"user_quiet_hours_schedule": { "user_quiet_hours_schedule": {
"allow_user_custom": true, "allow_user_custom": true,
"default_schedule": "string" "default_schedule": "string"
}, },
"verbose": true, "verbose": true,
"web_terminal_renderer": "string", "web_terminal_renderer": "string",
"wgtunnel_host": "string", "wgtunnel_host": "string",
"wildcard_access_url": "string", "wildcard_access_url": "string",
"write_config": true "write_config": true
}, },
"options": [ "options": [
{ {
"annotations": { "annotations": {
"property1": "string", "property1": "string",
"property2": "string" "property2": "string"
}, },
"default": "string", "default": "string",
"description": "string", "description": "string",
"env": "string", "env": "string",
"flag": "string", "flag": "string",
"flag_shorthand": "string", "flag_shorthand": "string",
"group": { "group": {
"description": "string", "description": "string",
"name": "string", "name": "string",
"parent": { "parent": {
"description": "string", "description": "string",
"name": "string", "name": "string",
"parent": {}, "parent": {},
"yaml": "string" "yaml": "string"
}, },
"yaml": "string" "yaml": "string"
}, },
"hidden": true, "hidden": true,
"name": "string", "name": "string",
"required": true, "required": true,
"use_instead": [{}], "use_instead": [{}],
"value": null, "value": null,
"value_source": "", "value_source": "",
"yaml": "string" "yaml": "string"
} }
] ]
} }
``` ```
@@ -525,11 +525,11 @@ curl -X GET http://coder-server:8080/api/v2/deployment/ssh \
```json ```json
{ {
"hostname_prefix": "string", "hostname_prefix": "string",
"ssh_config_options": { "ssh_config_options": {
"property1": "string", "property1": "string",
"property2": "string" "property2": "string"
} }
} }
``` ```
@@ -560,28 +560,28 @@ curl -X GET http://coder-server:8080/api/v2/deployment/stats \
```json ```json
{ {
"aggregated_from": "2019-08-24T14:15:22Z", "aggregated_from": "2019-08-24T14:15:22Z",
"collected_at": "2019-08-24T14:15:22Z", "collected_at": "2019-08-24T14:15:22Z",
"next_update_at": "2019-08-24T14:15:22Z", "next_update_at": "2019-08-24T14:15:22Z",
"session_count": { "session_count": {
"jetbrains": 0, "jetbrains": 0,
"reconnecting_pty": 0, "reconnecting_pty": 0,
"ssh": 0, "ssh": 0,
"vscode": 0 "vscode": 0
}, },
"workspaces": { "workspaces": {
"building": 0, "building": 0,
"connection_latency_ms": { "connection_latency_ms": {
"p50": 0, "p50": 0,
"p95": 0 "p95": 0
}, },
"failed": 0, "failed": 0,
"pending": 0, "pending": 0,
"running": 0, "running": 0,
"rx_bytes": 0, "rx_bytes": 0,
"stopped": 0, "stopped": 0,
"tx_bytes": 0 "tx_bytes": 0
} }
} }
``` ```
@@ -685,9 +685,9 @@ curl -X GET http://coder-server:8080/api/v2/updatecheck \
```json ```json
{ {
"current": true, "current": true,
"url": "string", "url": "string",
"version": "string" "version": "string"
} }
``` ```
@@ -722,7 +722,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/keys/tokens/tokenconfig
```json ```json
{ {
"max_token_lifetime": 0 "max_token_lifetime": 0
} }
``` ```
+37 -37
View File
@@ -19,13 +19,13 @@ curl -X GET http://coder-server:8080/api/v2/external-auth \
```json ```json
{ {
"authenticated": true, "authenticated": true,
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"expires": "2019-08-24T14:15:22Z", "expires": "2019-08-24T14:15:22Z",
"has_refresh_token": true, "has_refresh_token": true,
"provider_id": "string", "provider_id": "string",
"updated_at": "2019-08-24T14:15:22Z", "updated_at": "2019-08-24T14:15:22Z",
"validate_error": "string" "validate_error": "string"
} }
``` ```
@@ -62,31 +62,31 @@ curl -X GET http://coder-server:8080/api/v2/external-auth/{externalauth} \
```json ```json
{ {
"app_install_url": "string", "app_install_url": "string",
"app_installable": true, "app_installable": true,
"authenticated": true, "authenticated": true,
"device": true, "device": true,
"display_name": "string", "display_name": "string",
"installations": [ "installations": [
{ {
"account": { "account": {
"avatar_url": "string", "avatar_url": "string",
"id": 0, "id": 0,
"login": "string", "login": "string",
"name": "string", "name": "string",
"profile_url": "string" "profile_url": "string"
}, },
"configure_url": "string", "configure_url": "string",
"id": 0 "id": 0
} }
], ],
"user": { "user": {
"avatar_url": "string", "avatar_url": "string",
"id": 0, "id": 0,
"login": "string", "login": "string",
"name": "string", "name": "string",
"profile_url": "string" "profile_url": "string"
} }
} }
``` ```
@@ -149,11 +149,11 @@ curl -X GET http://coder-server:8080/api/v2/external-auth/{externalauth}/device
```json ```json
{ {
"device_code": "string", "device_code": "string",
"expires_in": 0, "expires_in": 0,
"interval": 0, "interval": 0,
"user_code": "string", "user_code": "string",
"verification_uri": "string" "verification_uri": "string"
} }
``` ```
+87 -87
View File
@@ -25,13 +25,13 @@ curl -X GET http://coder-server:8080/api/v2/insights/daus?tz_offset=0 \
```json ```json
{ {
"entries": [ "entries": [
{ {
"amount": 0, "amount": 0,
"date": "string" "date": "string"
} }
], ],
"tz_hour_offset": 0 "tz_hour_offset": 0
} }
``` ```
@@ -78,55 +78,55 @@ curl -X GET http://coder-server:8080/api/v2/insights/templates?start_time=2019-0
```json ```json
{ {
"interval_reports": [ "interval_reports": [
{ {
"active_users": 14, "active_users": 14,
"end_time": "2019-08-24T14:15:22Z", "end_time": "2019-08-24T14:15:22Z",
"interval": "week", "interval": "week",
"start_time": "2019-08-24T14:15:22Z", "start_time": "2019-08-24T14:15:22Z",
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"] "template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"]
} }
], ],
"report": { "report": {
"active_users": 22, "active_users": 22,
"apps_usage": [ "apps_usage": [
{ {
"display_name": "Visual Studio Code", "display_name": "Visual Studio Code",
"icon": "string", "icon": "string",
"seconds": 80500, "seconds": 80500,
"slug": "vscode", "slug": "vscode",
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"times_used": 2, "times_used": 2,
"type": "builtin" "type": "builtin"
} }
], ],
"end_time": "2019-08-24T14:15:22Z", "end_time": "2019-08-24T14:15:22Z",
"parameters_usage": [ "parameters_usage": [
{ {
"description": "string", "description": "string",
"display_name": "string", "display_name": "string",
"name": "string", "name": "string",
"options": [ "options": [
{ {
"description": "string", "description": "string",
"icon": "string", "icon": "string",
"name": "string", "name": "string",
"value": "string" "value": "string"
} }
], ],
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"type": "string", "type": "string",
"values": [ "values": [
{ {
"count": 0, "count": 0,
"value": "string" "value": "string"
} }
] ]
} }
], ],
"start_time": "2019-08-24T14:15:22Z", "start_time": "2019-08-24T14:15:22Z",
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"] "template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"]
} }
} }
``` ```
@@ -165,20 +165,20 @@ curl -X GET http://coder-server:8080/api/v2/insights/user-activity?start_time=20
```json ```json
{ {
"report": { "report": {
"end_time": "2019-08-24T14:15:22Z", "end_time": "2019-08-24T14:15:22Z",
"start_time": "2019-08-24T14:15:22Z", "start_time": "2019-08-24T14:15:22Z",
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"users": [ "users": [
{ {
"avatar_url": "http://example.com", "avatar_url": "http://example.com",
"seconds": 80500, "seconds": 80500,
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5", "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5",
"username": "string" "username": "string"
} }
] ]
} }
} }
``` ```
@@ -217,23 +217,23 @@ curl -X GET http://coder-server:8080/api/v2/insights/user-latency?start_time=201
```json ```json
{ {
"report": { "report": {
"end_time": "2019-08-24T14:15:22Z", "end_time": "2019-08-24T14:15:22Z",
"start_time": "2019-08-24T14:15:22Z", "start_time": "2019-08-24T14:15:22Z",
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"users": [ "users": [
{ {
"avatar_url": "http://example.com", "avatar_url": "http://example.com",
"latency_ms": { "latency_ms": {
"p50": 31.312, "p50": 31.312,
"p95": 119.832 "p95": 119.832
}, },
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5", "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5",
"username": "string" "username": "string"
} }
] ]
} }
} }
``` ```
+227 -227
View File
@@ -25,30 +25,30 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/members
```json ```json
[ [
{ {
"avatar_url": "string", "avatar_url": "string",
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"email": "string", "email": "string",
"global_roles": [ "global_roles": [
{ {
"display_name": "string", "display_name": "string",
"name": "string", "name": "string",
"organization_id": "string" "organization_id": "string"
} }
], ],
"name": "string", "name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"roles": [ "roles": [
{ {
"display_name": "string", "display_name": "string",
"name": "string", "name": "string",
"organization_id": "string" "organization_id": "string"
} }
], ],
"updated_at": "2019-08-24T14:15:22Z", "updated_at": "2019-08-24T14:15:22Z",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5", "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5",
"username": "string" "username": "string"
} }
] ]
``` ```
@@ -106,34 +106,34 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/members
```json ```json
[ [
{ {
"assignable": true, "assignable": true,
"built_in": true, "built_in": true,
"display_name": "string", "display_name": "string",
"name": "string", "name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"organization_permissions": [ "organization_permissions": [
{ {
"action": "application_connect", "action": "application_connect",
"negate": true, "negate": true,
"resource_type": "*" "resource_type": "*"
} }
], ],
"site_permissions": [ "site_permissions": [
{ {
"action": "application_connect", "action": "application_connect",
"negate": true, "negate": true,
"resource_type": "*" "resource_type": "*"
} }
], ],
"user_permissions": [ "user_permissions": [
{ {
"action": "application_connect", "action": "application_connect",
"negate": true, "negate": true,
"resource_type": "*" "resource_type": "*"
} }
] ]
} }
] ]
``` ```
@@ -229,29 +229,29 @@ curl -X PUT http://coder-server:8080/api/v2/organizations/{organization}/members
```json ```json
{ {
"display_name": "string", "display_name": "string",
"name": "string", "name": "string",
"organization_permissions": [ "organization_permissions": [
{ {
"action": "application_connect", "action": "application_connect",
"negate": true, "negate": true,
"resource_type": "*" "resource_type": "*"
} }
], ],
"site_permissions": [ "site_permissions": [
{ {
"action": "application_connect", "action": "application_connect",
"negate": true, "negate": true,
"resource_type": "*" "resource_type": "*"
} }
], ],
"user_permissions": [ "user_permissions": [
{ {
"action": "application_connect", "action": "application_connect",
"negate": true, "negate": true,
"resource_type": "*" "resource_type": "*"
} }
] ]
} }
``` ```
@@ -268,32 +268,32 @@ curl -X PUT http://coder-server:8080/api/v2/organizations/{organization}/members
```json ```json
[ [
{ {
"display_name": "string", "display_name": "string",
"name": "string", "name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"organization_permissions": [ "organization_permissions": [
{ {
"action": "application_connect", "action": "application_connect",
"negate": true, "negate": true,
"resource_type": "*" "resource_type": "*"
} }
], ],
"site_permissions": [ "site_permissions": [
{ {
"action": "application_connect", "action": "application_connect",
"negate": true, "negate": true,
"resource_type": "*" "resource_type": "*"
} }
], ],
"user_permissions": [ "user_permissions": [
{ {
"action": "application_connect", "action": "application_connect",
"negate": true, "negate": true,
"resource_type": "*" "resource_type": "*"
} }
] ]
} }
] ]
``` ```
@@ -387,29 +387,29 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member
```json ```json
{ {
"display_name": "string", "display_name": "string",
"name": "string", "name": "string",
"organization_permissions": [ "organization_permissions": [
{ {
"action": "application_connect", "action": "application_connect",
"negate": true, "negate": true,
"resource_type": "*" "resource_type": "*"
} }
], ],
"site_permissions": [ "site_permissions": [
{ {
"action": "application_connect", "action": "application_connect",
"negate": true, "negate": true,
"resource_type": "*" "resource_type": "*"
} }
], ],
"user_permissions": [ "user_permissions": [
{ {
"action": "application_connect", "action": "application_connect",
"negate": true, "negate": true,
"resource_type": "*" "resource_type": "*"
} }
] ]
} }
``` ```
@@ -426,32 +426,32 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member
```json ```json
[ [
{ {
"display_name": "string", "display_name": "string",
"name": "string", "name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"organization_permissions": [ "organization_permissions": [
{ {
"action": "application_connect", "action": "application_connect",
"negate": true, "negate": true,
"resource_type": "*" "resource_type": "*"
} }
], ],
"site_permissions": [ "site_permissions": [
{ {
"action": "application_connect", "action": "application_connect",
"negate": true, "negate": true,
"resource_type": "*" "resource_type": "*"
} }
], ],
"user_permissions": [ "user_permissions": [
{ {
"action": "application_connect", "action": "application_connect",
"negate": true, "negate": true,
"resource_type": "*" "resource_type": "*"
} }
] ]
} }
] ]
``` ```
@@ -553,32 +553,32 @@ curl -X DELETE http://coder-server:8080/api/v2/organizations/{organization}/memb
```json ```json
[ [
{ {
"display_name": "string", "display_name": "string",
"name": "string", "name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"organization_permissions": [ "organization_permissions": [
{ {
"action": "application_connect", "action": "application_connect",
"negate": true, "negate": true,
"resource_type": "*" "resource_type": "*"
} }
], ],
"site_permissions": [ "site_permissions": [
{ {
"action": "application_connect", "action": "application_connect",
"negate": true, "negate": true,
"resource_type": "*" "resource_type": "*"
} }
], ],
"user_permissions": [ "user_permissions": [
{ {
"action": "application_connect", "action": "application_connect",
"negate": true, "negate": true,
"resource_type": "*" "resource_type": "*"
} }
] ]
} }
] ]
``` ```
@@ -680,17 +680,17 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member
```json ```json
{ {
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"roles": [ "roles": [
{ {
"display_name": "string", "display_name": "string",
"name": "string", "name": "string",
"organization_id": "string" "organization_id": "string"
} }
], ],
"updated_at": "2019-08-24T14:15:22Z", "updated_at": "2019-08-24T14:15:22Z",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5" "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
} }
``` ```
@@ -747,7 +747,7 @@ curl -X PUT http://coder-server:8080/api/v2/organizations/{organization}/members
```json ```json
{ {
"roles": ["string"] "roles": ["string"]
} }
``` ```
@@ -765,17 +765,17 @@ curl -X PUT http://coder-server:8080/api/v2/organizations/{organization}/members
```json ```json
{ {
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"roles": [ "roles": [
{ {
"display_name": "string", "display_name": "string",
"name": "string", "name": "string",
"organization_id": "string" "organization_id": "string"
} }
], ],
"updated_at": "2019-08-24T14:15:22Z", "updated_at": "2019-08-24T14:15:22Z",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5" "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
} }
``` ```
@@ -806,34 +806,34 @@ curl -X GET http://coder-server:8080/api/v2/users/roles \
```json ```json
[ [
{ {
"assignable": true, "assignable": true,
"built_in": true, "built_in": true,
"display_name": "string", "display_name": "string",
"name": "string", "name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"organization_permissions": [ "organization_permissions": [
{ {
"action": "application_connect", "action": "application_connect",
"negate": true, "negate": true,
"resource_type": "*" "resource_type": "*"
} }
], ],
"site_permissions": [ "site_permissions": [
{ {
"action": "application_connect", "action": "application_connect",
"negate": true, "negate": true,
"resource_type": "*" "resource_type": "*"
} }
], ],
"user_permissions": [ "user_permissions": [
{ {
"action": "application_connect", "action": "application_connect",
"negate": true, "negate": true,
"resource_type": "*" "resource_type": "*"
} }
] ]
} }
] ]
``` ```
+31 -31
View File
@@ -19,10 +19,10 @@ curl -X GET http://coder-server:8080/api/v2/notifications/dispatch-methods \
```json ```json
[ [
{ {
"available": ["string"], "available": ["string"],
"default": "string" "default": "string"
} }
] ]
``` ```
@@ -63,7 +63,7 @@ curl -X GET http://coder-server:8080/api/v2/notifications/settings \
```json ```json
{ {
"notifier_paused": true "notifier_paused": true
} }
``` ```
@@ -93,7 +93,7 @@ curl -X PUT http://coder-server:8080/api/v2/notifications/settings \
```json ```json
{ {
"notifier_paused": true "notifier_paused": true
} }
``` ```
@@ -109,7 +109,7 @@ curl -X PUT http://coder-server:8080/api/v2/notifications/settings \
```json ```json
{ {
"notifier_paused": true "notifier_paused": true
} }
``` ```
@@ -141,16 +141,16 @@ curl -X GET http://coder-server:8080/api/v2/notifications/templates/system \
```json ```json
[ [
{ {
"actions": "string", "actions": "string",
"body_template": "string", "body_template": "string",
"group": "string", "group": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"kind": "string", "kind": "string",
"method": "string", "method": "string",
"name": "string", "name": "string",
"title_template": "string" "title_template": "string"
} }
] ]
``` ```
@@ -203,11 +203,11 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/notifications/preferenc
```json ```json
[ [
{ {
"disabled": true, "disabled": true,
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"updated_at": "2019-08-24T14:15:22Z" "updated_at": "2019-08-24T14:15:22Z"
} }
] ]
``` ```
@@ -248,10 +248,10 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/notifications/preferenc
```json ```json
{ {
"template_disabled_map": { "template_disabled_map": {
"property1": true, "property1": true,
"property2": true "property2": true
} }
} }
``` ```
@@ -268,11 +268,11 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/notifications/preferenc
```json ```json
[ [
{ {
"disabled": true, "disabled": true,
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"updated_at": "2019-08-24T14:15:22Z" "updated_at": "2019-08-24T14:15:22Z"
} }
] ]
``` ```
+63 -63
View File
@@ -18,7 +18,7 @@ curl -X POST http://coder-server:8080/api/v2/licenses \
```json ```json
{ {
"license": "string" "license": "string"
} }
``` ```
@@ -34,10 +34,10 @@ curl -X POST http://coder-server:8080/api/v2/licenses \
```json ```json
{ {
"claims": {}, "claims": {},
"id": 0, "id": 0,
"uploaded_at": "2019-08-24T14:15:22Z", "uploaded_at": "2019-08-24T14:15:22Z",
"uuid": "095be615-a8ad-4c33-8e9c-c7612fbf6c9f" "uuid": "095be615-a8ad-4c33-8e9c-c7612fbf6c9f"
} }
``` ```
@@ -68,14 +68,14 @@ curl -X POST http://coder-server:8080/api/v2/licenses/refresh-entitlements \
```json ```json
{ {
"detail": "string", "detail": "string",
"message": "string", "message": "string",
"validations": [ "validations": [
{ {
"detail": "string", "detail": "string",
"field": "string" "field": "string"
} }
] ]
} }
``` ```
@@ -106,16 +106,16 @@ curl -X GET http://coder-server:8080/api/v2/organizations \
```json ```json
[ [
{ {
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"description": "string", "description": "string",
"display_name": "string", "display_name": "string",
"icon": "string", "icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_default": true, "is_default": true,
"name": "string", "name": "string",
"updated_at": "2019-08-24T14:15:22Z" "updated_at": "2019-08-24T14:15:22Z"
} }
] ]
``` ```
@@ -161,10 +161,10 @@ curl -X POST http://coder-server:8080/api/v2/organizations \
```json ```json
{ {
"description": "string", "description": "string",
"display_name": "string", "display_name": "string",
"icon": "string", "icon": "string",
"name": "string" "name": "string"
} }
``` ```
@@ -180,14 +180,14 @@ curl -X POST http://coder-server:8080/api/v2/organizations \
```json ```json
{ {
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"description": "string", "description": "string",
"display_name": "string", "display_name": "string",
"icon": "string", "icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_default": true, "is_default": true,
"name": "string", "name": "string",
"updated_at": "2019-08-24T14:15:22Z" "updated_at": "2019-08-24T14:15:22Z"
} }
``` ```
@@ -224,14 +224,14 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization} \
```json ```json
{ {
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"description": "string", "description": "string",
"display_name": "string", "display_name": "string",
"icon": "string", "icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_default": true, "is_default": true,
"name": "string", "name": "string",
"updated_at": "2019-08-24T14:15:22Z" "updated_at": "2019-08-24T14:15:22Z"
} }
``` ```
@@ -268,14 +268,14 @@ curl -X DELETE http://coder-server:8080/api/v2/organizations/{organization} \
```json ```json
{ {
"detail": "string", "detail": "string",
"message": "string", "message": "string",
"validations": [ "validations": [
{ {
"detail": "string", "detail": "string",
"field": "string" "field": "string"
} }
] ]
} }
``` ```
@@ -305,10 +305,10 @@ curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization} \
```json ```json
{ {
"description": "string", "description": "string",
"display_name": "string", "display_name": "string",
"icon": "string", "icon": "string",
"name": "string" "name": "string"
} }
``` ```
@@ -325,14 +325,14 @@ curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization} \
```json ```json
{ {
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"description": "string", "description": "string",
"display_name": "string", "display_name": "string",
"icon": "string", "icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_default": true, "is_default": true,
"name": "string", "name": "string",
"updated_at": "2019-08-24T14:15:22Z" "updated_at": "2019-08-24T14:15:22Z"
} }
``` ```
+11 -11
View File
@@ -17,8 +17,8 @@ curl -X DELETE http://coder-server:8080/api/v2/workspaces/{workspace}/port-share
```json ```json
{ {
"agent_name": "string", "agent_name": "string",
"port": 0 "port": 0
} }
``` ```
@@ -55,10 +55,10 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/port-share \
```json ```json
{ {
"agent_name": "string", "agent_name": "string",
"port": 0, "port": 0,
"protocol": "http", "protocol": "http",
"share_level": "owner" "share_level": "owner"
} }
``` ```
@@ -75,11 +75,11 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/port-share \
```json ```json
{ {
"agent_name": "string", "agent_name": "string",
"port": 0, "port": 0,
"protocol": "http", "protocol": "http",
"share_level": "owner", "share_level": "owner",
"workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9"
} }
``` ```
+4379 -4379
View File
File diff suppressed because it is too large Load Diff
+951 -951
View File
File diff suppressed because it is too large Load Diff
+301 -301
View File
@@ -28,30 +28,30 @@ curl -X GET http://coder-server:8080/api/v2/users \
```json ```json
{ {
"count": 0, "count": 0,
"users": [ "users": [
{ {
"avatar_url": "http://example.com", "avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "", "login_type": "",
"name": "string", "name": "string",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
"display_name": "string", "display_name": "string",
"name": "string", "name": "string",
"organization_id": "string" "organization_id": "string"
} }
], ],
"status": "active", "status": "active",
"theme_preference": "string", "theme_preference": "string",
"updated_at": "2019-08-24T14:15:22Z", "updated_at": "2019-08-24T14:15:22Z",
"username": "string" "username": "string"
} }
] ]
} }
``` ```
@@ -81,13 +81,13 @@ curl -X POST http://coder-server:8080/api/v2/users \
```json ```json
{ {
"disable_login": true, "disable_login": true,
"email": "user@example.com", "email": "user@example.com",
"login_type": "", "login_type": "",
"name": "string", "name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"password": "string", "password": "string",
"username": "string" "username": "string"
} }
``` ```
@@ -103,25 +103,25 @@ curl -X POST http://coder-server:8080/api/v2/users \
```json ```json
{ {
"avatar_url": "http://example.com", "avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "", "login_type": "",
"name": "string", "name": "string",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
"display_name": "string", "display_name": "string",
"name": "string", "name": "string",
"organization_id": "string" "organization_id": "string"
} }
], ],
"status": "active", "status": "active",
"theme_preference": "string", "theme_preference": "string",
"updated_at": "2019-08-24T14:15:22Z", "updated_at": "2019-08-24T14:15:22Z",
"username": "string" "username": "string"
} }
``` ```
@@ -152,18 +152,18 @@ curl -X GET http://coder-server:8080/api/v2/users/authmethods \
```json ```json
{ {
"github": { "github": {
"enabled": true "enabled": true
}, },
"oidc": { "oidc": {
"enabled": true, "enabled": true,
"iconUrl": "string", "iconUrl": "string",
"signInText": "string" "signInText": "string"
}, },
"password": { "password": {
"enabled": true "enabled": true
}, },
"terms_of_service_url": "string" "terms_of_service_url": "string"
} }
``` ```
@@ -194,14 +194,14 @@ curl -X GET http://coder-server:8080/api/v2/users/first \
```json ```json
{ {
"detail": "string", "detail": "string",
"message": "string", "message": "string",
"validations": [ "validations": [
{ {
"detail": "string", "detail": "string",
"field": "string" "field": "string"
} }
] ]
} }
``` ```
@@ -231,20 +231,20 @@ curl -X POST http://coder-server:8080/api/v2/users/first \
```json ```json
{ {
"email": "string", "email": "string",
"name": "string", "name": "string",
"password": "string", "password": "string",
"trial": true, "trial": true,
"trial_info": { "trial_info": {
"company_name": "string", "company_name": "string",
"country": "string", "country": "string",
"developers": "string", "developers": "string",
"first_name": "string", "first_name": "string",
"job_title": "string", "job_title": "string",
"last_name": "string", "last_name": "string",
"phone_number": "string" "phone_number": "string"
}, },
"username": "string" "username": "string"
} }
``` ```
@@ -260,8 +260,8 @@ curl -X POST http://coder-server:8080/api/v2/users/first \
```json ```json
{ {
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5" "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
} }
``` ```
@@ -292,14 +292,14 @@ curl -X POST http://coder-server:8080/api/v2/users/logout \
```json ```json
{ {
"detail": "string", "detail": "string",
"message": "string", "message": "string",
"validations": [ "validations": [
{ {
"detail": "string", "detail": "string",
"field": "string" "field": "string"
} }
] ]
} }
``` ```
@@ -376,25 +376,25 @@ curl -X GET http://coder-server:8080/api/v2/users/{user} \
```json ```json
{ {
"avatar_url": "http://example.com", "avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "", "login_type": "",
"name": "string", "name": "string",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
"display_name": "string", "display_name": "string",
"name": "string", "name": "string",
"organization_id": "string" "organization_id": "string"
} }
], ],
"status": "active", "status": "active",
"theme_preference": "string", "theme_preference": "string",
"updated_at": "2019-08-24T14:15:22Z", "updated_at": "2019-08-24T14:15:22Z",
"username": "string" "username": "string"
} }
``` ```
@@ -450,7 +450,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/appearance \
```json ```json
{ {
"theme_preference": "string" "theme_preference": "string"
} }
``` ```
@@ -467,25 +467,25 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/appearance \
```json ```json
{ {
"avatar_url": "http://example.com", "avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "", "login_type": "",
"name": "string", "name": "string",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
"display_name": "string", "display_name": "string",
"name": "string", "name": "string",
"organization_id": "string" "organization_id": "string"
} }
], ],
"status": "active", "status": "active",
"theme_preference": "string", "theme_preference": "string",
"updated_at": "2019-08-24T14:15:22Z", "updated_at": "2019-08-24T14:15:22Z",
"username": "string" "username": "string"
} }
``` ```
@@ -523,10 +523,10 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/autofill-parameters?tem
```json ```json
[ [
{ {
"name": "string", "name": "string",
"value": "string" "value": "string"
} }
] ]
``` ```
@@ -573,10 +573,10 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/gitsshkey \
```json ```json
{ {
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"public_key": "string", "public_key": "string",
"updated_at": "2019-08-24T14:15:22Z", "updated_at": "2019-08-24T14:15:22Z",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5" "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
} }
``` ```
@@ -613,10 +613,10 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/gitsshkey \
```json ```json
{ {
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"public_key": "string", "public_key": "string",
"updated_at": "2019-08-24T14:15:22Z", "updated_at": "2019-08-24T14:15:22Z",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5" "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
} }
``` ```
@@ -653,7 +653,7 @@ curl -X POST http://coder-server:8080/api/v2/users/{user}/keys \
```json ```json
{ {
"key": "string" "key": "string"
} }
``` ```
@@ -690,18 +690,18 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/keys/tokens \
```json ```json
[ [
{ {
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"expires_at": "2019-08-24T14:15:22Z", "expires_at": "2019-08-24T14:15:22Z",
"id": "string", "id": "string",
"last_used": "2019-08-24T14:15:22Z", "last_used": "2019-08-24T14:15:22Z",
"lifetime_seconds": 0, "lifetime_seconds": 0,
"login_type": "password", "login_type": "password",
"scope": "all", "scope": "all",
"token_name": "string", "token_name": "string",
"updated_at": "2019-08-24T14:15:22Z", "updated_at": "2019-08-24T14:15:22Z",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5" "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
} }
] ]
``` ```
@@ -760,9 +760,9 @@ curl -X POST http://coder-server:8080/api/v2/users/{user}/keys/tokens \
```json ```json
{ {
"lifetime": 0, "lifetime": 0,
"scope": "all", "scope": "all",
"token_name": "string" "token_name": "string"
} }
``` ```
@@ -779,7 +779,7 @@ curl -X POST http://coder-server:8080/api/v2/users/{user}/keys/tokens \
```json ```json
{ {
"key": "string" "key": "string"
} }
``` ```
@@ -817,16 +817,16 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/keys/tokens/{keyname} \
```json ```json
{ {
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"expires_at": "2019-08-24T14:15:22Z", "expires_at": "2019-08-24T14:15:22Z",
"id": "string", "id": "string",
"last_used": "2019-08-24T14:15:22Z", "last_used": "2019-08-24T14:15:22Z",
"lifetime_seconds": 0, "lifetime_seconds": 0,
"login_type": "password", "login_type": "password",
"scope": "all", "scope": "all",
"token_name": "string", "token_name": "string",
"updated_at": "2019-08-24T14:15:22Z", "updated_at": "2019-08-24T14:15:22Z",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5" "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
} }
``` ```
@@ -864,16 +864,16 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/keys/{keyid} \
```json ```json
{ {
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"expires_at": "2019-08-24T14:15:22Z", "expires_at": "2019-08-24T14:15:22Z",
"id": "string", "id": "string",
"last_used": "2019-08-24T14:15:22Z", "last_used": "2019-08-24T14:15:22Z",
"lifetime_seconds": 0, "lifetime_seconds": 0,
"login_type": "password", "login_type": "password",
"scope": "all", "scope": "all",
"token_name": "string", "token_name": "string",
"updated_at": "2019-08-24T14:15:22Z", "updated_at": "2019-08-24T14:15:22Z",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5" "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
} }
``` ```
@@ -937,7 +937,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/login-type \
```json ```json
{ {
"login_type": "" "login_type": ""
} }
``` ```
@@ -974,16 +974,16 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/organizations \
```json ```json
[ [
{ {
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"description": "string", "description": "string",
"display_name": "string", "display_name": "string",
"icon": "string", "icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_default": true, "is_default": true,
"name": "string", "name": "string",
"updated_at": "2019-08-24T14:15:22Z" "updated_at": "2019-08-24T14:15:22Z"
} }
] ]
``` ```
@@ -1037,14 +1037,14 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/organizations/{organiza
```json ```json
{ {
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"description": "string", "description": "string",
"display_name": "string", "display_name": "string",
"icon": "string", "icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_default": true, "is_default": true,
"name": "string", "name": "string",
"updated_at": "2019-08-24T14:15:22Z" "updated_at": "2019-08-24T14:15:22Z"
} }
``` ```
@@ -1073,8 +1073,8 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/password \
```json ```json
{ {
"old_password": "string", "old_password": "string",
"password": "string" "password": "string"
} }
``` ```
@@ -1111,8 +1111,8 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/profile \
```json ```json
{ {
"name": "string", "name": "string",
"username": "string" "username": "string"
} }
``` ```
@@ -1129,25 +1129,25 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/profile \
```json ```json
{ {
"avatar_url": "http://example.com", "avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "", "login_type": "",
"name": "string", "name": "string",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
"display_name": "string", "display_name": "string",
"name": "string", "name": "string",
"organization_id": "string" "organization_id": "string"
} }
], ],
"status": "active", "status": "active",
"theme_preference": "string", "theme_preference": "string",
"updated_at": "2019-08-24T14:15:22Z", "updated_at": "2019-08-24T14:15:22Z",
"username": "string" "username": "string"
} }
``` ```
@@ -1184,25 +1184,25 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/roles \
```json ```json
{ {
"avatar_url": "http://example.com", "avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "", "login_type": "",
"name": "string", "name": "string",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
"display_name": "string", "display_name": "string",
"name": "string", "name": "string",
"organization_id": "string" "organization_id": "string"
} }
], ],
"status": "active", "status": "active",
"theme_preference": "string", "theme_preference": "string",
"updated_at": "2019-08-24T14:15:22Z", "updated_at": "2019-08-24T14:15:22Z",
"username": "string" "username": "string"
} }
``` ```
@@ -1232,7 +1232,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/roles \
```json ```json
{ {
"roles": ["string"] "roles": ["string"]
} }
``` ```
@@ -1249,25 +1249,25 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/roles \
```json ```json
{ {
"avatar_url": "http://example.com", "avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "", "login_type": "",
"name": "string", "name": "string",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
"display_name": "string", "display_name": "string",
"name": "string", "name": "string",
"organization_id": "string" "organization_id": "string"
} }
], ],
"status": "active", "status": "active",
"theme_preference": "string", "theme_preference": "string",
"updated_at": "2019-08-24T14:15:22Z", "updated_at": "2019-08-24T14:15:22Z",
"username": "string" "username": "string"
} }
``` ```
@@ -1304,25 +1304,25 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/status/activate \
```json ```json
{ {
"avatar_url": "http://example.com", "avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "", "login_type": "",
"name": "string", "name": "string",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
"display_name": "string", "display_name": "string",
"name": "string", "name": "string",
"organization_id": "string" "organization_id": "string"
} }
], ],
"status": "active", "status": "active",
"theme_preference": "string", "theme_preference": "string",
"updated_at": "2019-08-24T14:15:22Z", "updated_at": "2019-08-24T14:15:22Z",
"username": "string" "username": "string"
} }
``` ```
@@ -1359,25 +1359,25 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/status/suspend \
```json ```json
{ {
"avatar_url": "http://example.com", "avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com", "email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "", "login_type": "",
"name": "string", "name": "string",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [ "roles": [
{ {
"display_name": "string", "display_name": "string",
"name": "string", "name": "string",
"organization_id": "string" "organization_id": "string"
} }
], ],
"status": "active", "status": "active",
"theme_preference": "string", "theme_preference": "string",
"updated_at": "2019-08-24T14:15:22Z", "updated_at": "2019-08-24T14:15:22Z",
"username": "string" "username": "string"
} }
``` ```
+11 -11
View File
@@ -19,17 +19,17 @@ curl -X GET http://coder-server:8080/api/v2/regions \
```json ```json
{ {
"regions": [ "regions": [
{ {
"display_name": "string", "display_name": "string",
"healthy": true, "healthy": true,
"icon_url": "string", "icon_url": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"name": "string", "name": "string",
"path_app_url": "string", "path_app_url": "string",
"wildcard_hostname": "string" "wildcard_hostname": "string"
} }
] ]
} }
``` ```
+1104 -1104
View File
File diff suppressed because it is too large Load Diff
+22 -22
View File
@@ -254,28 +254,28 @@ The raw logs will look something like this:
```json ```json
{ {
"ts": "2022-02-28T20:29:38.038452202Z", "ts": "2022-02-28T20:29:38.038452202Z",
"level": "INFO", "level": "INFO",
"msg": "exec", "msg": "exec",
"fields": { "fields": {
"labels": { "labels": {
"user_email": "jessie@coder.com", "user_email": "jessie@coder.com",
"user_id": "5e876e9a-121663f01ebd1522060d5270", "user_id": "5e876e9a-121663f01ebd1522060d5270",
"username": "jessie", "username": "jessie",
"workspace_id": "621d2e52-a6987ef6c56210058ee2593c", "workspace_id": "621d2e52-a6987ef6c56210058ee2593c",
"workspace_name": "main" "workspace_name": "main"
}, },
"cmdline": "uname -a", "cmdline": "uname -a",
"event": { "event": {
"filename": "/usr/bin/uname", "filename": "/usr/bin/uname",
"argv": ["uname", "-a"], "argv": ["uname", "-a"],
"truncated": false, "truncated": false,
"pid": 920684, "pid": 920684,
"uid": 101000, "uid": 101000,
"gid": 101000, "gid": 101000,
"comm": "bash" "comm": "bash"
} }
} }
} }
``` ```
+6 -6
View File
@@ -1,9 +1,9 @@
{ {
"name": "Develop Coder on Coder using Envbuilder", "name": "Develop Coder on Coder using Envbuilder",
"build": { "build": {
"dockerfile": "Dockerfile" "dockerfile": "Dockerfile"
}, },
"features": {}, "features": {},
"runArgs": ["--cap-add=SYS_PTRACE"] "runArgs": ["--cap-add=SYS_PTRACE"]
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"registry-mirrors": ["https://mirror.gcr.io"] "registry-mirrors": ["https://mirror.gcr.io"]
} }
+166 -166
View File
@@ -1,169 +1,169 @@
// Code generated by examplegen. DO NOT EDIT. // Code generated by examplegen. DO NOT EDIT.
[ [
{ {
"id": "aws-devcontainer", "id": "aws-devcontainer",
"url": "", "url": "",
"name": "AWS EC2 (Devcontainer)", "name": "AWS EC2 (Devcontainer)",
"description": "Provision AWS EC2 VMs with a devcontainer as Coder workspaces", "description": "Provision AWS EC2 VMs with a devcontainer as Coder workspaces",
"icon": "/icon/aws.svg", "icon": "/icon/aws.svg",
"tags": [ "tags": [
"vm", "vm",
"linux", "linux",
"aws", "aws",
"persistent", "persistent",
"devcontainer" "devcontainer"
], ],
"markdown": "\n# Remote Development on AWS EC2 VMs using a Devcontainer\n\nProvision AWS EC2 VMs as [Coder workspaces](https://coder.com/docs) with this example template.\n![Architecture Diagram](./architecture.svg)\n\n\u003c!-- TODO: Add screenshot --\u003e\n\n## Prerequisites\n\n### Authentication\n\nBy default, this template authenticates to AWS using the provider's default [authentication methods](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication-and-configuration).\n\nThe simplest way (without making changes to the template) is via environment variables (e.g. `AWS_ACCESS_KEY_ID`) or a [credentials file](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html#cli-configure-files-format). If you are running Coder on a VM, this file must be in `/home/coder/aws/credentials`.\n\nTo use another [authentication method](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication), edit the template.\n\n## Required permissions / policy\n\nThe following sample policy allows Coder to create EC2 instances and modify\ninstances provisioned by Coder:\n\n```json\n{\n \"Version\": \"2012-10-17\",\n \"Statement\": [\n {\n \"Sid\": \"VisualEditor0\",\n \"Effect\": \"Allow\",\n \"Action\": [\n \"ec2:GetDefaultCreditSpecification\",\n \"ec2:DescribeIamInstanceProfileAssociations\",\n \"ec2:DescribeTags\",\n \"ec2:DescribeInstances\",\n \"ec2:DescribeInstanceTypes\",\n \"ec2:CreateTags\",\n \"ec2:RunInstances\",\n \"ec2:DescribeInstanceCreditSpecifications\",\n \"ec2:DescribeImages\",\n \"ec2:ModifyDefaultCreditSpecification\",\n \"ec2:DescribeVolumes\"\n ],\n \"Resource\": \"*\"\n },\n {\n \"Sid\": \"CoderResources\",\n \"Effect\": \"Allow\",\n \"Action\": [\n \"ec2:DescribeInstanceAttribute\",\n \"ec2:UnmonitorInstances\",\n \"ec2:TerminateInstances\",\n \"ec2:StartInstances\",\n \"ec2:StopInstances\",\n \"ec2:DeleteTags\",\n \"ec2:MonitorInstances\",\n \"ec2:CreateTags\",\n \"ec2:RunInstances\",\n \"ec2:ModifyInstanceAttribute\",\n \"ec2:ModifyInstanceCreditSpecification\"\n ],\n \"Resource\": \"arn:aws:ec2:*:*:instance/*\",\n \"Condition\": {\n \"StringEquals\": {\n \"aws:ResourceTag/Coder_Provisioned\": \"true\"\n }\n }\n }\n ]\n}\n```\n\n## Architecture\n\nThis template provisions the following resources:\n\n- AWS Instance\n\nCoder uses `aws_ec2_instance_state` to start and stop the VM. This example template is fully persistent, meaning the full filesystem is preserved when the workspace restarts. See this [community example](https://github.com/bpmct/coder-templates/tree/main/aws-linux-ephemeral) of an ephemeral AWS instance.\n\n\u003e **Note**\n\u003e This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.\n\n## code-server\n\n`code-server` is installed via the [`code-server`](https://registry.coder.com/modules/code-server) registry module. For a list of all modules and templates pplease check [Coder Registry](https://registry.coder.com).\n" "markdown": "\n# Remote Development on AWS EC2 VMs using a Devcontainer\n\nProvision AWS EC2 VMs as [Coder workspaces](https://coder.com/docs) with this example template.\n![Architecture Diagram](./architecture.svg)\n\n\u003c!-- TODO: Add screenshot --\u003e\n\n## Prerequisites\n\n### Authentication\n\nBy default, this template authenticates to AWS using the provider's default [authentication methods](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication-and-configuration).\n\nThe simplest way (without making changes to the template) is via environment variables (e.g. `AWS_ACCESS_KEY_ID`) or a [credentials file](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html#cli-configure-files-format). If you are running Coder on a VM, this file must be in `/home/coder/aws/credentials`.\n\nTo use another [authentication method](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication), edit the template.\n\n## Required permissions / policy\n\nThe following sample policy allows Coder to create EC2 instances and modify\ninstances provisioned by Coder:\n\n```json\n{\n\t\"Version\": \"2012-10-17\",\n\t\"Statement\": [\n\t\t{\n\t\t\t\"Sid\": \"VisualEditor0\",\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Action\": [\n\t\t\t\t\"ec2:GetDefaultCreditSpecification\",\n\t\t\t\t\"ec2:DescribeIamInstanceProfileAssociations\",\n\t\t\t\t\"ec2:DescribeTags\",\n\t\t\t\t\"ec2:DescribeInstances\",\n\t\t\t\t\"ec2:DescribeInstanceTypes\",\n\t\t\t\t\"ec2:CreateTags\",\n\t\t\t\t\"ec2:RunInstances\",\n\t\t\t\t\"ec2:DescribeInstanceCreditSpecifications\",\n\t\t\t\t\"ec2:DescribeImages\",\n\t\t\t\t\"ec2:ModifyDefaultCreditSpecification\",\n\t\t\t\t\"ec2:DescribeVolumes\"\n\t\t\t],\n\t\t\t\"Resource\": \"*\"\n\t\t},\n\t\t{\n\t\t\t\"Sid\": \"CoderResources\",\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Action\": [\n\t\t\t\t\"ec2:DescribeInstanceAttribute\",\n\t\t\t\t\"ec2:UnmonitorInstances\",\n\t\t\t\t\"ec2:TerminateInstances\",\n\t\t\t\t\"ec2:StartInstances\",\n\t\t\t\t\"ec2:StopInstances\",\n\t\t\t\t\"ec2:DeleteTags\",\n\t\t\t\t\"ec2:MonitorInstances\",\n\t\t\t\t\"ec2:CreateTags\",\n\t\t\t\t\"ec2:RunInstances\",\n\t\t\t\t\"ec2:ModifyInstanceAttribute\",\n\t\t\t\t\"ec2:ModifyInstanceCreditSpecification\"\n\t\t\t],\n\t\t\t\"Resource\": \"arn:aws:ec2:*:*:instance/*\",\n\t\t\t\"Condition\": {\n\t\t\t\t\"StringEquals\": {\n\t\t\t\t\t\"aws:ResourceTag/Coder_Provisioned\": \"true\"\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n```\n\n## Architecture\n\nThis template provisions the following resources:\n\n- AWS Instance\n\nCoder uses `aws_ec2_instance_state` to start and stop the VM. This example template is fully persistent, meaning the full filesystem is preserved when the workspace restarts. See this [community example](https://github.com/bpmct/coder-templates/tree/main/aws-linux-ephemeral) of an ephemeral AWS instance.\n\n\u003e **Note**\n\u003e This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.\n\n## code-server\n\n`code-server` is installed via the [`code-server`](https://registry.coder.com/modules/code-server) registry module. For a list of all modules and templates pplease check [Coder Registry](https://registry.coder.com).\n"
}, },
{ {
"id": "aws-linux", "id": "aws-linux",
"url": "", "url": "",
"name": "AWS EC2 (Linux)", "name": "AWS EC2 (Linux)",
"description": "Provision AWS EC2 VMs as Coder workspaces", "description": "Provision AWS EC2 VMs as Coder workspaces",
"icon": "/icon/aws.svg", "icon": "/icon/aws.svg",
"tags": [ "tags": [
"vm", "vm",
"linux", "linux",
"aws", "aws",
"persistent-vm" "persistent-vm"
], ],
"markdown": "\n# Remote Development on AWS EC2 VMs (Linux)\n\nProvision AWS EC2 VMs as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.\n\n\u003c!-- TODO: Add screenshot --\u003e\n\n## Prerequisites\n\n### Authentication\n\nBy default, this template authenticates to AWS using the provider's default [authentication methods](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication-and-configuration).\n\nThe simplest way (without making changes to the template) is via environment variables (e.g. `AWS_ACCESS_KEY_ID`) or a [credentials file](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html#cli-configure-files-format). If you are running Coder on a VM, this file must be in `/home/coder/aws/credentials`.\n\nTo use another [authentication method](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication), edit the template.\n\n## Required permissions / policy\n\nThe following sample policy allows Coder to create EC2 instances and modify\ninstances provisioned by Coder:\n\n```json\n{\n \"Version\": \"2012-10-17\",\n \"Statement\": [\n {\n \"Sid\": \"VisualEditor0\",\n \"Effect\": \"Allow\",\n \"Action\": [\n \"ec2:GetDefaultCreditSpecification\",\n \"ec2:DescribeIamInstanceProfileAssociations\",\n \"ec2:DescribeTags\",\n \"ec2:DescribeInstances\",\n \"ec2:DescribeInstanceTypes\",\n \"ec2:CreateTags\",\n \"ec2:RunInstances\",\n \"ec2:DescribeInstanceCreditSpecifications\",\n \"ec2:DescribeImages\",\n \"ec2:ModifyDefaultCreditSpecification\",\n \"ec2:DescribeVolumes\"\n ],\n \"Resource\": \"*\"\n },\n {\n \"Sid\": \"CoderResources\",\n \"Effect\": \"Allow\",\n \"Action\": [\n \"ec2:DescribeInstanceAttribute\",\n \"ec2:UnmonitorInstances\",\n \"ec2:TerminateInstances\",\n \"ec2:StartInstances\",\n \"ec2:StopInstances\",\n \"ec2:DeleteTags\",\n \"ec2:MonitorInstances\",\n \"ec2:CreateTags\",\n \"ec2:RunInstances\",\n \"ec2:ModifyInstanceAttribute\",\n \"ec2:ModifyInstanceCreditSpecification\"\n ],\n \"Resource\": \"arn:aws:ec2:*:*:instance/*\",\n \"Condition\": {\n \"StringEquals\": {\n \"aws:ResourceTag/Coder_Provisioned\": \"true\"\n }\n }\n }\n ]\n}\n```\n\n## Architecture\n\nThis template provisions the following resources:\n\n- AWS Instance\n\nCoder uses `aws_ec2_instance_state` to start and stop the VM. This example template is fully persistent, meaning the full filesystem is preserved when the workspace restarts. See this [community example](https://github.com/bpmct/coder-templates/tree/main/aws-linux-ephemeral) of an ephemeral AWS instance.\n\n\u003e **Note**\n\u003e This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.\n\n## code-server\n\n`code-server` is installed via the `startup_script` argument in the `coder_agent`\nresource block. The `coder_app` resource is defined to access `code-server` through\nthe dashboard UI over `localhost:13337`.\n" "markdown": "\n# Remote Development on AWS EC2 VMs (Linux)\n\nProvision AWS EC2 VMs as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.\n\n\u003c!-- TODO: Add screenshot --\u003e\n\n## Prerequisites\n\n### Authentication\n\nBy default, this template authenticates to AWS using the provider's default [authentication methods](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication-and-configuration).\n\nThe simplest way (without making changes to the template) is via environment variables (e.g. `AWS_ACCESS_KEY_ID`) or a [credentials file](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html#cli-configure-files-format). If you are running Coder on a VM, this file must be in `/home/coder/aws/credentials`.\n\nTo use another [authentication method](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication), edit the template.\n\n## Required permissions / policy\n\nThe following sample policy allows Coder to create EC2 instances and modify\ninstances provisioned by Coder:\n\n```json\n{\n\t\"Version\": \"2012-10-17\",\n\t\"Statement\": [\n\t\t{\n\t\t\t\"Sid\": \"VisualEditor0\",\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Action\": [\n\t\t\t\t\"ec2:GetDefaultCreditSpecification\",\n\t\t\t\t\"ec2:DescribeIamInstanceProfileAssociations\",\n\t\t\t\t\"ec2:DescribeTags\",\n\t\t\t\t\"ec2:DescribeInstances\",\n\t\t\t\t\"ec2:DescribeInstanceTypes\",\n\t\t\t\t\"ec2:CreateTags\",\n\t\t\t\t\"ec2:RunInstances\",\n\t\t\t\t\"ec2:DescribeInstanceCreditSpecifications\",\n\t\t\t\t\"ec2:DescribeImages\",\n\t\t\t\t\"ec2:ModifyDefaultCreditSpecification\",\n\t\t\t\t\"ec2:DescribeVolumes\"\n\t\t\t],\n\t\t\t\"Resource\": \"*\"\n\t\t},\n\t\t{\n\t\t\t\"Sid\": \"CoderResources\",\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Action\": [\n\t\t\t\t\"ec2:DescribeInstanceAttribute\",\n\t\t\t\t\"ec2:UnmonitorInstances\",\n\t\t\t\t\"ec2:TerminateInstances\",\n\t\t\t\t\"ec2:StartInstances\",\n\t\t\t\t\"ec2:StopInstances\",\n\t\t\t\t\"ec2:DeleteTags\",\n\t\t\t\t\"ec2:MonitorInstances\",\n\t\t\t\t\"ec2:CreateTags\",\n\t\t\t\t\"ec2:RunInstances\",\n\t\t\t\t\"ec2:ModifyInstanceAttribute\",\n\t\t\t\t\"ec2:ModifyInstanceCreditSpecification\"\n\t\t\t],\n\t\t\t\"Resource\": \"arn:aws:ec2:*:*:instance/*\",\n\t\t\t\"Condition\": {\n\t\t\t\t\"StringEquals\": {\n\t\t\t\t\t\"aws:ResourceTag/Coder_Provisioned\": \"true\"\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n```\n\n## Architecture\n\nThis template provisions the following resources:\n\n- AWS Instance\n\nCoder uses `aws_ec2_instance_state` to start and stop the VM. This example template is fully persistent, meaning the full filesystem is preserved when the workspace restarts. See this [community example](https://github.com/bpmct/coder-templates/tree/main/aws-linux-ephemeral) of an ephemeral AWS instance.\n\n\u003e **Note**\n\u003e This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.\n\n## code-server\n\n`code-server` is installed via the `startup_script` argument in the `coder_agent`\nresource block. The `coder_app` resource is defined to access `code-server` through\nthe dashboard UI over `localhost:13337`.\n"
}, },
{ {
"id": "aws-windows", "id": "aws-windows",
"url": "", "url": "",
"name": "AWS EC2 (Windows)", "name": "AWS EC2 (Windows)",
"description": "Provision AWS EC2 VMs as Coder workspaces", "description": "Provision AWS EC2 VMs as Coder workspaces",
"icon": "/icon/aws.svg", "icon": "/icon/aws.svg",
"tags": [ "tags": [
"vm", "vm",
"windows", "windows",
"aws" "aws"
], ],
"markdown": "\n# Remote Development on AWS EC2 VMs (Windows)\n\nProvision AWS EC2 Windows VMs as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.\n\n\u003c!-- TODO: Add screenshot --\u003e\n\n## Prerequisites\n\n### Authentication\n\nBy default, this template authenticates to AWS with using the provider's default [authentication methods](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication-and-configuration).\n\nThe simplest way (without making changes to the template) is via environment variables (e.g. `AWS_ACCESS_KEY_ID`) or a [credentials file](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html#cli-configure-files-format). If you are running Coder on a VM, this file must be in `/home/coder/aws/credentials`.\n\nTo use another [authentication method](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication), edit the template.\n\n## Required permissions / policy\n\nThe following sample policy allows Coder to create EC2 instances and modify\ninstances provisioned by Coder:\n\n```json\n{\n \"Version\": \"2012-10-17\",\n \"Statement\": [\n {\n \"Sid\": \"VisualEditor0\",\n \"Effect\": \"Allow\",\n \"Action\": [\n \"ec2:GetDefaultCreditSpecification\",\n \"ec2:DescribeIamInstanceProfileAssociations\",\n \"ec2:DescribeTags\",\n \"ec2:DescribeInstances\",\n \"ec2:DescribeInstanceTypes\",\n \"ec2:CreateTags\",\n \"ec2:RunInstances\",\n \"ec2:DescribeInstanceCreditSpecifications\",\n \"ec2:DescribeImages\",\n \"ec2:ModifyDefaultCreditSpecification\",\n \"ec2:DescribeVolumes\"\n ],\n \"Resource\": \"*\"\n },\n {\n \"Sid\": \"CoderResources\",\n \"Effect\": \"Allow\",\n \"Action\": [\n \"ec2:DescribeInstanceAttribute\",\n \"ec2:UnmonitorInstances\",\n \"ec2:TerminateInstances\",\n \"ec2:StartInstances\",\n \"ec2:StopInstances\",\n \"ec2:DeleteTags\",\n \"ec2:MonitorInstances\",\n \"ec2:CreateTags\",\n \"ec2:RunInstances\",\n \"ec2:ModifyInstanceAttribute\",\n \"ec2:ModifyInstanceCreditSpecification\"\n ],\n \"Resource\": \"arn:aws:ec2:*:*:instance/*\",\n \"Condition\": {\n \"StringEquals\": {\n \"aws:ResourceTag/Coder_Provisioned\": \"true\"\n }\n }\n }\n ]\n}\n```\n\n## Architecture\n\nThis template provisions the following resources:\n\n- AWS Instance\n\nCoder uses `aws_ec2_instance_state` to start and stop the VM. This example template is fully persistent, meaning the full filesystem is preserved when the workspace restarts. See this [community example](https://github.com/bpmct/coder-templates/tree/main/aws-linux-ephemeral) of an ephemeral AWS instance.\n\n\u003e **Note**\n\u003e This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.\n\n## code-server\n\n`code-server` is installed via the `startup_script` argument in the `coder_agent`\nresource block. The `coder_app` resource is defined to access `code-server` through\nthe dashboard UI over `localhost:13337`.\n" "markdown": "\n# Remote Development on AWS EC2 VMs (Windows)\n\nProvision AWS EC2 Windows VMs as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.\n\n\u003c!-- TODO: Add screenshot --\u003e\n\n## Prerequisites\n\n### Authentication\n\nBy default, this template authenticates to AWS with using the provider's default [authentication methods](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication-and-configuration).\n\nThe simplest way (without making changes to the template) is via environment variables (e.g. `AWS_ACCESS_KEY_ID`) or a [credentials file](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html#cli-configure-files-format). If you are running Coder on a VM, this file must be in `/home/coder/aws/credentials`.\n\nTo use another [authentication method](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication), edit the template.\n\n## Required permissions / policy\n\nThe following sample policy allows Coder to create EC2 instances and modify\ninstances provisioned by Coder:\n\n```json\n{\n\t\"Version\": \"2012-10-17\",\n\t\"Statement\": [\n\t\t{\n\t\t\t\"Sid\": \"VisualEditor0\",\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Action\": [\n\t\t\t\t\"ec2:GetDefaultCreditSpecification\",\n\t\t\t\t\"ec2:DescribeIamInstanceProfileAssociations\",\n\t\t\t\t\"ec2:DescribeTags\",\n\t\t\t\t\"ec2:DescribeInstances\",\n\t\t\t\t\"ec2:DescribeInstanceTypes\",\n\t\t\t\t\"ec2:CreateTags\",\n\t\t\t\t\"ec2:RunInstances\",\n\t\t\t\t\"ec2:DescribeInstanceCreditSpecifications\",\n\t\t\t\t\"ec2:DescribeImages\",\n\t\t\t\t\"ec2:ModifyDefaultCreditSpecification\",\n\t\t\t\t\"ec2:DescribeVolumes\"\n\t\t\t],\n\t\t\t\"Resource\": \"*\"\n\t\t},\n\t\t{\n\t\t\t\"Sid\": \"CoderResources\",\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Action\": [\n\t\t\t\t\"ec2:DescribeInstanceAttribute\",\n\t\t\t\t\"ec2:UnmonitorInstances\",\n\t\t\t\t\"ec2:TerminateInstances\",\n\t\t\t\t\"ec2:StartInstances\",\n\t\t\t\t\"ec2:StopInstances\",\n\t\t\t\t\"ec2:DeleteTags\",\n\t\t\t\t\"ec2:MonitorInstances\",\n\t\t\t\t\"ec2:CreateTags\",\n\t\t\t\t\"ec2:RunInstances\",\n\t\t\t\t\"ec2:ModifyInstanceAttribute\",\n\t\t\t\t\"ec2:ModifyInstanceCreditSpecification\"\n\t\t\t],\n\t\t\t\"Resource\": \"arn:aws:ec2:*:*:instance/*\",\n\t\t\t\"Condition\": {\n\t\t\t\t\"StringEquals\": {\n\t\t\t\t\t\"aws:ResourceTag/Coder_Provisioned\": \"true\"\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n```\n\n## Architecture\n\nThis template provisions the following resources:\n\n- AWS Instance\n\nCoder uses `aws_ec2_instance_state` to start and stop the VM. This example template is fully persistent, meaning the full filesystem is preserved when the workspace restarts. See this [community example](https://github.com/bpmct/coder-templates/tree/main/aws-linux-ephemeral) of an ephemeral AWS instance.\n\n\u003e **Note**\n\u003e This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.\n\n## code-server\n\n`code-server` is installed via the `startup_script` argument in the `coder_agent`\nresource block. The `coder_app` resource is defined to access `code-server` through\nthe dashboard UI over `localhost:13337`.\n"
}, },
{ {
"id": "azure-linux", "id": "azure-linux",
"url": "", "url": "",
"name": "Azure VM (Linux)", "name": "Azure VM (Linux)",
"description": "Provision Azure VMs as Coder workspaces", "description": "Provision Azure VMs as Coder workspaces",
"icon": "/icon/azure.png", "icon": "/icon/azure.png",
"tags": [ "tags": [
"vm", "vm",
"linux", "linux",
"azure" "azure"
], ],
"markdown": "\n# Remote Development on Azure VMs (Linux)\n\nProvision Azure Linux VMs as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.\n\n\u003c!-- TODO: Add screenshot --\u003e\n\n## Prerequisites\n\n### Authentication\n\nThis template assumes that coderd is run in an environment that is authenticated\nwith Azure. For example, run `az login` then `az account set --subscription=\u003cid\u003e`\nto import credentials on the system and user running coderd. For other ways to\nauthenticate, [consult the Terraform docs](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs#authenticating-to-azure).\n\n## Architecture\n\nThis template provisions the following resources:\n\n- Azure VM (ephemeral, deleted on stop)\n- Managed disk (persistent, mounted to `/home/coder`)\n\nThis means, when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the VM image, or use a [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script). Alternatively, individual developers can [personalize](https://coder.com/docs/dotfiles) their workspaces with dotfiles.\n\n\u003e **Note**\n\u003e This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.\n\n## code-server\n\n`code-server` is installed via the `startup_script` argument in the `coder_agent`\nresource block. The `coder_app` resource is defined to access `code-server` through\nthe dashboard UI over `localhost:13337`.\n" "markdown": "\n# Remote Development on Azure VMs (Linux)\n\nProvision Azure Linux VMs as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.\n\n\u003c!-- TODO: Add screenshot --\u003e\n\n## Prerequisites\n\n### Authentication\n\nThis template assumes that coderd is run in an environment that is authenticated\nwith Azure. For example, run `az login` then `az account set --subscription=\u003cid\u003e`\nto import credentials on the system and user running coderd. For other ways to\nauthenticate, [consult the Terraform docs](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs#authenticating-to-azure).\n\n## Architecture\n\nThis template provisions the following resources:\n\n- Azure VM (ephemeral, deleted on stop)\n- Managed disk (persistent, mounted to `/home/coder`)\n\nThis means, when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the VM image, or use a [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script). Alternatively, individual developers can [personalize](https://coder.com/docs/dotfiles) their workspaces with dotfiles.\n\n\u003e **Note**\n\u003e This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.\n\n## code-server\n\n`code-server` is installed via the `startup_script` argument in the `coder_agent`\nresource block. The `coder_app` resource is defined to access `code-server` through\nthe dashboard UI over `localhost:13337`.\n"
}, },
{ {
"id": "do-linux", "id": "do-linux",
"url": "", "url": "",
"name": "DigitalOcean Droplet (Linux)", "name": "DigitalOcean Droplet (Linux)",
"description": "Provision DigitalOcean Droplets as Coder workspaces", "description": "Provision DigitalOcean Droplets as Coder workspaces",
"icon": "/icon/do.png", "icon": "/icon/do.png",
"tags": [ "tags": [
"vm", "vm",
"linux", "linux",
"digitalocean" "digitalocean"
], ],
"markdown": "\n# Remote Development on DigitalOcean Droplets\n\nProvision DigitalOcean Droplets as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.\n\n\u003c!-- TODO: Add screenshot --\u003e\n\n## Prerequisites\n\nTo deploy workspaces as DigitalOcean Droplets, you'll need:\n\n- DigitalOcean [personal access token (PAT)](https://docs.digitalocean.com/reference/api/create-personal-access-token/)\n\n- DigitalOcean project ID (you can get your project information via the `doctl`\n CLI by running `doctl projects list`)\n\n- Remove the following sections from the `main.tf` file if you don't want to\n associate your workspaces with a project:\n\n - `variable \"step2_do_project_id\"`\n - `resource \"digitalocean_project_resources\" \"project\"`\n\n- **Optional:** DigitalOcean SSH key ID (obtain via the `doctl` CLI by running\n `doctl compute ssh-key list`)\n\n- Note that this is only required for Fedora images to work.\n\n### Authentication\n\nThis template assumes that coderd is run in an environment that is authenticated\nwith Digital Ocean. Obtain a [Digital Ocean Personal Access\nToken](https://cloud.digitalocean.com/account/api/tokens) and set the\nenvironment variable `DIGITALOCEAN_TOKEN` to the access token before starting\ncoderd. For other ways to authenticate [consult the Terraform docs](https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs).\n\n## Architecture\n\nThis template provisions the following resources:\n\n- Azure VM (ephemeral, deleted on stop)\n- Managed disk (persistent, mounted to `/home/coder`)\n\nThis means, when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the VM image, or use a [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script).\n\n\u003e **Note**\n\u003e This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.\n" "markdown": "\n# Remote Development on DigitalOcean Droplets\n\nProvision DigitalOcean Droplets as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.\n\n\u003c!-- TODO: Add screenshot --\u003e\n\n## Prerequisites\n\nTo deploy workspaces as DigitalOcean Droplets, you'll need:\n\n- DigitalOcean [personal access token (PAT)](https://docs.digitalocean.com/reference/api/create-personal-access-token/)\n\n- DigitalOcean project ID (you can get your project information via the `doctl`\n CLI by running `doctl projects list`)\n\n- Remove the following sections from the `main.tf` file if you don't want to\n associate your workspaces with a project:\n\n - `variable \"step2_do_project_id\"`\n - `resource \"digitalocean_project_resources\" \"project\"`\n\n- **Optional:** DigitalOcean SSH key ID (obtain via the `doctl` CLI by running\n `doctl compute ssh-key list`)\n\n- Note that this is only required for Fedora images to work.\n\n### Authentication\n\nThis template assumes that coderd is run in an environment that is authenticated\nwith Digital Ocean. Obtain a [Digital Ocean Personal Access\nToken](https://cloud.digitalocean.com/account/api/tokens) and set the\nenvironment variable `DIGITALOCEAN_TOKEN` to the access token before starting\ncoderd. For other ways to authenticate [consult the Terraform docs](https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs).\n\n## Architecture\n\nThis template provisions the following resources:\n\n- Azure VM (ephemeral, deleted on stop)\n- Managed disk (persistent, mounted to `/home/coder`)\n\nThis means, when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the VM image, or use a [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script).\n\n\u003e **Note**\n\u003e This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.\n"
}, },
{ {
"id": "docker", "id": "docker",
"url": "", "url": "",
"name": "Docker Containers", "name": "Docker Containers",
"description": "Provision Docker containers as Coder workspaces", "description": "Provision Docker containers as Coder workspaces",
"icon": "/icon/docker.png", "icon": "/icon/docker.png",
"tags": [ "tags": [
"docker", "docker",
"container" "container"
], ],
"markdown": "\n# Remote Development on Docker Containers\n\nProvision Docker containers as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.\n\n\u003c!-- TODO: Add screenshot --\u003e\n\n## Prerequisites\n\n### Infrastructure\n\nThe VM you run Coder on must have a running Docker socket and the `coder` user must be added to the Docker group:\n\n```sh\n# Add coder user to Docker group\nsudo adduser coder docker\n\n# Restart Coder server\nsudo systemctl restart coder\n\n# Test Docker\nsudo -u coder docker ps\n```\n\n## Architecture\n\nThis template provisions the following resources:\n\n- Docker image (built by Docker socket and kept locally)\n- Docker container pod (ephemeral)\n- Docker volume (persistent on `/home/coder`)\n\nThis means, when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the container image. Alternatively, individual developers can [personalize](https://coder.com/docs/dotfiles) their workspaces with dotfiles.\n\n\u003e **Note**\n\u003e This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.\n\n### Editing the image\n\nEdit the `Dockerfile` and run `coder templates push` to update workspaces.\n" "markdown": "\n# Remote Development on Docker Containers\n\nProvision Docker containers as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.\n\n\u003c!-- TODO: Add screenshot --\u003e\n\n## Prerequisites\n\n### Infrastructure\n\nThe VM you run Coder on must have a running Docker socket and the `coder` user must be added to the Docker group:\n\n```sh\n# Add coder user to Docker group\nsudo adduser coder docker\n\n# Restart Coder server\nsudo systemctl restart coder\n\n# Test Docker\nsudo -u coder docker ps\n```\n\n## Architecture\n\nThis template provisions the following resources:\n\n- Docker image (built by Docker socket and kept locally)\n- Docker container pod (ephemeral)\n- Docker volume (persistent on `/home/coder`)\n\nThis means, when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the container image. Alternatively, individual developers can [personalize](https://coder.com/docs/dotfiles) their workspaces with dotfiles.\n\n\u003e **Note**\n\u003e This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.\n\n### Editing the image\n\nEdit the `Dockerfile` and run `coder templates push` to update workspaces.\n"
}, },
{ {
"id": "gcp-devcontainer", "id": "gcp-devcontainer",
"url": "", "url": "",
"name": "Google Compute Engine (Devcontainer)", "name": "Google Compute Engine (Devcontainer)",
"description": "Provision a Devcontainer on Google Compute Engine instances as Coder workspaces", "description": "Provision a Devcontainer on Google Compute Engine instances as Coder workspaces",
"icon": "/icon/gcp.png", "icon": "/icon/gcp.png",
"tags": [ "tags": [
"vm", "vm",
"linux", "linux",
"gcp", "gcp",
"devcontainer" "devcontainer"
], ],
"markdown": "\n# Remote Development in a Devcontainer on Google Compute Engine\n\n![Architecture Diagram](./architecture.svg)\n\n## Prerequisites\n\n### Authentication\n\nThis template assumes that coderd is run in an environment that is authenticated\nwith Google Cloud. For example, run `gcloud auth application-default login` to\nimport credentials on the system and user running coderd. For other ways to\nauthenticate [consult the Terraform\ndocs](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/getting_started#adding-credentials).\n\nCoder requires a Google Cloud Service Account to provision workspaces. To create\na service account:\n\n1. Navigate to the [CGP\n console](https://console.cloud.google.com/projectselector/iam-admin/serviceaccounts/create),\n and select your Cloud project (if you have more than one project associated\n with your account)\n\n1. Provide a service account name (this name is used to generate the service\n account ID)\n\n1. Click **Create and continue**, and choose the following IAM roles to grant to\n the service account:\n\n - Compute Admin\n - Service Account User\n\n Click **Continue**.\n\n1. Click on the created key, and navigate to the **Keys** tab.\n\n1. Click **Add key** \u003e **Create new key**.\n\n1. Generate a **JSON private key**, which will be what you provide to Coder\n during the setup process.\n\n## Architecture\n\nThis template provisions the following resources:\n\n- GCP VM (persistent)\n- GCP Disk (persistent, mounted to root)\n\nCoder persists the root volume. The full filesystem is preserved when the workspace restarts.\n\n\u003e **Note**\n\u003e This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.\n\n## code-server\n\n`code-server` is installed via the [`code-server`](https://registry.coder.com/modules/code-server) registry module. Please check [Coder Registry](https://registry.coder.com) for a list of all modules and templates.\n" "markdown": "\n# Remote Development in a Devcontainer on Google Compute Engine\n\n![Architecture Diagram](./architecture.svg)\n\n## Prerequisites\n\n### Authentication\n\nThis template assumes that coderd is run in an environment that is authenticated\nwith Google Cloud. For example, run `gcloud auth application-default login` to\nimport credentials on the system and user running coderd. For other ways to\nauthenticate [consult the Terraform\ndocs](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/getting_started#adding-credentials).\n\nCoder requires a Google Cloud Service Account to provision workspaces. To create\na service account:\n\n1. Navigate to the [CGP\n console](https://console.cloud.google.com/projectselector/iam-admin/serviceaccounts/create),\n and select your Cloud project (if you have more than one project associated\n with your account)\n\n1. Provide a service account name (this name is used to generate the service\n account ID)\n\n1. Click **Create and continue**, and choose the following IAM roles to grant to\n the service account:\n\n - Compute Admin\n - Service Account User\n\n Click **Continue**.\n\n1. Click on the created key, and navigate to the **Keys** tab.\n\n1. Click **Add key** \u003e **Create new key**.\n\n1. Generate a **JSON private key**, which will be what you provide to Coder\n during the setup process.\n\n## Architecture\n\nThis template provisions the following resources:\n\n- GCP VM (persistent)\n- GCP Disk (persistent, mounted to root)\n\nCoder persists the root volume. The full filesystem is preserved when the workspace restarts.\n\n\u003e **Note**\n\u003e This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.\n\n## code-server\n\n`code-server` is installed via the [`code-server`](https://registry.coder.com/modules/code-server) registry module. Please check [Coder Registry](https://registry.coder.com) for a list of all modules and templates.\n"
}, },
{ {
"id": "gcp-linux", "id": "gcp-linux",
"url": "", "url": "",
"name": "Google Compute Engine (Linux)", "name": "Google Compute Engine (Linux)",
"description": "Provision Google Compute Engine instances as Coder workspaces", "description": "Provision Google Compute Engine instances as Coder workspaces",
"icon": "/icon/gcp.png", "icon": "/icon/gcp.png",
"tags": [ "tags": [
"vm", "vm",
"linux", "linux",
"gcp" "gcp"
], ],
"markdown": "\n# Remote Development on Google Compute Engine (Linux)\n\n## Prerequisites\n\n### Authentication\n\nThis template assumes that coderd is run in an environment that is authenticated\nwith Google Cloud. For example, run `gcloud auth application-default login` to\nimport credentials on the system and user running coderd. For other ways to\nauthenticate [consult the Terraform\ndocs](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/getting_started#adding-credentials).\n\nCoder requires a Google Cloud Service Account to provision workspaces. To create\na service account:\n\n1. Navigate to the [CGP\n console](https://console.cloud.google.com/projectselector/iam-admin/serviceaccounts/create),\n and select your Cloud project (if you have more than one project associated\n with your account)\n\n1. Provide a service account name (this name is used to generate the service\n account ID)\n\n1. Click **Create and continue**, and choose the following IAM roles to grant to\n the service account:\n\n - Compute Admin\n - Service Account User\n\n Click **Continue**.\n\n1. Click on the created key, and navigate to the **Keys** tab.\n\n1. Click **Add key** \u003e **Create new key**.\n\n1. Generate a **JSON private key**, which will be what you provide to Coder\n during the setup process.\n\n## Architecture\n\nThis template provisions the following resources:\n\n- GCP VM (ephemeral)\n- GCP Disk (persistent, mounted to root)\n\nCoder persists the root volume. The full filesystem is preserved when the workspace restarts. See this [community example](https://github.com/bpmct/coder-templates/tree/main/aws-linux-ephemeral) of an ephemeral AWS instance.\n\n\u003e **Note**\n\u003e This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.\n\n## code-server\n\n`code-server` is installed via the `startup_script` argument in the `coder_agent`\nresource block. The `coder_app` resource is defined to access `code-server` through\nthe dashboard UI over `localhost:13337`.\n" "markdown": "\n# Remote Development on Google Compute Engine (Linux)\n\n## Prerequisites\n\n### Authentication\n\nThis template assumes that coderd is run in an environment that is authenticated\nwith Google Cloud. For example, run `gcloud auth application-default login` to\nimport credentials on the system and user running coderd. For other ways to\nauthenticate [consult the Terraform\ndocs](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/getting_started#adding-credentials).\n\nCoder requires a Google Cloud Service Account to provision workspaces. To create\na service account:\n\n1. Navigate to the [CGP\n console](https://console.cloud.google.com/projectselector/iam-admin/serviceaccounts/create),\n and select your Cloud project (if you have more than one project associated\n with your account)\n\n1. Provide a service account name (this name is used to generate the service\n account ID)\n\n1. Click **Create and continue**, and choose the following IAM roles to grant to\n the service account:\n\n - Compute Admin\n - Service Account User\n\n Click **Continue**.\n\n1. Click on the created key, and navigate to the **Keys** tab.\n\n1. Click **Add key** \u003e **Create new key**.\n\n1. Generate a **JSON private key**, which will be what you provide to Coder\n during the setup process.\n\n## Architecture\n\nThis template provisions the following resources:\n\n- GCP VM (ephemeral)\n- GCP Disk (persistent, mounted to root)\n\nCoder persists the root volume. The full filesystem is preserved when the workspace restarts. See this [community example](https://github.com/bpmct/coder-templates/tree/main/aws-linux-ephemeral) of an ephemeral AWS instance.\n\n\u003e **Note**\n\u003e This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.\n\n## code-server\n\n`code-server` is installed via the `startup_script` argument in the `coder_agent`\nresource block. The `coder_app` resource is defined to access `code-server` through\nthe dashboard UI over `localhost:13337`.\n"
}, },
{ {
"id": "gcp-vm-container", "id": "gcp-vm-container",
"url": "", "url": "",
"name": "Google Compute Engine (VM Container)", "name": "Google Compute Engine (VM Container)",
"description": "Provision Google Compute Engine instances as Coder workspaces", "description": "Provision Google Compute Engine instances as Coder workspaces",
"icon": "/icon/gcp.png", "icon": "/icon/gcp.png",
"tags": [ "tags": [
"vm-container", "vm-container",
"linux", "linux",
"gcp" "gcp"
], ],
"markdown": "\n# Remote Development on Google Compute Engine (VM Container)\n\n## Prerequisites\n\n### Authentication\n\nThis template assumes that coderd is run in an environment that is authenticated\nwith Google Cloud. For example, run `gcloud auth application-default login` to\nimport credentials on the system and user running coderd. For other ways to\nauthenticate [consult the Terraform\ndocs](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/getting_started#adding-credentials).\n\nCoder requires a Google Cloud Service Account to provision workspaces. To create\na service account:\n\n1. Navigate to the [CGP\n console](https://console.cloud.google.com/projectselector/iam-admin/serviceaccounts/create),\n and select your Cloud project (if you have more than one project associated\n with your account)\n\n1. Provide a service account name (this name is used to generate the service\n account ID)\n\n1. Click **Create and continue**, and choose the following IAM roles to grant to\n the service account:\n\n - Compute Admin\n - Service Account User\n\n Click **Continue**.\n\n1. Click on the created key, and navigate to the **Keys** tab.\n\n1. Click **Add key** \u003e **Create new key**.\n\n1. Generate a **JSON private key**, which will be what you provide to Coder\n during the setup process.\n\n## Architecture\n\nThis template provisions the following resources:\n\n- GCP VM (ephemeral, deleted on stop)\n - Container in VM\n- Managed disk (persistent, mounted to `/home/coder` in container)\n\nThis means, when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the container image, or use a [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script).\n\n\u003e **Note**\n\u003e This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.\n\n## code-server\n\n`code-server` is installed via the `startup_script` argument in the `coder_agent`\nresource block. The `coder_app` resource is defined to access `code-server` through\nthe dashboard UI over `localhost:13337`.\n" "markdown": "\n# Remote Development on Google Compute Engine (VM Container)\n\n## Prerequisites\n\n### Authentication\n\nThis template assumes that coderd is run in an environment that is authenticated\nwith Google Cloud. For example, run `gcloud auth application-default login` to\nimport credentials on the system and user running coderd. For other ways to\nauthenticate [consult the Terraform\ndocs](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/getting_started#adding-credentials).\n\nCoder requires a Google Cloud Service Account to provision workspaces. To create\na service account:\n\n1. Navigate to the [CGP\n console](https://console.cloud.google.com/projectselector/iam-admin/serviceaccounts/create),\n and select your Cloud project (if you have more than one project associated\n with your account)\n\n1. Provide a service account name (this name is used to generate the service\n account ID)\n\n1. Click **Create and continue**, and choose the following IAM roles to grant to\n the service account:\n\n - Compute Admin\n - Service Account User\n\n Click **Continue**.\n\n1. Click on the created key, and navigate to the **Keys** tab.\n\n1. Click **Add key** \u003e **Create new key**.\n\n1. Generate a **JSON private key**, which will be what you provide to Coder\n during the setup process.\n\n## Architecture\n\nThis template provisions the following resources:\n\n- GCP VM (ephemeral, deleted on stop)\n - Container in VM\n- Managed disk (persistent, mounted to `/home/coder` in container)\n\nThis means, when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the container image, or use a [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script).\n\n\u003e **Note**\n\u003e This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.\n\n## code-server\n\n`code-server` is installed via the `startup_script` argument in the `coder_agent`\nresource block. The `coder_app` resource is defined to access `code-server` through\nthe dashboard UI over `localhost:13337`.\n"
}, },
{ {
"id": "gcp-windows", "id": "gcp-windows",
"url": "", "url": "",
"name": "Google Compute Engine (Windows)", "name": "Google Compute Engine (Windows)",
"description": "Provision Google Compute Engine instances as Coder workspaces", "description": "Provision Google Compute Engine instances as Coder workspaces",
"icon": "/icon/gcp.png", "icon": "/icon/gcp.png",
"tags": [ "tags": [
"vm", "vm",
"windows", "windows",
"gcp" "gcp"
], ],
"markdown": "\n# Remote Development on Google Compute Engine (Windows)\n\n## Prerequisites\n\n### Authentication\n\nThis template assumes that coderd is run in an environment that is authenticated\nwith Google Cloud. For example, run `gcloud auth application-default login` to\nimport credentials on the system and user running coderd. For other ways to\nauthenticate [consult the Terraform\ndocs](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/getting_started#adding-credentials).\n\nCoder requires a Google Cloud Service Account to provision workspaces. To create\na service account:\n\n1. Navigate to the [CGP\n console](https://console.cloud.google.com/projectselector/iam-admin/serviceaccounts/create),\n and select your Cloud project (if you have more than one project associated\n with your account)\n\n1. Provide a service account name (this name is used to generate the service\n account ID)\n\n1. Click **Create and continue**, and choose the following IAM roles to grant to\n the service account:\n\n - Compute Admin\n - Service Account User\n\n Click **Continue**.\n\n1. Click on the created key, and navigate to the **Keys** tab.\n\n1. Click **Add key** \u003e **Create new key**.\n\n1. Generate a **JSON private key**, which will be what you provide to Coder\n during the setup process.\n\n## Architecture\n\nThis template provisions the following resources:\n\n- GCP VM (ephemeral)\n- GCP Disk (persistent, mounted to root)\n\nCoder persists the root volume. The full filesystem is preserved when the workspace restarts. See this [community example](https://github.com/bpmct/coder-templates/tree/main/aws-linux-ephemeral) of an ephemeral AWS instance.\n\n\u003e **Note**\n\u003e This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.\n\n## code-server\n\n`code-server` is installed via the `startup_script` argument in the `coder_agent`\nresource block. The `coder_app` resource is defined to access `code-server` through\nthe dashboard UI over `localhost:13337`.\n" "markdown": "\n# Remote Development on Google Compute Engine (Windows)\n\n## Prerequisites\n\n### Authentication\n\nThis template assumes that coderd is run in an environment that is authenticated\nwith Google Cloud. For example, run `gcloud auth application-default login` to\nimport credentials on the system and user running coderd. For other ways to\nauthenticate [consult the Terraform\ndocs](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/getting_started#adding-credentials).\n\nCoder requires a Google Cloud Service Account to provision workspaces. To create\na service account:\n\n1. Navigate to the [CGP\n console](https://console.cloud.google.com/projectselector/iam-admin/serviceaccounts/create),\n and select your Cloud project (if you have more than one project associated\n with your account)\n\n1. Provide a service account name (this name is used to generate the service\n account ID)\n\n1. Click **Create and continue**, and choose the following IAM roles to grant to\n the service account:\n\n - Compute Admin\n - Service Account User\n\n Click **Continue**.\n\n1. Click on the created key, and navigate to the **Keys** tab.\n\n1. Click **Add key** \u003e **Create new key**.\n\n1. Generate a **JSON private key**, which will be what you provide to Coder\n during the setup process.\n\n## Architecture\n\nThis template provisions the following resources:\n\n- GCP VM (ephemeral)\n- GCP Disk (persistent, mounted to root)\n\nCoder persists the root volume. The full filesystem is preserved when the workspace restarts. See this [community example](https://github.com/bpmct/coder-templates/tree/main/aws-linux-ephemeral) of an ephemeral AWS instance.\n\n\u003e **Note**\n\u003e This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.\n\n## code-server\n\n`code-server` is installed via the `startup_script` argument in the `coder_agent`\nresource block. The `coder_app` resource is defined to access `code-server` through\nthe dashboard UI over `localhost:13337`.\n"
}, },
{ {
"id": "kubernetes", "id": "kubernetes",
"url": "", "url": "",
"name": "Kubernetes (Deployment)", "name": "Kubernetes (Deployment)",
"description": "Provision Kubernetes Deployments as Coder workspaces", "description": "Provision Kubernetes Deployments as Coder workspaces",
"icon": "/icon/k8s.png", "icon": "/icon/k8s.png",
"tags": [ "tags": [
"kubernetes", "kubernetes",
"container" "container"
], ],
"markdown": "\n# Remote Development on Kubernetes Pods\n\nProvision Kubernetes Pods as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.\n\n\u003c!-- TODO: Add screenshot --\u003e\n\n## Prerequisites\n\n### Infrastructure\n\n**Cluster**: This template requires an existing Kubernetes cluster\n\n**Container Image**: This template uses the [codercom/enterprise-base:ubuntu image](https://github.com/coder/enterprise-images/tree/main/images/base) with some dev tools preinstalled. To add additional tools, extend this image or build it yourself.\n\n### Authentication\n\nThis template authenticates using a `~/.kube/config`, if present on the server, or via built-in authentication if the Coder provisioner is running on Kubernetes with an authorized ServiceAccount. To use another [authentication method](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs#authentication), edit the template.\n\n## Architecture\n\nThis template provisions the following resources:\n\n- Kubernetes pod (ephemeral)\n- Kubernetes persistent volume claim (persistent on `/home/coder`)\n\nThis means, when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the container image. Alternatively, individual developers can [personalize](https://coder.com/docs/dotfiles) their workspaces with dotfiles.\n\n\u003e **Note**\n\u003e This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.\n" "markdown": "\n# Remote Development on Kubernetes Pods\n\nProvision Kubernetes Pods as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.\n\n\u003c!-- TODO: Add screenshot --\u003e\n\n## Prerequisites\n\n### Infrastructure\n\n**Cluster**: This template requires an existing Kubernetes cluster\n\n**Container Image**: This template uses the [codercom/enterprise-base:ubuntu image](https://github.com/coder/enterprise-images/tree/main/images/base) with some dev tools preinstalled. To add additional tools, extend this image or build it yourself.\n\n### Authentication\n\nThis template authenticates using a `~/.kube/config`, if present on the server, or via built-in authentication if the Coder provisioner is running on Kubernetes with an authorized ServiceAccount. To use another [authentication method](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs#authentication), edit the template.\n\n## Architecture\n\nThis template provisions the following resources:\n\n- Kubernetes pod (ephemeral)\n- Kubernetes persistent volume claim (persistent on `/home/coder`)\n\nThis means, when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the container image. Alternatively, individual developers can [personalize](https://coder.com/docs/dotfiles) their workspaces with dotfiles.\n\n\u003e **Note**\n\u003e This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.\n"
}, },
{ {
"id": "nomad-docker", "id": "nomad-docker",
"url": "", "url": "",
"name": "Nomad", "name": "Nomad",
"description": "Provision Nomad Jobs as Coder workspaces", "description": "Provision Nomad Jobs as Coder workspaces",
"icon": "/icon/nomad.svg", "icon": "/icon/nomad.svg",
"tags": [ "tags": [
"nomad", "nomad",
"container" "container"
], ],
"markdown": "\n# Remote Development on Nomad\n\nProvision Nomad Jobs as [Coder workspaces](https://coder.com/docs/workspaces) with this example template. This example shows how to use Nomad service tasks to be used as a development environment using docker and host csi volumes.\n\n\u003c!-- TODO: Add screenshot --\u003e\n\n\u003e **Note**\n\u003e This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.\n\n## Prerequisites\n\n- [Nomad](https://www.nomadproject.io/downloads)\n- [Docker](https://docs.docker.com/get-docker/)\n\n## Setup\n\n### 1. Start the CSI Host Volume Plugin\n\nThe CSI Host Volume plugin is used to mount host volumes into Nomad tasks. This is useful for development environments where you want to mount persistent volumes into your container workspace.\n\n1. Login to the Nomad server using SSH.\n\n2. Append the following stanza to your Nomad server configuration file and restart the nomad service.\n\n ```hcl\n plugin \"docker\" {\n config {\n allow_privileged = true\n }\n }\n ```\n\n ```shell\n sudo systemctl restart nomad\n ```\n\n3. Create a file `hostpath.nomad` with following content:\n\n ```hcl\n job \"hostpath-csi-plugin\" {\n datacenters = [\"dc1\"]\n type = \"system\"\n\n group \"csi\" {\n task \"plugin\" {\n driver = \"docker\"\n\n config {\n image = \"registry.k8s.io/sig-storage/hostpathplugin:v1.10.0\"\n\n args = [\n \"--drivername=csi-hostpath\",\n \"--v=5\",\n \"--endpoint=${CSI_ENDPOINT}\",\n \"--nodeid=node-${NOMAD_ALLOC_INDEX}\",\n ]\n\n privileged = true\n }\n\n csi_plugin {\n id = \"hostpath\"\n type = \"monolith\"\n mount_dir = \"/csi\"\n }\n\n resources {\n cpu = 256\n memory = 128\n }\n }\n }\n }\n ```\n\n4. Run the job:\n\n ```shell\n nomad job run hostpath.nomad\n ```\n\n### 2. Setup the Nomad Template\n\n1. Create the template by running the following command:\n\n ```shell\n coder template init nomad-docker\n cd nomad-docker\n coder template push\n ```\n\n2. Set up Nomad server address and optional authentication:\n\n3. Create a new workspace and start developing.\n" "markdown": "\n# Remote Development on Nomad\n\nProvision Nomad Jobs as [Coder workspaces](https://coder.com/docs/workspaces) with this example template. This example shows how to use Nomad service tasks to be used as a development environment using docker and host csi volumes.\n\n\u003c!-- TODO: Add screenshot --\u003e\n\n\u003e **Note**\n\u003e This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.\n\n## Prerequisites\n\n- [Nomad](https://www.nomadproject.io/downloads)\n- [Docker](https://docs.docker.com/get-docker/)\n\n## Setup\n\n### 1. Start the CSI Host Volume Plugin\n\nThe CSI Host Volume plugin is used to mount host volumes into Nomad tasks. This is useful for development environments where you want to mount persistent volumes into your container workspace.\n\n1. Login to the Nomad server using SSH.\n\n2. Append the following stanza to your Nomad server configuration file and restart the nomad service.\n\n ```hcl\n plugin \"docker\" {\n config {\n allow_privileged = true\n }\n }\n ```\n\n ```shell\n sudo systemctl restart nomad\n ```\n\n3. Create a file `hostpath.nomad` with following content:\n\n ```hcl\n job \"hostpath-csi-plugin\" {\n datacenters = [\"dc1\"]\n type = \"system\"\n\n group \"csi\" {\n task \"plugin\" {\n driver = \"docker\"\n\n config {\n image = \"registry.k8s.io/sig-storage/hostpathplugin:v1.10.0\"\n\n args = [\n \"--drivername=csi-hostpath\",\n \"--v=5\",\n \"--endpoint=${CSI_ENDPOINT}\",\n \"--nodeid=node-${NOMAD_ALLOC_INDEX}\",\n ]\n\n privileged = true\n }\n\n csi_plugin {\n id = \"hostpath\"\n type = \"monolith\"\n mount_dir = \"/csi\"\n }\n\n resources {\n cpu = 256\n memory = 128\n }\n }\n }\n }\n ```\n\n4. Run the job:\n\n ```shell\n nomad job run hostpath.nomad\n ```\n\n### 2. Setup the Nomad Template\n\n1. Create the template by running the following command:\n\n ```shell\n coder template init nomad-docker\n cd nomad-docker\n coder template push\n ```\n\n2. Set up Nomad server address and optional authentication:\n\n3. Create a new workspace and start developing.\n"
}, },
{ {
"id": "scratch", "id": "scratch",
"url": "", "url": "",
"name": "Scratch", "name": "Scratch",
"description": "A minimal starter template for Coder", "description": "A minimal starter template for Coder",
"icon": "/emojis/1f4e6.png", "icon": "/emojis/1f4e6.png",
"tags": [], "tags": [],
"markdown": "\n# A minimal Scaffolding for a Coder Template\n\nUse this starter template as a basis to create your own unique template from scratch.\n" "markdown": "\n# A minimal Scaffolding for a Coder Template\n\nUse this starter template as a basis to create your own unique template from scratch.\n"
} }
] ]
File diff suppressed because it is too large Load Diff
+44 -44
View File
@@ -31,50 +31,50 @@ instances provisioned by Coder:
```json ```json
{ {
"Version": "2012-10-17", "Version": "2012-10-17",
"Statement": [ "Statement": [
{ {
"Sid": "VisualEditor0", "Sid": "VisualEditor0",
"Effect": "Allow", "Effect": "Allow",
"Action": [ "Action": [
"ec2:GetDefaultCreditSpecification", "ec2:GetDefaultCreditSpecification",
"ec2:DescribeIamInstanceProfileAssociations", "ec2:DescribeIamInstanceProfileAssociations",
"ec2:DescribeTags", "ec2:DescribeTags",
"ec2:DescribeInstances", "ec2:DescribeInstances",
"ec2:DescribeInstanceTypes", "ec2:DescribeInstanceTypes",
"ec2:CreateTags", "ec2:CreateTags",
"ec2:RunInstances", "ec2:RunInstances",
"ec2:DescribeInstanceCreditSpecifications", "ec2:DescribeInstanceCreditSpecifications",
"ec2:DescribeImages", "ec2:DescribeImages",
"ec2:ModifyDefaultCreditSpecification", "ec2:ModifyDefaultCreditSpecification",
"ec2:DescribeVolumes" "ec2:DescribeVolumes"
], ],
"Resource": "*" "Resource": "*"
}, },
{ {
"Sid": "CoderResources", "Sid": "CoderResources",
"Effect": "Allow", "Effect": "Allow",
"Action": [ "Action": [
"ec2:DescribeInstanceAttribute", "ec2:DescribeInstanceAttribute",
"ec2:UnmonitorInstances", "ec2:UnmonitorInstances",
"ec2:TerminateInstances", "ec2:TerminateInstances",
"ec2:StartInstances", "ec2:StartInstances",
"ec2:StopInstances", "ec2:StopInstances",
"ec2:DeleteTags", "ec2:DeleteTags",
"ec2:MonitorInstances", "ec2:MonitorInstances",
"ec2:CreateTags", "ec2:CreateTags",
"ec2:RunInstances", "ec2:RunInstances",
"ec2:ModifyInstanceAttribute", "ec2:ModifyInstanceAttribute",
"ec2:ModifyInstanceCreditSpecification" "ec2:ModifyInstanceCreditSpecification"
], ],
"Resource": "arn:aws:ec2:*:*:instance/*", "Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": { "Condition": {
"StringEquals": { "StringEquals": {
"aws:ResourceTag/Coder_Provisioned": "true" "aws:ResourceTag/Coder_Provisioned": "true"
} }
} }
} }
] ]
} }
``` ```
+44 -44
View File
@@ -30,50 +30,50 @@ instances provisioned by Coder:
```json ```json
{ {
"Version": "2012-10-17", "Version": "2012-10-17",
"Statement": [ "Statement": [
{ {
"Sid": "VisualEditor0", "Sid": "VisualEditor0",
"Effect": "Allow", "Effect": "Allow",
"Action": [ "Action": [
"ec2:GetDefaultCreditSpecification", "ec2:GetDefaultCreditSpecification",
"ec2:DescribeIamInstanceProfileAssociations", "ec2:DescribeIamInstanceProfileAssociations",
"ec2:DescribeTags", "ec2:DescribeTags",
"ec2:DescribeInstances", "ec2:DescribeInstances",
"ec2:DescribeInstanceTypes", "ec2:DescribeInstanceTypes",
"ec2:CreateTags", "ec2:CreateTags",
"ec2:RunInstances", "ec2:RunInstances",
"ec2:DescribeInstanceCreditSpecifications", "ec2:DescribeInstanceCreditSpecifications",
"ec2:DescribeImages", "ec2:DescribeImages",
"ec2:ModifyDefaultCreditSpecification", "ec2:ModifyDefaultCreditSpecification",
"ec2:DescribeVolumes" "ec2:DescribeVolumes"
], ],
"Resource": "*" "Resource": "*"
}, },
{ {
"Sid": "CoderResources", "Sid": "CoderResources",
"Effect": "Allow", "Effect": "Allow",
"Action": [ "Action": [
"ec2:DescribeInstanceAttribute", "ec2:DescribeInstanceAttribute",
"ec2:UnmonitorInstances", "ec2:UnmonitorInstances",
"ec2:TerminateInstances", "ec2:TerminateInstances",
"ec2:StartInstances", "ec2:StartInstances",
"ec2:StopInstances", "ec2:StopInstances",
"ec2:DeleteTags", "ec2:DeleteTags",
"ec2:MonitorInstances", "ec2:MonitorInstances",
"ec2:CreateTags", "ec2:CreateTags",
"ec2:RunInstances", "ec2:RunInstances",
"ec2:ModifyInstanceAttribute", "ec2:ModifyInstanceAttribute",
"ec2:ModifyInstanceCreditSpecification" "ec2:ModifyInstanceCreditSpecification"
], ],
"Resource": "arn:aws:ec2:*:*:instance/*", "Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": { "Condition": {
"StringEquals": { "StringEquals": {
"aws:ResourceTag/Coder_Provisioned": "true" "aws:ResourceTag/Coder_Provisioned": "true"
} }
} }
} }
] ]
} }
``` ```
+44 -44
View File
@@ -30,50 +30,50 @@ instances provisioned by Coder:
```json ```json
{ {
"Version": "2012-10-17", "Version": "2012-10-17",
"Statement": [ "Statement": [
{ {
"Sid": "VisualEditor0", "Sid": "VisualEditor0",
"Effect": "Allow", "Effect": "Allow",
"Action": [ "Action": [
"ec2:GetDefaultCreditSpecification", "ec2:GetDefaultCreditSpecification",
"ec2:DescribeIamInstanceProfileAssociations", "ec2:DescribeIamInstanceProfileAssociations",
"ec2:DescribeTags", "ec2:DescribeTags",
"ec2:DescribeInstances", "ec2:DescribeInstances",
"ec2:DescribeInstanceTypes", "ec2:DescribeInstanceTypes",
"ec2:CreateTags", "ec2:CreateTags",
"ec2:RunInstances", "ec2:RunInstances",
"ec2:DescribeInstanceCreditSpecifications", "ec2:DescribeInstanceCreditSpecifications",
"ec2:DescribeImages", "ec2:DescribeImages",
"ec2:ModifyDefaultCreditSpecification", "ec2:ModifyDefaultCreditSpecification",
"ec2:DescribeVolumes" "ec2:DescribeVolumes"
], ],
"Resource": "*" "Resource": "*"
}, },
{ {
"Sid": "CoderResources", "Sid": "CoderResources",
"Effect": "Allow", "Effect": "Allow",
"Action": [ "Action": [
"ec2:DescribeInstanceAttribute", "ec2:DescribeInstanceAttribute",
"ec2:UnmonitorInstances", "ec2:UnmonitorInstances",
"ec2:TerminateInstances", "ec2:TerminateInstances",
"ec2:StartInstances", "ec2:StartInstances",
"ec2:StopInstances", "ec2:StopInstances",
"ec2:DeleteTags", "ec2:DeleteTags",
"ec2:MonitorInstances", "ec2:MonitorInstances",
"ec2:CreateTags", "ec2:CreateTags",
"ec2:RunInstances", "ec2:RunInstances",
"ec2:ModifyInstanceAttribute", "ec2:ModifyInstanceAttribute",
"ec2:ModifyInstanceCreditSpecification" "ec2:ModifyInstanceCreditSpecification"
], ],
"Resource": "arn:aws:ec2:*:*:instance/*", "Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": { "Condition": {
"StringEquals": { "StringEquals": {
"aws:ResourceTag/Coder_Provisioned": "true" "aws:ResourceTag/Coder_Provisioned": "true"
} }
} }
} }
] ]
} }
``` ```
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"extends": "next/core-web-vitals" "extends": "next/core-web-vitals"
} }
+3 -3
View File
@@ -1,8 +1,8 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
output: "export", output: "export",
reactStrictMode: true, reactStrictMode: true,
trailingSlash: true, trailingSlash: true,
}; };
module.exports = nextConfig; module.exports = nextConfig;
+43 -43
View File
@@ -1,45 +1,45 @@
{ {
"name": "coder-docs-generator", "name": "coder-docs-generator",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "pnpm copy-images && next dev", "dev": "pnpm copy-images && next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"export": "pnpm copy-images && next build", "export": "pnpm copy-images && next build",
"copy-images": "sh ./scripts/copyImages.sh", "copy-images": "sh ./scripts/copyImages.sh",
"lint": "pnpm run lint:types", "lint": "pnpm run lint:types",
"lint:types": "tsc --noEmit", "lint:types": "tsc --noEmit",
"format": "prettier --cache --write './**/*.{css,html,js,json,jsx,md,ts,tsx,yaml,yml}'", "format": "prettier --cache --write './**/*.{css,html,js,json,jsx,md,ts,tsx,yaml,yml}'",
"format:check": "prettier --cache --check './**/*.{css,html,js,json,jsx,md,ts,tsx,yaml,yml}'" "format:check": "prettier --cache --check './**/*.{css,html,js,json,jsx,md,ts,tsx,yaml,yml}'"
}, },
"dependencies": { "dependencies": {
"@chakra-ui/react": "2.8.2", "@chakra-ui/react": "2.8.2",
"@emotion/react": "11.11.4", "@emotion/react": "11.11.4",
"@emotion/styled": "11.11.5", "@emotion/styled": "11.11.5",
"archiver": "6.0.2", "archiver": "6.0.2",
"framer-motion": "^10.17.6", "framer-motion": "^10.17.6",
"front-matter": "4.0.2", "front-matter": "4.0.2",
"lodash": "4.17.21", "lodash": "4.17.21",
"next": "14.2.4", "next": "14.2.4",
"react": "18.3.1", "react": "18.3.1",
"react-dom": "18.3.1", "react-dom": "18.3.1",
"react-icons": "4.12.0", "react-icons": "4.12.0",
"react-markdown": "9.0.1", "react-markdown": "9.0.1",
"rehype-raw": "7.0.0", "rehype-raw": "7.0.0",
"remark-gfm": "4.0.0" "remark-gfm": "4.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/lodash": "4.14.196", "@types/lodash": "4.14.196",
"@types/node": "18.19.0", "@types/node": "18.19.0",
"@types/react": "18.3.3", "@types/react": "18.3.3",
"@types/react-dom": "18.3.0", "@types/react-dom": "18.3.0",
"eslint": "8.56.0", "eslint": "8.56.0",
"eslint-config-next": "14.0.1", "eslint-config-next": "14.0.1",
"prettier": "3.3.3", "prettier": "3.3.3",
"typescript": "5.3.2" "typescript": "5.3.2"
}, },
"engines": { "engines": {
"npm": ">=9.0.0 <10.0.0", "npm": ">=9.0.0 <10.0.0",
"node": ">=18.0.0 <21.0.0" "node": ">=18.0.0 <21.0.0"
} }
} }
+419 -419
View File
@@ -1,29 +1,29 @@
import { import {
Box, Box,
Button, Button,
Code, Code,
Drawer, Drawer,
DrawerBody, DrawerBody,
DrawerCloseButton, DrawerCloseButton,
DrawerContent, DrawerContent,
DrawerOverlay, DrawerOverlay,
Flex, Flex,
Grid, Grid,
GridProps, GridProps,
Heading, Heading,
Icon, Icon,
Img, Img,
Link, Link,
OrderedList, OrderedList,
Table, Table,
TableContainer, TableContainer,
Td, Td,
Text, Text,
Th, Th,
Thead, Thead,
Tr, Tr,
UnorderedList, UnorderedList,
useDisclosure, useDisclosure,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import fm from "front-matter"; import fm from "front-matter";
import { readFileSync } from "fs"; import { readFileSync } from "fs";
@@ -42,19 +42,19 @@ import remarkGfm from "remark-gfm";
type FilePath = string; type FilePath = string;
type UrlPath = string; type UrlPath = string;
type Route = { type Route = {
path: FilePath; path: FilePath;
title: string; title: string;
description?: string; description?: string;
children?: Route[]; children?: Route[];
}; };
type Manifest = { versions: string[]; routes: Route[] }; type Manifest = { versions: string[]; routes: Route[] };
type NavItem = { title: string; path: UrlPath; children?: NavItem[] }; type NavItem = { title: string; path: UrlPath; children?: NavItem[] };
type Nav = NavItem[]; type Nav = NavItem[];
const readContentFile = (filePath: string) => { const readContentFile = (filePath: string) => {
const baseDir = process.cwd(); const baseDir = process.cwd();
const docsPath = path.join(baseDir, "..", "docs"); const docsPath = path.join(baseDir, "..", "docs");
return readFileSync(path.join(docsPath, filePath), { encoding: "utf-8" }); return readFileSync(path.join(docsPath, filePath), { encoding: "utf-8" });
}; };
const removeTrailingSlash = (path: string) => path.replace(/\/+$/, ""); const removeTrailingSlash = (path: string) => path.replace(/\/+$/, "");
@@ -62,19 +62,19 @@ const removeTrailingSlash = (path: string) => path.replace(/\/+$/, "");
const removeMkdExtension = (path: string) => path.replace(/\.md/g, ""); const removeMkdExtension = (path: string) => path.replace(/\.md/g, "");
const removeIndexFilename = (path: string) => { const removeIndexFilename = (path: string) => {
if (path.endsWith("index")) { if (path.endsWith("index")) {
path = path.replace("index", ""); path = path.replace("index", "");
} }
return path; return path;
}; };
const removeREADMEName = (path: string) => { const removeREADMEName = (path: string) => {
if (path.startsWith("README")) { if (path.startsWith("README")) {
path = path.replace("README", ""); path = path.replace("README", "");
} }
return path; return path;
}; };
// transformLinkUri converts the links in the markdown file to // transformLinkUri converts the links in the markdown file to
@@ -87,466 +87,466 @@ const removeREADMEName = (path: string) => {
// file.md -> ./subdir/file = ../subdir/file // file.md -> ./subdir/file = ../subdir/file
// file.md -> ../file-next-to-file = ../file-next-to-file // file.md -> ../file-next-to-file = ../file-next-to-file
const transformLinkUriSource = (sourceFile: string) => { const transformLinkUriSource = (sourceFile: string) => {
return (href = "") => { return (href = "") => {
const isExternal = href.startsWith("http") || href.startsWith("https"); const isExternal = href.startsWith("http") || href.startsWith("https");
if (!isExternal) { if (!isExternal) {
// Remove .md form the path // Remove .md form the path
href = removeMkdExtension(href); href = removeMkdExtension(href);
// Add the extra '..' if not an index file. // Add the extra '..' if not an index file.
sourceFile = removeMkdExtension(sourceFile); sourceFile = removeMkdExtension(sourceFile);
if (!sourceFile.endsWith("index")) { if (!sourceFile.endsWith("index")) {
href = "../" + href; href = "../" + href;
} }
// Remove the index path // Remove the index path
href = removeIndexFilename(href); href = removeIndexFilename(href);
href = removeREADMEName(href); href = removeREADMEName(href);
} }
return href; return href;
}; };
}; };
const transformFilePathToUrlPath = (filePath: string) => { const transformFilePathToUrlPath = (filePath: string) => {
// Remove markdown extension // Remove markdown extension
let urlPath = removeMkdExtension(filePath); let urlPath = removeMkdExtension(filePath);
// Remove relative path // Remove relative path
if (urlPath.startsWith("./")) { if (urlPath.startsWith("./")) {
urlPath = urlPath.replace("./", ""); urlPath = urlPath.replace("./", "");
} }
// Remove index from the root file // Remove index from the root file
urlPath = removeIndexFilename(urlPath); urlPath = removeIndexFilename(urlPath);
urlPath = removeREADMEName(urlPath); urlPath = removeREADMEName(urlPath);
// Remove trailing slash // Remove trailing slash
if (urlPath.endsWith("/")) { if (urlPath.endsWith("/")) {
urlPath = removeTrailingSlash(urlPath); urlPath = removeTrailingSlash(urlPath);
} }
return urlPath; return urlPath;
}; };
const mapRoutes = (manifest: Manifest): Record<UrlPath, Route> => { const mapRoutes = (manifest: Manifest): Record<UrlPath, Route> => {
const paths: Record<UrlPath, Route> = {}; const paths: Record<UrlPath, Route> = {};
const addPaths = (routes: Route[]) => { const addPaths = (routes: Route[]) => {
for (const route of routes) { for (const route of routes) {
paths[transformFilePathToUrlPath(route.path)] = route; paths[transformFilePathToUrlPath(route.path)] = route;
if (route.children) { if (route.children) {
addPaths(route.children); addPaths(route.children);
} }
} }
}; };
addPaths(manifest.routes); addPaths(manifest.routes);
return paths; return paths;
}; };
let manifest: Manifest | undefined; let manifest: Manifest | undefined;
const getManifest = () => { const getManifest = () => {
if (manifest) { if (manifest) {
return manifest; return manifest;
} }
const manifestContent = readContentFile("manifest.json"); const manifestContent = readContentFile("manifest.json");
manifest = JSON.parse(manifestContent) as Manifest; manifest = JSON.parse(manifestContent) as Manifest;
return manifest; return manifest;
}; };
let navigation: Nav | undefined; let navigation: Nav | undefined;
const getNavigation = (manifest: Manifest): Nav => { const getNavigation = (manifest: Manifest): Nav => {
if (navigation) { if (navigation) {
return navigation; return navigation;
} }
const getNavItem = (route: Route, parentPath?: UrlPath): NavItem => { const getNavItem = (route: Route, parentPath?: UrlPath): NavItem => {
const path = parentPath const path = parentPath
? `${parentPath}/${transformFilePathToUrlPath(route.path)}` ? `${parentPath}/${transformFilePathToUrlPath(route.path)}`
: transformFilePathToUrlPath(route.path); : transformFilePathToUrlPath(route.path);
const navItem: NavItem = { const navItem: NavItem = {
title: route.title, title: route.title,
path, path,
}; };
if (route.children) { if (route.children) {
navItem.children = []; navItem.children = [];
for (const childRoute of route.children) { for (const childRoute of route.children) {
navItem.children.push(getNavItem(childRoute)); navItem.children.push(getNavItem(childRoute));
} }
} }
return navItem; return navItem;
}; };
navigation = []; navigation = [];
for (const route of manifest.routes) { for (const route of manifest.routes) {
navigation.push(getNavItem(route)); navigation.push(getNavItem(route));
} }
return navigation; return navigation;
}; };
const removeHtmlComments = (string: string) => { const removeHtmlComments = (string: string) => {
return string.replace(/<!--[\s\S]*?-->/g, ""); return string.replace(/<!--[\s\S]*?-->/g, "");
}; };
export const getStaticPaths: GetStaticPaths = () => { export const getStaticPaths: GetStaticPaths = () => {
const manifest = getManifest(); const manifest = getManifest();
const routes = mapRoutes(manifest); const routes = mapRoutes(manifest);
const paths = Object.keys(routes).map((urlPath) => ({ const paths = Object.keys(routes).map((urlPath) => ({
params: { slug: urlPath.split("/") }, params: { slug: urlPath.split("/") },
})); }));
return { return {
paths, paths,
fallback: false, fallback: false,
}; };
}; };
export const getStaticProps: GetStaticProps = (context) => { export const getStaticProps: GetStaticProps = (context) => {
// When it is home page, the slug is undefined because there is no url path // When it is home page, the slug is undefined because there is no url path
// so we make it an empty string to work good with the mapRoutes // so we make it an empty string to work good with the mapRoutes
const { slug = [""] } = context.params as { slug: string[] }; const { slug = [""] } = context.params as { slug: string[] };
const manifest = getManifest(); const manifest = getManifest();
const routes = mapRoutes(manifest); const routes = mapRoutes(manifest);
const urlPath = slug.join("/"); const urlPath = slug.join("/");
const route = routes[urlPath]; const route = routes[urlPath];
const { body } = fm(readContentFile(route.path)); const { body } = fm(readContentFile(route.path));
// Serialize MDX to support custom components // Serialize MDX to support custom components
const content = removeHtmlComments(body); const content = removeHtmlComments(body);
const navigation = getNavigation(manifest); const navigation = getNavigation(manifest);
const version = manifest.versions[0]; const version = manifest.versions[0];
return { return {
props: { props: {
content, content,
navigation, navigation,
route, route,
version, version,
}, },
}; };
}; };
const SidebarNavItem: React.FC<{ item: NavItem; nav: Nav }> = ({ const SidebarNavItem: React.FC<{ item: NavItem; nav: Nav }> = ({
item, item,
nav, nav,
}) => { }) => {
const router = useRouter(); const router = useRouter();
let isActive = router.asPath.startsWith(`/${item.path}`); let isActive = router.asPath.startsWith(`/${item.path}`);
// Special case to handle the home path // Special case to handle the home path
if (item.path === "") { if (item.path === "") {
isActive = router.asPath === "/"; isActive = router.asPath === "/";
// Special case to handle the home path children // Special case to handle the home path children
const homeNav = nav.find((navItem) => navItem.path === "") as NavItem; const homeNav = nav.find((navItem) => navItem.path === "") as NavItem;
const homeNavPaths = const homeNavPaths =
homeNav.children?.map((item) => `/${item.path}/`) ?? []; homeNav.children?.map((item) => `/${item.path}/`) ?? [];
if (homeNavPaths.includes(router.asPath)) { if (homeNavPaths.includes(router.asPath)) {
isActive = true; isActive = true;
} }
} }
return ( return (
<Box> <Box>
<NextLink href={"/" + item.path} passHref legacyBehavior> <NextLink href={"/" + item.path} passHref legacyBehavior>
<Link <Link
fontWeight={isActive ? 600 : 400} fontWeight={isActive ? 600 : 400}
color={isActive ? "gray.900" : "gray.700"} color={isActive ? "gray.900" : "gray.700"}
> >
{item.title} {item.title}
</Link> </Link>
</NextLink> </NextLink>
{isActive && item.children && ( {isActive && item.children && (
<Grid <Grid
as="nav" as="nav"
pt={2} pt={2}
pl={3} pl={3}
maxW="sm" maxW="sm"
autoFlow="row" autoFlow="row"
gap={2} gap={2}
autoRows="min-content" autoRows="min-content"
> >
{item.children.map((subItem) => ( {item.children.map((subItem) => (
<SidebarNavItem key={subItem.path} item={subItem} nav={nav} /> <SidebarNavItem key={subItem.path} item={subItem} nav={nav} />
))} ))}
</Grid> </Grid>
)} )}
</Box> </Box>
); );
}; };
const SidebarNav: React.FC<{ nav: Nav; version: string } & GridProps> = ({ const SidebarNav: React.FC<{ nav: Nav; version: string } & GridProps> = ({
nav, nav,
version, version,
...gridProps ...gridProps
}) => { }) => {
return ( return (
<Grid <Grid
h="100vh" h="100vh"
overflowY="scroll" overflowY="scroll"
as="nav" as="nav"
p={8} p={8}
w="300px" w="300px"
autoFlow="row" autoFlow="row"
gap={2} gap={2}
autoRows="min-content" autoRows="min-content"
bgColor="white" bgColor="white"
borderRightWidth={1} borderRightWidth={1}
borderColor="gray.200" borderColor="gray.200"
borderStyle="solid" borderStyle="solid"
{...gridProps} {...gridProps}
> >
<Box mb={6}> <Box mb={6}>
<Img src="/logo.svg" alt="Coder logo" /> <Img src="/logo.svg" alt="Coder logo" />
</Box> </Box>
{nav.map((navItem) => ( {nav.map((navItem) => (
<SidebarNavItem key={navItem.path} item={navItem} nav={nav} /> <SidebarNavItem key={navItem.path} item={navItem} nav={nav} />
))} ))}
</Grid> </Grid>
); );
}; };
const MobileNavbar: React.FC<{ nav: Nav; version: string }> = ({ const MobileNavbar: React.FC<{ nav: Nav; version: string }> = ({
nav, nav,
version, version,
}) => { }) => {
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
return ( return (
<> <>
<Flex <Flex
bgColor="white" bgColor="white"
px={6} px={6}
alignItems="center" alignItems="center"
h={16} h={16}
borderBottomWidth={1} borderBottomWidth={1}
> >
<Img src="/logo.svg" alt="Coder logo" w={28} /> <Img src="/logo.svg" alt="Coder logo" w={28} />
<Button variant="ghost" ml="auto" onClick={onOpen}> <Button variant="ghost" ml="auto" onClick={onOpen}>
<Icon as={MdMenu} fontSize="2xl" /> <Icon as={MdMenu} fontSize="2xl" />
</Button> </Button>
</Flex> </Flex>
<Drawer onClose={onClose} isOpen={isOpen}> <Drawer onClose={onClose} isOpen={isOpen}>
<DrawerOverlay /> <DrawerOverlay />
<DrawerContent> <DrawerContent>
<DrawerCloseButton /> <DrawerCloseButton />
<DrawerBody p={0}> <DrawerBody p={0}>
<SidebarNav nav={nav} version={version} border={0} /> <SidebarNav nav={nav} version={version} border={0} />
</DrawerBody> </DrawerBody>
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
</> </>
); );
}; };
const slugifyTitle = (titleSource: ReactNode) => { const slugifyTitle = (titleSource: ReactNode) => {
if (Array.isArray(titleSource) && typeof titleSource[0] === "string") { if (Array.isArray(titleSource) && typeof titleSource[0] === "string") {
return _.kebabCase(titleSource[0].toLowerCase()); return _.kebabCase(titleSource[0].toLowerCase());
} }
return undefined; return undefined;
}; };
const getImageUrl = (src: string | undefined) => { const getImageUrl = (src: string | undefined) => {
if (src === undefined) { if (src === undefined) {
return ""; return "";
} }
const assetPath = src.split("images/")[1]; const assetPath = src.split("images/")[1];
return `/images/${assetPath}`; return `/images/${assetPath}`;
}; };
const DocsPage: NextPage<{ const DocsPage: NextPage<{
content: string; content: string;
navigation: Nav; navigation: Nav;
route: Route; route: Route;
version: string; version: string;
}> = ({ content, navigation, route, version }) => { }> = ({ content, navigation, route, version }) => {
return ( return (
<> <>
<Head> <Head>
<title>{route.title}</title> <title>{route.title}</title>
<meta name="source" content={route.path} /> <meta name="source" content={route.path} />
</Head> </Head>
<Box <Box
display={{ md: "grid" }} display={{ md: "grid" }}
gridTemplateColumns="max-content 1fr" gridTemplateColumns="max-content 1fr"
fontSize="md" fontSize="md"
color="gray.700" color="gray.700"
> >
<Box display={{ base: "none", md: "block" }}> <Box display={{ base: "none", md: "block" }}>
<SidebarNav nav={navigation} version={version} /> <SidebarNav nav={navigation} version={version} />
</Box> </Box>
<Box display={{ base: "block", md: "none" }}> <Box display={{ base: "block", md: "none" }}>
<MobileNavbar nav={navigation} version={version} /> <MobileNavbar nav={navigation} version={version} />
</Box> </Box>
<Box <Box
as="main" as="main"
w="full" w="full"
pb={20} pb={20}
px={{ base: 6, md: 10 }} px={{ base: 6, md: 10 }}
pl={{ base: 6, md: 20 }} pl={{ base: 6, md: 20 }}
h="100vh" h="100vh"
overflowY="auto" overflowY="auto"
> >
<Box maxW="872"> <Box maxW="872">
<Box lineHeight="tall"> <Box lineHeight="tall">
{/* Some docs don't have the title */} {/* Some docs don't have the title */}
<Heading <Heading
as="h1" as="h1"
fontSize="4xl" fontSize="4xl"
pt={10} pt={10}
pb={2} pb={2}
// Hide this title if the doc has the title already // Hide this title if the doc has the title already
sx={{ "& + h1": { display: "none" } }} sx={{ "& + h1": { display: "none" } }}
> >
{route.title} {route.title}
</Heading> </Heading>
<ReactMarkdown <ReactMarkdown
rehypePlugins={[rehypeRaw]} rehypePlugins={[rehypeRaw]}
remarkPlugins={[remarkGfm]} remarkPlugins={[remarkGfm]}
urlTransform={transformLinkUriSource(route.path)} urlTransform={transformLinkUriSource(route.path)}
components={{ components={{
h1: ({ children }) => ( h1: ({ children }) => (
<Heading <Heading
as="h1" as="h1"
fontSize="4xl" fontSize="4xl"
pt={10} pt={10}
pb={2} pb={2}
id={slugifyTitle(children)} id={slugifyTitle(children)}
> >
{children} {children}
</Heading> </Heading>
), ),
h2: ({ children }) => ( h2: ({ children }) => (
<Heading <Heading
as="h2" as="h2"
fontSize="3xl" fontSize="3xl"
pt={10} pt={10}
pb={2} pb={2}
id={slugifyTitle(children)} id={slugifyTitle(children)}
> >
{children} {children}
</Heading> </Heading>
), ),
h3: ({ children }) => ( h3: ({ children }) => (
<Heading <Heading
as="h3" as="h3"
fontSize="2xl" fontSize="2xl"
pt={10} pt={10}
pb={2} pb={2}
id={slugifyTitle(children)} id={slugifyTitle(children)}
> >
{children} {children}
</Heading> </Heading>
), ),
img: ({ src }) => ( img: ({ src }) => (
<Img <Img
src={getImageUrl(src)} src={getImageUrl(src)}
mb={2} mb={2}
borderWidth={1} borderWidth={1}
borderColor="gray.200" borderColor="gray.200"
borderStyle="solid" borderStyle="solid"
rounded="md" rounded="md"
height="auto" height="auto"
/> />
), ),
p: ({ children }) => ( p: ({ children }) => (
<Text pt={2} pb={2}> <Text pt={2} pb={2}>
{children} {children}
</Text> </Text>
), ),
ul: ({ children }) => ( ul: ({ children }) => (
<UnorderedList <UnorderedList
mb={4} mb={4}
display="grid" display="grid"
gridAutoFlow="row" gridAutoFlow="row"
gap={2} gap={2}
> >
{children} {children}
</UnorderedList> </UnorderedList>
), ),
ol: ({ children }) => ( ol: ({ children }) => (
<OrderedList <OrderedList
mb={4} mb={4}
display="grid" display="grid"
gridAutoFlow="row" gridAutoFlow="row"
gap={2} gap={2}
> >
{children} {children}
</OrderedList> </OrderedList>
), ),
a: ({ children, href = "" }) => { a: ({ children, href = "" }) => {
const isExternal = const isExternal =
href.startsWith("http") || href.startsWith("https"); href.startsWith("http") || href.startsWith("https");
return ( return (
<Link <Link
href={href} href={href}
target={isExternal ? "_blank" : undefined} target={isExternal ? "_blank" : undefined}
fontWeight={500} fontWeight={500}
color="blue.600" color="blue.600"
> >
{children} {children}
</Link> </Link>
); );
}, },
code: ({ node, ...props }) => ( code: ({ node, ...props }) => (
<Code {...props} bgColor="gray.100" /> <Code {...props} bgColor="gray.100" />
), ),
pre: ({ children }) => ( pre: ({ children }) => (
<Box <Box
as="pre" as="pre"
w="full" w="full"
sx={{ "& > code": { w: "full", p: 4, rounded: "md" } }} sx={{ "& > code": { w: "full", p: 4, rounded: "md" } }}
mb={2} mb={2}
> >
{children} {children}
</Box> </Box>
), ),
table: ({ children }) => ( table: ({ children }) => (
<TableContainer <TableContainer
mt={1} mt={1}
mb={2} mb={2}
bgColor="white" bgColor="white"
rounded="md" rounded="md"
borderWidth={1} borderWidth={1}
borderColor="gray.100" borderColor="gray.100"
borderStyle="solid" borderStyle="solid"
> >
<Table variant="simple">{children}</Table> <Table variant="simple">{children}</Table>
</TableContainer> </TableContainer>
), ),
thead: ({ children }) => <Thead>{children}</Thead>, thead: ({ children }) => <Thead>{children}</Thead>,
th: ({ children }) => <Th>{children}</Th>, th: ({ children }) => <Th>{children}</Th>,
td: ({ children }) => <Td>{children}</Td>, td: ({ children }) => <Td>{children}</Td>,
tr: ({ children }) => <Tr>{children}</Tr>, tr: ({ children }) => <Tr>{children}</Tr>,
}} }}
> >
{content} {content}
</ReactMarkdown> </ReactMarkdown>
</Box> </Box>
</Box> </Box>
</Box> </Box>
</Box> </Box>
</> </>
); );
}; };
export default DocsPage; export default DocsPage;
+18 -18
View File
@@ -3,27 +3,27 @@ import type { AppProps } from "next/app";
import Head from "next/head"; import Head from "next/head";
const theme = extendTheme({ const theme = extendTheme({
styles: { styles: {
global: { global: {
body: { body: {
bg: "gray.50", bg: "gray.50",
}, },
}, },
}, },
}); });
const MyApp: React.FC<AppProps> = ({ Component, pageProps }) => { const MyApp: React.FC<AppProps> = ({ Component, pageProps }) => {
return ( return (
<> <>
<Head> <Head>
<link rel="mask-icon" href="/favicon.svg" color="#000000" /> <link rel="mask-icon" href="/favicon.svg" color="#000000" />
<link rel="alternate icon" type="image/png" href="/favicon.png" /> <link rel="alternate icon" type="image/png" href="/favicon.png" />
</Head> </Head>
<ChakraProvider theme={theme}> <ChakraProvider theme={theme}>
<Component {...pageProps} /> <Component {...pageProps} />
</ChakraProvider> </ChakraProvider>
</> </>
); );
}; };
export default MyApp; export default MyApp;
+18 -18
View File
@@ -1,20 +1,20 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es5",
"lib": ["dom", "dom.iterable", "esnext"], "lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"noEmit": true, "noEmit": true,
"esModuleInterop": true, "esModuleInterop": true,
"module": "esnext", "module": "esnext",
"moduleResolution": "node", "moduleResolution": "node",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "jsx": "preserve",
"incremental": true "incremental": true
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules", "docs"] "exclude": ["node_modules", "docs"]
} }
+12 -12
View File
@@ -1,14 +1,14 @@
{ {
"_comment": "This version doesn't matter, it's just to allow importing from other repos.", "_comment": "This version doesn't matter, it's just to allow importing from other repos.",
"name": "coder", "name": "coder",
"version": "0.0.0", "version": "0.0.0",
"packageManager": "pnpm@9.7.1+sha512.faf344af2d6ca65c4c5c8c2224ea77a81a5e8859cbc4e06b1511ddce2f0151512431dd19e6aff31f2c6a8f5f2aced9bd2273e1fed7dd4de1868984059d2c4247", "packageManager": "pnpm@9.7.1+sha512.faf344af2d6ca65c4c5c8c2224ea77a81a5e8859cbc4e06b1511ddce2f0151512431dd19e6aff31f2c6a8f5f2aced9bd2273e1fed7dd4de1868984059d2c4247",
"scripts": { "scripts": {
"format": "prettier --cache --write '**/*.{css,html,json,md,yaml,yml}'", "format": "prettier --cache --write '**/*.{css,html,json,md,yaml,yml}'",
"format:check": "prettier --cache --check '**/*.{css,html,json,md,yaml,yml}'", "format:check": "prettier --cache --check '**/*.{css,html,json,md,yaml,yml}'",
"storybook": "pnpm run -C site/ storybook" "storybook": "pnpm run -C site/ storybook"
}, },
"devDependencies": { "devDependencies": {
"prettier": "3.3.3" "prettier": "3.3.3"
} }
} }
File diff suppressed because it is too large Load Diff
+7 -7
View File
@@ -1,9 +1,9 @@
{ {
"dependencies": { "dependencies": {
"widdershins": "^4.0.1" "widdershins": "^4.0.1"
}, },
"resolutions": { "resolutions": {
"semver": "7.5.3", "semver": "7.5.3",
"jsonpointer": "5.0.1" "jsonpointer": "5.0.1"
} }
} }
+1 -1
View File
@@ -39,7 +39,7 @@ var (
// CLI option types: // CLI option types:
"github.com/coder/serpent", "github.com/coder/serpent",
} }
indent = " " indent = "\t"
) )
func main() { func main() {
+4 -4
View File
@@ -1,17 +1,17 @@
// From codersdk/genericmap.go // From codersdk/genericmap.go
export interface Buzz { export interface Buzz {
readonly foo: Foo readonly foo: Foo
readonly bazz: string readonly bazz: string
} }
// From codersdk/genericmap.go // From codersdk/genericmap.go
export interface Foo { export interface Foo {
readonly bar: string readonly bar: string
} }
// From codersdk/genericmap.go // From codersdk/genericmap.go
export interface FooBuzz<R extends Custom> { export interface FooBuzz<R extends Custom> {
readonly something: (readonly R[]) readonly something: (readonly R[])
} }
// From codersdk/genericmap.go // From codersdk/genericmap.go
+14 -14
View File
@@ -1,35 +1,35 @@
// From codersdk/generics.go // From codersdk/generics.go
export interface Complex<C extends comparable, S extends Single, T extends Custom> { export interface Complex<C extends comparable, S extends Single, T extends Custom> {
readonly dynamic: Fields<C, boolean, string, S> readonly dynamic: Fields<C, boolean, string, S>
readonly order: FieldsDiffOrder<C, string, S, T> readonly order: FieldsDiffOrder<C, string, S, T>
readonly comparable: C readonly comparable: C
readonly single: S readonly single: S
readonly static: Static readonly static: Static
} }
// From codersdk/generics.go // From codersdk/generics.go
export interface Dynamic<A extends any, S extends Single> { export interface Dynamic<A extends any, S extends Single> {
readonly dynamic: Fields<boolean, A, string, S> readonly dynamic: Fields<boolean, A, string, S>
readonly comparable: boolean readonly comparable: boolean
} }
// From codersdk/generics.go // From codersdk/generics.go
export interface Fields<C extends comparable, A extends any, T extends Custom, S extends Single> { export interface Fields<C extends comparable, A extends any, T extends Custom, S extends Single> {
readonly comparable: C readonly comparable: C
readonly any: A readonly any: A
readonly custom: T readonly custom: T
readonly again: T readonly again: T
readonly single_constraint: S readonly single_constraint: S
} }
// From codersdk/generics.go // From codersdk/generics.go
export interface FieldsDiffOrder<A extends any, C extends comparable, S extends Single, T extends Custom> { export interface FieldsDiffOrder<A extends any, C extends comparable, S extends Single, T extends Custom> {
readonly Fields: Fields<C, A, T, S> readonly Fields: Fields<C, A, T, S>
} }
// From codersdk/generics.go // From codersdk/generics.go
export interface Static { export interface Static {
readonly static: Fields<string, number, number, string> readonly static: Fields<string, number, number, string>
} }
// From codersdk/generics.go // From codersdk/generics.go
+3 -3
View File
@@ -1,10 +1,10 @@
// From codersdk/genericslice.go // From codersdk/genericslice.go
export interface Bar { export interface Bar {
readonly Bar: string readonly Bar: string
} }
// From codersdk/genericslice.go // From codersdk/genericslice.go
export interface Foo<R extends any> { export interface Foo<R extends any> {
readonly Slice: (readonly R[]) readonly Slice: (readonly R[])
readonly TwoD: (readonly (readonly R[])[]) readonly TwoD: (readonly (readonly R[])[])
} }
+1 -1
View File
@@ -98,7 +98,7 @@ func run(lint bool) error {
} }
enc := json.NewEncoder(w) enc := json.NewEncoder(w)
enc.SetIndent("", " ") enc.SetIndent("", "\t")
return enc.Encode(examples) return enc.Encode(examples)
} }
+39 -43
View File
@@ -1,45 +1,41 @@
{ {
"files": { "files": {
"ignore": ["**/*Generated.ts"] "ignore": ["**/*Generated.ts"]
}, },
"formatter": { "linter": {
"indentStyle": "space", "rules": {
"indentWidth": 2 "a11y": {
}, "noSvgWithoutTitle": { "level": "off" },
"linter": { "useButtonType": { "level": "off" }
"rules": { },
"a11y": { "style": {
"noSvgWithoutTitle": { "level": "off" }, "noNonNullAssertion": { "level": "off" },
"useButtonType": { "level": "off" } "noParameterAssign": { "level": "off" },
}, "useDefaultParameterLast": { "level": "off" },
"style": { "useSelfClosingElements": { "level": "off" }
"noNonNullAssertion": { "level": "off" }, },
"noParameterAssign": { "level": "off" }, "suspicious": {
"useDefaultParameterLast": { "level": "off" }, "noArrayIndexKey": { "level": "off" },
"useSelfClosingElements": { "level": "off" } "noThenProperty": { "level": "off" }
}, },
"suspicious": { "nursery": {
"noArrayIndexKey": { "level": "off" }, "noRestrictedImports": {
"noThenProperty": { "level": "off" } "level": "error",
}, "options": {
"nursery": { "paths": {
"noRestrictedImports": { "@mui/material": "Use @mui/material/<name> instead. See: https://material-ui.com/guides/minimizing-bundle-size/.",
"level": "error", "@mui/icons-material": "Use @mui/icons-material/<name> instead. See: https://material-ui.com/guides/minimizing-bundle-size/.",
"options": { "@mui/material/Avatar": "Use components/Avatar/Avatar instead.",
"paths": { "@mui/material/Alert": "Use components/Alert/Alert instead.",
"@mui/material": "Use @mui/material/<name> instead. See: https://material-ui.com/guides/minimizing-bundle-size/.", "@mui/material/Popover": "Use components/Popover/Popover instead.",
"@mui/icons-material": "Use @mui/icons-material/<name> instead. See: https://material-ui.com/guides/minimizing-bundle-size/.", "@mui/material/Typography": "Use native HTML elements instead. Eg: <span>, <p>, <h1>, etc.",
"@mui/material/Avatar": "Use components/Avatar/Avatar instead.", "@mui/material/Box": "Use a <div> instead.",
"@mui/material/Alert": "Use components/Alert/Alert instead.", "@mui/material/styles": "Import from @emotion/react instead.",
"@mui/material/Popover": "Use components/Popover/Popover instead.", "lodash": "Use lodash/<name> instead."
"@mui/material/Typography": "Use native HTML elements instead. Eg: <span>, <p>, <h1>, etc.", }
"@mui/material/Box": "Use a <div> instead.", }
"@mui/material/styles": "Import from @emotion/react instead.", }
"lodash": "Use lodash/<name> instead." }
} }
} }
}
}
}
}
} }
+122 -122
View File
@@ -9,174 +9,174 @@ import { findSessionToken, randomName } from "./helpers";
let currentOrgId: string; let currentOrgId: string;
export const setupApiCalls = async (page: Page) => { export const setupApiCalls = async (page: Page) => {
try { try {
const token = await findSessionToken(page); const token = await findSessionToken(page);
API.setSessionToken(token); API.setSessionToken(token);
} catch { } catch {
// If this fails, we have an unauthenticated client. // If this fails, we have an unauthenticated client.
} }
API.setHost(`http://127.0.0.1:${coderPort}`); API.setHost(`http://127.0.0.1:${coderPort}`);
}; };
export const getCurrentOrgId = async (): Promise<string> => { export const getCurrentOrgId = async (): Promise<string> => {
if (currentOrgId) { if (currentOrgId) {
return currentOrgId; return currentOrgId;
} }
const currentUser = await API.getAuthenticatedUser(); const currentUser = await API.getAuthenticatedUser();
currentOrgId = currentUser.organization_ids[0]; currentOrgId = currentUser.organization_ids[0];
return currentOrgId; return currentOrgId;
}; };
export const createUser = async (orgId: string) => { export const createUser = async (orgId: string) => {
const name = randomName(); const name = randomName();
const user = await API.createUser({ const user = await API.createUser({
email: `${name}@coder.com`, email: `${name}@coder.com`,
username: name, username: name,
name: name, name: name,
password: "s3cure&password!", password: "s3cure&password!",
login_type: "password", login_type: "password",
disable_login: false, disable_login: false,
organization_id: orgId, organization_id: orgId,
}); });
return user; return user;
}; };
export const createGroup = async (orgId: string) => { export const createGroup = async (orgId: string) => {
const name = randomName(); const name = randomName();
const group = await API.createGroup(orgId, { const group = await API.createGroup(orgId, {
name, name,
display_name: `Display ${name}`, display_name: `Display ${name}`,
avatar_url: "/emojis/1f60d.png", avatar_url: "/emojis/1f60d.png",
quota_allowance: 0, quota_allowance: 0,
}); });
return group; return group;
}; };
export const createOrganization = async () => { export const createOrganization = async () => {
const name = randomName(); const name = randomName();
const org = await API.createOrganization({ const org = await API.createOrganization({
name, name,
display_name: `Org ${name}`, display_name: `Org ${name}`,
description: `Org description ${name}`, description: `Org description ${name}`,
icon: "/emojis/1f957.png", icon: "/emojis/1f957.png",
}); });
return org; return org;
}; };
export async function verifyConfigFlagBoolean( export async function verifyConfigFlagBoolean(
page: Page, page: Page,
config: DeploymentConfig, config: DeploymentConfig,
flag: string, flag: string,
) { ) {
const opt = findConfigOption(config, flag); const opt = findConfigOption(config, flag);
const type = opt.value ? "option-enabled" : "option-disabled"; const type = opt.value ? "option-enabled" : "option-disabled";
const value = opt.value ? "Enabled" : "Disabled"; const value = opt.value ? "Enabled" : "Disabled";
const configOption = page.locator( const configOption = page.locator(
`div.options-table .option-${flag} .${type}`, `div.options-table .option-${flag} .${type}`,
); );
await expect(configOption).toHaveText(value); await expect(configOption).toHaveText(value);
} }
export async function verifyConfigFlagNumber( export async function verifyConfigFlagNumber(
page: Page, page: Page,
config: DeploymentConfig, config: DeploymentConfig,
flag: string, flag: string,
) { ) {
const opt = findConfigOption(config, flag); const opt = findConfigOption(config, flag);
const configOption = page.locator( const configOption = page.locator(
`div.options-table .option-${flag} .option-value-number`, `div.options-table .option-${flag} .option-value-number`,
); );
await expect(configOption).toHaveText(String(opt.value)); await expect(configOption).toHaveText(String(opt.value));
} }
export async function verifyConfigFlagString( export async function verifyConfigFlagString(
page: Page, page: Page,
config: DeploymentConfig, config: DeploymentConfig,
flag: string, flag: string,
) { ) {
const opt = findConfigOption(config, flag); const opt = findConfigOption(config, flag);
const configOption = page.locator( const configOption = page.locator(
`div.options-table .option-${flag} .option-value-string`, `div.options-table .option-${flag} .option-value-string`,
); );
await expect(configOption).toHaveText(opt.value); await expect(configOption).toHaveText(opt.value);
} }
export async function verifyConfigFlagEmpty(page: Page, flag: string) { export async function verifyConfigFlagEmpty(page: Page, flag: string) {
const configOption = page.locator( const configOption = page.locator(
`div.options-table .option-${flag} .option-value-empty`, `div.options-table .option-${flag} .option-value-empty`,
); );
await expect(configOption).toHaveText("Not set"); await expect(configOption).toHaveText("Not set");
} }
export async function verifyConfigFlagArray( export async function verifyConfigFlagArray(
page: Page, page: Page,
config: DeploymentConfig, config: DeploymentConfig,
flag: string, flag: string,
) { ) {
const opt = findConfigOption(config, flag); const opt = findConfigOption(config, flag);
const configOption = page.locator( const configOption = page.locator(
`div.options-table .option-${flag} .option-array`, `div.options-table .option-${flag} .option-array`,
); );
// Verify array of options with simple dots // Verify array of options with simple dots
for (const item of opt.value) { for (const item of opt.value) {
await expect(configOption.locator("li", { hasText: item })).toBeVisible(); await expect(configOption.locator("li", { hasText: item })).toBeVisible();
} }
} }
export async function verifyConfigFlagEntries( export async function verifyConfigFlagEntries(
page: Page, page: Page,
config: DeploymentConfig, config: DeploymentConfig,
flag: string, flag: string,
) { ) {
const opt = findConfigOption(config, flag); const opt = findConfigOption(config, flag);
const configOption = page.locator( const configOption = page.locator(
`div.options-table .option-${flag} .option-array`, `div.options-table .option-${flag} .option-array`,
); );
// Verify array of options with green marks. // Verify array of options with green marks.
Object.entries(opt.value) Object.entries(opt.value)
.sort((a, b) => a[0].localeCompare(b[0])) .sort((a, b) => a[0].localeCompare(b[0]))
.map(async ([item]) => { .map(async ([item]) => {
await expect( await expect(
configOption.locator(`.option-array-item-${item}.option-enabled`, { configOption.locator(`.option-array-item-${item}.option-enabled`, {
hasText: item, hasText: item,
}), }),
).toBeVisible(); ).toBeVisible();
}); });
} }
export async function verifyConfigFlagDuration( export async function verifyConfigFlagDuration(
page: Page, page: Page,
config: DeploymentConfig, config: DeploymentConfig,
flag: string, flag: string,
) { ) {
const opt = findConfigOption(config, flag); const opt = findConfigOption(config, flag);
const configOption = page.locator( const configOption = page.locator(
`div.options-table .option-${flag} .option-value-string`, `div.options-table .option-${flag} .option-value-string`,
); );
await expect(configOption).toHaveText( await expect(configOption).toHaveText(
formatDuration( formatDuration(
// intervalToDuration takes ms, so convert nanoseconds to ms // intervalToDuration takes ms, so convert nanoseconds to ms
intervalToDuration({ intervalToDuration({
start: 0, start: 0,
end: (opt.value as number) / 1e6, end: (opt.value as number) / 1e6,
}), }),
), ),
); );
} }
export function findConfigOption( export function findConfigOption(
config: DeploymentConfig, config: DeploymentConfig,
flag: string, flag: string,
): SerpentOption { ): SerpentOption {
const opt = config.options.find((option) => option.flag === flag); const opt = config.options.find((option) => option.flag === flag);
if (opt === undefined) { if (opt === undefined) {
// must be undefined as `false` is expected // must be undefined as `false` is expected
throw new Error(`Option with env ${flag} has undefined value.`); throw new Error(`Option with env ${flag} has undefined value.`);
} }
return opt; return opt;
} }
+15 -15
View File
@@ -4,8 +4,8 @@ export const coderMain = path.join(__dirname, "../../enterprise/cmd/coder");
// Default port from the server // Default port from the server
export const coderPort = process.env.CODER_E2E_PORT export const coderPort = process.env.CODER_E2E_PORT
? Number(process.env.CODER_E2E_PORT) ? Number(process.env.CODER_E2E_PORT)
: 3111; : 3111;
export const prometheusPort = 2114; export const prometheusPort = 2114;
export const workspaceProxyPort = 3112; export const workspaceProxyPort = 3112;
@@ -19,23 +19,23 @@ export const password = "SomeSecurePassword!";
export const email = "admin@coder.com"; export const email = "admin@coder.com";
export const gitAuth = { export const gitAuth = {
deviceProvider: "device", deviceProvider: "device",
webProvider: "web", webProvider: "web",
// These ports need to be hardcoded so that they can be // These ports need to be hardcoded so that they can be
// used in `playwright.config.ts` to set the environment // used in `playwright.config.ts` to set the environment
// variables for the server. // variables for the server.
devicePort: 50515, devicePort: 50515,
webPort: 50516, webPort: 50516,
authPath: "/auth", authPath: "/auth",
tokenPath: "/token", tokenPath: "/token",
codePath: "/code", codePath: "/code",
validatePath: "/validate", validatePath: "/validate",
installationsPath: "/installations", installationsPath: "/installations",
}; };
export const requireEnterpriseTests = Boolean( export const requireEnterpriseTests = Boolean(
process.env.CODER_E2E_REQUIRE_ENTERPRISE_TESTS, process.env.CODER_E2E_REQUIRE_ENTERPRISE_TESTS,
); );
export const enterpriseLicense = process.env.CODER_E2E_ENTERPRISE_LICENSE ?? ""; export const enterpriseLicense = process.env.CODER_E2E_ENTERPRISE_LICENSE ?? "";
+30 -30
View File
@@ -3,35 +3,35 @@ import { type Page, expect } from "@playwright/test";
type PollingOptions = { timeout?: number; intervals?: number[] }; type PollingOptions = { timeout?: number; intervals?: number[] };
export const expectUrl = expect.extend({ export const expectUrl = expect.extend({
/** /**
* toHavePathName is an alternative to `toHaveURL` that won't fail if the URL contains query parameters. * toHavePathName is an alternative to `toHaveURL` that won't fail if the URL contains query parameters.
*/ */
async toHavePathName(page: Page, expected: string, options?: PollingOptions) { async toHavePathName(page: Page, expected: string, options?: PollingOptions) {
let actual: string = new URL(page.url()).pathname; let actual: string = new URL(page.url()).pathname;
let pass: boolean; let pass: boolean;
try { try {
await expect await expect
.poll(() => { .poll(() => {
actual = new URL(page.url()).pathname; actual = new URL(page.url()).pathname;
return actual; return actual;
}, options) }, options)
.toBe(expected); .toBe(expected);
pass = true; pass = true;
} catch { } catch {
pass = false; pass = false;
} }
return { return {
name: "toHavePathName", name: "toHavePathName",
pass, pass,
actual, actual,
expected, expected,
message: () => message: () =>
`The page does not have the expected URL pathname.\nExpected: ${ `The page does not have the expected URL pathname.\nExpected: ${
this.isNot ? "not" : "" this.isNot ? "not" : ""
}${this.utils.printExpected( }${this.utils.printExpected(
expected, expected,
)}\nActual: ${this.utils.printReceived(actual)}`, )}\nActual: ${this.utils.printReceived(actual)}`,
}; };
}, },
}); });
+30 -30
View File
@@ -7,41 +7,41 @@ import { expectUrl } from "./expectUrl";
import { storageState } from "./playwright.config"; import { storageState } from "./playwright.config";
test("setup deployment", async ({ page }) => { test("setup deployment", async ({ page }) => {
await page.goto("/", { waitUntil: "domcontentloaded" }); await page.goto("/", { waitUntil: "domcontentloaded" });
await setupApiCalls(page); await setupApiCalls(page);
const exists = await API.hasFirstUser(); const exists = await API.hasFirstUser();
// First user already exists, abort early. All tests execute this as a dependency, // First user already exists, abort early. All tests execute this as a dependency,
// if you run multiple tests in the UI, this will fail unless we check this. // if you run multiple tests in the UI, this will fail unless we check this.
if (exists) { if (exists) {
return; return;
} }
// Setup first user // Setup first user
await page.getByLabel(Language.usernameLabel).fill(constants.username); await page.getByLabel(Language.usernameLabel).fill(constants.username);
await page.getByLabel(Language.emailLabel).fill(constants.email); await page.getByLabel(Language.emailLabel).fill(constants.email);
await page.getByLabel(Language.passwordLabel).fill(constants.password); await page.getByLabel(Language.passwordLabel).fill(constants.password);
await page.getByTestId("create").click(); await page.getByTestId("create").click();
await expectUrl(page).toHavePathName("/workspaces"); await expectUrl(page).toHavePathName("/workspaces");
await page.context().storageState({ path: storageState }); await page.context().storageState({ path: storageState });
await page.getByTestId("button-select-template").isVisible(); await page.getByTestId("button-select-template").isVisible();
// Setup license // Setup license
if (constants.requireEnterpriseTests || constants.enterpriseLicense) { if (constants.requireEnterpriseTests || constants.enterpriseLicense) {
// Make sure that we have something that looks like a real license // Make sure that we have something that looks like a real license
expect(constants.enterpriseLicense).toBeTruthy(); expect(constants.enterpriseLicense).toBeTruthy();
expect(constants.enterpriseLicense.length).toBeGreaterThan(92); // the signature alone should be this long expect(constants.enterpriseLicense.length).toBeGreaterThan(92); // the signature alone should be this long
expect(constants.enterpriseLicense.split(".").length).toBe(3); // otherwise it's invalid expect(constants.enterpriseLicense.split(".").length).toBe(3); // otherwise it's invalid
await page.goto("/deployment/licenses", { waitUntil: "domcontentloaded" }); await page.goto("/deployment/licenses", { waitUntil: "domcontentloaded" });
await page.getByText("Add a license").click(); await page.getByText("Add a license").click();
await page.getByRole("textbox").fill(constants.enterpriseLicense); await page.getByRole("textbox").fill(constants.enterpriseLicense);
await page.getByText("Upload License").click(); await page.getByText("Upload License").click();
await expect( await expect(
page.getByText("You have successfully added a license"), page.getByText("You have successfully added a license"),
).toBeVisible(); ).toBeVisible();
} }
}); });
+706 -706
View File
File diff suppressed because it is too large Load Diff
+68 -68
View File
@@ -3,88 +3,88 @@ import type { BrowserContext, Page } from "@playwright/test";
import { coderPort, gitAuth } from "./constants"; import { coderPort, gitAuth } from "./constants";
export const beforeCoderTest = async (page: Page) => { export const beforeCoderTest = async (page: Page) => {
// eslint-disable-next-line no-console -- Show everything that was printed with console.log() // eslint-disable-next-line no-console -- Show everything that was printed with console.log()
page.on("console", (msg) => console.log(`[onConsole] ${msg.text()}`)); page.on("console", (msg) => console.log(`[onConsole] ${msg.text()}`));
page.on("request", (request) => { page.on("request", (request) => {
if (!isApiCall(request.url())) { if (!isApiCall(request.url())) {
return; return;
} }
// eslint-disable-next-line no-console -- Log HTTP requests for debugging purposes // eslint-disable-next-line no-console -- Log HTTP requests for debugging purposes
console.log( console.log(
`[onRequest] method=${request.method()} url=${request.url()} postData=${ `[onRequest] method=${request.method()} url=${request.url()} postData=${
request.postData() ? request.postData() : "" request.postData() ? request.postData() : ""
}`, }`,
); );
}); });
page.on("response", async (response) => { page.on("response", async (response) => {
if (!isApiCall(response.url())) { if (!isApiCall(response.url())) {
return; return;
} }
const shouldLogResponse = const shouldLogResponse =
!response.url().endsWith("/api/v2/deployment/config") && !response.url().endsWith("/api/v2/deployment/config") &&
!response.url().endsWith("/api/v2/debug/health?force=false"); !response.url().endsWith("/api/v2/debug/health?force=false");
let responseText = ""; let responseText = "";
try { try {
if (shouldLogResponse) { if (shouldLogResponse) {
const buffer = await response.body(); const buffer = await response.body();
responseText = buffer.toString("utf-8"); responseText = buffer.toString("utf-8");
responseText = responseText.replace(/\n$/g, ""); responseText = responseText.replace(/\n$/g, "");
} else { } else {
responseText = "skipped..."; responseText = "skipped...";
} }
} catch (error) { } catch (error) {
responseText = "not_available"; responseText = "not_available";
} }
// eslint-disable-next-line no-console -- Log HTTP requests for debugging purposes // eslint-disable-next-line no-console -- Log HTTP requests for debugging purposes
console.log( console.log(
`[onResponse] url=${response.url()} status=${response.status()} body=${responseText}`, `[onResponse] url=${response.url()} status=${response.status()} body=${responseText}`,
); );
}); });
}; };
export const resetExternalAuthKey = async (context: BrowserContext) => { export const resetExternalAuthKey = async (context: BrowserContext) => {
// Find the session token so we can destroy the external auth link between tests, to ensure valid authentication happens each time. // Find the session token so we can destroy the external auth link between tests, to ensure valid authentication happens each time.
const cookies = await context.cookies(); const cookies = await context.cookies();
const sessionCookie = cookies.find((c) => c.name === "coder_session_token"); const sessionCookie = cookies.find((c) => c.name === "coder_session_token");
const options = { const options = {
method: "DELETE", method: "DELETE",
hostname: "127.0.0.1", hostname: "127.0.0.1",
port: coderPort, port: coderPort,
path: `/api/v2/external-auth/${gitAuth.webProvider}?coder_session_token=${sessionCookie?.value}`, path: `/api/v2/external-auth/${gitAuth.webProvider}?coder_session_token=${sessionCookie?.value}`,
}; };
const req = http.request(options, (res) => { const req = http.request(options, (res) => {
let data = ""; let data = "";
res.on("data", (chunk) => { res.on("data", (chunk) => {
data += chunk; data += chunk;
}); });
res.on("end", () => { res.on("end", () => {
// Both 200 (key deleted successfully) and 500 (key was not found) are valid responses. // Both 200 (key deleted successfully) and 500 (key was not found) are valid responses.
if (res.statusCode !== 200 && res.statusCode !== 500) { if (res.statusCode !== 200 && res.statusCode !== 500) {
console.error("failed to delete external auth link", data); console.error("failed to delete external auth link", data);
throw new Error( throw new Error(
`failed to delete external auth link: HTTP response ${res.statusCode}`, `failed to delete external auth link: HTTP response ${res.statusCode}`,
); );
} }
}); });
}); });
req.on("error", (err) => { req.on("error", (err) => {
throw err.message; throw err.message;
}); });
req.end(); req.end();
}; };
const isApiCall = (urlString: string): boolean => { const isApiCall = (urlString: string): boolean => {
const url = new URL(urlString); const url = new URL(urlString);
const apiPath = "/api/v2"; const apiPath = "/api/v2";
return url.pathname.startsWith(apiPath); return url.pathname.startsWith(apiPath);
}; };
+107 -107
View File
@@ -3,162 +3,162 @@ import type { RichParameter } from "./provisionerGenerated";
// Rich parameters // Rich parameters
export const emptyParameter: RichParameter = { export const emptyParameter: RichParameter = {
name: "", name: "",
description: "", description: "",
type: "", type: "",
mutable: false, mutable: false,
defaultValue: "", defaultValue: "",
icon: "", icon: "",
options: [], options: [],
validationRegex: "", validationRegex: "",
validationError: "", validationError: "",
validationMin: undefined, validationMin: undefined,
validationMax: undefined, validationMax: undefined,
validationMonotonic: "", validationMonotonic: "",
required: false, required: false,
displayName: "", displayName: "",
order: 0, order: 0,
ephemeral: false, ephemeral: false,
}; };
// firstParameter is mutable string with a default value (parameter value not required). // firstParameter is mutable string with a default value (parameter value not required).
export const firstParameter: RichParameter = { export const firstParameter: RichParameter = {
...emptyParameter, ...emptyParameter,
name: "first_parameter", name: "first_parameter",
displayName: "First parameter", displayName: "First parameter",
type: "number", type: "number",
description: "This is first parameter.", description: "This is first parameter.",
icon: "/emojis/1f310.png", icon: "/emojis/1f310.png",
defaultValue: "123", defaultValue: "123",
mutable: true, mutable: true,
order: 1, order: 1,
}; };
// secondParameter is immutable string with a default value (parameter value not required). // secondParameter is immutable string with a default value (parameter value not required).
export const secondParameter: RichParameter = { export const secondParameter: RichParameter = {
...emptyParameter, ...emptyParameter,
name: "second_parameter", name: "second_parameter",
displayName: "Second parameter", displayName: "Second parameter",
type: "string", type: "string",
description: "This is second parameter.", description: "This is second parameter.",
defaultValue: "abc", defaultValue: "abc",
order: 2, order: 2,
}; };
// thirdParameter is mutable string with an empty default value (parameter value not required). // thirdParameter is mutable string with an empty default value (parameter value not required).
export const thirdParameter: RichParameter = { export const thirdParameter: RichParameter = {
...emptyParameter, ...emptyParameter,
name: "third_parameter", name: "third_parameter",
type: "string", type: "string",
description: "This is third parameter.", description: "This is third parameter.",
defaultValue: "", defaultValue: "",
mutable: true, mutable: true,
order: 3, order: 3,
}; };
// fourthParameter is immutable boolean with a default "true" value (parameter value not required). // fourthParameter is immutable boolean with a default "true" value (parameter value not required).
export const fourthParameter: RichParameter = { export const fourthParameter: RichParameter = {
...emptyParameter, ...emptyParameter,
name: "fourth_parameter", name: "fourth_parameter",
type: "bool", type: "bool",
description: "This is fourth parameter.", description: "This is fourth parameter.",
defaultValue: "true", defaultValue: "true",
order: 3, order: 3,
}; };
// fifthParameter is immutable "string with options", with a default option selected (parameter value not required). // fifthParameter is immutable "string with options", with a default option selected (parameter value not required).
export const fifthParameter: RichParameter = { export const fifthParameter: RichParameter = {
...emptyParameter, ...emptyParameter,
name: "fifth_parameter", name: "fifth_parameter",
displayName: "Fifth parameter", displayName: "Fifth parameter",
type: "string", type: "string",
options: [ options: [
{ {
name: "ABC", name: "ABC",
description: "This is ABC", description: "This is ABC",
value: "abc", value: "abc",
icon: "", icon: "",
}, },
{ {
name: "DEF", name: "DEF",
description: "This is DEF", description: "This is DEF",
value: "def", value: "def",
icon: "", icon: "",
}, },
{ {
name: "GHI", name: "GHI",
description: "This is GHI", description: "This is GHI",
value: "ghi", value: "ghi",
icon: "", icon: "",
}, },
], ],
description: "This is fifth parameter.", description: "This is fifth parameter.",
defaultValue: "def", defaultValue: "def",
order: 3, order: 3,
}; };
// sixthParameter is mutable string without a default value (parameter value is required). // sixthParameter is mutable string without a default value (parameter value is required).
export const sixthParameter: RichParameter = { export const sixthParameter: RichParameter = {
...emptyParameter, ...emptyParameter,
name: "sixth_parameter", name: "sixth_parameter",
displayName: "Sixth parameter", displayName: "Sixth parameter",
type: "number", type: "number",
description: "This is sixth parameter.", description: "This is sixth parameter.",
icon: "/emojis/1f310.png", icon: "/emojis/1f310.png",
required: true, required: true,
mutable: true, mutable: true,
order: 1, order: 1,
}; };
// seventhParameter is immutable string without a default value (parameter value is required). // seventhParameter is immutable string without a default value (parameter value is required).
export const seventhParameter: RichParameter = { export const seventhParameter: RichParameter = {
...emptyParameter, ...emptyParameter,
name: "seventh_parameter", name: "seventh_parameter",
displayName: "Seventh parameter", displayName: "Seventh parameter",
type: "string", type: "string",
description: "This is seventh parameter.", description: "This is seventh parameter.",
required: true, required: true,
order: 1, order: 1,
}; };
// randParamName returns a new parameter with a random name. // randParamName returns a new parameter with a random name.
// It helps to avoid cross-test interference when user-auto-fill triggers on // It helps to avoid cross-test interference when user-auto-fill triggers on
// the same parameter name. // the same parameter name.
export const randParamName = (p: RichParameter): RichParameter => { export const randParamName = (p: RichParameter): RichParameter => {
const name = `${p.name}_${Math.random().toString(36).substring(7)}`; const name = `${p.name}_${Math.random().toString(36).substring(7)}`;
return { ...p, name: name }; return { ...p, name: name };
}; };
// Build options // Build options
export const firstBuildOption: RichParameter = { export const firstBuildOption: RichParameter = {
...emptyParameter, ...emptyParameter,
name: "first_build_option", name: "first_build_option",
displayName: "First build option", displayName: "First build option",
type: "string", type: "string",
description: "This is first build option.", description: "This is first build option.",
icon: "/emojis/1f310.png", icon: "/emojis/1f310.png",
defaultValue: "ABCDEF", defaultValue: "ABCDEF",
mutable: true, mutable: true,
ephemeral: true, ephemeral: true,
}; };
export const secondBuildOption: RichParameter = { export const secondBuildOption: RichParameter = {
...emptyParameter, ...emptyParameter,
name: "second_build_option", name: "second_build_option",
displayName: "Second build option", displayName: "Second build option",
type: "bool", type: "bool",
description: "This is second build option.", description: "This is second build option.",
defaultValue: "false", defaultValue: "false",
mutable: true, mutable: true,
ephemeral: true, ephemeral: true,
}; };
+127 -127
View File
@@ -2,13 +2,13 @@ import { execSync } from "node:child_process";
import * as path from "node:path"; import * as path from "node:path";
import { defineConfig } from "@playwright/test"; import { defineConfig } from "@playwright/test";
import { import {
coderMain, coderMain,
coderPort, coderPort,
coderdPProfPort, coderdPProfPort,
e2eFakeExperiment1, e2eFakeExperiment1,
e2eFakeExperiment2, e2eFakeExperiment2,
gitAuth, gitAuth,
requireTerraformTests, requireTerraformTests,
} from "./constants"; } from "./constants";
export const wsEndpoint = process.env.CODER_E2E_WS_ENDPOINT; export const wsEndpoint = process.env.CODER_E2E_WS_ENDPOINT;
@@ -24,141 +24,141 @@ export const storageState = path.join(__dirname, ".auth.json");
let hasTerraform = false; let hasTerraform = false;
let hasDocker = false; let hasDocker = false;
try { try {
execSync("terraform --version"); execSync("terraform --version");
hasTerraform = true; hasTerraform = true;
} catch { } catch {
/* empty */ /* empty */
} }
try { try {
execSync("docker --version"); execSync("docker --version");
hasDocker = true; hasDocker = true;
} catch { } catch {
/* empty */ /* empty */
} }
if (!hasTerraform || !hasDocker) { if (!hasTerraform || !hasDocker) {
const msg = `Terraform provisioners require docker & terraform binaries to function. \n${ const msg = `Terraform provisioners require docker & terraform binaries to function. \n${
hasTerraform hasTerraform
? "" ? ""
: "\tThe `terraform` executable is not present in the runtime environment.\n" : "\tThe `terraform` executable is not present in the runtime environment.\n"
}${ }${
hasDocker hasDocker
? "" ? ""
: "\tThe `docker` executable is not present in the runtime environment.\n" : "\tThe `docker` executable is not present in the runtime environment.\n"
}`; }`;
throw new Error(msg); throw new Error(msg);
} }
const localURL = (port: number, path: string): string => { const localURL = (port: number, path: string): string => {
return `http://localhost:${port}${path}`; return `http://localhost:${port}${path}`;
}; };
export default defineConfig({ export default defineConfig({
projects: [ projects: [
{ {
name: "testsSetup", name: "testsSetup",
testMatch: /global.setup\.ts/, testMatch: /global.setup\.ts/,
}, },
{ {
name: "tests", name: "tests",
testMatch: /.*\.spec\.ts/, testMatch: /.*\.spec\.ts/,
dependencies: ["testsSetup"], dependencies: ["testsSetup"],
use: { storageState }, use: { storageState },
timeout: 50_000, timeout: 50_000,
}, },
], ],
reporter: [["./reporter.ts"]], reporter: [["./reporter.ts"]],
use: { use: {
baseURL: `http://localhost:${coderPort}`, baseURL: `http://localhost:${coderPort}`,
video: "retain-on-failure", video: "retain-on-failure",
...(wsEndpoint ...(wsEndpoint
? { ? {
connectOptions: { connectOptions: {
wsEndpoint: wsEndpoint, wsEndpoint: wsEndpoint,
}, },
} }
: { : {
launchOptions: { launchOptions: {
args: ["--disable-webgl"], args: ["--disable-webgl"],
}, },
}), }),
}, },
webServer: { webServer: {
url: `http://localhost:${coderPort}/api/v2/deployment/config`, url: `http://localhost:${coderPort}/api/v2/deployment/config`,
command: [ command: [
`go run -tags embed ${coderMain} server`, `go run -tags embed ${coderMain} server`,
"--global-config $(mktemp -d -t e2e-XXXXXXXXXX)", "--global-config $(mktemp -d -t e2e-XXXXXXXXXX)",
`--access-url=http://localhost:${coderPort}`, `--access-url=http://localhost:${coderPort}`,
`--http-address=0.0.0.0:${coderPort}`, `--http-address=0.0.0.0:${coderPort}`,
"--in-memory", "--in-memory",
"--telemetry=false", "--telemetry=false",
"--dangerous-disable-rate-limits", "--dangerous-disable-rate-limits",
"--provisioner-daemons 10", "--provisioner-daemons 10",
// TODO: Enable some terraform provisioners // TODO: Enable some terraform provisioners
`--provisioner-types=echo${requireTerraformTests ? ",terraform" : ""}`, `--provisioner-types=echo${requireTerraformTests ? ",terraform" : ""}`,
"--provisioner-daemons=10", "--provisioner-daemons=10",
"--web-terminal-renderer=dom", "--web-terminal-renderer=dom",
"--pprof-enable", "--pprof-enable",
] ]
.filter(Boolean) .filter(Boolean)
.join(" "), .join(" "),
env: { env: {
...process.env, ...process.env,
// Otherwise, the runner fails on Mac with: could not determine kind of name for C.uuid_string_t // Otherwise, the runner fails on Mac with: could not determine kind of name for C.uuid_string_t
CGO_ENABLED: "0", CGO_ENABLED: "0",
// This is the test provider for git auth with devices! // This is the test provider for git auth with devices!
CODER_GITAUTH_0_ID: gitAuth.deviceProvider, CODER_GITAUTH_0_ID: gitAuth.deviceProvider,
CODER_GITAUTH_0_TYPE: "github", CODER_GITAUTH_0_TYPE: "github",
CODER_GITAUTH_0_CLIENT_ID: "client", CODER_GITAUTH_0_CLIENT_ID: "client",
CODER_GITAUTH_0_CLIENT_SECRET: "secret", CODER_GITAUTH_0_CLIENT_SECRET: "secret",
CODER_GITAUTH_0_DEVICE_FLOW: "true", CODER_GITAUTH_0_DEVICE_FLOW: "true",
CODER_GITAUTH_0_APP_INSTALL_URL: CODER_GITAUTH_0_APP_INSTALL_URL:
"https://github.com/apps/coder/installations/new", "https://github.com/apps/coder/installations/new",
CODER_GITAUTH_0_APP_INSTALLATIONS_URL: localURL( CODER_GITAUTH_0_APP_INSTALLATIONS_URL: localURL(
gitAuth.devicePort, gitAuth.devicePort,
gitAuth.installationsPath, gitAuth.installationsPath,
), ),
CODER_GITAUTH_0_TOKEN_URL: localURL( CODER_GITAUTH_0_TOKEN_URL: localURL(
gitAuth.devicePort, gitAuth.devicePort,
gitAuth.tokenPath, gitAuth.tokenPath,
), ),
CODER_GITAUTH_0_DEVICE_CODE_URL: localURL( CODER_GITAUTH_0_DEVICE_CODE_URL: localURL(
gitAuth.devicePort, gitAuth.devicePort,
gitAuth.codePath, gitAuth.codePath,
), ),
CODER_GITAUTH_0_VALIDATE_URL: localURL( CODER_GITAUTH_0_VALIDATE_URL: localURL(
gitAuth.devicePort, gitAuth.devicePort,
gitAuth.validatePath, gitAuth.validatePath,
), ),
CODER_GITAUTH_1_ID: gitAuth.webProvider, CODER_GITAUTH_1_ID: gitAuth.webProvider,
CODER_GITAUTH_1_TYPE: "github", CODER_GITAUTH_1_TYPE: "github",
CODER_GITAUTH_1_CLIENT_ID: "client", CODER_GITAUTH_1_CLIENT_ID: "client",
CODER_GITAUTH_1_CLIENT_SECRET: "secret", CODER_GITAUTH_1_CLIENT_SECRET: "secret",
CODER_GITAUTH_1_AUTH_URL: localURL(gitAuth.webPort, gitAuth.authPath), CODER_GITAUTH_1_AUTH_URL: localURL(gitAuth.webPort, gitAuth.authPath),
CODER_GITAUTH_1_TOKEN_URL: localURL(gitAuth.webPort, gitAuth.tokenPath), CODER_GITAUTH_1_TOKEN_URL: localURL(gitAuth.webPort, gitAuth.tokenPath),
CODER_GITAUTH_1_DEVICE_CODE_URL: localURL( CODER_GITAUTH_1_DEVICE_CODE_URL: localURL(
gitAuth.webPort, gitAuth.webPort,
gitAuth.codePath, gitAuth.codePath,
), ),
CODER_GITAUTH_1_VALIDATE_URL: localURL( CODER_GITAUTH_1_VALIDATE_URL: localURL(
gitAuth.webPort, gitAuth.webPort,
gitAuth.validatePath, gitAuth.validatePath,
), ),
CODER_PPROF_ADDRESS: `127.0.0.1:${coderdPProfPort}`, CODER_PPROF_ADDRESS: `127.0.0.1:${coderdPProfPort}`,
CODER_EXPERIMENTS: `multi-organization,${e2eFakeExperiment1},${e2eFakeExperiment2}`, CODER_EXPERIMENTS: `multi-organization,${e2eFakeExperiment1},${e2eFakeExperiment2}`,
// Tests for Deployment / User Authentication / OIDC // Tests for Deployment / User Authentication / OIDC
CODER_OIDC_ISSUER_URL: "https://accounts.google.com", CODER_OIDC_ISSUER_URL: "https://accounts.google.com",
CODER_OIDC_EMAIL_DOMAIN: "coder.com", CODER_OIDC_EMAIL_DOMAIN: "coder.com",
CODER_OIDC_CLIENT_ID: "1234567890", CODER_OIDC_CLIENT_ID: "1234567890",
CODER_OIDC_CLIENT_SECRET: "1234567890Secret", CODER_OIDC_CLIENT_SECRET: "1234567890Secret",
CODER_OIDC_ALLOW_SIGNUPS: "false", CODER_OIDC_ALLOW_SIGNUPS: "false",
CODER_OIDC_SIGN_IN_TEXT: "Hello", CODER_OIDC_SIGN_IN_TEXT: "Hello",
CODER_OIDC_ICON_URL: "/icon/google.svg", CODER_OIDC_ICON_URL: "/icon/google.svg",
}, },
reuseExistingServer: false, reuseExistingServer: false,
}, },
}); });
+847 -847
View File
File diff suppressed because it is too large Load Diff
+28 -28
View File
@@ -3,36 +3,36 @@ import { coderMain, coderPort, workspaceProxyPort } from "./constants";
import { waitUntilUrlIsNotResponding } from "./helpers"; import { waitUntilUrlIsNotResponding } from "./helpers";
export const startWorkspaceProxy = async ( export const startWorkspaceProxy = async (
token: string, token: string,
): Promise<ChildProcess> => { ): Promise<ChildProcess> => {
const cp = spawn("go", ["run", coderMain, "wsproxy", "server"], { const cp = spawn("go", ["run", coderMain, "wsproxy", "server"], {
env: { env: {
...process.env, ...process.env,
CODER_PRIMARY_ACCESS_URL: `http://127.0.0.1:${coderPort}`, CODER_PRIMARY_ACCESS_URL: `http://127.0.0.1:${coderPort}`,
CODER_PROXY_SESSION_TOKEN: token, CODER_PROXY_SESSION_TOKEN: token,
CODER_HTTP_ADDRESS: `localhost:${workspaceProxyPort}`, CODER_HTTP_ADDRESS: `localhost:${workspaceProxyPort}`,
}, },
}); });
cp.stdout.on("data", (data: Buffer) => { cp.stdout.on("data", (data: Buffer) => {
// eslint-disable-next-line no-console -- Log wsproxy activity // eslint-disable-next-line no-console -- Log wsproxy activity
console.log( console.log(
`[wsproxy] [stdout] [onData] ${data.toString().replace(/\n$/g, "")}`, `[wsproxy] [stdout] [onData] ${data.toString().replace(/\n$/g, "")}`,
); );
}); });
cp.stderr.on("data", (data: Buffer) => { cp.stderr.on("data", (data: Buffer) => {
// eslint-disable-next-line no-console -- Log wsproxy activity // eslint-disable-next-line no-console -- Log wsproxy activity
console.log( console.log(
`[wsproxy] [stderr] [onData] ${data.toString().replace(/\n$/g, "")}`, `[wsproxy] [stderr] [onData] ${data.toString().replace(/\n$/g, "")}`,
); );
}); });
return cp; return cp;
}; };
export const stopWorkspaceProxy = async (cp: ChildProcess, goRun = true) => { export const stopWorkspaceProxy = async (cp: ChildProcess, goRun = true) => {
exec(goRun ? `pkill -P ${cp.pid}` : `kill ${cp.pid}`, (error) => { exec(goRun ? `pkill -P ${cp.pid}` : `kill ${cp.pid}`, (error) => {
if (error) { if (error) {
throw new Error(`exec error: ${JSON.stringify(error)}`); throw new Error(`exec error: ${JSON.stringify(error)}`);
} }
}); });
await waitUntilUrlIsNotResponding(`http://127.0.0.1:${workspaceProxyPort}`); await waitUntilUrlIsNotResponding(`http://127.0.0.1:${workspaceProxyPort}`);
}; };
+135 -135
View File
@@ -2,172 +2,172 @@ import * as fs from "node:fs/promises";
import type { Writable } from "node:stream"; import type { Writable } from "node:stream";
/* eslint-disable no-console -- Logging is sort of the whole point here */ /* eslint-disable no-console -- Logging is sort of the whole point here */
import type { import type {
FullConfig, FullConfig,
FullResult, FullResult,
Reporter, Reporter,
Suite, Suite,
TestCase, TestCase,
TestError, TestError,
TestResult, TestResult,
} from "@playwright/test/reporter"; } from "@playwright/test/reporter";
import { API } from "api/api"; import { API } from "api/api";
import { coderdPProfPort, enterpriseLicense } from "./constants"; import { coderdPProfPort, enterpriseLicense } from "./constants";
class CoderReporter implements Reporter { class CoderReporter implements Reporter {
config: FullConfig | null = null; config: FullConfig | null = null;
testOutput = new Map<string, Array<[Writable, string]>>(); testOutput = new Map<string, Array<[Writable, string]>>();
passedCount = 0; passedCount = 0;
skippedCount = 0; skippedCount = 0;
failedTests: TestCase[] = []; failedTests: TestCase[] = [];
timedOutTests: TestCase[] = []; timedOutTests: TestCase[] = [];
onBegin(config: FullConfig, suite: Suite) { onBegin(config: FullConfig, suite: Suite) {
this.config = config; this.config = config;
console.log(`==> Running ${suite.allTests().length} tests`); console.log(`==> Running ${suite.allTests().length} tests`);
} }
onTestBegin(test: TestCase) { onTestBegin(test: TestCase) {
this.testOutput.set(test.id, []); this.testOutput.set(test.id, []);
console.log(`==> Starting test ${test.title}`); console.log(`==> Starting test ${test.title}`);
} }
onStdOut(chunk: string, test?: TestCase, _?: TestResult): void { onStdOut(chunk: string, test?: TestCase, _?: TestResult): void {
// If there's no associated test, just print it now // If there's no associated test, just print it now
if (!test) { if (!test) {
for (const line of logLines(chunk)) { for (const line of logLines(chunk)) {
console.log(`[stdout] ${line}`); console.log(`[stdout] ${line}`);
} }
return; return;
} }
// Will be printed if the test fails // Will be printed if the test fails
this.testOutput.get(test.id)!.push([process.stdout, chunk]); this.testOutput.get(test.id)!.push([process.stdout, chunk]);
} }
onStdErr(chunk: string, test?: TestCase, _?: TestResult): void { onStdErr(chunk: string, test?: TestCase, _?: TestResult): void {
// If there's no associated test, just print it now // If there's no associated test, just print it now
if (!test) { if (!test) {
for (const line of logLines(chunk)) { for (const line of logLines(chunk)) {
console.error(`[stderr] ${line}`); console.error(`[stderr] ${line}`);
} }
return; return;
} }
// Will be printed if the test fails // Will be printed if the test fails
this.testOutput.get(test.id)!.push([process.stderr, chunk]); this.testOutput.get(test.id)!.push([process.stderr, chunk]);
} }
async onTestEnd(test: TestCase, result: TestResult) { async onTestEnd(test: TestCase, result: TestResult) {
try { try {
if (test.expectedStatus === "skipped") { if (test.expectedStatus === "skipped") {
console.log(`==> Skipping test ${test.title}`); console.log(`==> Skipping test ${test.title}`);
this.skippedCount++; this.skippedCount++;
return; return;
} }
console.log(`==> Finished test ${test.title}: ${result.status}`); console.log(`==> Finished test ${test.title}: ${result.status}`);
if (result.status === "passed") { if (result.status === "passed") {
this.passedCount++; this.passedCount++;
return; return;
} }
if (result.status === "failed") { if (result.status === "failed") {
this.failedTests.push(test); this.failedTests.push(test);
} }
if (result.status === "timedOut") { if (result.status === "timedOut") {
this.timedOutTests.push(test); this.timedOutTests.push(test);
} }
const fsTestTitle = test.title.replaceAll(" ", "-"); const fsTestTitle = test.title.replaceAll(" ", "-");
const outputFile = `test-results/debug-pprof-goroutine-${fsTestTitle}.txt`; const outputFile = `test-results/debug-pprof-goroutine-${fsTestTitle}.txt`;
await exportDebugPprof(outputFile); await exportDebugPprof(outputFile);
console.log(`Data from pprof has been saved to ${outputFile}`); console.log(`Data from pprof has been saved to ${outputFile}`);
console.log("==> Output"); console.log("==> Output");
const output = this.testOutput.get(test.id)!; const output = this.testOutput.get(test.id)!;
for (const [target, chunk] of output) { for (const [target, chunk] of output) {
target.write(`${chunk.replace(/\n$/g, "")}\n`); target.write(`${chunk.replace(/\n$/g, "")}\n`);
} }
if (result.errors.length > 0) { if (result.errors.length > 0) {
console.log("==> Errors"); console.log("==> Errors");
for (const error of result.errors) { for (const error of result.errors) {
reportError(error); reportError(error);
} }
} }
if (result.attachments.length > 0) { if (result.attachments.length > 0) {
console.log("==> Attachments"); console.log("==> Attachments");
for (const attachment of result.attachments) { for (const attachment of result.attachments) {
console.log(attachment); console.log(attachment);
} }
} }
} finally { } finally {
this.testOutput.delete(test.id); this.testOutput.delete(test.id);
} }
} }
onEnd(result: FullResult) { onEnd(result: FullResult) {
console.log(`==> Tests ${result.status}`); console.log(`==> Tests ${result.status}`);
if (!enterpriseLicense) { if (!enterpriseLicense) {
console.log( console.log(
"==> Enterprise tests were skipped, because no license was provided", "==> Enterprise tests were skipped, because no license was provided",
); );
} }
console.log(`${this.passedCount} passed`); console.log(`${this.passedCount} passed`);
if (this.skippedCount > 0) { if (this.skippedCount > 0) {
console.log(`${this.skippedCount} skipped`); console.log(`${this.skippedCount} skipped`);
} }
if (this.failedTests.length > 0) { if (this.failedTests.length > 0) {
console.log(`${this.failedTests.length} failed`); console.log(`${this.failedTests.length} failed`);
for (const test of this.failedTests) { for (const test of this.failedTests) {
console.log(` ${test.location.file} ${test.title}`); console.log(` ${test.location.file} ${test.title}`);
} }
} }
if (this.timedOutTests.length > 0) { if (this.timedOutTests.length > 0) {
console.log(`${this.timedOutTests.length} timed out`); console.log(`${this.timedOutTests.length} timed out`);
for (const test of this.timedOutTests) { for (const test of this.timedOutTests) {
console.log(` ${test.location.file} ${test.title}`); console.log(` ${test.location.file} ${test.title}`);
} }
} }
} }
} }
const logLines = (chunk: string | Buffer): string[] => { const logLines = (chunk: string | Buffer): string[] => {
if (chunk instanceof Buffer) { if (chunk instanceof Buffer) {
// When running in a debugger, the input to this is a Buffer instead of a string. // When running in a debugger, the input to this is a Buffer instead of a string.
// Unsure why, but this prevents the `trimEnd` from throwing an error. // Unsure why, but this prevents the `trimEnd` from throwing an error.
return [chunk.toString()]; return [chunk.toString()];
} }
return chunk.trimEnd().split("\n"); return chunk.trimEnd().split("\n");
}; };
const exportDebugPprof = async (outputFile: string) => { const exportDebugPprof = async (outputFile: string) => {
const axiosInstance = API.getAxiosInstance(); const axiosInstance = API.getAxiosInstance();
const response = await axiosInstance.get( const response = await axiosInstance.get(
`http://127.0.0.1:${coderdPProfPort}/debug/pprof/goroutine?debug=1`, `http://127.0.0.1:${coderdPProfPort}/debug/pprof/goroutine?debug=1`,
); );
if (response.status !== 200) { if (response.status !== 200) {
throw new Error(`Error: Received status code ${response.status}`); throw new Error(`Error: Received status code ${response.status}`);
} }
await fs.writeFile(outputFile, response.data); await fs.writeFile(outputFile, response.data);
}; };
const reportError = (error: TestError) => { const reportError = (error: TestError) => {
if (error.location) { if (error.location) {
console.log(`${error.location.file}:${error.location.line}:`); console.log(`${error.location.file}:${error.location.line}:`);
} }
if (error.snippet) { if (error.snippet) {
console.log(error.snippet); console.log(error.snippet);
} }
if (error.message) { if (error.message) {
console.log(error.message); console.log(error.message);
} else { } else {
console.log(error); console.log(error);
} }
}; };
// eslint-disable-next-line no-unused-vars -- Playwright config uses it // eslint-disable-next-line no-unused-vars -- Playwright config uses it
+53 -53
View File
@@ -2,65 +2,65 @@ import { randomUUID } from "node:crypto";
import * as http from "node:http"; import * as http from "node:http";
import { test } from "@playwright/test"; import { test } from "@playwright/test";
import { import {
createTemplate, createTemplate,
createWorkspace, createWorkspace,
startAgent, startAgent,
stopAgent, stopAgent,
stopWorkspace, stopWorkspace,
} from "../helpers"; } from "../helpers";
import { beforeCoderTest } from "../hooks"; import { beforeCoderTest } from "../hooks";
test.beforeEach(({ page }) => beforeCoderTest(page)); test.beforeEach(({ page }) => beforeCoderTest(page));
test("app", async ({ context, page }) => { test("app", async ({ context, page }) => {
const appContent = "Hello World"; const appContent = "Hello World";
const token = randomUUID(); const token = randomUUID();
const srv = http const srv = http
.createServer((req, res) => { .createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" }); res.writeHead(200, { "Content-Type": "text/plain" });
res.end(appContent); res.end(appContent);
}) })
.listen(0); .listen(0);
const addr = srv.address(); const addr = srv.address();
if (typeof addr !== "object" || !addr) { if (typeof addr !== "object" || !addr) {
throw new Error("Expected addr to be an object"); throw new Error("Expected addr to be an object");
} }
const appName = "test-app"; const appName = "test-app";
const template = await createTemplate(page, { const template = await createTemplate(page, {
apply: [ apply: [
{ {
apply: { apply: {
resources: [ resources: [
{ {
agents: [ agents: [
{ {
token, token,
apps: [ apps: [
{ {
url: `http://localhost:${addr.port}`, url: `http://localhost:${addr.port}`,
displayName: appName, displayName: appName,
order: 0, order: 0,
}, },
], ],
order: 0, order: 0,
}, },
], ],
}, },
], ],
}, },
}, },
], ],
}); });
const workspaceName = await createWorkspace(page, template); const workspaceName = await createWorkspace(page, template);
const agent = await startAgent(page, token); const agent = await startAgent(page, token);
// Wait for the web terminal to open in a new tab // Wait for the web terminal to open in a new tab
const pagePromise = context.waitForEvent("page"); const pagePromise = context.waitForEvent("page");
await page.getByText(appName).click(); await page.getByText(appName).click();
const app = await pagePromise; const app = await pagePromise;
await app.waitForLoadState("domcontentloaded"); await app.waitForLoadState("domcontentloaded");
await app.getByText(appContent).isVisible(); await app.getByText(appContent).isVisible();
await stopWorkspace(page, workspaceName); await stopWorkspace(page, workspaceName);
await stopAgent(agent); await stopAgent(agent);
}); });
+52 -52
View File
@@ -1,69 +1,69 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { import {
createTemplate, createTemplate,
createWorkspace, createWorkspace,
requiresEnterpriseLicense, requiresEnterpriseLicense,
} from "../helpers"; } from "../helpers";
import { beforeCoderTest } from "../hooks"; import { beforeCoderTest } from "../hooks";
test.beforeEach(({ page }) => beforeCoderTest(page)); test.beforeEach(({ page }) => beforeCoderTest(page));
test("inspecting and filtering audit logs", async ({ page }) => { test("inspecting and filtering audit logs", async ({ page }) => {
requiresEnterpriseLicense(); requiresEnterpriseLicense();
const userName = "admin"; const userName = "admin";
// Do some stuff that should show up in the audit logs // Do some stuff that should show up in the audit logs
const templateName = await createTemplate(page); const templateName = await createTemplate(page);
const workspaceName = await createWorkspace(page, templateName); const workspaceName = await createWorkspace(page, templateName);
// Go to the audit history // Go to the audit history
await page.goto("/audit"); await page.goto("/audit");
// Make sure those things we did all actually show up // Make sure those things we did all actually show up
await expect(page.getByText(`${userName} logged in`)).toBeVisible(); await expect(page.getByText(`${userName} logged in`)).toBeVisible();
await expect( await expect(
page.getByText(`${userName} created template ${templateName}`), page.getByText(`${userName} created template ${templateName}`),
).toBeVisible(); ).toBeVisible();
await expect( await expect(
page.getByText(`${userName} created workspace ${workspaceName}`), page.getByText(`${userName} created workspace ${workspaceName}`),
).toBeVisible(); ).toBeVisible();
await expect( await expect(
page.getByText(`${userName} started workspace ${workspaceName}`), page.getByText(`${userName} started workspace ${workspaceName}`),
).toBeVisible(); ).toBeVisible();
// Make sure we can inspect the details of the log item // Make sure we can inspect the details of the log item
const createdWorkspace = page.locator(".MuiTableRow-root", { const createdWorkspace = page.locator(".MuiTableRow-root", {
hasText: `${userName} created workspace ${workspaceName}`, hasText: `${userName} created workspace ${workspaceName}`,
}); });
await createdWorkspace.getByLabel("open-dropdown").click(); await createdWorkspace.getByLabel("open-dropdown").click();
await expect( await expect(
createdWorkspace.getByText(`automatic_updates: "never"`), createdWorkspace.getByText(`automatic_updates: "never"`),
).toBeVisible(); ).toBeVisible();
await expect( await expect(
createdWorkspace.getByText(`name: "${workspaceName}"`), createdWorkspace.getByText(`name: "${workspaceName}"`),
).toBeVisible(); ).toBeVisible();
const startedWorkspaceMessage = `${userName} started workspace ${workspaceName}`; const startedWorkspaceMessage = `${userName} started workspace ${workspaceName}`;
const loginMessage = `${userName} logged in`; const loginMessage = `${userName} logged in`;
// Filter by resource type // Filter by resource type
await page.getByText("All resource types").click(); await page.getByText("All resource types").click();
await page.getByRole("menu").getByText("Workspace Build").click(); await page.getByRole("menu").getByText("Workspace Build").click();
// Our workspace build should be visible // Our workspace build should be visible
await expect(page.getByText(startedWorkspaceMessage)).toBeVisible(); await expect(page.getByText(startedWorkspaceMessage)).toBeVisible();
// Logins should no longer be visible // Logins should no longer be visible
await expect(page.getByText(loginMessage)).not.toBeVisible(); await expect(page.getByText(loginMessage)).not.toBeVisible();
// Clear filters, everything should be visible again // Clear filters, everything should be visible again
await page.getByLabel("Clear filter").click(); await page.getByLabel("Clear filter").click();
await expect(page.getByText(startedWorkspaceMessage)).toBeVisible(); await expect(page.getByText(startedWorkspaceMessage)).toBeVisible();
await expect(page.getByText(loginMessage)).toBeVisible(); await expect(page.getByText(loginMessage)).toBeVisible();
// Filter by action type // Filter by action type
await page.getByText("All actions").click(); await page.getByText("All actions").click();
await page.getByRole("menu").getByText("Login").click(); await page.getByRole("menu").getByText("Login").click();
// Logins should be visible // Logins should be visible
await expect(page.getByText(loginMessage)).toBeVisible(); await expect(page.getByText(loginMessage)).toBeVisible();
// Our workspace build should no longer be visible // Our workspace build should no longer be visible
await expect(page.getByText(startedWorkspaceMessage)).not.toBeVisible(); await expect(page.getByText(startedWorkspaceMessage)).not.toBeVisible();
}); });
+53 -53
View File
@@ -3,80 +3,80 @@ import { expectUrl } from "../../expectUrl";
import { randomName, requiresEnterpriseLicense } from "../../helpers"; import { randomName, requiresEnterpriseLicense } from "../../helpers";
test("set application name", async ({ page }) => { test("set application name", async ({ page }) => {
requiresEnterpriseLicense(); requiresEnterpriseLicense();
await page.goto("/deployment/appearance", { waitUntil: "domcontentloaded" }); await page.goto("/deployment/appearance", { waitUntil: "domcontentloaded" });
const applicationName = randomName(); const applicationName = randomName();
// Fill out the form // Fill out the form
const form = page.locator("form", { hasText: "Application name" }); const form = page.locator("form", { hasText: "Application name" });
await form await form
.getByLabel("Application name", { exact: true }) .getByLabel("Application name", { exact: true })
.fill(applicationName); .fill(applicationName);
await form.getByRole("button", { name: "Submit" }).click(); await form.getByRole("button", { name: "Submit" }).click();
// Open a new session without cookies to see the login page // Open a new session without cookies to see the login page
const browser = await chromium.launch(); const browser = await chromium.launch();
const incognitoContext = await browser.newContext(); const incognitoContext = await browser.newContext();
await incognitoContext.clearCookies(); await incognitoContext.clearCookies();
const incognitoPage = await incognitoContext.newPage(); const incognitoPage = await incognitoContext.newPage();
await incognitoPage.goto("/", { waitUntil: "domcontentloaded" }); await incognitoPage.goto("/", { waitUntil: "domcontentloaded" });
// Verify the application name // Verify the application name
const name = incognitoPage.locator("h1", { hasText: applicationName }); const name = incognitoPage.locator("h1", { hasText: applicationName });
await expect(name).toBeVisible(); await expect(name).toBeVisible();
// Shut down browser // Shut down browser
await incognitoPage.close(); await incognitoPage.close();
await browser.close(); await browser.close();
}); });
test("set application logo", async ({ page }) => { test("set application logo", async ({ page }) => {
requiresEnterpriseLicense(); requiresEnterpriseLicense();
await page.goto("/deployment/appearance", { waitUntil: "domcontentloaded" }); await page.goto("/deployment/appearance", { waitUntil: "domcontentloaded" });
const imageLink = "/icon/azure.png"; const imageLink = "/icon/azure.png";
// Fill out the form // Fill out the form
const form = page.locator("form", { hasText: "Logo URL" }); const form = page.locator("form", { hasText: "Logo URL" });
await form.getByLabel("Logo URL", { exact: true }).fill(imageLink); await form.getByLabel("Logo URL", { exact: true }).fill(imageLink);
await form.getByRole("button", { name: "Submit" }).click(); await form.getByRole("button", { name: "Submit" }).click();
// Open a new session without cookies to see the login page // Open a new session without cookies to see the login page
const browser = await chromium.launch(); const browser = await chromium.launch();
const incognitoContext = await browser.newContext(); const incognitoContext = await browser.newContext();
await incognitoContext.clearCookies(); await incognitoContext.clearCookies();
const incognitoPage = await incognitoContext.newPage(); const incognitoPage = await incognitoContext.newPage();
await incognitoPage.goto("/", { waitUntil: "domcontentloaded" }); await incognitoPage.goto("/", { waitUntil: "domcontentloaded" });
// Verify banner // Verify banner
const logo = incognitoPage.locator("img.application-logo"); const logo = incognitoPage.locator("img.application-logo");
await expect(logo).toHaveAttribute("src", imageLink); await expect(logo).toHaveAttribute("src", imageLink);
// Shut down browser // Shut down browser
await incognitoPage.close(); await incognitoPage.close();
await browser.close(); await browser.close();
}); });
test("set service banner", async ({ page }) => { test("set service banner", async ({ page }) => {
requiresEnterpriseLicense(); requiresEnterpriseLicense();
await page.goto("/deployment/appearance", { waitUntil: "domcontentloaded" }); await page.goto("/deployment/appearance", { waitUntil: "domcontentloaded" });
const message = "Mary has a little lamb."; const message = "Mary has a little lamb.";
// Fill out the form // Fill out the form
const form = page.locator("form", { hasText: "Service Banner" }); const form = page.locator("form", { hasText: "Service Banner" });
await form.getByLabel("Enabled", { exact: true }).check(); await form.getByLabel("Enabled", { exact: true }).check();
await form.getByLabel("Message", { exact: true }).fill(message); await form.getByLabel("Message", { exact: true }).fill(message);
await form.getByRole("button", { name: "Submit" }).click(); await form.getByRole("button", { name: "Submit" }).click();
// Verify service banner // Verify service banner
await page.goto("/workspaces", { waitUntil: "domcontentloaded" }); await page.goto("/workspaces", { waitUntil: "domcontentloaded" });
await expectUrl(page).toHavePathName("/workspaces"); await expectUrl(page).toHavePathName("/workspaces");
const bar = page.locator("div.service-banner", { hasText: message }); const bar = page.locator("div.service-banner", { hasText: message });
await expect(bar).toBeVisible(); await expect(bar).toBeVisible();
}); });
+27 -27
View File
@@ -4,36 +4,36 @@ import { setupApiCalls } from "../../api";
import { e2eFakeExperiment1, e2eFakeExperiment2 } from "../../constants"; import { e2eFakeExperiment1, e2eFakeExperiment2 } from "../../constants";
test("experiments", async ({ page }) => { test("experiments", async ({ page }) => {
await setupApiCalls(page); await setupApiCalls(page);
// Load experiments from backend API // Load experiments from backend API
const availableExperiments = await API.getAvailableExperiments(); const availableExperiments = await API.getAvailableExperiments();
// Verify if the site lists the same experiments // Verify if the site lists the same experiments
await page.goto("/deployment/general", { waitUntil: "networkidle" }); await page.goto("/deployment/general", { waitUntil: "networkidle" });
const experimentsLocator = page.locator( const experimentsLocator = page.locator(
"div.options-table tr.option-experiments ul.option-array", "div.options-table tr.option-experiments ul.option-array",
); );
await expect(experimentsLocator).toBeVisible(); await expect(experimentsLocator).toBeVisible();
// Firstly, check if all enabled experiments are listed // Firstly, check if all enabled experiments are listed
expect( expect(
experimentsLocator.locator( experimentsLocator.locator(
`li.option-array-item-${e2eFakeExperiment1}.option-enabled`, `li.option-array-item-${e2eFakeExperiment1}.option-enabled`,
), ),
).toBeVisible; ).toBeVisible;
expect( expect(
experimentsLocator.locator( experimentsLocator.locator(
`li.option-array-item-${e2eFakeExperiment2}.option-enabled`, `li.option-array-item-${e2eFakeExperiment2}.option-enabled`,
), ),
).toBeVisible; ).toBeVisible;
// Secondly, check if available experiments are listed // Secondly, check if available experiments are listed
for (const experiment of availableExperiments.safe) { for (const experiment of availableExperiments.safe) {
const experimentLocator = experimentsLocator.locator( const experimentLocator = experimentsLocator.locator(
`li.option-array-item-${experiment}`, `li.option-array-item-${experiment}`,
); );
await expect(experimentLocator).toBeVisible(); await expect(experimentLocator).toBeVisible();
} }
}); });
+20 -20
View File
@@ -2,29 +2,29 @@ import { expect, test } from "@playwright/test";
import { requiresEnterpriseLicense } from "../../helpers"; import { requiresEnterpriseLicense } from "../../helpers";
test("license was added successfully", async ({ page }) => { test("license was added successfully", async ({ page }) => {
requiresEnterpriseLicense(); requiresEnterpriseLicense();
await page.goto("/deployment/licenses", { waitUntil: "domcontentloaded" }); await page.goto("/deployment/licenses", { waitUntil: "domcontentloaded" });
const firstLicense = page.locator(".licenses > .license-card", { const firstLicense = page.locator(".licenses > .license-card", {
hasText: "#1", hasText: "#1",
}); });
await expect(firstLicense).toBeVisible(); await expect(firstLicense).toBeVisible();
// Trial vs. Enterprise? // Trial vs. Enterprise?
const accountType = firstLicense.locator(".account-type"); const accountType = firstLicense.locator(".account-type");
await expect(accountType).toHaveText("Enterprise"); await expect(accountType).toHaveText("Enterprise");
// User limit 1/1 // User limit 1/1
const userLimit = firstLicense.locator(".user-limit"); const userLimit = firstLicense.locator(".user-limit");
await expect(userLimit).toHaveText("1 / 1"); await expect(userLimit).toHaveText("1 / 1");
// License should not be expired yet // License should not be expired yet
const licenseExpires = firstLicense.locator(".license-expires"); const licenseExpires = firstLicense.locator(".license-expires");
const licenseExpiresDate = new Date(await licenseExpires.innerText()); const licenseExpiresDate = new Date(await licenseExpires.innerText());
const now = new Date(); const now = new Date();
expect(licenseExpiresDate.getTime()).toBeGreaterThan(now.getTime()); expect(licenseExpiresDate.getTime()).toBeGreaterThan(now.getTime());
// "Remove" button should be visible // "Remove" button should be visible
const removeButton = firstLicense.locator(".remove-button"); const removeButton = firstLicense.locator(".remove-button");
await expect(removeButton).toBeVisible(); await expect(removeButton).toBeVisible();
}); });
+31 -31
View File
@@ -1,40 +1,40 @@
import { test } from "@playwright/test"; import { test } from "@playwright/test";
import { API } from "api/api"; import { API } from "api/api";
import { import {
setupApiCalls, setupApiCalls,
verifyConfigFlagArray, verifyConfigFlagArray,
verifyConfigFlagBoolean, verifyConfigFlagBoolean,
verifyConfigFlagDuration, verifyConfigFlagDuration,
verifyConfigFlagNumber, verifyConfigFlagNumber,
verifyConfigFlagString, verifyConfigFlagString,
} from "../../api"; } from "../../api";
test("enabled network settings", async ({ page }) => { test("enabled network settings", async ({ page }) => {
await setupApiCalls(page); await setupApiCalls(page);
const config = await API.getDeploymentConfig(); const config = await API.getDeploymentConfig();
await page.goto("/deployment/network", { waitUntil: "domcontentloaded" }); await page.goto("/deployment/network", { waitUntil: "domcontentloaded" });
await verifyConfigFlagString(page, config, "access-url"); await verifyConfigFlagString(page, config, "access-url");
await verifyConfigFlagBoolean(page, config, "block-direct-connections"); await verifyConfigFlagBoolean(page, config, "block-direct-connections");
await verifyConfigFlagBoolean(page, config, "browser-only"); await verifyConfigFlagBoolean(page, config, "browser-only");
await verifyConfigFlagBoolean(page, config, "derp-force-websockets"); await verifyConfigFlagBoolean(page, config, "derp-force-websockets");
await verifyConfigFlagBoolean(page, config, "derp-server-enable"); await verifyConfigFlagBoolean(page, config, "derp-server-enable");
await verifyConfigFlagString(page, config, "derp-server-region-code"); await verifyConfigFlagString(page, config, "derp-server-region-code");
await verifyConfigFlagString(page, config, "derp-server-region-code"); await verifyConfigFlagString(page, config, "derp-server-region-code");
await verifyConfigFlagNumber(page, config, "derp-server-region-id"); await verifyConfigFlagNumber(page, config, "derp-server-region-id");
await verifyConfigFlagString(page, config, "derp-server-region-name"); await verifyConfigFlagString(page, config, "derp-server-region-name");
await verifyConfigFlagArray(page, config, "derp-server-stun-addresses"); await verifyConfigFlagArray(page, config, "derp-server-stun-addresses");
await verifyConfigFlagBoolean(page, config, "disable-password-auth"); await verifyConfigFlagBoolean(page, config, "disable-password-auth");
await verifyConfigFlagBoolean(page, config, "disable-session-expiry-refresh"); await verifyConfigFlagBoolean(page, config, "disable-session-expiry-refresh");
await verifyConfigFlagDuration(page, config, "max-token-lifetime"); await verifyConfigFlagDuration(page, config, "max-token-lifetime");
await verifyConfigFlagDuration(page, config, "proxy-health-interval"); await verifyConfigFlagDuration(page, config, "proxy-health-interval");
await verifyConfigFlagBoolean(page, config, "redirect-to-access-url"); await verifyConfigFlagBoolean(page, config, "redirect-to-access-url");
await verifyConfigFlagBoolean(page, config, "secure-auth-cookie"); await verifyConfigFlagBoolean(page, config, "secure-auth-cookie");
await verifyConfigFlagDuration(page, config, "session-duration"); await verifyConfigFlagDuration(page, config, "session-duration");
await verifyConfigFlagString(page, config, "tls-address"); await verifyConfigFlagString(page, config, "tls-address");
await verifyConfigFlagBoolean(page, config, "tls-allow-insecure-ciphers"); await verifyConfigFlagBoolean(page, config, "tls-allow-insecure-ciphers");
await verifyConfigFlagString(page, config, "tls-client-auth"); await verifyConfigFlagString(page, config, "tls-client-auth");
await verifyConfigFlagBoolean(page, config, "tls-enable"); await verifyConfigFlagBoolean(page, config, "tls-enable");
await verifyConfigFlagString(page, config, "tls-min-version"); await verifyConfigFlagString(page, config, "tls-min-version");
}); });
+30 -30
View File
@@ -1,39 +1,39 @@
import { test } from "@playwright/test"; import { test } from "@playwright/test";
import { API } from "api/api"; import { API } from "api/api";
import { import {
setupApiCalls, setupApiCalls,
verifyConfigFlagArray, verifyConfigFlagArray,
verifyConfigFlagBoolean, verifyConfigFlagBoolean,
verifyConfigFlagDuration, verifyConfigFlagDuration,
verifyConfigFlagEmpty, verifyConfigFlagEmpty,
verifyConfigFlagString, verifyConfigFlagString,
} from "../../api"; } from "../../api";
test("enabled observability settings", async ({ page }) => { test("enabled observability settings", async ({ page }) => {
await setupApiCalls(page); await setupApiCalls(page);
const config = await API.getDeploymentConfig(); const config = await API.getDeploymentConfig();
await page.goto("/deployment/observability", { await page.goto("/deployment/observability", {
waitUntil: "domcontentloaded", waitUntil: "domcontentloaded",
}); });
await verifyConfigFlagBoolean(page, config, "trace-logs"); await verifyConfigFlagBoolean(page, config, "trace-logs");
await verifyConfigFlagBoolean(page, config, "enable-terraform-debug-mode"); await verifyConfigFlagBoolean(page, config, "enable-terraform-debug-mode");
await verifyConfigFlagBoolean(page, config, "enable-terraform-debug-mode"); await verifyConfigFlagBoolean(page, config, "enable-terraform-debug-mode");
await verifyConfigFlagDuration(page, config, "health-check-refresh"); await verifyConfigFlagDuration(page, config, "health-check-refresh");
await verifyConfigFlagEmpty(page, "health-check-threshold-database"); await verifyConfigFlagEmpty(page, "health-check-threshold-database");
await verifyConfigFlagString(page, config, "log-human"); await verifyConfigFlagString(page, config, "log-human");
await verifyConfigFlagString(page, config, "prometheus-address"); await verifyConfigFlagString(page, config, "prometheus-address");
await verifyConfigFlagArray( await verifyConfigFlagArray(
page, page,
config, config,
"prometheus-aggregate-agent-stats-by", "prometheus-aggregate-agent-stats-by",
); );
await verifyConfigFlagBoolean(page, config, "prometheus-collect-agent-stats"); await verifyConfigFlagBoolean(page, config, "prometheus-collect-agent-stats");
await verifyConfigFlagBoolean(page, config, "prometheus-collect-db-metrics"); await verifyConfigFlagBoolean(page, config, "prometheus-collect-db-metrics");
await verifyConfigFlagBoolean(page, config, "prometheus-enable"); await verifyConfigFlagBoolean(page, config, "prometheus-enable");
await verifyConfigFlagBoolean(page, config, "trace-datadog"); await verifyConfigFlagBoolean(page, config, "trace-datadog");
await verifyConfigFlagBoolean(page, config, "trace"); await verifyConfigFlagBoolean(page, config, "trace");
await verifyConfigFlagBoolean(page, config, "verbose"); await verifyConfigFlagBoolean(page, config, "verbose");
await verifyConfigFlagBoolean(page, config, "pprof-enable"); await verifyConfigFlagBoolean(page, config, "pprof-enable");
}); });
+30 -30
View File
@@ -2,45 +2,45 @@ import type { Page } from "@playwright/test";
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { API, type DeploymentConfig } from "api/api"; import { API, type DeploymentConfig } from "api/api";
import { import {
findConfigOption, findConfigOption,
setupApiCalls, setupApiCalls,
verifyConfigFlagBoolean, verifyConfigFlagBoolean,
verifyConfigFlagNumber, verifyConfigFlagNumber,
verifyConfigFlagString, verifyConfigFlagString,
} from "../../api"; } from "../../api";
test("enabled security settings", async ({ page }) => { test("enabled security settings", async ({ page }) => {
await setupApiCalls(page); await setupApiCalls(page);
const config = await API.getDeploymentConfig(); const config = await API.getDeploymentConfig();
await page.goto("/deployment/security", { waitUntil: "domcontentloaded" }); await page.goto("/deployment/security", { waitUntil: "domcontentloaded" });
await verifyConfigFlagString(page, config, "ssh-keygen-algorithm"); await verifyConfigFlagString(page, config, "ssh-keygen-algorithm");
await verifyConfigFlagBoolean(page, config, "secure-auth-cookie"); await verifyConfigFlagBoolean(page, config, "secure-auth-cookie");
await verifyConfigFlagBoolean(page, config, "disable-owner-workspace-access"); await verifyConfigFlagBoolean(page, config, "disable-owner-workspace-access");
await verifyConfigFlagBoolean(page, config, "tls-redirect-http-to-https"); await verifyConfigFlagBoolean(page, config, "tls-redirect-http-to-https");
await verifyStrictTransportSecurity(page, config); await verifyStrictTransportSecurity(page, config);
await verifyConfigFlagString(page, config, "tls-address"); await verifyConfigFlagString(page, config, "tls-address");
await verifyConfigFlagBoolean(page, config, "tls-allow-insecure-ciphers"); await verifyConfigFlagBoolean(page, config, "tls-allow-insecure-ciphers");
await verifyConfigFlagString(page, config, "tls-client-auth"); await verifyConfigFlagString(page, config, "tls-client-auth");
await verifyConfigFlagBoolean(page, config, "tls-enable"); await verifyConfigFlagBoolean(page, config, "tls-enable");
await verifyConfigFlagString(page, config, "tls-min-version"); await verifyConfigFlagString(page, config, "tls-min-version");
}); });
async function verifyStrictTransportSecurity( async function verifyStrictTransportSecurity(
page: Page, page: Page,
config: DeploymentConfig, config: DeploymentConfig,
) { ) {
const flag = "strict-transport-security"; const flag = "strict-transport-security";
const opt = findConfigOption(config, flag); const opt = findConfigOption(config, flag);
if (opt.value !== 0) { if (opt.value !== 0) {
await verifyConfigFlagNumber(page, config, flag); await verifyConfigFlagNumber(page, config, flag);
return; return;
} }
const configOption = page.locator( const configOption = page.locator(
`div.options-table .option-${flag} .option-value-string`, `div.options-table .option-${flag} .option-value-string`,
); );
await expect(configOption).toHaveText("Disabled"); await expect(configOption).toHaveText("Disabled");
} }
+24 -24
View File
@@ -1,33 +1,33 @@
import { test } from "@playwright/test"; import { test } from "@playwright/test";
import { API } from "api/api"; import { API } from "api/api";
import { import {
setupApiCalls, setupApiCalls,
verifyConfigFlagArray, verifyConfigFlagArray,
verifyConfigFlagBoolean, verifyConfigFlagBoolean,
verifyConfigFlagEntries, verifyConfigFlagEntries,
verifyConfigFlagString, verifyConfigFlagString,
} from "../../api"; } from "../../api";
test("login with OIDC", async ({ page }) => { test("login with OIDC", async ({ page }) => {
await setupApiCalls(page); await setupApiCalls(page);
const config = await API.getDeploymentConfig(); const config = await API.getDeploymentConfig();
await page.goto("/deployment/userauth", { waitUntil: "domcontentloaded" }); await page.goto("/deployment/userauth", { waitUntil: "domcontentloaded" });
await verifyConfigFlagBoolean(page, config, "oidc-group-auto-create"); await verifyConfigFlagBoolean(page, config, "oidc-group-auto-create");
await verifyConfigFlagBoolean(page, config, "oidc-allow-signups"); await verifyConfigFlagBoolean(page, config, "oidc-allow-signups");
await verifyConfigFlagEntries(page, config, "oidc-auth-url-params"); await verifyConfigFlagEntries(page, config, "oidc-auth-url-params");
await verifyConfigFlagString(page, config, "oidc-client-id"); await verifyConfigFlagString(page, config, "oidc-client-id");
await verifyConfigFlagArray(page, config, "oidc-email-domain"); await verifyConfigFlagArray(page, config, "oidc-email-domain");
await verifyConfigFlagString(page, config, "oidc-email-field"); await verifyConfigFlagString(page, config, "oidc-email-field");
await verifyConfigFlagEntries(page, config, "oidc-group-mapping"); await verifyConfigFlagEntries(page, config, "oidc-group-mapping");
await verifyConfigFlagBoolean(page, config, "oidc-ignore-email-verified"); await verifyConfigFlagBoolean(page, config, "oidc-ignore-email-verified");
await verifyConfigFlagBoolean(page, config, "oidc-ignore-userinfo"); await verifyConfigFlagBoolean(page, config, "oidc-ignore-userinfo");
await verifyConfigFlagString(page, config, "oidc-issuer-url"); await verifyConfigFlagString(page, config, "oidc-issuer-url");
await verifyConfigFlagString(page, config, "oidc-group-regex-filter"); await verifyConfigFlagString(page, config, "oidc-group-regex-filter");
await verifyConfigFlagArray(page, config, "oidc-scopes"); await verifyConfigFlagArray(page, config, "oidc-scopes");
await verifyConfigFlagEntries(page, config, "oidc-user-role-mapping"); await verifyConfigFlagEntries(page, config, "oidc-user-role-mapping");
await verifyConfigFlagString(page, config, "oidc-username-field"); await verifyConfigFlagString(page, config, "oidc-username-field");
await verifyConfigFlagString(page, config, "oidc-sign-in-text"); await verifyConfigFlagString(page, config, "oidc-sign-in-text");
await verifyConfigFlagString(page, config, "oidc-icon-url"); await verifyConfigFlagString(page, config, "oidc-icon-url");
}); });
@@ -6,100 +6,100 @@ import { randomName, requiresEnterpriseLicense } from "../../helpers";
import { startWorkspaceProxy, stopWorkspaceProxy } from "../../proxy"; import { startWorkspaceProxy, stopWorkspaceProxy } from "../../proxy";
test("default proxy is online", async ({ page }) => { test("default proxy is online", async ({ page }) => {
requiresEnterpriseLicense(); requiresEnterpriseLicense();
await setupApiCalls(page); await setupApiCalls(page);
await page.goto("/deployment/workspace-proxies", { await page.goto("/deployment/workspace-proxies", {
waitUntil: "domcontentloaded", waitUntil: "domcontentloaded",
}); });
// Verify if the default proxy is healthy // Verify if the default proxy is healthy
const workspaceProxyPrimary = page.locator( const workspaceProxyPrimary = page.locator(
`table.MuiTable-root tr[data-testid="primary"]`, `table.MuiTable-root tr[data-testid="primary"]`,
); );
const workspaceProxyName = workspaceProxyPrimary.locator("td.name span"); const workspaceProxyName = workspaceProxyPrimary.locator("td.name span");
const workspaceProxyURL = workspaceProxyPrimary.locator("td.url"); const workspaceProxyURL = workspaceProxyPrimary.locator("td.url");
const workspaceProxyStatus = workspaceProxyPrimary.locator("td.status span"); const workspaceProxyStatus = workspaceProxyPrimary.locator("td.status span");
await expect(workspaceProxyName).toHaveText("Default"); await expect(workspaceProxyName).toHaveText("Default");
await expect(workspaceProxyURL).toHaveText(`http://localhost:${coderPort}`); await expect(workspaceProxyURL).toHaveText(`http://localhost:${coderPort}`);
await expect(workspaceProxyStatus).toHaveText("Healthy"); await expect(workspaceProxyStatus).toHaveText("Healthy");
}); });
test("custom proxy is online", async ({ page }) => { test("custom proxy is online", async ({ page }) => {
requiresEnterpriseLicense(); requiresEnterpriseLicense();
await setupApiCalls(page); await setupApiCalls(page);
const proxyName = randomName(); const proxyName = randomName();
// Register workspace proxy // Register workspace proxy
const proxyResponse = await API.createWorkspaceProxy({ const proxyResponse = await API.createWorkspaceProxy({
name: proxyName, name: proxyName,
display_name: "", display_name: "",
icon: "/emojis/1f1e7-1f1f7.png", icon: "/emojis/1f1e7-1f1f7.png",
}); });
expect(proxyResponse.proxy_token).toBeDefined(); expect(proxyResponse.proxy_token).toBeDefined();
// Start "wsproxy server" // Start "wsproxy server"
const proxyServer = await startWorkspaceProxy(proxyResponse.proxy_token); const proxyServer = await startWorkspaceProxy(proxyResponse.proxy_token);
await waitUntilWorkspaceProxyIsHealthy(page, proxyName); await waitUntilWorkspaceProxyIsHealthy(page, proxyName);
// Verify if custom proxy is healthy // Verify if custom proxy is healthy
await page.goto("/deployment/workspace-proxies", { await page.goto("/deployment/workspace-proxies", {
waitUntil: "domcontentloaded", waitUntil: "domcontentloaded",
}); });
const workspaceProxy = page.locator("table.MuiTable-root tr", { const workspaceProxy = page.locator("table.MuiTable-root tr", {
hasText: proxyName, hasText: proxyName,
}); });
const workspaceProxyName = workspaceProxy.locator("td.name span"); const workspaceProxyName = workspaceProxy.locator("td.name span");
const workspaceProxyURL = workspaceProxy.locator("td.url"); const workspaceProxyURL = workspaceProxy.locator("td.url");
const workspaceProxyStatus = workspaceProxy.locator("td.status span"); const workspaceProxyStatus = workspaceProxy.locator("td.status span");
await expect(workspaceProxyName).toHaveText(proxyName); await expect(workspaceProxyName).toHaveText(proxyName);
await expect(workspaceProxyURL).toHaveText( await expect(workspaceProxyURL).toHaveText(
`http://127.0.0.1:${workspaceProxyPort}`, `http://127.0.0.1:${workspaceProxyPort}`,
); );
await expect(workspaceProxyStatus).toHaveText("Healthy"); await expect(workspaceProxyStatus).toHaveText("Healthy");
// Tear down the proxy // Tear down the proxy
await stopWorkspaceProxy(proxyServer); await stopWorkspaceProxy(proxyServer);
}); });
const waitUntilWorkspaceProxyIsHealthy = async ( const waitUntilWorkspaceProxyIsHealthy = async (
page: Page, page: Page,
proxyName: string, proxyName: string,
) => { ) => {
await page.goto("/deployment/workspace-proxies", { await page.goto("/deployment/workspace-proxies", {
waitUntil: "domcontentloaded", waitUntil: "domcontentloaded",
}); });
const maxRetries = 30; const maxRetries = 30;
const retryIntervalMs = 1000; const retryIntervalMs = 1000;
let retries = 0; let retries = 0;
while (retries < maxRetries) { while (retries < maxRetries) {
await page.reload(); await page.reload();
const workspaceProxy = page.locator("table.MuiTable-root tr", { const workspaceProxy = page.locator("table.MuiTable-root tr", {
hasText: proxyName, hasText: proxyName,
}); });
const workspaceProxyStatus = workspaceProxy.locator("td.status span"); const workspaceProxyStatus = workspaceProxy.locator("td.status span");
try { try {
await expect(workspaceProxyStatus).toHaveText("Healthy", { await expect(workspaceProxyStatus).toHaveText("Healthy", {
timeout: 1_000, timeout: 1_000,
}); });
return; // healthy! return; // healthy!
} catch { } catch {
retries++; retries++;
await new Promise((resolve) => setTimeout(resolve, retryIntervalMs)); await new Promise((resolve) => setTimeout(resolve, retryIntervalMs));
} }
} }
throw new Error( throw new Error(
`Workspace proxy "${proxyName}" is unhealthy after ${ `Workspace proxy "${proxyName}" is unhealthy after ${
maxRetries * retryIntervalMs maxRetries * retryIntervalMs
}ms`, }ms`,
); );
}; };
+131 -131
View File
@@ -3,32 +3,32 @@ import { test } from "@playwright/test";
import type { ExternalAuthDevice } from "api/typesGenerated"; import type { ExternalAuthDevice } from "api/typesGenerated";
import { gitAuth } from "../constants"; import { gitAuth } from "../constants";
import { import {
Awaiter, Awaiter,
createServer, createServer,
createTemplate, createTemplate,
createWorkspace, createWorkspace,
echoResponsesWithExternalAuth, echoResponsesWithExternalAuth,
} from "../helpers"; } from "../helpers";
import { beforeCoderTest, resetExternalAuthKey } from "../hooks"; import { beforeCoderTest, resetExternalAuthKey } from "../hooks";
test.beforeAll(async ({ baseURL }) => { test.beforeAll(async ({ baseURL }) => {
const srv = await createServer(gitAuth.webPort); const srv = await createServer(gitAuth.webPort);
// The GitHub validate endpoint returns the currently authenticated user! // The GitHub validate endpoint returns the currently authenticated user!
srv.use(gitAuth.validatePath, (req, res) => { srv.use(gitAuth.validatePath, (req, res) => {
res.write(JSON.stringify(ghUser)); res.write(JSON.stringify(ghUser));
res.end(); res.end();
}); });
srv.use(gitAuth.tokenPath, (req, res) => { srv.use(gitAuth.tokenPath, (req, res) => {
const r = (Math.random() + 1).toString(36).substring(7); const r = (Math.random() + 1).toString(36).substring(7);
res.write(JSON.stringify({ access_token: r })); res.write(JSON.stringify({ access_token: r }));
res.end(); res.end();
}); });
srv.use(gitAuth.authPath, (req, res) => { srv.use(gitAuth.authPath, (req, res) => {
res.redirect( res.redirect(
`${baseURL}/external-auth/${gitAuth.webProvider}/callback?code=1234&state=${req.query.state}`, `${baseURL}/external-auth/${gitAuth.webProvider}/callback?code=1234&state=${req.query.state}`,
); );
}); });
}); });
test.beforeEach(async ({ context }) => resetExternalAuthKey(context)); test.beforeEach(async ({ context }) => resetExternalAuthKey(context));
@@ -37,130 +37,130 @@ test.beforeEach(({ page }) => beforeCoderTest(page));
// Ensures that a Git auth provider with the device flow functions and completes! // Ensures that a Git auth provider with the device flow functions and completes!
test("external auth device", async ({ page }) => { test("external auth device", async ({ page }) => {
const device: ExternalAuthDevice = { const device: ExternalAuthDevice = {
device_code: "1234", device_code: "1234",
user_code: "1234-5678", user_code: "1234-5678",
expires_in: 900, expires_in: 900,
interval: 1, interval: 1,
verification_uri: "", verification_uri: "",
}; };
// Start a server to mock the GitHub API. // Start a server to mock the GitHub API.
const srv = await createServer(gitAuth.devicePort); const srv = await createServer(gitAuth.devicePort);
srv.use(gitAuth.validatePath, (req, res) => { srv.use(gitAuth.validatePath, (req, res) => {
res.write(JSON.stringify(ghUser)); res.write(JSON.stringify(ghUser));
res.end(); res.end();
}); });
srv.use(gitAuth.codePath, (req, res) => { srv.use(gitAuth.codePath, (req, res) => {
res.write(JSON.stringify(device)); res.write(JSON.stringify(device));
res.end(); res.end();
}); });
srv.use(gitAuth.installationsPath, (req, res) => { srv.use(gitAuth.installationsPath, (req, res) => {
res.write(JSON.stringify(ghInstall)); res.write(JSON.stringify(ghInstall));
res.end(); res.end();
}); });
const token = { const token = {
access_token: "", access_token: "",
error: "authorization_pending", error: "authorization_pending",
error_description: "", error_description: "",
}; };
// First we send a result from the API that the token hasn't been // First we send a result from the API that the token hasn't been
// authorized yet to ensure the UI reacts properly. // authorized yet to ensure the UI reacts properly.
const sentPending = new Awaiter(); const sentPending = new Awaiter();
srv.use(gitAuth.tokenPath, (req, res) => { srv.use(gitAuth.tokenPath, (req, res) => {
res.write(JSON.stringify(token)); res.write(JSON.stringify(token));
res.end(); res.end();
sentPending.done(); sentPending.done();
}); });
await page.goto(`/external-auth/${gitAuth.deviceProvider}`, { await page.goto(`/external-auth/${gitAuth.deviceProvider}`, {
waitUntil: "domcontentloaded", waitUntil: "domcontentloaded",
}); });
await page.getByText(device.user_code).isVisible(); await page.getByText(device.user_code).isVisible();
await sentPending.wait(); await sentPending.wait();
// Update the token to be valid and ensure the UI updates! // Update the token to be valid and ensure the UI updates!
token.error = ""; token.error = "";
token.access_token = "hello-world"; token.access_token = "hello-world";
await page.waitForSelector("text=1 organization authorized"); await page.waitForSelector("text=1 organization authorized");
}); });
test("external auth web", async ({ page }) => { test("external auth web", async ({ page }) => {
await page.goto(`/external-auth/${gitAuth.webProvider}`, { await page.goto(`/external-auth/${gitAuth.webProvider}`, {
waitUntil: "domcontentloaded", waitUntil: "domcontentloaded",
}); });
// This endpoint doesn't have the installations URL set intentionally! // This endpoint doesn't have the installations URL set intentionally!
await page.waitForSelector("text=You've authenticated with GitHub!"); await page.waitForSelector("text=You've authenticated with GitHub!");
}); });
test("successful external auth from workspace", async ({ page }) => { test("successful external auth from workspace", async ({ page }) => {
const templateName = await createTemplate( const templateName = await createTemplate(
page, page,
echoResponsesWithExternalAuth([ echoResponsesWithExternalAuth([
{ id: gitAuth.webProvider, optional: false }, { id: gitAuth.webProvider, optional: false },
]), ]),
); );
await createWorkspace(page, templateName, [], [], gitAuth.webProvider); await createWorkspace(page, templateName, [], [], gitAuth.webProvider);
}); });
const ghUser: Endpoints["GET /user"]["response"]["data"] = { const ghUser: Endpoints["GET /user"]["response"]["data"] = {
login: "kylecarbs", login: "kylecarbs",
id: 7122116, id: 7122116,
node_id: "MDQ6VXNlcjcxMjIxMTY=", node_id: "MDQ6VXNlcjcxMjIxMTY=",
avatar_url: "https://avatars.githubusercontent.com/u/7122116?v=4", avatar_url: "https://avatars.githubusercontent.com/u/7122116?v=4",
gravatar_id: "", gravatar_id: "",
url: "https://api.github.com/users/kylecarbs", url: "https://api.github.com/users/kylecarbs",
html_url: "https://github.com/kylecarbs", html_url: "https://github.com/kylecarbs",
followers_url: "https://api.github.com/users/kylecarbs/followers", followers_url: "https://api.github.com/users/kylecarbs/followers",
following_url: following_url:
"https://api.github.com/users/kylecarbs/following{/other_user}", "https://api.github.com/users/kylecarbs/following{/other_user}",
gists_url: "https://api.github.com/users/kylecarbs/gists{/gist_id}", gists_url: "https://api.github.com/users/kylecarbs/gists{/gist_id}",
starred_url: "https://api.github.com/users/kylecarbs/starred{/owner}{/repo}", starred_url: "https://api.github.com/users/kylecarbs/starred{/owner}{/repo}",
subscriptions_url: "https://api.github.com/users/kylecarbs/subscriptions", subscriptions_url: "https://api.github.com/users/kylecarbs/subscriptions",
organizations_url: "https://api.github.com/users/kylecarbs/orgs", organizations_url: "https://api.github.com/users/kylecarbs/orgs",
repos_url: "https://api.github.com/users/kylecarbs/repos", repos_url: "https://api.github.com/users/kylecarbs/repos",
events_url: "https://api.github.com/users/kylecarbs/events{/privacy}", events_url: "https://api.github.com/users/kylecarbs/events{/privacy}",
received_events_url: "https://api.github.com/users/kylecarbs/received_events", received_events_url: "https://api.github.com/users/kylecarbs/received_events",
type: "User", type: "User",
site_admin: false, site_admin: false,
name: "Kyle Carberry", name: "Kyle Carberry",
company: "@coder", company: "@coder",
blog: "https://carberry.com", blog: "https://carberry.com",
location: "Austin, TX", location: "Austin, TX",
email: "kyle@carberry.com", email: "kyle@carberry.com",
hireable: null, hireable: null,
bio: "hey there", bio: "hey there",
twitter_username: "kylecarbs", twitter_username: "kylecarbs",
public_repos: 52, public_repos: 52,
public_gists: 9, public_gists: 9,
followers: 208, followers: 208,
following: 31, following: 31,
created_at: "2014-04-01T02:24:41Z", created_at: "2014-04-01T02:24:41Z",
updated_at: "2023-06-26T13:03:09Z", updated_at: "2023-06-26T13:03:09Z",
}; };
const ghInstall: Endpoints["GET /user/installations"]["response"]["data"] = { const ghInstall: Endpoints["GET /user/installations"]["response"]["data"] = {
installations: [ installations: [
{ {
id: 1, id: 1,
access_tokens_url: "", access_tokens_url: "",
account: ghUser, account: ghUser,
app_id: 1, app_id: 1,
app_slug: "coder", app_slug: "coder",
created_at: "2014-04-01T02:24:41Z", created_at: "2014-04-01T02:24:41Z",
events: [], events: [],
html_url: "", html_url: "",
permissions: {}, permissions: {},
repositories_url: "", repositories_url: "",
repository_selection: "all", repository_selection: "all",
single_file_name: "", single_file_name: "",
suspended_at: null, suspended_at: null,
suspended_by: null, suspended_by: null,
target_id: 1, target_id: 1,
target_type: "", target_type: "",
updated_at: "2023-06-26T13:03:09Z", updated_at: "2023-06-26T13:03:09Z",
}, },
], ],
total_count: 1, total_count: 1,
}; };
+22 -22
View File
@@ -1,9 +1,9 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { import {
createGroup, createGroup,
createUser, createUser,
getCurrentOrgId, getCurrentOrgId,
setupApiCalls, setupApiCalls,
} from "../../api"; } from "../../api";
import { requiresEnterpriseLicense } from "../../helpers"; import { requiresEnterpriseLicense } from "../../helpers";
import { beforeCoderTest } from "../../hooks"; import { beforeCoderTest } from "../../hooks";
@@ -11,24 +11,24 @@ import { beforeCoderTest } from "../../hooks";
test.beforeEach(async ({ page }) => await beforeCoderTest(page)); test.beforeEach(async ({ page }) => await beforeCoderTest(page));
test("add members", async ({ page, baseURL }) => { test("add members", async ({ page, baseURL }) => {
requiresEnterpriseLicense(); requiresEnterpriseLicense();
await setupApiCalls(page); await setupApiCalls(page);
const orgId = await getCurrentOrgId(); const orgId = await getCurrentOrgId();
const group = await createGroup(orgId); const group = await createGroup(orgId);
const numberOfMembers = 3; const numberOfMembers = 3;
const users = await Promise.all( const users = await Promise.all(
Array.from({ length: numberOfMembers }, () => createUser(orgId)), Array.from({ length: numberOfMembers }, () => createUser(orgId)),
); );
await page.goto(`${baseURL}/groups/${group.name}`, { await page.goto(`${baseURL}/groups/${group.name}`, {
waitUntil: "domcontentloaded", waitUntil: "domcontentloaded",
}); });
await expect(page).toHaveTitle(`${group.display_name} - Coder`); await expect(page).toHaveTitle(`${group.display_name} - Coder`);
for (const user of users) { for (const user of users) {
await page.getByPlaceholder("User email or username").fill(user.username); await page.getByPlaceholder("User email or username").fill(user.username);
await page.getByRole("option", { name: user.email }).click(); await page.getByRole("option", { name: user.email }).click();
await page.getByRole("button", { name: "Add user" }).click(); await page.getByRole("button", { name: "Add user" }).click();
await expect(page.getByRole("row", { name: user.username })).toBeVisible(); await expect(page.getByRole("row", { name: user.username })).toBeVisible();
} }
}); });
@@ -8,25 +8,25 @@ test.beforeEach(async ({ page }) => await beforeCoderTest(page));
const DEFAULT_GROUP_NAME = "Everyone"; const DEFAULT_GROUP_NAME = "Everyone";
test(`Every user should be automatically added to the default '${DEFAULT_GROUP_NAME}' group upon creation`, async ({ test(`Every user should be automatically added to the default '${DEFAULT_GROUP_NAME}' group upon creation`, async ({
page, page,
baseURL, baseURL,
}) => { }) => {
requiresEnterpriseLicense(); requiresEnterpriseLicense();
await setupApiCalls(page); await setupApiCalls(page);
const orgId = await getCurrentOrgId(); const orgId = await getCurrentOrgId();
const numberOfMembers = 3; const numberOfMembers = 3;
const users = await Promise.all( const users = await Promise.all(
Array.from({ length: numberOfMembers }, () => createUser(orgId)), Array.from({ length: numberOfMembers }, () => createUser(orgId)),
); );
await page.goto(`${baseURL}/groups`, { waitUntil: "domcontentloaded" }); await page.goto(`${baseURL}/groups`, { waitUntil: "domcontentloaded" });
await expect(page).toHaveTitle("Groups - Coder"); await expect(page).toHaveTitle("Groups - Coder");
const groupRow = page.getByRole("row", { name: DEFAULT_GROUP_NAME }); const groupRow = page.getByRole("row", { name: DEFAULT_GROUP_NAME });
await groupRow.click(); await groupRow.click();
await expect(page).toHaveTitle(`${DEFAULT_GROUP_NAME} - Coder`); await expect(page).toHaveTitle(`${DEFAULT_GROUP_NAME} - Coder`);
for (const user of users) { for (const user of users) {
await expect(page.getByRole("row", { name: user.username })).toBeVisible(); await expect(page.getByRole("row", { name: user.username })).toBeVisible();
} }
}); });
+18 -18
View File
@@ -5,26 +5,26 @@ import { beforeCoderTest } from "../../hooks";
test.beforeEach(async ({ page }) => await beforeCoderTest(page)); test.beforeEach(async ({ page }) => await beforeCoderTest(page));
test("create group", async ({ page, baseURL }) => { test("create group", async ({ page, baseURL }) => {
requiresEnterpriseLicense(); requiresEnterpriseLicense();
await page.goto(`${baseURL}/groups`, { waitUntil: "domcontentloaded" }); await page.goto(`${baseURL}/groups`, { waitUntil: "domcontentloaded" });
await expect(page).toHaveTitle("Groups - Coder"); await expect(page).toHaveTitle("Groups - Coder");
await page.getByText("Create group").click(); await page.getByText("Create group").click();
await expect(page).toHaveTitle("Create Group - Coder"); await expect(page).toHaveTitle("Create Group - Coder");
const name = randomName(); const name = randomName();
const groupValues = { const groupValues = {
name: name, name: name,
displayName: `Display Name for ${name}`, displayName: `Display Name for ${name}`,
avatarURL: "/emojis/1f60d.png", avatarURL: "/emojis/1f60d.png",
}; };
await page.getByLabel("Name", { exact: true }).fill(groupValues.name); await page.getByLabel("Name", { exact: true }).fill(groupValues.name);
await page.getByLabel("Display Name").fill(groupValues.displayName); await page.getByLabel("Display Name").fill(groupValues.displayName);
await page.getByLabel("Avatar URL").fill(groupValues.avatarURL); await page.getByLabel("Avatar URL").fill(groupValues.avatarURL);
await page.getByRole("button", { name: "Submit" }).click(); await page.getByRole("button", { name: "Submit" }).click();
await expect(page).toHaveTitle(`${groupValues.displayName} - Coder`); await expect(page).toHaveTitle(`${groupValues.displayName} - Coder`);
await expect(page.getByText(groupValues.displayName)).toBeVisible(); await expect(page.getByText(groupValues.displayName)).toBeVisible();
await expect(page.getByText("No members yet")).toBeVisible(); await expect(page.getByText("No members yet")).toBeVisible();
}); });
@@ -6,18 +6,18 @@ import { beforeCoderTest } from "../../hooks";
test.beforeEach(async ({ page }) => await beforeCoderTest(page)); test.beforeEach(async ({ page }) => await beforeCoderTest(page));
test("navigate to group page", async ({ page, baseURL }) => { test("navigate to group page", async ({ page, baseURL }) => {
requiresEnterpriseLicense(); requiresEnterpriseLicense();
await setupApiCalls(page); await setupApiCalls(page);
const orgId = await getCurrentOrgId(); const orgId = await getCurrentOrgId();
const group = await createGroup(orgId); const group = await createGroup(orgId);
await page.goto(`${baseURL}/users`, { waitUntil: "domcontentloaded" }); await page.goto(`${baseURL}/users`, { waitUntil: "domcontentloaded" });
await expect(page).toHaveTitle("Users - Coder"); await expect(page).toHaveTitle("Users - Coder");
await page.getByRole("link", { name: "Groups" }).click(); await page.getByRole("link", { name: "Groups" }).click();
await expect(page).toHaveTitle("Groups - Coder"); await expect(page).toHaveTitle("Groups - Coder");
const groupRow = page.getByRole("row", { name: group.display_name }); const groupRow = page.getByRole("row", { name: group.display_name });
await groupRow.click(); await groupRow.click();
await expect(page).toHaveTitle(`${group.display_name} - Coder`); await expect(page).toHaveTitle(`${group.display_name} - Coder`);
}); });
+14 -14
View File
@@ -6,21 +6,21 @@ import { beforeCoderTest } from "../../hooks";
test.beforeEach(async ({ page }) => await beforeCoderTest(page)); test.beforeEach(async ({ page }) => await beforeCoderTest(page));
test("remove group", async ({ page, baseURL }) => { test("remove group", async ({ page, baseURL }) => {
requiresEnterpriseLicense(); requiresEnterpriseLicense();
await setupApiCalls(page); await setupApiCalls(page);
const orgId = await getCurrentOrgId(); const orgId = await getCurrentOrgId();
const group = await createGroup(orgId); const group = await createGroup(orgId);
await page.goto(`${baseURL}/groups/${group.name}`, { await page.goto(`${baseURL}/groups/${group.name}`, {
waitUntil: "domcontentloaded", waitUntil: "domcontentloaded",
}); });
await expect(page).toHaveTitle(`${group.display_name} - Coder`); await expect(page).toHaveTitle(`${group.display_name} - Coder`);
await page.getByRole("button", { name: "Delete" }).click(); await page.getByRole("button", { name: "Delete" }).click();
const dialog = page.getByTestId("dialog"); const dialog = page.getByTestId("dialog");
await dialog.getByLabel("Name of the group to delete").fill(group.name); await dialog.getByLabel("Name of the group to delete").fill(group.name);
await dialog.getByRole("button", { name: "Delete" }).click(); await dialog.getByRole("button", { name: "Delete" }).click();
await expect(page.getByText("Group deleted successfully.")).toBeVisible(); await expect(page.getByText("Group deleted successfully.")).toBeVisible();
await expect(page).toHaveTitle("Groups - Coder"); await expect(page).toHaveTitle("Groups - Coder");
}); });
+21 -21
View File
@@ -1,10 +1,10 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { API } from "api/api"; import { API } from "api/api";
import { import {
createGroup, createGroup,
createUser, createUser,
getCurrentOrgId, getCurrentOrgId,
setupApiCalls, setupApiCalls,
} from "../../api"; } from "../../api";
import { requiresEnterpriseLicense } from "../../helpers"; import { requiresEnterpriseLicense } from "../../helpers";
import { beforeCoderTest } from "../../hooks"; import { beforeCoderTest } from "../../hooks";
@@ -12,25 +12,25 @@ import { beforeCoderTest } from "../../hooks";
test.beforeEach(async ({ page }) => await beforeCoderTest(page)); test.beforeEach(async ({ page }) => await beforeCoderTest(page));
test("remove member", async ({ page, baseURL }) => { test("remove member", async ({ page, baseURL }) => {
requiresEnterpriseLicense(); requiresEnterpriseLicense();
await setupApiCalls(page); await setupApiCalls(page);
const orgId = await getCurrentOrgId(); const orgId = await getCurrentOrgId();
const [group, member] = await Promise.all([ const [group, member] = await Promise.all([
createGroup(orgId), createGroup(orgId),
createUser(orgId), createUser(orgId),
]); ]);
await API.addMember(group.id, member.id); await API.addMember(group.id, member.id);
await page.goto(`${baseURL}/groups/${group.name}`, { await page.goto(`${baseURL}/groups/${group.name}`, {
waitUntil: "domcontentloaded", waitUntil: "domcontentloaded",
}); });
await expect(page).toHaveTitle(`${group.display_name} - Coder`); await expect(page).toHaveTitle(`${group.display_name} - Coder`);
const userRow = page.getByRole("row", { name: member.username }); const userRow = page.getByRole("row", { name: member.username });
await userRow.getByRole("button", { name: "More options" }).click(); await userRow.getByRole("button", { name: "More options" }).click();
const menu = page.locator("#more-options"); const menu = page.locator("#more-options");
await menu.getByText("Remove").click({ timeout: 1_000 }); await menu.getByText("Remove").click({ timeout: 1_000 });
await expect(page.getByText("Member removed successfully.")).toBeVisible(); await expect(page.getByText("Member removed successfully.")).toBeVisible();
}); });
+20 -20
View File
@@ -5,32 +5,32 @@ import { requiresEnterpriseLicense } from "../helpers";
import { beforeCoderTest } from "../hooks"; import { beforeCoderTest } from "../hooks";
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await beforeCoderTest(page); await beforeCoderTest(page);
await setupApiCalls(page); await setupApiCalls(page);
}); });
test("create and delete organization", async ({ page, baseURL }) => { test("create and delete organization", async ({ page, baseURL }) => {
requiresEnterpriseLicense(); requiresEnterpriseLicense();
// Create an organization // Create an organization
await page.goto(`${baseURL}/organizations/new`, { await page.goto(`${baseURL}/organizations/new`, {
waitUntil: "domcontentloaded", waitUntil: "domcontentloaded",
}); });
await page.getByLabel("Name", { exact: true }).fill("floop"); await page.getByLabel("Name", { exact: true }).fill("floop");
await page.getByLabel("Display name").fill("Floop"); await page.getByLabel("Display name").fill("Floop");
await page.getByLabel("Description").fill("Org description floop"); await page.getByLabel("Description").fill("Org description floop");
await page.getByLabel("Icon", { exact: true }).fill("/emojis/1f957.png"); await page.getByLabel("Icon", { exact: true }).fill("/emojis/1f957.png");
await page.getByRole("button", { name: "Submit" }).click(); await page.getByRole("button", { name: "Submit" }).click();
// Expect to be redirected to the new organization // Expect to be redirected to the new organization
await expectUrl(page).toHavePathName("/organizations/floop"); await expectUrl(page).toHavePathName("/organizations/floop");
await expect(page.getByText("Organization created.")).toBeVisible(); await expect(page.getByText("Organization created.")).toBeVisible();
await page.getByRole("button", { name: "Delete this organization" }).click(); await page.getByRole("button", { name: "Delete this organization" }).click();
const dialog = page.getByTestId("dialog"); const dialog = page.getByTestId("dialog");
await dialog.getByLabel("Name").fill("floop"); await dialog.getByLabel("Name").fill("floop");
await dialog.getByRole("button", { name: "Delete" }).click(); await dialog.getByRole("button", { name: "Delete" }).click();
await expect(page.getByText("Organization deleted.")).toBeVisible(); await expect(page.getByText("Organization deleted.")).toBeVisible();
}); });
+48 -48
View File
@@ -1,13 +1,13 @@
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { test } from "@playwright/test"; import { test } from "@playwright/test";
import { import {
createTemplate, createTemplate,
createWorkspace, createWorkspace,
downloadCoderVersion, downloadCoderVersion,
sshIntoWorkspace, sshIntoWorkspace,
startAgentWithCommand, startAgentWithCommand,
stopAgent, stopAgent,
stopWorkspace, stopWorkspace,
} from "../helpers"; } from "../helpers";
import { beforeCoderTest } from "../hooks"; import { beforeCoderTest } from "../hooks";
@@ -17,48 +17,48 @@ const agentVersion = "v2.12.1";
test.beforeEach(({ page }) => beforeCoderTest(page)); test.beforeEach(({ page }) => beforeCoderTest(page));
test(`ssh with agent ${agentVersion}`, async ({ page }) => { test(`ssh with agent ${agentVersion}`, async ({ page }) => {
test.setTimeout(40_000); // This is a slow test, 20s may not be enough on Mac. test.setTimeout(40_000); // This is a slow test, 20s may not be enough on Mac.
const token = randomUUID(); const token = randomUUID();
const template = await createTemplate(page, { const template = await createTemplate(page, {
apply: [ apply: [
{ {
apply: { apply: {
resources: [ resources: [
{ {
agents: [ agents: [
{ {
token, token,
order: 0, order: 0,
}, },
], ],
}, },
], ],
}, },
}, },
], ],
}); });
const workspaceName = await createWorkspace(page, template); const workspaceName = await createWorkspace(page, template);
const binaryPath = await downloadCoderVersion(agentVersion); const binaryPath = await downloadCoderVersion(agentVersion);
const agent = await startAgentWithCommand(page, token, binaryPath); const agent = await startAgentWithCommand(page, token, binaryPath);
const client = await sshIntoWorkspace(page, workspaceName); const client = await sshIntoWorkspace(page, workspaceName);
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
// We just exec a command to be certain the agent is running! // We just exec a command to be certain the agent is running!
client.exec("exit 0", (err, stream) => { client.exec("exit 0", (err, stream) => {
if (err) { if (err) {
return reject(err); return reject(err);
} }
stream.on("exit", (code) => { stream.on("exit", (code) => {
if (code !== 0) { if (code !== 0) {
return reject(new Error(`Command exited with code ${code}`)); return reject(new Error(`Command exited with code ${code}`));
} }
client.end(); client.end();
resolve(); resolve();
}); });
}); });
}); });
await stopWorkspace(page, workspaceName); await stopWorkspace(page, workspaceName);
await stopAgent(agent, false); await stopAgent(agent, false);
}); });
+47 -47
View File
@@ -1,13 +1,13 @@
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { test } from "@playwright/test"; import { test } from "@playwright/test";
import { import {
createTemplate, createTemplate,
createWorkspace, createWorkspace,
downloadCoderVersion, downloadCoderVersion,
sshIntoWorkspace, sshIntoWorkspace,
startAgent, startAgent,
stopAgent, stopAgent,
stopWorkspace, stopWorkspace,
} from "../helpers"; } from "../helpers";
import { beforeCoderTest } from "../hooks"; import { beforeCoderTest } from "../hooks";
@@ -17,46 +17,46 @@ const clientVersion = "v0.27.0";
test.beforeEach(({ page }) => beforeCoderTest(page)); test.beforeEach(({ page }) => beforeCoderTest(page));
test(`ssh with client ${clientVersion}`, async ({ page }) => { test(`ssh with client ${clientVersion}`, async ({ page }) => {
const token = randomUUID(); const token = randomUUID();
const template = await createTemplate(page, { const template = await createTemplate(page, {
apply: [ apply: [
{ {
apply: { apply: {
resources: [ resources: [
{ {
agents: [ agents: [
{ {
token, token,
order: 0, order: 0,
}, },
], ],
}, },
], ],
}, },
}, },
], ],
}); });
const workspaceName = await createWorkspace(page, template); const workspaceName = await createWorkspace(page, template);
const agent = await startAgent(page, token); const agent = await startAgent(page, token);
const binaryPath = await downloadCoderVersion(clientVersion); const binaryPath = await downloadCoderVersion(clientVersion);
const client = await sshIntoWorkspace(page, workspaceName, binaryPath); const client = await sshIntoWorkspace(page, workspaceName, binaryPath);
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
// We just exec a command to be certain the agent is running! // We just exec a command to be certain the agent is running!
client.exec("exit 0", (err, stream) => { client.exec("exit 0", (err, stream) => {
if (err) { if (err) {
return reject(err); return reject(err);
} }
stream.on("exit", (code) => { stream.on("exit", (code) => {
if (code !== 0) { if (code !== 0) {
return reject(new Error(`Command exited with code ${code}`)); return reject(new Error(`Command exited with code ${code}`));
} }
client.end(); client.end();
resolve(); resolve();
}); });
}); });
}); });
await stopWorkspace(page, workspaceName); await stopWorkspace(page, workspaceName);
await stopAgent(agent); await stopAgent(agent);
}); });
@@ -4,6 +4,6 @@ import { beforeCoderTest } from "../../hooks";
test.beforeEach(({ page }) => beforeCoderTest(page)); test.beforeEach(({ page }) => beforeCoderTest(page));
test("list templates", async ({ page, baseURL }) => { test("list templates", async ({ page, baseURL }) => {
await page.goto(`${baseURL}/templates`, { waitUntil: "domcontentloaded" }); await page.goto(`${baseURL}/templates`, { waitUntil: "domcontentloaded" });
await expect(page).toHaveTitle("Templates - Coder"); await expect(page).toHaveTitle("Templates - Coder");
}); });
@@ -6,40 +6,40 @@ import { beforeCoderTest } from "../../hooks";
test.beforeEach(({ page }) => beforeCoderTest(page)); test.beforeEach(({ page }) => beforeCoderTest(page));
test("update template schedule settings without override other settings", async ({ test("update template schedule settings without override other settings", async ({
page, page,
baseURL, baseURL,
}) => { }) => {
await setupApiCalls(page); await setupApiCalls(page);
const orgId = await getCurrentOrgId(); const orgId = await getCurrentOrgId();
const templateVersion = await API.createTemplateVersion(orgId, { const templateVersion = await API.createTemplateVersion(orgId, {
storage_method: "file" as const, storage_method: "file" as const,
provisioner: "echo", provisioner: "echo",
user_variable_values: [], user_variable_values: [],
example_id: "docker", example_id: "docker",
tags: {}, tags: {},
}); });
const template = await API.createTemplate(orgId, { const template = await API.createTemplate(orgId, {
name: "test-template", name: "test-template",
display_name: "Test Template", display_name: "Test Template",
template_version_id: templateVersion.id, template_version_id: templateVersion.id,
disable_everyone_group_access: false, disable_everyone_group_access: false,
require_active_version: true, require_active_version: true,
}); });
await page.goto(`${baseURL}/templates/${template.name}/settings/schedule`, { await page.goto(`${baseURL}/templates/${template.name}/settings/schedule`, {
waitUntil: "domcontentloaded", waitUntil: "domcontentloaded",
}); });
await page.getByLabel("Default autostop (hours)").fill("48"); await page.getByLabel("Default autostop (hours)").fill("48");
await page.getByRole("button", { name: "Submit" }).click(); await page.getByRole("button", { name: "Submit" }).click();
await expect(page.getByText("Template updated successfully")).toBeVisible(); await expect(page.getByText("Template updated successfully")).toBeVisible();
const updatedTemplate = await API.getTemplate(template.id); const updatedTemplate = await API.getTemplate(template.id);
// Validate that the template data remains consistent, with the exception of // Validate that the template data remains consistent, with the exception of
// the 'default_ttl_ms' field (updated during the test) and the 'updated at' // the 'default_ttl_ms' field (updated during the test) and the 'updated at'
// field (automatically updated by the backend). // field (automatically updated by the backend).
expect({ expect({
...template, ...template,
default_ttl_ms: 48 * 60 * 60 * 1000, default_ttl_ms: 48 * 60 * 60 * 1000,
updated_at: updatedTemplate.updated_at, updated_at: updatedTemplate.updated_at,
}).toStrictEqual(updatedTemplate); }).toStrictEqual(updatedTemplate);
}); });
+47 -47
View File
@@ -1,73 +1,73 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { expectUrl } from "../expectUrl"; import { expectUrl } from "../expectUrl";
import { import {
createGroup, createGroup,
createTemplate, createTemplate,
requiresEnterpriseLicense, requiresEnterpriseLicense,
updateTemplateSettings, updateTemplateSettings,
} from "../helpers"; } from "../helpers";
import { beforeCoderTest } from "../hooks"; import { beforeCoderTest } from "../hooks";
test.beforeEach(({ page }) => beforeCoderTest(page)); test.beforeEach(({ page }) => beforeCoderTest(page));
test("template update with new name redirects on successful submit", async ({ test("template update with new name redirects on successful submit", async ({
page, page,
}) => { }) => {
const templateName = await createTemplate(page); const templateName = await createTemplate(page);
await updateTemplateSettings(page, templateName, { await updateTemplateSettings(page, templateName, {
name: "new-name", name: "new-name",
}); });
}); });
test("add and remove a group", async ({ page }) => { test("add and remove a group", async ({ page }) => {
requiresEnterpriseLicense(); requiresEnterpriseLicense();
const templateName = await createTemplate(page); const templateName = await createTemplate(page);
const groupName = await createGroup(page); const groupName = await createGroup(page);
await page.goto(`/templates/${templateName}/settings/permissions`, { await page.goto(`/templates/${templateName}/settings/permissions`, {
waitUntil: "domcontentloaded", waitUntil: "domcontentloaded",
}); });
await expectUrl(page).toHavePathName( await expectUrl(page).toHavePathName(
`/templates/${templateName}/settings/permissions`, `/templates/${templateName}/settings/permissions`,
); );
// Type the first half of the group name // Type the first half of the group name
await page await page
.getByPlaceholder("Search for user or group", { exact: true }) .getByPlaceholder("Search for user or group", { exact: true })
.fill(groupName.slice(0, 4)); .fill(groupName.slice(0, 4));
// Select the group from the list and add it // Select the group from the list and add it
await page.getByText(groupName).click(); await page.getByText(groupName).click();
await page.getByText("Add member").click(); await page.getByText("Add member").click();
const row = page.locator(".MuiTableRow-root", { hasText: groupName }); const row = page.locator(".MuiTableRow-root", { hasText: groupName });
await expect(row).toBeVisible(); await expect(row).toBeVisible();
// Now remove the group // Now remove the group
await row.getByLabel("More options").click(); await row.getByLabel("More options").click();
await page.getByText("Remove").click(); await page.getByText("Remove").click();
await expect(page.getByText("Group removed successfully!")).toBeVisible(); await expect(page.getByText("Group removed successfully!")).toBeVisible();
await expect(row).not.toBeVisible(); await expect(row).not.toBeVisible();
}); });
test("require latest version", async ({ page }) => { test("require latest version", async ({ page }) => {
requiresEnterpriseLicense(); requiresEnterpriseLicense();
const templateName = await createTemplate(page); const templateName = await createTemplate(page);
await page.goto(`/templates/${templateName}/settings`, { await page.goto(`/templates/${templateName}/settings`, {
waitUntil: "domcontentloaded", waitUntil: "domcontentloaded",
}); });
await expectUrl(page).toHavePathName(`/templates/${templateName}/settings`); await expectUrl(page).toHavePathName(`/templates/${templateName}/settings`);
let checkbox = await page.waitForSelector("#require_active_version"); let checkbox = await page.waitForSelector("#require_active_version");
await checkbox.click(); await checkbox.click();
await page.getByTestId("form-submit").click(); await page.getByTestId("form-submit").click();
await page.goto(`/templates/${templateName}/settings`, { await page.goto(`/templates/${templateName}/settings`, {
waitUntil: "domcontentloaded", waitUntil: "domcontentloaded",
}); });
checkbox = await page.waitForSelector("#require_active_version"); checkbox = await page.waitForSelector("#require_active_version");
await checkbox.scrollIntoViewIfNeeded(); await checkbox.scrollIntoViewIfNeeded();
expect(await checkbox.isChecked()).toBe(true); expect(await checkbox.isChecked()).toBe(true);
}); });
@@ -5,63 +5,63 @@ import { beforeCoderTest } from "../../hooks";
test.beforeEach(async ({ page }) => await beforeCoderTest(page)); test.beforeEach(async ({ page }) => await beforeCoderTest(page));
test("create user with password", async ({ page, baseURL }) => { test("create user with password", async ({ page, baseURL }) => {
await page.goto(`${baseURL}/users`, { waitUntil: "domcontentloaded" }); await page.goto(`${baseURL}/users`, { waitUntil: "domcontentloaded" });
await expect(page).toHaveTitle("Users - Coder"); await expect(page).toHaveTitle("Users - Coder");
await page.getByRole("button", { name: "Create user" }).click(); await page.getByRole("button", { name: "Create user" }).click();
await expect(page).toHaveTitle("Create User - Coder"); await expect(page).toHaveTitle("Create User - Coder");
const name = randomName(); const name = randomName();
const userValues = { const userValues = {
username: name, username: name,
name: name, name: name,
email: `${name}@coder.com`, email: `${name}@coder.com`,
loginType: "password", loginType: "password",
password: "s3cure&password!", password: "s3cure&password!",
}; };
await page.getByLabel("Username").fill(userValues.username); await page.getByLabel("Username").fill(userValues.username);
await page.getByLabel("Full name").fill(userValues.username); await page.getByLabel("Full name").fill(userValues.username);
await page.getByLabel("Email").fill(userValues.email); await page.getByLabel("Email").fill(userValues.email);
await page.getByLabel("Login Type").click(); await page.getByLabel("Login Type").click();
await page.getByRole("option", { name: "Password", exact: false }).click(); await page.getByRole("option", { name: "Password", exact: false }).click();
// Using input[name=password] due to the select element utilizing 'password' // Using input[name=password] due to the select element utilizing 'password'
// as the label for the currently active option. // as the label for the currently active option.
const passwordField = page.locator("input[name=password]"); const passwordField = page.locator("input[name=password]");
await passwordField.fill(userValues.password); await passwordField.fill(userValues.password);
await page.getByRole("button", { name: "Create user" }).click(); await page.getByRole("button", { name: "Create user" }).click();
await expect(page.getByText("Successfully created user.")).toBeVisible(); await expect(page.getByText("Successfully created user.")).toBeVisible();
await expect(page).toHaveTitle("Users - Coder"); await expect(page).toHaveTitle("Users - Coder");
await expect(page.locator("tr", { hasText: userValues.email })).toBeVisible(); await expect(page.locator("tr", { hasText: userValues.email })).toBeVisible();
}); });
test("create user without full name is optional", async ({ page, baseURL }) => { test("create user without full name is optional", async ({ page, baseURL }) => {
await page.goto(`${baseURL}/users`, { waitUntil: "domcontentloaded" }); await page.goto(`${baseURL}/users`, { waitUntil: "domcontentloaded" });
await expect(page).toHaveTitle("Users - Coder"); await expect(page).toHaveTitle("Users - Coder");
await page.getByRole("button", { name: "Create user" }).click(); await page.getByRole("button", { name: "Create user" }).click();
await expect(page).toHaveTitle("Create User - Coder"); await expect(page).toHaveTitle("Create User - Coder");
const name = randomName(); const name = randomName();
const userValues = { const userValues = {
username: name, username: name,
email: `${name}@coder.com`, email: `${name}@coder.com`,
loginType: "password", loginType: "password",
password: "s3cure&password!", password: "s3cure&password!",
}; };
await page.getByLabel("Username").fill(userValues.username); await page.getByLabel("Username").fill(userValues.username);
await page.getByLabel("Email").fill(userValues.email); await page.getByLabel("Email").fill(userValues.email);
await page.getByLabel("Login Type").click(); await page.getByLabel("Login Type").click();
await page.getByRole("option", { name: "Password", exact: false }).click(); await page.getByRole("option", { name: "Password", exact: false }).click();
// Using input[name=password] due to the select element utilizing 'password' // Using input[name=password] due to the select element utilizing 'password'
// as the label for the currently active option. // as the label for the currently active option.
const passwordField = page.locator("input[name=password]"); const passwordField = page.locator("input[name=password]");
await passwordField.fill(userValues.password); await passwordField.fill(userValues.password);
await page.getByRole("button", { name: "Create user" }).click(); await page.getByRole("button", { name: "Create user" }).click();
await expect(page.getByText("Successfully created user.")).toBeVisible(); await expect(page.getByText("Successfully created user.")).toBeVisible();
await expect(page).toHaveTitle("Users - Coder"); await expect(page).toHaveTitle("Users - Coder");
await expect(page.locator("tr", { hasText: userValues.email })).toBeVisible(); await expect(page.locator("tr", { hasText: userValues.email })).toBeVisible();
}); });
+13 -13
View File
@@ -5,21 +5,21 @@ import { beforeCoderTest } from "../../hooks";
test.beforeEach(async ({ page }) => await beforeCoderTest(page)); test.beforeEach(async ({ page }) => await beforeCoderTest(page));
test("remove user", async ({ page, baseURL }) => { test("remove user", async ({ page, baseURL }) => {
await setupApiCalls(page); await setupApiCalls(page);
const orgId = await getCurrentOrgId(); const orgId = await getCurrentOrgId();
const user = await createUser(orgId); const user = await createUser(orgId);
await page.goto(`${baseURL}/users`, { waitUntil: "domcontentloaded" }); await page.goto(`${baseURL}/users`, { waitUntil: "domcontentloaded" });
await expect(page).toHaveTitle("Users - Coder"); await expect(page).toHaveTitle("Users - Coder");
const userRow = page.getByRole("row", { name: user.email }); const userRow = page.getByRole("row", { name: user.email });
await userRow.getByRole("button", { name: "More options" }).click(); await userRow.getByRole("button", { name: "More options" }).click();
const menu = page.locator("#more-options"); const menu = page.locator("#more-options");
await menu.getByText("Delete").click(); await menu.getByText("Delete").click();
const dialog = page.getByTestId("dialog"); const dialog = page.getByTestId("dialog");
await dialog.getByLabel("Name of the user to delete").fill(user.username); await dialog.getByLabel("Name of the user to delete").fill(user.username);
await dialog.getByRole("button", { name: "Delete" }).click(); await dialog.getByRole("button", { name: "Delete" }).click();
await expect(page.getByText("Successfully deleted the user.")).toBeVisible(); await expect(page.getByText("Successfully deleted the user.")).toBeVisible();
}); });
+56 -56
View File
@@ -1,71 +1,71 @@
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { test } from "@playwright/test"; import { test } from "@playwright/test";
import { import {
createTemplate, createTemplate,
createWorkspace, createWorkspace,
openTerminalWindow, openTerminalWindow,
startAgent, startAgent,
stopAgent, stopAgent,
} from "../helpers"; } from "../helpers";
import { beforeCoderTest } from "../hooks"; import { beforeCoderTest } from "../hooks";
test.beforeEach(({ page }) => beforeCoderTest(page)); test.beforeEach(({ page }) => beforeCoderTest(page));
test("web terminal", async ({ context, page }) => { test("web terminal", async ({ context, page }) => {
const token = randomUUID(); const token = randomUUID();
const template = await createTemplate(page, { const template = await createTemplate(page, {
apply: [ apply: [
{ {
apply: { apply: {
resources: [ resources: [
{ {
agents: [ agents: [
{ {
token, token,
displayApps: { displayApps: {
webTerminal: true, webTerminal: true,
}, },
order: 0, order: 0,
}, },
], ],
}, },
], ],
}, },
}, },
], ],
}); });
const workspaceName = await createWorkspace(page, template); const workspaceName = await createWorkspace(page, template);
const agent = await startAgent(page, token); const agent = await startAgent(page, token);
const terminal = await openTerminalWindow(page, context, workspaceName); const terminal = await openTerminalWindow(page, context, workspaceName);
await terminal.waitForSelector("div.xterm-rows", { await terminal.waitForSelector("div.xterm-rows", {
state: "visible", state: "visible",
}); });
// Workaround: delay next steps as "div.xterm-rows" can be recreated/reattached // Workaround: delay next steps as "div.xterm-rows" can be recreated/reattached
// after a couple of milliseconds. // after a couple of milliseconds.
await terminal.waitForTimeout(2000); await terminal.waitForTimeout(2000);
// Ensure that we can type in it // Ensure that we can type in it
await terminal.keyboard.type("echo he${justabreak}llo123456"); await terminal.keyboard.type("echo he${justabreak}llo123456");
await terminal.keyboard.press("Enter"); await terminal.keyboard.press("Enter");
// Check if "echo" command was executed // Check if "echo" command was executed
// try-catch is used temporarily to find the root cause: https://github.com/coder/coder/actions/runs/6176958762/job/16767089943 // try-catch is used temporarily to find the root cause: https://github.com/coder/coder/actions/runs/6176958762/job/16767089943
try { try {
await terminal.waitForSelector( await terminal.waitForSelector(
'div.xterm-rows span:text-matches("hello123456")', 'div.xterm-rows span:text-matches("hello123456")',
{ {
state: "visible", state: "visible",
timeout: 10 * 1000, timeout: 10 * 1000,
}, },
); );
} catch (error) { } catch (error) {
const pageContent = await terminal.content(); const pageContent = await terminal.content();
// eslint-disable-next-line no-console -- Let's see what is inside of xterm-rows // eslint-disable-next-line no-console -- Let's see what is inside of xterm-rows
console.log("Unable to find echoed text:", pageContent); console.log("Unable to find echoed text:", pageContent);
throw error; throw error;
} }
await stopAgent(agent); await stopAgent(agent);
}); });
@@ -1,65 +1,65 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { username } from "../../constants"; import { username } from "../../constants";
import { import {
createTemplate, createTemplate,
createWorkspace, createWorkspace,
echoResponsesWithParameters, echoResponsesWithParameters,
} from "../../helpers"; } from "../../helpers";
import { emptyParameter } from "../../parameters"; import { emptyParameter } from "../../parameters";
import type { RichParameter } from "../../provisionerGenerated"; import type { RichParameter } from "../../provisionerGenerated";
test("create workspace in auto mode", async ({ page }) => { test("create workspace in auto mode", async ({ page }) => {
const richParameters: RichParameter[] = [ const richParameters: RichParameter[] = [
{ ...emptyParameter, name: "repo", type: "string" }, { ...emptyParameter, name: "repo", type: "string" },
]; ];
const template = await createTemplate( const template = await createTemplate(
page, page,
echoResponsesWithParameters(richParameters), echoResponsesWithParameters(richParameters),
); );
const name = "test-workspace"; const name = "test-workspace";
await page.goto( await page.goto(
`/templates/${template}/workspace?mode=auto&param.repo=example&name=${name}`, `/templates/${template}/workspace?mode=auto&param.repo=example&name=${name}`,
{ {
waitUntil: "domcontentloaded", waitUntil: "domcontentloaded",
}, },
); );
await expect(page).toHaveTitle(`${username}/${name} - Coder`); await expect(page).toHaveTitle(`${username}/${name} - Coder`);
}); });
test("use an existing workspace that matches the `match` parameter instead of creating a new one", async ({ test("use an existing workspace that matches the `match` parameter instead of creating a new one", async ({
page, page,
}) => { }) => {
const richParameters: RichParameter[] = [ const richParameters: RichParameter[] = [
{ ...emptyParameter, name: "repo", type: "string" }, { ...emptyParameter, name: "repo", type: "string" },
]; ];
const template = await createTemplate( const template = await createTemplate(
page, page,
echoResponsesWithParameters(richParameters), echoResponsesWithParameters(richParameters),
); );
const prevWorkspace = await createWorkspace(page, template); const prevWorkspace = await createWorkspace(page, template);
await page.goto( await page.goto(
`/templates/${template}/workspace?mode=auto&param.repo=example&name=new-name&match=name:${prevWorkspace}`, `/templates/${template}/workspace?mode=auto&param.repo=example&name=new-name&match=name:${prevWorkspace}`,
{ {
waitUntil: "domcontentloaded", waitUntil: "domcontentloaded",
}, },
); );
await expect(page).toHaveTitle(`${username}/${prevWorkspace} - Coder`); await expect(page).toHaveTitle(`${username}/${prevWorkspace} - Coder`);
}); });
test("show error if `match` parameter is invalid", async ({ page }) => { test("show error if `match` parameter is invalid", async ({ page }) => {
const richParameters: RichParameter[] = [ const richParameters: RichParameter[] = [
{ ...emptyParameter, name: "repo", type: "string" }, { ...emptyParameter, name: "repo", type: "string" },
]; ];
const template = await createTemplate( const template = await createTemplate(
page, page,
echoResponsesWithParameters(richParameters), echoResponsesWithParameters(richParameters),
); );
const prevWorkspace = await createWorkspace(page, template); const prevWorkspace = await createWorkspace(page, template);
await page.goto( await page.goto(
`/templates/${template}/workspace?mode=auto&param.repo=example&name=new-name&match=not-valid-query:${prevWorkspace}`, `/templates/${template}/workspace?mode=auto&param.repo=example&name=new-name&match=not-valid-query:${prevWorkspace}`,
{ {
waitUntil: "domcontentloaded", waitUntil: "domcontentloaded",
}, },
); );
await expect(page.getByText("Invalid match value")).toBeVisible(); await expect(page.getByText("Invalid match value")).toBeVisible();
}); });
+151 -151
View File
@@ -1,191 +1,191 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { import {
StarterTemplates, StarterTemplates,
createTemplate, createTemplate,
createWorkspace, createWorkspace,
echoResponsesWithParameters, echoResponsesWithParameters,
openTerminalWindow, openTerminalWindow,
requireTerraformProvisioner, requireTerraformProvisioner,
verifyParameters, verifyParameters,
} from "../../helpers"; } from "../../helpers";
import { beforeCoderTest } from "../../hooks"; import { beforeCoderTest } from "../../hooks";
import { import {
fifthParameter, fifthParameter,
firstParameter, firstParameter,
fourthParameter, fourthParameter,
randParamName, randParamName,
secondParameter, secondParameter,
seventhParameter, seventhParameter,
sixthParameter, sixthParameter,
thirdParameter, thirdParameter,
} from "../../parameters"; } from "../../parameters";
import type { RichParameter } from "../../provisionerGenerated"; import type { RichParameter } from "../../provisionerGenerated";
test.beforeEach(({ page }) => beforeCoderTest(page)); test.beforeEach(({ page }) => beforeCoderTest(page));
test("create workspace", async ({ page }) => { test("create workspace", async ({ page }) => {
const template = await createTemplate(page, { const template = await createTemplate(page, {
apply: [ apply: [
{ {
apply: { apply: {
resources: [ resources: [
{ {
name: "example", name: "example",
}, },
], ],
}, },
}, },
], ],
}); });
await createWorkspace(page, template); await createWorkspace(page, template);
}); });
test("create workspace with default immutable parameters", async ({ page }) => { test("create workspace with default immutable parameters", async ({ page }) => {
const richParameters: RichParameter[] = [ const richParameters: RichParameter[] = [
secondParameter, secondParameter,
fourthParameter, fourthParameter,
fifthParameter, fifthParameter,
]; ];
const template = await createTemplate( const template = await createTemplate(
page, page,
echoResponsesWithParameters(richParameters), echoResponsesWithParameters(richParameters),
); );
const workspaceName = await createWorkspace(page, template); const workspaceName = await createWorkspace(page, template);
await verifyParameters(page, workspaceName, richParameters, [ await verifyParameters(page, workspaceName, richParameters, [
{ name: secondParameter.name, value: secondParameter.defaultValue }, { name: secondParameter.name, value: secondParameter.defaultValue },
{ name: fourthParameter.name, value: fourthParameter.defaultValue }, { name: fourthParameter.name, value: fourthParameter.defaultValue },
{ name: fifthParameter.name, value: fifthParameter.defaultValue }, { name: fifthParameter.name, value: fifthParameter.defaultValue },
]); ]);
}); });
test("create workspace with default mutable parameters", async ({ page }) => { test("create workspace with default mutable parameters", async ({ page }) => {
const richParameters: RichParameter[] = [firstParameter, thirdParameter]; const richParameters: RichParameter[] = [firstParameter, thirdParameter];
const template = await createTemplate( const template = await createTemplate(
page, page,
echoResponsesWithParameters(richParameters), echoResponsesWithParameters(richParameters),
); );
const workspaceName = await createWorkspace(page, template); const workspaceName = await createWorkspace(page, template);
await verifyParameters(page, workspaceName, richParameters, [ await verifyParameters(page, workspaceName, richParameters, [
{ name: firstParameter.name, value: firstParameter.defaultValue }, { name: firstParameter.name, value: firstParameter.defaultValue },
{ name: thirdParameter.name, value: thirdParameter.defaultValue }, { name: thirdParameter.name, value: thirdParameter.defaultValue },
]); ]);
}); });
test("create workspace with default and required parameters", async ({ test("create workspace with default and required parameters", async ({
page, page,
}) => { }) => {
const richParameters: RichParameter[] = [ const richParameters: RichParameter[] = [
secondParameter, secondParameter,
fourthParameter, fourthParameter,
sixthParameter, sixthParameter,
seventhParameter, seventhParameter,
]; ];
const buildParameters = [ const buildParameters = [
{ name: sixthParameter.name, value: "12345" }, { name: sixthParameter.name, value: "12345" },
{ name: seventhParameter.name, value: "abcdef" }, { name: seventhParameter.name, value: "abcdef" },
]; ];
const template = await createTemplate( const template = await createTemplate(
page, page,
echoResponsesWithParameters(richParameters), echoResponsesWithParameters(richParameters),
); );
const workspaceName = await createWorkspace( const workspaceName = await createWorkspace(
page, page,
template, template,
richParameters, richParameters,
buildParameters, buildParameters,
); );
await verifyParameters(page, workspaceName, richParameters, [ await verifyParameters(page, workspaceName, richParameters, [
// user values: // user values:
...buildParameters, ...buildParameters,
// default values: // default values:
{ name: secondParameter.name, value: secondParameter.defaultValue }, { name: secondParameter.name, value: secondParameter.defaultValue },
{ name: fourthParameter.name, value: fourthParameter.defaultValue }, { name: fourthParameter.name, value: fourthParameter.defaultValue },
]); ]);
}); });
test("create workspace and overwrite default parameters", async ({ page }) => { test("create workspace and overwrite default parameters", async ({ page }) => {
// We use randParamName to prevent the new values from corrupting user_history // We use randParamName to prevent the new values from corrupting user_history
// and thus affecting other tests. // and thus affecting other tests.
const richParameters: RichParameter[] = [ const richParameters: RichParameter[] = [
randParamName(secondParameter), randParamName(secondParameter),
randParamName(fourthParameter), randParamName(fourthParameter),
]; ];
const buildParameters = [ const buildParameters = [
{ name: richParameters[0].name, value: "AAAAA" }, { name: richParameters[0].name, value: "AAAAA" },
{ name: richParameters[1].name, value: "false" }, { name: richParameters[1].name, value: "false" },
]; ];
const template = await createTemplate( const template = await createTemplate(
page, page,
echoResponsesWithParameters(richParameters), echoResponsesWithParameters(richParameters),
); );
const workspaceName = await createWorkspace( const workspaceName = await createWorkspace(
page, page,
template, template,
richParameters, richParameters,
buildParameters, buildParameters,
); );
await verifyParameters(page, workspaceName, richParameters, buildParameters); await verifyParameters(page, workspaceName, richParameters, buildParameters);
}); });
test("create workspace with disable_param search params", async ({ page }) => { test("create workspace with disable_param search params", async ({ page }) => {
const richParameters: RichParameter[] = [ const richParameters: RichParameter[] = [
firstParameter, // mutable firstParameter, // mutable
secondParameter, //immutable secondParameter, //immutable
]; ];
const templateName = await createTemplate( const templateName = await createTemplate(
page, page,
echoResponsesWithParameters(richParameters), echoResponsesWithParameters(richParameters),
); );
await page.goto( await page.goto(
`/templates/${templateName}/workspace?disable_params=first_parameter,second_parameter`, `/templates/${templateName}/workspace?disable_params=first_parameter,second_parameter`,
{ {
waitUntil: "domcontentloaded", waitUntil: "domcontentloaded",
}, },
); );
await expect(page.getByLabel(/First parameter/i)).toBeDisabled(); await expect(page.getByLabel(/First parameter/i)).toBeDisabled();
await expect(page.getByLabel(/Second parameter/i)).toBeDisabled(); await expect(page.getByLabel(/Second parameter/i)).toBeDisabled();
}); });
test("create docker workspace", async ({ context, page }) => { test("create docker workspace", async ({ context, page }) => {
test.skip( test.skip(
true, true,
"creating docker containers is currently leaky. They are not cleaned up when the tests are over.", "creating docker containers is currently leaky. They are not cleaned up when the tests are over.",
); );
requireTerraformProvisioner(); requireTerraformProvisioner();
const template = await createTemplate(page, StarterTemplates.STARTER_DOCKER); const template = await createTemplate(page, StarterTemplates.STARTER_DOCKER);
const workspaceName = await createWorkspace(page, template); const workspaceName = await createWorkspace(page, template);
// The workspace agents must be ready before we try to interact with the workspace. // The workspace agents must be ready before we try to interact with the workspace.
await page.waitForSelector( await page.waitForSelector(
`//div[@role="status"][@data-testid="agent-status-ready"]`, `//div[@role="status"][@data-testid="agent-status-ready"]`,
{ {
state: "visible", state: "visible",
}, },
); );
// Wait for the terminal button to be visible, and click it. // Wait for the terminal button to be visible, and click it.
const terminalButton = const terminalButton =
"//a[@data-testid='terminal'][normalize-space()='Terminal']"; "//a[@data-testid='terminal'][normalize-space()='Terminal']";
await page.waitForSelector(terminalButton, { await page.waitForSelector(terminalButton, {
state: "visible", state: "visible",
}); });
const terminal = await openTerminalWindow( const terminal = await openTerminalWindow(
page, page,
context, context,
workspaceName, workspaceName,
"main", "main",
); );
await terminal.waitForSelector( await terminal.waitForSelector(
`//textarea[contains(@class,"xterm-helper-textarea")]`, `//textarea[contains(@class,"xterm-helper-textarea")]`,
{ {
state: "visible", state: "visible",
}, },
); );
}); });
@@ -1,10 +1,10 @@
import { test } from "@playwright/test"; import { test } from "@playwright/test";
import { import {
buildWorkspaceWithParameters, buildWorkspaceWithParameters,
createTemplate, createTemplate,
createWorkspace, createWorkspace,
echoResponsesWithParameters, echoResponsesWithParameters,
verifyParameters, verifyParameters,
} from "../../helpers"; } from "../../helpers";
import { beforeCoderTest } from "../../hooks"; import { beforeCoderTest } from "../../hooks";
import { firstBuildOption, secondBuildOption } from "../../parameters"; import { firstBuildOption, secondBuildOption } from "../../parameters";
@@ -13,35 +13,35 @@ import type { RichParameter } from "../../provisionerGenerated";
test.beforeEach(({ page }) => beforeCoderTest(page)); test.beforeEach(({ page }) => beforeCoderTest(page));
test("restart workspace with ephemeral parameters", async ({ page }) => { test("restart workspace with ephemeral parameters", async ({ page }) => {
const richParameters: RichParameter[] = [firstBuildOption, secondBuildOption]; const richParameters: RichParameter[] = [firstBuildOption, secondBuildOption];
const template = await createTemplate( const template = await createTemplate(
page, page,
echoResponsesWithParameters(richParameters), echoResponsesWithParameters(richParameters),
); );
const workspaceName = await createWorkspace(page, template); const workspaceName = await createWorkspace(page, template);
// Verify that build options are default (not selected). // Verify that build options are default (not selected).
await verifyParameters(page, workspaceName, richParameters, [ await verifyParameters(page, workspaceName, richParameters, [
{ name: richParameters[0].name, value: firstBuildOption.defaultValue }, { name: richParameters[0].name, value: firstBuildOption.defaultValue },
{ name: richParameters[1].name, value: secondBuildOption.defaultValue }, { name: richParameters[1].name, value: secondBuildOption.defaultValue },
]); ]);
// Now, restart the workspace with ephemeral parameters selected. // Now, restart the workspace with ephemeral parameters selected.
const buildParameters = [ const buildParameters = [
{ name: richParameters[0].name, value: "AAAAA" }, { name: richParameters[0].name, value: "AAAAA" },
{ name: richParameters[1].name, value: "true" }, { name: richParameters[1].name, value: "true" },
]; ];
await buildWorkspaceWithParameters( await buildWorkspaceWithParameters(
page, page,
workspaceName, workspaceName,
richParameters, richParameters,
buildParameters, buildParameters,
true, true,
); );
// Verify that build options are default (not selected). // Verify that build options are default (not selected).
await verifyParameters(page, workspaceName, richParameters, [ await verifyParameters(page, workspaceName, richParameters, [
{ name: richParameters[0].name, value: firstBuildOption.defaultValue }, { name: richParameters[0].name, value: firstBuildOption.defaultValue },
{ name: richParameters[1].name, value: secondBuildOption.defaultValue }, { name: richParameters[1].name, value: secondBuildOption.defaultValue },
]); ]);
}); });
@@ -1,11 +1,11 @@
import { test } from "@playwright/test"; import { test } from "@playwright/test";
import { import {
buildWorkspaceWithParameters, buildWorkspaceWithParameters,
createTemplate, createTemplate,
createWorkspace, createWorkspace,
echoResponsesWithParameters, echoResponsesWithParameters,
stopWorkspace, stopWorkspace,
verifyParameters, verifyParameters,
} from "../../helpers"; } from "../../helpers";
import { beforeCoderTest } from "../../hooks"; import { beforeCoderTest } from "../../hooks";
import { firstBuildOption, secondBuildOption } from "../../parameters"; import { firstBuildOption, secondBuildOption } from "../../parameters";
@@ -14,38 +14,38 @@ import type { RichParameter } from "../../provisionerGenerated";
test.beforeEach(({ page }) => beforeCoderTest(page)); test.beforeEach(({ page }) => beforeCoderTest(page));
test("start workspace with ephemeral parameters", async ({ page }) => { test("start workspace with ephemeral parameters", async ({ page }) => {
const richParameters: RichParameter[] = [firstBuildOption, secondBuildOption]; const richParameters: RichParameter[] = [firstBuildOption, secondBuildOption];
const template = await createTemplate( const template = await createTemplate(
page, page,
echoResponsesWithParameters(richParameters), echoResponsesWithParameters(richParameters),
); );
const workspaceName = await createWorkspace(page, template); const workspaceName = await createWorkspace(page, template);
// Verify that build options are default (not selected). // Verify that build options are default (not selected).
await verifyParameters(page, workspaceName, richParameters, [ await verifyParameters(page, workspaceName, richParameters, [
{ name: richParameters[0].name, value: firstBuildOption.defaultValue }, { name: richParameters[0].name, value: firstBuildOption.defaultValue },
{ name: richParameters[1].name, value: secondBuildOption.defaultValue }, { name: richParameters[1].name, value: secondBuildOption.defaultValue },
]); ]);
// Stop the workspace // Stop the workspace
await stopWorkspace(page, workspaceName); await stopWorkspace(page, workspaceName);
// Now, start the workspace with ephemeral parameters selected. // Now, start the workspace with ephemeral parameters selected.
const buildParameters = [ const buildParameters = [
{ name: richParameters[0].name, value: "AAAAA" }, { name: richParameters[0].name, value: "AAAAA" },
{ name: richParameters[1].name, value: "true" }, { name: richParameters[1].name, value: "true" },
]; ];
await buildWorkspaceWithParameters( await buildWorkspaceWithParameters(
page, page,
workspaceName, workspaceName,
richParameters, richParameters,
buildParameters, buildParameters,
); );
// Verify that build options are default (not selected). // Verify that build options are default (not selected).
await verifyParameters(page, workspaceName, richParameters, [ await verifyParameters(page, workspaceName, richParameters, [
{ name: richParameters[0].name, value: firstBuildOption.defaultValue }, { name: richParameters[0].name, value: firstBuildOption.defaultValue },
{ name: richParameters[1].name, value: secondBuildOption.defaultValue }, { name: richParameters[1].name, value: secondBuildOption.defaultValue },
]); ]);
}); });
@@ -1,132 +1,132 @@
import { test } from "@playwright/test"; import { test } from "@playwright/test";
import { import {
createTemplate, createTemplate,
createWorkspace, createWorkspace,
echoResponsesWithParameters, echoResponsesWithParameters,
updateTemplate, updateTemplate,
updateWorkspace, updateWorkspace,
updateWorkspaceParameters, updateWorkspaceParameters,
verifyParameters, verifyParameters,
} from "../../helpers"; } from "../../helpers";
import { beforeCoderTest } from "../../hooks"; import { beforeCoderTest } from "../../hooks";
import { import {
fifthParameter, fifthParameter,
firstParameter, firstParameter,
secondBuildOption, secondBuildOption,
secondParameter, secondParameter,
sixthParameter, sixthParameter,
} from "../../parameters"; } from "../../parameters";
import type { RichParameter } from "../../provisionerGenerated"; import type { RichParameter } from "../../provisionerGenerated";
test.beforeEach(({ page }) => beforeCoderTest(page)); test.beforeEach(({ page }) => beforeCoderTest(page));
test("update workspace, new optional, immutable parameter added", async ({ test("update workspace, new optional, immutable parameter added", async ({
page, page,
}) => { }) => {
const richParameters: RichParameter[] = [firstParameter, secondParameter]; const richParameters: RichParameter[] = [firstParameter, secondParameter];
const template = await createTemplate( const template = await createTemplate(
page, page,
echoResponsesWithParameters(richParameters), echoResponsesWithParameters(richParameters),
); );
const workspaceName = await createWorkspace(page, template); const workspaceName = await createWorkspace(page, template);
// Verify that parameter values are default. // Verify that parameter values are default.
await verifyParameters(page, workspaceName, richParameters, [ await verifyParameters(page, workspaceName, richParameters, [
{ name: firstParameter.name, value: firstParameter.defaultValue }, { name: firstParameter.name, value: firstParameter.defaultValue },
{ name: secondParameter.name, value: secondParameter.defaultValue }, { name: secondParameter.name, value: secondParameter.defaultValue },
]); ]);
// Push updated template. // Push updated template.
const updatedRichParameters = [...richParameters, fifthParameter]; const updatedRichParameters = [...richParameters, fifthParameter];
await updateTemplate( await updateTemplate(
page, page,
template, template,
echoResponsesWithParameters(updatedRichParameters), echoResponsesWithParameters(updatedRichParameters),
); );
// Now, update the workspace, and select the value for immutable parameter. // Now, update the workspace, and select the value for immutable parameter.
await updateWorkspace(page, workspaceName, updatedRichParameters, [ await updateWorkspace(page, workspaceName, updatedRichParameters, [
{ name: fifthParameter.name, value: fifthParameter.options[0].value }, { name: fifthParameter.name, value: fifthParameter.options[0].value },
]); ]);
// Verify parameter values. // Verify parameter values.
await verifyParameters(page, workspaceName, updatedRichParameters, [ await verifyParameters(page, workspaceName, updatedRichParameters, [
{ name: firstParameter.name, value: firstParameter.defaultValue }, { name: firstParameter.name, value: firstParameter.defaultValue },
{ name: secondParameter.name, value: secondParameter.defaultValue }, { name: secondParameter.name, value: secondParameter.defaultValue },
{ name: fifthParameter.name, value: fifthParameter.options[0].value }, { name: fifthParameter.name, value: fifthParameter.options[0].value },
]); ]);
}); });
test("update workspace, new required, mutable parameter added", async ({ test("update workspace, new required, mutable parameter added", async ({
page, page,
}) => { }) => {
const richParameters: RichParameter[] = [firstParameter, secondParameter]; const richParameters: RichParameter[] = [firstParameter, secondParameter];
const template = await createTemplate( const template = await createTemplate(
page, page,
echoResponsesWithParameters(richParameters), echoResponsesWithParameters(richParameters),
); );
const workspaceName = await createWorkspace(page, template); const workspaceName = await createWorkspace(page, template);
// Verify that parameter values are default. // Verify that parameter values are default.
await verifyParameters(page, workspaceName, richParameters, [ await verifyParameters(page, workspaceName, richParameters, [
{ name: firstParameter.name, value: firstParameter.defaultValue }, { name: firstParameter.name, value: firstParameter.defaultValue },
{ name: secondParameter.name, value: secondParameter.defaultValue }, { name: secondParameter.name, value: secondParameter.defaultValue },
]); ]);
// Push updated template. // Push updated template.
const updatedRichParameters = [...richParameters, sixthParameter]; const updatedRichParameters = [...richParameters, sixthParameter];
await updateTemplate( await updateTemplate(
page, page,
template, template,
echoResponsesWithParameters(updatedRichParameters), echoResponsesWithParameters(updatedRichParameters),
); );
// Now, update the workspace, and provide the parameter value. // Now, update the workspace, and provide the parameter value.
const buildParameters = [{ name: sixthParameter.name, value: "99" }]; const buildParameters = [{ name: sixthParameter.name, value: "99" }];
await updateWorkspace( await updateWorkspace(
page, page,
workspaceName, workspaceName,
updatedRichParameters, updatedRichParameters,
buildParameters, buildParameters,
); );
// Verify parameter values. // Verify parameter values.
await verifyParameters(page, workspaceName, updatedRichParameters, [ await verifyParameters(page, workspaceName, updatedRichParameters, [
{ name: firstParameter.name, value: firstParameter.defaultValue }, { name: firstParameter.name, value: firstParameter.defaultValue },
{ name: secondParameter.name, value: secondParameter.defaultValue }, { name: secondParameter.name, value: secondParameter.defaultValue },
...buildParameters, ...buildParameters,
]); ]);
}); });
test("update workspace with ephemeral parameter enabled", async ({ page }) => { test("update workspace with ephemeral parameter enabled", async ({ page }) => {
const richParameters: RichParameter[] = [firstParameter, secondBuildOption]; const richParameters: RichParameter[] = [firstParameter, secondBuildOption];
const template = await createTemplate( const template = await createTemplate(
page, page,
echoResponsesWithParameters(richParameters), echoResponsesWithParameters(richParameters),
); );
const workspaceName = await createWorkspace(page, template); const workspaceName = await createWorkspace(page, template);
// Verify that parameter values are default. // Verify that parameter values are default.
await verifyParameters(page, workspaceName, richParameters, [ await verifyParameters(page, workspaceName, richParameters, [
{ name: firstParameter.name, value: firstParameter.defaultValue }, { name: firstParameter.name, value: firstParameter.defaultValue },
{ name: secondBuildOption.name, value: secondBuildOption.defaultValue }, { name: secondBuildOption.name, value: secondBuildOption.defaultValue },
]); ]);
// Now, update the workspace, and select the value for ephemeral parameter. // Now, update the workspace, and select the value for ephemeral parameter.
const buildParameters = [{ name: secondBuildOption.name, value: "true" }]; const buildParameters = [{ name: secondBuildOption.name, value: "true" }];
await updateWorkspaceParameters( await updateWorkspaceParameters(
page, page,
workspaceName, workspaceName,
richParameters, richParameters,
buildParameters, buildParameters,
); );
// Verify that parameter values are default. // Verify that parameter values are default.
await verifyParameters(page, workspaceName, richParameters, [ await verifyParameters(page, workspaceName, richParameters, [
{ name: firstParameter.name, value: firstParameter.defaultValue }, { name: firstParameter.name, value: firstParameter.defaultValue },
{ name: secondBuildOption.name, value: secondBuildOption.defaultValue }, { name: secondBuildOption.name, value: secondBuildOption.defaultValue },
]); ]);
}); });

Some files were not shown because too many files have changed in this diff Show More