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",
"image": "codercom/oss-dogfood:latest",
"name": "Development environments on your infrastructure",
"image": "codercom/oss-dogfood:latest",
"features": {
// See all possible options here https://github.com/devcontainers/features/tree/main/src/docker-in-docker
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"moby": "false"
}
},
// SYS_PTRACE to enable go debugging
"runArgs": ["--cap-add=SYS_PTRACE"]
"features": {
// See all possible options here https://github.com/devcontainers/features/tree/main/src/docker-in-docker
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"moby": "false"
}
},
// SYS_PTRACE to enable go debugging
"runArgs": ["--cap-add=SYS_PTRACE"]
}
-4
View File
@@ -95,10 +95,6 @@ updates:
- "@emotion*"
exclude-patterns:
- "jest-runner-eslint"
eslint:
patterns:
- "eslint*"
- "@typescript-eslint*"
jest:
patterns:
- "jest"
+24 -24
View File
@@ -1,26 +1,26 @@
{
"ignorePatterns": [
{
"pattern": "://localhost"
},
{
"pattern": "://.*.?example\\.com"
},
{
"pattern": "developer.github.com"
},
{
"pattern": "docs.github.com"
},
{
"pattern": "support.google.com"
},
{
"pattern": "tailscale.com"
},
{
"pattern": "wireguard.com"
}
],
"aliveStatusCodes": [200, 0]
"ignorePatterns": [
{
"pattern": "://localhost"
},
{
"pattern": "://.*.?example\\.com"
},
{
"pattern": "developer.github.com"
},
{
"pattern": "docs.github.com"
},
{
"pattern": "support.google.com"
},
{
"pattern": "tailscale.com"
},
{
"pattern": "wireguard.com"
}
],
"aliveStatusCodes": [200, 0]
}
+1 -1
View File
@@ -4,7 +4,7 @@
printWidth: 80
proseWrap: always
trailingComma: all
useTabs: false
useTabs: true
tabWidth: 2
overrides:
- files:
+13 -13
View File
@@ -1,15 +1,15 @@
{
"recommendations": [
"github.vscode-codeql",
"golang.go",
"hashicorp.terraform",
"esbenp.prettier-vscode",
"foxundermoon.shell-format",
"emeraldwalk.runonsave",
"zxh404.vscode-proto3",
"redhat.vscode-yaml",
"streetsidesoftware.code-spell-checker",
"EditorConfig.EditorConfig",
"biomejs.biome"
]
"recommendations": [
"github.vscode-codeql",
"golang.go",
"hashicorp.terraform",
"esbenp.prettier-vscode",
"foxundermoon.shell-format",
"emeraldwalk.runonsave",
"zxh404.vscode-proto3",
"redhat.vscode-yaml",
"streetsidesoftware.code-spell-checker",
"EditorConfig.EditorConfig",
"biomejs.biome"
]
}
+234 -234
View File
@@ -1,238 +1,238 @@
{
"cSpell.words": [
"afero",
"agentsdk",
"apps",
"ASKPASS",
"authcheck",
"autostop",
"awsidentity",
"bodyclose",
"buildinfo",
"buildname",
"circbuf",
"cliflag",
"cliui",
"codecov",
"coderd",
"coderdenttest",
"coderdtest",
"codersdk",
"contravariance",
"cronstrue",
"databasefake",
"dbgen",
"dbmem",
"dbtype",
"DERP",
"derphttp",
"derpmap",
"devel",
"devtunnel",
"dflags",
"drpc",
"drpcconn",
"drpcmux",
"drpcserver",
"Dsts",
"embeddedpostgres",
"enablements",
"enterprisemeta",
"errgroup",
"eventsourcemock",
"externalauth",
"Failf",
"fatih",
"Formik",
"gitauth",
"gitsshkey",
"goarch",
"gographviz",
"goleak",
"gonet",
"gossh",
"gsyslog",
"GTTY",
"hashicorp",
"hclsyntax",
"httpapi",
"httpmw",
"idtoken",
"Iflag",
"incpatch",
"initialisms",
"ipnstate",
"isatty",
"Jobf",
"Keygen",
"kirsle",
"Kubernetes",
"ldflags",
"magicsock",
"manifoldco",
"mapstructure",
"mattn",
"mitchellh",
"moby",
"namesgenerator",
"namespacing",
"netaddr",
"netip",
"netmap",
"netns",
"netstack",
"nettype",
"nfpms",
"nhooyr",
"nmcfg",
"nolint",
"nosec",
"ntqry",
"OIDC",
"oneof",
"opty",
"paralleltest",
"parameterscopeid",
"pqtype",
"prometheusmetrics",
"promhttp",
"protobuf",
"provisionerd",
"provisionerdserver",
"provisionersdk",
"ptty",
"ptys",
"ptytest",
"quickstart",
"reconfig",
"replicasync",
"retrier",
"rpty",
"SCIM",
"sdkproto",
"sdktrace",
"Signup",
"slogtest",
"sourcemapped",
"spinbutton",
"Srcs",
"stdbuf",
"stretchr",
"STTY",
"stuntest",
"tailbroker",
"tailcfg",
"tailexchange",
"tailnet",
"tailnettest",
"Tailscale",
"tanstack",
"tbody",
"TCGETS",
"tcpip",
"TCSETS",
"templateversions",
"testdata",
"testid",
"testutil",
"tfexec",
"tfjson",
"tfplan",
"tfstate",
"thead",
"tios",
"tmpdir",
"tokenconfig",
"Topbar",
"tparallel",
"trialer",
"trimprefix",
"tsdial",
"tslogger",
"tstun",
"turnconn",
"typegen",
"typesafe",
"unconvert",
"Untar",
"Userspace",
"VMID",
"walkthrough",
"weblinks",
"webrtc",
"wgcfg",
"wgconfig",
"wgengine",
"wgmonitor",
"wgnet",
"workspaceagent",
"workspaceagents",
"workspaceapp",
"workspaceapps",
"workspacebuilds",
"workspacename",
"wsjson",
"xerrors",
"xlarge",
"xsmall",
"yamux"
],
"cSpell.ignorePaths": ["site/package.json", ".vscode/settings.json"],
"emeraldwalk.runonsave": {
"commands": [
{
"match": "database/queries/*.sql",
"cmd": "make gen"
},
{
"match": "provisionerd/proto/provisionerd.proto",
"cmd": "make provisionerd/proto/provisionerd.pb.go"
}
]
},
"search.exclude": {
"**.pb.go": true,
"**/*.gen.json": true,
"**/testdata/*": true,
"coderd/apidoc/**": true,
"docs/reference/api/*.md": true,
"docs/reference/cli/*.md": true,
"docs/templates/*.md": true,
"LICENSE": true,
"scripts/metricsdocgen/metrics": true,
"site/out/**": true,
"site/storybook-static/**": true,
"**.map": true,
"pnpm-lock.yaml": true
},
// Ensure files always have a newline.
"files.insertFinalNewline": true,
"go.lintTool": "golangci-lint",
"go.lintFlags": ["--fast"],
"go.coverageDecorator": {
"type": "gutter",
"coveredGutterStyle": "blockgreen",
"uncoveredGutterStyle": "blockred"
},
// The codersdk is used by coderd another other packages extensively.
// To reduce redundancy in tests, it's covered by other packages.
// Since package coverage pairing can't be defined, all packages cover
// all other packages.
"go.testFlags": ["-short", "-coverpkg=./..."],
// We often use a version of TypeScript that's ahead of the version shipped
// with VS Code.
"typescript.tsdk": "./site/node_modules/typescript/lib",
// Playwright tests in VSCode will open a browser to live "view" the test.
"playwright.reuseBrowser": true,
"cSpell.words": [
"afero",
"agentsdk",
"apps",
"ASKPASS",
"authcheck",
"autostop",
"awsidentity",
"bodyclose",
"buildinfo",
"buildname",
"circbuf",
"cliflag",
"cliui",
"codecov",
"coderd",
"coderdenttest",
"coderdtest",
"codersdk",
"contravariance",
"cronstrue",
"databasefake",
"dbgen",
"dbmem",
"dbtype",
"DERP",
"derphttp",
"derpmap",
"devel",
"devtunnel",
"dflags",
"drpc",
"drpcconn",
"drpcmux",
"drpcserver",
"Dsts",
"embeddedpostgres",
"enablements",
"enterprisemeta",
"errgroup",
"eventsourcemock",
"externalauth",
"Failf",
"fatih",
"Formik",
"gitauth",
"gitsshkey",
"goarch",
"gographviz",
"goleak",
"gonet",
"gossh",
"gsyslog",
"GTTY",
"hashicorp",
"hclsyntax",
"httpapi",
"httpmw",
"idtoken",
"Iflag",
"incpatch",
"initialisms",
"ipnstate",
"isatty",
"Jobf",
"Keygen",
"kirsle",
"Kubernetes",
"ldflags",
"magicsock",
"manifoldco",
"mapstructure",
"mattn",
"mitchellh",
"moby",
"namesgenerator",
"namespacing",
"netaddr",
"netip",
"netmap",
"netns",
"netstack",
"nettype",
"nfpms",
"nhooyr",
"nmcfg",
"nolint",
"nosec",
"ntqry",
"OIDC",
"oneof",
"opty",
"paralleltest",
"parameterscopeid",
"pqtype",
"prometheusmetrics",
"promhttp",
"protobuf",
"provisionerd",
"provisionerdserver",
"provisionersdk",
"ptty",
"ptys",
"ptytest",
"quickstart",
"reconfig",
"replicasync",
"retrier",
"rpty",
"SCIM",
"sdkproto",
"sdktrace",
"Signup",
"slogtest",
"sourcemapped",
"spinbutton",
"Srcs",
"stdbuf",
"stretchr",
"STTY",
"stuntest",
"tailbroker",
"tailcfg",
"tailexchange",
"tailnet",
"tailnettest",
"Tailscale",
"tanstack",
"tbody",
"TCGETS",
"tcpip",
"TCSETS",
"templateversions",
"testdata",
"testid",
"testutil",
"tfexec",
"tfjson",
"tfplan",
"tfstate",
"thead",
"tios",
"tmpdir",
"tokenconfig",
"Topbar",
"tparallel",
"trialer",
"trimprefix",
"tsdial",
"tslogger",
"tstun",
"turnconn",
"typegen",
"typesafe",
"unconvert",
"Untar",
"Userspace",
"VMID",
"walkthrough",
"weblinks",
"webrtc",
"wgcfg",
"wgconfig",
"wgengine",
"wgmonitor",
"wgnet",
"workspaceagent",
"workspaceagents",
"workspaceapp",
"workspaceapps",
"workspacebuilds",
"workspacename",
"wsjson",
"xerrors",
"xlarge",
"xsmall",
"yamux"
],
"cSpell.ignorePaths": ["site/package.json", ".vscode/settings.json"],
"emeraldwalk.runonsave": {
"commands": [
{
"match": "database/queries/*.sql",
"cmd": "make gen"
},
{
"match": "provisionerd/proto/provisionerd.proto",
"cmd": "make provisionerd/proto/provisionerd.pb.go"
}
]
},
"search.exclude": {
"**.pb.go": true,
"**/*.gen.json": true,
"**/testdata/*": true,
"coderd/apidoc/**": true,
"docs/reference/api/*.md": true,
"docs/reference/cli/*.md": true,
"docs/templates/*.md": true,
"LICENSE": true,
"scripts/metricsdocgen/metrics": true,
"site/out/**": true,
"site/storybook-static/**": true,
"**.map": true,
"pnpm-lock.yaml": true
},
// Ensure files always have a newline.
"files.insertFinalNewline": true,
"go.lintTool": "golangci-lint",
"go.lintFlags": ["--fast"],
"go.coverageDecorator": {
"type": "gutter",
"coveredGutterStyle": "blockgreen",
"uncoveredGutterStyle": "blockred"
},
// The codersdk is used by coderd another other packages extensively.
// To reduce redundancy in tests, it's covered by other packages.
// Since package coverage pairing can't be defined, all packages cover
// all other packages.
"go.testFlags": ["-short", "-coverpkg=./..."],
// We often use a version of TypeScript that's ahead of the version shipped
// with VS Code.
"typescript.tsdk": "./site/node_modules/typescript/lib",
// Playwright tests in VSCode will open a browser to live "view" the test.
"playwright.reuseBrowser": true,
"[javascript][javascriptreact][json][jsonc][typescript][typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
// "editor.codeActionsOnSave": {
// "source.organizeImports.biome": "explicit"
// }
},
"[javascript][javascriptreact][json][jsonc][typescript][typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
// "editor.codeActionsOnSave": {
// "source.organizeImports.biome": "explicit"
// }
},
"[css][html][markdown][yaml]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
"[css][html][markdown][yaml]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
+4 -4
View File
@@ -491,7 +491,7 @@ gen: \
site/src/api/typesGenerated.ts \
coderd/rbac/object_gen.go \
codersdk/rbacresources_gen.go \
site/src/api/rbacresources_gen.ts \
site/src/api/rbacresourcesGenerated.ts \
docs/admin/prometheus.md \
docs/reference/cli/README.md \
docs/admin/audit-logs.md \
@@ -520,7 +520,7 @@ gen/mark-fresh:
site/src/api/typesGenerated.ts \
coderd/rbac/object_gen.go \
codersdk/rbacresources_gen.go \
site/src/api/rbacresources_gen.ts \
site/src/api/rbacresourcesGenerated.ts \
docs/admin/prometheus.md \
docs/reference/cli/README.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
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
go run scripts/rbacgen/main.go typescript > site/src/api/rbacresources_gen.ts
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 > "$@"
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",
"object": {
"id": "9046b041-58ed-47a3-9c3a-de302577875a",
"owner": "00000000-0000-0000-0000-000000000000",
"org_owner": "bf7b72bd-a2b1-4ef2-962c-1d698e0483f6",
"type": "workspace",
"acl_user_list": {
"f041847d-711b-40da-a89a-ede39f70dc7f": ["create"]
},
"acl_group_list": {}
},
"subject": {
"id": "10d03e62-7703-4df5-a358-4f76577d4e2f",
"roles": [
{
"name": "owner",
"display_name": "Owner",
"site": [
{
"negate": false,
"resource_type": "*",
"action": "*"
}
],
"org": {},
"user": []
}
],
"groups": ["b617a647-b5d0-4cbe-9e40-26f89710bf18"],
"scope": {
"name": "Scope_all",
"display_name": "All operations",
"site": [
{
"negate": false,
"resource_type": "*",
"action": "*"
}
],
"org": {},
"user": [],
"allow_list": ["*"]
}
}
"action": "never-match-action",
"object": {
"id": "9046b041-58ed-47a3-9c3a-de302577875a",
"owner": "00000000-0000-0000-0000-000000000000",
"org_owner": "bf7b72bd-a2b1-4ef2-962c-1d698e0483f6",
"type": "workspace",
"acl_user_list": {
"f041847d-711b-40da-a89a-ede39f70dc7f": ["create"]
},
"acl_group_list": {}
},
"subject": {
"id": "10d03e62-7703-4df5-a358-4f76577d4e2f",
"roles": [
{
"name": "owner",
"display_name": "Owner",
"site": [
{
"negate": false,
"resource_type": "*",
"action": "*"
}
],
"org": {},
"user": []
}
],
"groups": ["b617a647-b5d0-4cbe-9e40-26f89710bf18"],
"scope": {
"name": "Scope_all",
"display_name": "All operations",
"site": [
{
"negate": false,
"resource_type": "*",
"action": "*"
}
],
"org": {},
"user": [],
"allow_list": ["*"]
}
}
}
+28 -28
View File
@@ -82,34 +82,34 @@ entry:
```json
{
"ts": "2023-06-13T03:45:37.294730279Z",
"level": "INFO",
"msg": "audit_log",
"caller": "/home/runner/work/coder/coder/enterprise/audit/backends/slog.go:36",
"func": "github.com/coder/coder/enterprise/audit/backends.slogBackend.Export",
"logger_names": ["coderd"],
"fields": {
"ID": "033a9ffa-b54d-4c10-8ec3-2aaf9e6d741a",
"Time": "2023-06-13T03:45:37.288506Z",
"UserID": "6c405053-27e3-484a-9ad7-bcb64e7bfde6",
"OrganizationID": "00000000-0000-0000-0000-000000000000",
"Ip": "{IPNet:{IP:\u003cnil\u003e Mask:\u003cnil\u003e} Valid:false}",
"UserAgent": "{String: Valid:false}",
"ResourceType": "workspace_build",
"ResourceID": "ca5647e0-ef50-4202-a246-717e04447380",
"ResourceTarget": "",
"Action": "start",
"Diff": {},
"StatusCode": 200,
"AdditionalFields": {
"workspace_name": "linux-container",
"build_number": "9",
"build_reason": "initiator",
"workspace_owner": ""
},
"RequestID": "bb791ac3-f6ee-4da8-8ec2-f54e87013e93",
"ResourceIcon": ""
}
"ts": "2023-06-13T03:45:37.294730279Z",
"level": "INFO",
"msg": "audit_log",
"caller": "/home/runner/work/coder/coder/enterprise/audit/backends/slog.go:36",
"func": "github.com/coder/coder/enterprise/audit/backends.slogBackend.Export",
"logger_names": ["coderd"],
"fields": {
"ID": "033a9ffa-b54d-4c10-8ec3-2aaf9e6d741a",
"Time": "2023-06-13T03:45:37.288506Z",
"UserID": "6c405053-27e3-484a-9ad7-bcb64e7bfde6",
"OrganizationID": "00000000-0000-0000-0000-000000000000",
"Ip": "{IPNet:{IP:\u003cnil\u003e Mask:\u003cnil\u003e} Valid:false}",
"UserAgent": "{String: Valid:false}",
"ResourceType": "workspace_build",
"ResourceID": "ca5647e0-ef50-4202-a246-717e04447380",
"ResourceTarget": "",
"Action": "start",
"Diff": {},
"StatusCode": 200,
"AdditionalFields": {
"workspace_name": "linux-container",
"build_number": "9",
"build_reason": "initiator",
"workspace_owner": ""
},
"RequestID": "bb791ac3-f6ee-4da8-8ec2-f54e87013e93",
"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.
[azure-gids]:
https://github.com/MicrosoftDocs/azure-docs/issues/59766#issuecomment-664387195
https://github.com/MicrosoftDocs/azure-docs/issues/59766#issuecomment-664387195
### Group allowlist
+23 -23
View File
@@ -125,17 +125,17 @@ within the component's story.
```tsx
export const WithQuota: Story = {
parameters: {
queries: [
{
key: getWorkspaceQuotaQueryKey(MockUser.username),
data: {
credits_consumed: 2,
budget: 40,
},
},
],
},
parameters: {
queries: [
{
key: getWorkspaceQuotaQueryKey(MockUser.username),
data: {
credits_consumed: 2,
budget: 40,
},
},
],
},
};
```
@@ -150,12 +150,12 @@ example below:
```ts
export const getAgentListeningPorts = async (
agentID: string,
agentID: string,
): Promise<TypesGen.ListeningPortsResponse> => {
const response = await axiosInstance.get(
`/api/v2/workspaceagents/${agentID}/listening-ports`,
);
return response.data;
const response = await axiosInstance.get(
`/api/v2/workspaceagents/${agentID}/listening-ports`,
);
return response.data;
};
```
@@ -164,10 +164,10 @@ wrap it as a single function.
```ts
export const updateWorkspaceVersion = async (
workspace: TypesGen.Workspace,
workspace: TypesGen.Workspace,
): Promise<TypesGen.WorkspaceBuild> => {
const template = await getTemplate(workspace.template_id);
return startWorkspace(workspace.id, template.active_version_id);
const template = await getTemplate(workspace.template_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";
<Button>
<GearIcon />
<Box component="span" sx={visuallyHidden}>
Settings
</Box>
<GearIcon />
<Box component="span" sx={visuallyHidden}>
Settings
</Box>
</Button>;
```
+59 -59
View File
@@ -39,21 +39,21 @@ following:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "accounts.google.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"accounts.google.com:aud": "<enter-OAuth-client-ID-here"
}
}
}
]
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "accounts.google.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"accounts.google.com:aud": "<enter-OAuth-client-ID-here"
}
}
}
]
}
```
@@ -64,50 +64,50 @@ following policy to the role:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"ec2:GetDefaultCreditSpecification",
"ec2:DescribeIamInstanceProfileAssociations",
"ec2:DescribeTags",
"ec2:DescribeInstances",
"ec2:DescribeInstanceTypes",
"ec2:CreateTags",
"ec2:RunInstances",
"ec2:DescribeInstanceCreditSpecifications",
"ec2:DescribeImages",
"ec2:ModifyDefaultCreditSpecification",
"ec2:DescribeVolumes"
],
"Resource": "*"
},
{
"Sid": "CoderResources",
"Effect": "Allow",
"Action": [
"ec2:DescribeInstanceAttribute",
"ec2:UnmonitorInstances",
"ec2:TerminateInstances",
"ec2:StartInstances",
"ec2:StopInstances",
"ec2:DeleteTags",
"ec2:MonitorInstances",
"ec2:CreateTags",
"ec2:RunInstances",
"ec2:ModifyInstanceAttribute",
"ec2:ModifyInstanceCreditSpecification"
],
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringEquals": {
"aws:ResourceTag/Coder_Provisioned": "true"
}
}
}
]
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"ec2:GetDefaultCreditSpecification",
"ec2:DescribeIamInstanceProfileAssociations",
"ec2:DescribeTags",
"ec2:DescribeInstances",
"ec2:DescribeInstanceTypes",
"ec2:CreateTags",
"ec2:RunInstances",
"ec2:DescribeInstanceCreditSpecifications",
"ec2:DescribeImages",
"ec2:ModifyDefaultCreditSpecification",
"ec2:DescribeVolumes"
],
"Resource": "*"
},
{
"Sid": "CoderResources",
"Effect": "Allow",
"Action": [
"ec2:DescribeInstanceAttribute",
"ec2:UnmonitorInstances",
"ec2:TerminateInstances",
"ec2:StartInstances",
"ec2:StopInstances",
"ec2:DeleteTags",
"ec2:MonitorInstances",
"ec2:CreateTags",
"ec2:RunInstances",
"ec2:ModifyInstanceAttribute",
"ec2:ModifyInstanceCreditSpecification"
],
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringEquals": {
"aws:ResourceTag/Coder_Provisioned": "true"
}
}
}
]
}
```
+13 -13
View File
@@ -23,12 +23,12 @@ actual Docker registry URL, username, and password.
```json
{
"auths": {
"<your-registry>": {
"username": "<your-username>",
"password": "<your-password>"
}
}
"auths": {
"<your-registry>": {
"username": "<your-username>",
"password": "<your-password>"
}
}
}
```
@@ -54,13 +54,13 @@ The output should look similar to this:
```json
{
"auths": {
"your.private.registry.com": {
"username": "ericpaulsen",
"password": "xxxx",
"auth": "c3R...zE2"
}
}
"auths": {
"your.private.registry.com": {
"username": "ericpaulsen",
"password": "xxxx",
"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
{
"document": "string",
"signature": "string"
"document": "string",
"signature": "string"
}
```
@@ -55,7 +55,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaceagents/aws-instance-identi
```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
{
"encoding": "string",
"signature": "string"
"encoding": "string",
"signature": "string"
}
```
@@ -102,7 +102,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaceagents/azure-instance-iden
```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_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
{
"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
{
"access_token": "string",
"password": "string",
"token_extra": {},
"type": "string",
"url": "string",
"username": "string"
"access_token": "string",
"password": "string",
"token_extra": {},
"type": "string",
"url": "string",
"username": "string"
}
```
@@ -231,12 +231,12 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/gitauth?match=str
```json
{
"access_token": "string",
"password": "string",
"token_extra": {},
"type": "string",
"url": "string",
"username": "string"
"access_token": "string",
"password": "string",
"token_extra": {},
"type": "string",
"url": "string",
"username": "string"
}
```
@@ -267,8 +267,8 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/gitsshkey \
```json
{
"private_key": "string",
"public_key": "string"
"private_key": "string",
"public_key": "string"
}
```
@@ -298,9 +298,9 @@ curl -X POST http://coder-server:8080/api/v2/workspaceagents/me/log-source \
```json
{
"display_name": "string",
"icon": "string",
"id": "string"
"display_name": "string",
"icon": "string",
"id": "string"
}
```
@@ -316,11 +316,11 @@ curl -X POST http://coder-server:8080/api/v2/workspaceagents/me/log-source \
```json
{
"created_at": "2019-08-24T14:15:22Z",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"workspace_agent_id": "7ad2e618-fea7-4c1a-b70a-f501566a72f1"
"created_at": "2019-08-24T14:15:22Z",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"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
{
"log_source_id": "string",
"logs": [
{
"created_at": "string",
"level": "trace",
"output": "string"
}
]
"log_source_id": "string",
"logs": [
{
"created_at": "string",
"level": "trace",
"output": "string"
}
]
}
```
@@ -373,14 +373,14 @@ curl -X PATCH http://coder-server:8080/api/v2/workspaceagents/me/logs \
```json
{
"detail": "string",
"message": "string",
"validations": [
{
"detail": "string",
"field": "string"
}
]
"detail": "string",
"message": "string",
"validations": [
{
"detail": "string",
"field": "string"
}
]
}
```
@@ -417,91 +417,91 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent} \
```json
{
"api_version": "string",
"apps": [
{
"command": "string",
"display_name": "string",
"external": true,
"health": "disabled",
"healthcheck": {
"interval": 0,
"threshold": 0,
"url": "string"
},
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"sharing_level": "owner",
"slug": "string",
"subdomain": true,
"subdomain_name": "string",
"url": "string"
}
],
"architecture": "string",
"connection_timeout_seconds": 0,
"created_at": "2019-08-24T14:15:22Z",
"directory": "string",
"disconnected_at": "2019-08-24T14:15:22Z",
"display_apps": ["vscode"],
"environment_variables": {
"property1": "string",
"property2": "string"
},
"expanded_directory": "string",
"first_connected_at": "2019-08-24T14:15:22Z",
"health": {
"healthy": false,
"reason": "agent has lost connection"
},
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"instance_id": "string",
"last_connected_at": "2019-08-24T14:15:22Z",
"latency": {
"property1": {
"latency_ms": 0,
"preferred": true
},
"property2": {
"latency_ms": 0,
"preferred": true
}
},
"lifecycle_state": "created",
"log_sources": [
{
"created_at": "2019-08-24T14:15:22Z",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"workspace_agent_id": "7ad2e618-fea7-4c1a-b70a-f501566a72f1"
}
],
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
"resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f",
"scripts": [
{
"cron": "string",
"log_path": "string",
"log_source_id": "4197ab25-95cf-4b91-9c78-f7f2af5d353a",
"run_on_start": true,
"run_on_stop": true,
"script": "string",
"start_blocks_login": true,
"timeout": 0
}
],
"started_at": "2019-08-24T14:15:22Z",
"startup_script_behavior": "blocking",
"status": "connecting",
"subsystems": ["envbox"],
"troubleshooting_url": "string",
"updated_at": "2019-08-24T14:15:22Z",
"version": "string"
"api_version": "string",
"apps": [
{
"command": "string",
"display_name": "string",
"external": true,
"health": "disabled",
"healthcheck": {
"interval": 0,
"threshold": 0,
"url": "string"
},
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"sharing_level": "owner",
"slug": "string",
"subdomain": true,
"subdomain_name": "string",
"url": "string"
}
],
"architecture": "string",
"connection_timeout_seconds": 0,
"created_at": "2019-08-24T14:15:22Z",
"directory": "string",
"disconnected_at": "2019-08-24T14:15:22Z",
"display_apps": ["vscode"],
"environment_variables": {
"property1": "string",
"property2": "string"
},
"expanded_directory": "string",
"first_connected_at": "2019-08-24T14:15:22Z",
"health": {
"healthy": false,
"reason": "agent has lost connection"
},
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"instance_id": "string",
"last_connected_at": "2019-08-24T14:15:22Z",
"latency": {
"property1": {
"latency_ms": 0,
"preferred": true
},
"property2": {
"latency_ms": 0,
"preferred": true
}
},
"lifecycle_state": "created",
"log_sources": [
{
"created_at": "2019-08-24T14:15:22Z",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"workspace_agent_id": "7ad2e618-fea7-4c1a-b70a-f501566a72f1"
}
],
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
"resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f",
"scripts": [
{
"cron": "string",
"log_path": "string",
"log_source_id": "4197ab25-95cf-4b91-9c78-f7f2af5d353a",
"run_on_start": true,
"run_on_stop": true,
"script": "string",
"start_blocks_login": true,
"timeout": 0
}
],
"started_at": "2019-08-24T14:15:22Z",
"startup_script_behavior": "blocking",
"status": "connecting",
"subsystems": ["envbox"],
"troubleshooting_url": "string",
"updated_at": "2019-08-24T14:15:22Z",
"version": "string"
}
```
@@ -538,67 +538,67 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/con
```json
{
"derp_force_websockets": true,
"derp_map": {
"homeParams": {
"regionScore": {
"property1": 0,
"property2": 0
}
},
"omitDefaultRegions": true,
"regions": {
"property1": {
"avoid": true,
"embeddedRelay": true,
"nodes": [
{
"canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
"hostName": "string",
"insecureForTests": true,
"ipv4": "string",
"ipv6": "string",
"name": "string",
"regionID": 0,
"stunonly": true,
"stunport": 0,
"stuntestIP": "string"
}
],
"regionCode": "string",
"regionID": 0,
"regionName": "string"
},
"property2": {
"avoid": true,
"embeddedRelay": true,
"nodes": [
{
"canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
"hostName": "string",
"insecureForTests": true,
"ipv4": "string",
"ipv6": "string",
"name": "string",
"regionID": 0,
"stunonly": true,
"stunport": 0,
"stuntestIP": "string"
}
],
"regionCode": "string",
"regionID": 0,
"regionName": "string"
}
}
},
"disable_direct_connections": true
"derp_force_websockets": true,
"derp_map": {
"homeParams": {
"regionScore": {
"property1": 0,
"property2": 0
}
},
"omitDefaultRegions": true,
"regions": {
"property1": {
"avoid": true,
"embeddedRelay": true,
"nodes": [
{
"canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
"hostName": "string",
"insecureForTests": true,
"ipv4": "string",
"ipv6": "string",
"name": "string",
"regionID": 0,
"stunonly": true,
"stunport": 0,
"stuntestIP": "string"
}
],
"regionCode": "string",
"regionID": 0,
"regionName": "string"
},
"property2": {
"avoid": true,
"embeddedRelay": true,
"nodes": [
{
"canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
"hostName": "string",
"insecureForTests": true,
"ipv4": "string",
"ipv6": "string",
"name": "string",
"regionID": 0,
"stunonly": true,
"stunport": 0,
"stuntestIP": "string"
}
],
"regionCode": "string",
"regionID": 0,
"regionName": "string"
}
}
},
"disable_direct_connections": true
}
```
@@ -661,13 +661,13 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/lis
```json
{
"ports": [
{
"network": "string",
"port": 0,
"process_name": "string"
}
]
"ports": [
{
"network": "string",
"port": 0,
"process_name": "string"
}
]
}
```
@@ -708,13 +708,13 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/log
```json
[
{
"created_at": "2019-08-24T14:15:22Z",
"id": 0,
"level": "trace",
"output": "string",
"source_id": "ae50a35c-df42-4eff-ba26-f8bc28d2af81"
}
{
"created_at": "2019-08-24T14:15:22Z",
"id": 0,
"level": "trace",
"output": "string",
"source_id": "ae50a35c-df42-4eff-ba26-f8bc28d2af81"
}
]
```
@@ -804,13 +804,13 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/sta
```json
[
{
"created_at": "2019-08-24T14:15:22Z",
"id": 0,
"level": "trace",
"output": "string",
"source_id": "ae50a35c-df42-4eff-ba26-f8bc28d2af81"
}
{
"created_at": "2019-08-24T14:15:22Z",
"id": 0,
"level": "trace",
"output": "string",
"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
{
"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
{
"audit_logs": [
{
"action": "create",
"additional_fields": [0],
"description": "string",
"diff": {
"property1": {
"new": null,
"old": null,
"secret": true
},
"property2": {
"new": null,
"old": null,
"secret": true
}
},
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"ip": "string",
"is_deleted": true,
"organization": {
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"name": "string"
},
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"request_id": "266ea41d-adf5-480b-af50-15b940c2b846",
"resource_icon": "string",
"resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f",
"resource_link": "string",
"resource_target": "string",
"resource_type": "template",
"status_code": 0,
"time": "2019-08-24T14:15:22Z",
"user": {
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "",
"name": "string",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
"display_name": "string",
"name": "string",
"organization_id": "string"
}
],
"status": "active",
"theme_preference": "string",
"updated_at": "2019-08-24T14:15:22Z",
"username": "string"
},
"user_agent": "string"
}
],
"count": 0
"audit_logs": [
{
"action": "create",
"additional_fields": [0],
"description": "string",
"diff": {
"property1": {
"new": null,
"old": null,
"secret": true
},
"property2": {
"new": null,
"old": null,
"secret": true
}
},
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"ip": "string",
"is_deleted": true,
"organization": {
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"name": "string"
},
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"request_id": "266ea41d-adf5-480b-af50-15b940c2b846",
"resource_icon": "string",
"resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f",
"resource_link": "string",
"resource_target": "string",
"resource_type": "template",
"status_code": 0,
"time": "2019-08-24T14:15:22Z",
"user": {
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "",
"name": "string",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
"display_name": "string",
"name": "string",
"organization_id": "string"
}
],
"status": "active",
"theme_preference": "string",
"updated_at": "2019-08-24T14:15:22Z",
"username": "string"
},
"user_agent": "string"
}
],
"count": 0
}
```
+33 -33
View File
@@ -18,28 +18,28 @@ curl -X POST http://coder-server:8080/api/v2/authcheck \
```json
{
"checks": {
"property1": {
"action": "create",
"object": {
"any_org": true,
"organization_id": "string",
"owner_id": "string",
"resource_id": "string",
"resource_type": "*"
}
},
"property2": {
"action": "create",
"object": {
"any_org": true,
"organization_id": "string",
"owner_id": "string",
"resource_id": "string",
"resource_type": "*"
}
}
}
"checks": {
"property1": {
"action": "create",
"object": {
"any_org": true,
"organization_id": "string",
"owner_id": "string",
"resource_id": "string",
"resource_type": "*"
}
},
"property2": {
"action": "create",
"object": {
"any_org": true,
"organization_id": "string",
"owner_id": "string",
"resource_id": "string",
"resource_type": "*"
}
}
}
}
```
@@ -55,8 +55,8 @@ curl -X POST http://coder-server:8080/api/v2/authcheck \
```json
{
"property1": true,
"property2": true
"property1": true,
"property2": true
}
```
@@ -85,8 +85,8 @@ curl -X POST http://coder-server:8080/api/v2/users/login \
```json
{
"email": "user@example.com",
"password": "string"
"email": "user@example.com",
"password": "string"
}
```
@@ -102,7 +102,7 @@ curl -X POST http://coder-server:8080/api/v2/users/login \
```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
{
"password": "string",
"to_type": ""
"password": "string",
"to_type": ""
}
```
@@ -148,10 +148,10 @@ curl -X POST http://coder-server:8080/api/v2/users/{user}/convert-login \
```json
{
"expires_at": "2019-08-24T14:15:22Z",
"state_string": "string",
"to_type": "",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
"expires_at": "2019-08-24T14:15:22Z",
"state_string": "string",
"to_type": "",
"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
{
"access_url": {
"access_url": "string",
"dismissed": true,
"error": "string",
"healthy": true,
"healthz_response": "string",
"reachable": true,
"severity": "ok",
"status_code": 0,
"warnings": [
{
"code": "EUNKNOWN",
"message": "string"
}
]
},
"coder_version": "string",
"database": {
"dismissed": true,
"error": "string",
"healthy": true,
"latency": "string",
"latency_ms": 0,
"reachable": true,
"severity": "ok",
"threshold_ms": 0,
"warnings": [
{
"code": "EUNKNOWN",
"message": "string"
}
]
},
"derp": {
"dismissed": true,
"error": "string",
"healthy": true,
"netcheck": {
"captivePortal": "string",
"globalV4": "string",
"globalV6": "string",
"hairPinning": "string",
"icmpv4": true,
"ipv4": true,
"ipv4CanSend": true,
"ipv6": true,
"ipv6CanSend": true,
"mappingVariesByDestIP": "string",
"oshasIPv6": true,
"pcp": "string",
"pmp": "string",
"preferredDERP": 0,
"regionLatency": {
"property1": 0,
"property2": 0
},
"regionV4Latency": {
"property1": 0,
"property2": 0
},
"regionV6Latency": {
"property1": 0,
"property2": 0
},
"udp": true,
"upnP": "string"
},
"netcheck_err": "string",
"netcheck_logs": ["string"],
"regions": {
"property1": {
"error": "string",
"healthy": true,
"node_reports": [
{
"can_exchange_messages": true,
"client_errs": [["string"]],
"client_logs": [["string"]],
"error": "string",
"healthy": true,
"node": {
"canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
"hostName": "string",
"insecureForTests": true,
"ipv4": "string",
"ipv6": "string",
"name": "string",
"regionID": 0,
"stunonly": true,
"stunport": 0,
"stuntestIP": "string"
},
"node_info": {
"tokenBucketBytesBurst": 0,
"tokenBucketBytesPerSecond": 0
},
"round_trip_ping": "string",
"round_trip_ping_ms": 0,
"severity": "ok",
"stun": {
"canSTUN": true,
"enabled": true,
"error": "string"
},
"uses_websocket": true,
"warnings": [
{
"code": "EUNKNOWN",
"message": "string"
}
]
}
],
"region": {
"avoid": true,
"embeddedRelay": true,
"nodes": [
{
"canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
"hostName": "string",
"insecureForTests": true,
"ipv4": "string",
"ipv6": "string",
"name": "string",
"regionID": 0,
"stunonly": true,
"stunport": 0,
"stuntestIP": "string"
}
],
"regionCode": "string",
"regionID": 0,
"regionName": "string"
},
"severity": "ok",
"warnings": [
{
"code": "EUNKNOWN",
"message": "string"
}
]
},
"property2": {
"error": "string",
"healthy": true,
"node_reports": [
{
"can_exchange_messages": true,
"client_errs": [["string"]],
"client_logs": [["string"]],
"error": "string",
"healthy": true,
"node": {
"canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
"hostName": "string",
"insecureForTests": true,
"ipv4": "string",
"ipv6": "string",
"name": "string",
"regionID": 0,
"stunonly": true,
"stunport": 0,
"stuntestIP": "string"
},
"node_info": {
"tokenBucketBytesBurst": 0,
"tokenBucketBytesPerSecond": 0
},
"round_trip_ping": "string",
"round_trip_ping_ms": 0,
"severity": "ok",
"stun": {
"canSTUN": true,
"enabled": true,
"error": "string"
},
"uses_websocket": true,
"warnings": [
{
"code": "EUNKNOWN",
"message": "string"
}
]
}
],
"region": {
"avoid": true,
"embeddedRelay": true,
"nodes": [
{
"canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
"hostName": "string",
"insecureForTests": true,
"ipv4": "string",
"ipv6": "string",
"name": "string",
"regionID": 0,
"stunonly": true,
"stunport": 0,
"stuntestIP": "string"
}
],
"regionCode": "string",
"regionID": 0,
"regionName": "string"
},
"severity": "ok",
"warnings": [
{
"code": "EUNKNOWN",
"message": "string"
}
]
}
},
"severity": "ok",
"warnings": [
{
"code": "EUNKNOWN",
"message": "string"
}
]
},
"healthy": true,
"provisioner_daemons": {
"dismissed": true,
"error": "string",
"items": [
{
"provisioner_daemon": {
"api_version": "string",
"created_at": "2019-08-24T14:15:22Z",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"provisioners": ["string"],
"tags": {
"property1": "string",
"property2": "string"
},
"version": "string"
},
"warnings": [
{
"code": "EUNKNOWN",
"message": "string"
}
]
}
],
"severity": "ok",
"warnings": [
{
"code": "EUNKNOWN",
"message": "string"
}
]
},
"severity": "ok",
"time": "2019-08-24T14:15:22Z",
"websocket": {
"body": "string",
"code": 0,
"dismissed": true,
"error": "string",
"healthy": true,
"severity": "ok",
"warnings": [
{
"code": "EUNKNOWN",
"message": "string"
}
]
},
"workspace_proxy": {
"dismissed": true,
"error": "string",
"healthy": true,
"severity": "ok",
"warnings": [
{
"code": "EUNKNOWN",
"message": "string"
}
],
"workspace_proxies": {
"regions": [
{
"created_at": "2019-08-24T14:15:22Z",
"deleted": true,
"derp_enabled": true,
"derp_only": true,
"display_name": "string",
"healthy": true,
"icon_url": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"name": "string",
"path_app_url": "string",
"status": {
"checked_at": "2019-08-24T14:15:22Z",
"report": {
"errors": ["string"],
"warnings": ["string"]
},
"status": "ok"
},
"updated_at": "2019-08-24T14:15:22Z",
"version": "string",
"wildcard_hostname": "string"
}
]
}
}
"access_url": {
"access_url": "string",
"dismissed": true,
"error": "string",
"healthy": true,
"healthz_response": "string",
"reachable": true,
"severity": "ok",
"status_code": 0,
"warnings": [
{
"code": "EUNKNOWN",
"message": "string"
}
]
},
"coder_version": "string",
"database": {
"dismissed": true,
"error": "string",
"healthy": true,
"latency": "string",
"latency_ms": 0,
"reachable": true,
"severity": "ok",
"threshold_ms": 0,
"warnings": [
{
"code": "EUNKNOWN",
"message": "string"
}
]
},
"derp": {
"dismissed": true,
"error": "string",
"healthy": true,
"netcheck": {
"captivePortal": "string",
"globalV4": "string",
"globalV6": "string",
"hairPinning": "string",
"icmpv4": true,
"ipv4": true,
"ipv4CanSend": true,
"ipv6": true,
"ipv6CanSend": true,
"mappingVariesByDestIP": "string",
"oshasIPv6": true,
"pcp": "string",
"pmp": "string",
"preferredDERP": 0,
"regionLatency": {
"property1": 0,
"property2": 0
},
"regionV4Latency": {
"property1": 0,
"property2": 0
},
"regionV6Latency": {
"property1": 0,
"property2": 0
},
"udp": true,
"upnP": "string"
},
"netcheck_err": "string",
"netcheck_logs": ["string"],
"regions": {
"property1": {
"error": "string",
"healthy": true,
"node_reports": [
{
"can_exchange_messages": true,
"client_errs": [["string"]],
"client_logs": [["string"]],
"error": "string",
"healthy": true,
"node": {
"canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
"hostName": "string",
"insecureForTests": true,
"ipv4": "string",
"ipv6": "string",
"name": "string",
"regionID": 0,
"stunonly": true,
"stunport": 0,
"stuntestIP": "string"
},
"node_info": {
"tokenBucketBytesBurst": 0,
"tokenBucketBytesPerSecond": 0
},
"round_trip_ping": "string",
"round_trip_ping_ms": 0,
"severity": "ok",
"stun": {
"canSTUN": true,
"enabled": true,
"error": "string"
},
"uses_websocket": true,
"warnings": [
{
"code": "EUNKNOWN",
"message": "string"
}
]
}
],
"region": {
"avoid": true,
"embeddedRelay": true,
"nodes": [
{
"canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
"hostName": "string",
"insecureForTests": true,
"ipv4": "string",
"ipv6": "string",
"name": "string",
"regionID": 0,
"stunonly": true,
"stunport": 0,
"stuntestIP": "string"
}
],
"regionCode": "string",
"regionID": 0,
"regionName": "string"
},
"severity": "ok",
"warnings": [
{
"code": "EUNKNOWN",
"message": "string"
}
]
},
"property2": {
"error": "string",
"healthy": true,
"node_reports": [
{
"can_exchange_messages": true,
"client_errs": [["string"]],
"client_logs": [["string"]],
"error": "string",
"healthy": true,
"node": {
"canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
"hostName": "string",
"insecureForTests": true,
"ipv4": "string",
"ipv6": "string",
"name": "string",
"regionID": 0,
"stunonly": true,
"stunport": 0,
"stuntestIP": "string"
},
"node_info": {
"tokenBucketBytesBurst": 0,
"tokenBucketBytesPerSecond": 0
},
"round_trip_ping": "string",
"round_trip_ping_ms": 0,
"severity": "ok",
"stun": {
"canSTUN": true,
"enabled": true,
"error": "string"
},
"uses_websocket": true,
"warnings": [
{
"code": "EUNKNOWN",
"message": "string"
}
]
}
],
"region": {
"avoid": true,
"embeddedRelay": true,
"nodes": [
{
"canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
"hostName": "string",
"insecureForTests": true,
"ipv4": "string",
"ipv6": "string",
"name": "string",
"regionID": 0,
"stunonly": true,
"stunport": 0,
"stuntestIP": "string"
}
],
"regionCode": "string",
"regionID": 0,
"regionName": "string"
},
"severity": "ok",
"warnings": [
{
"code": "EUNKNOWN",
"message": "string"
}
]
}
},
"severity": "ok",
"warnings": [
{
"code": "EUNKNOWN",
"message": "string"
}
]
},
"healthy": true,
"provisioner_daemons": {
"dismissed": true,
"error": "string",
"items": [
{
"provisioner_daemon": {
"api_version": "string",
"created_at": "2019-08-24T14:15:22Z",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"provisioners": ["string"],
"tags": {
"property1": "string",
"property2": "string"
},
"version": "string"
},
"warnings": [
{
"code": "EUNKNOWN",
"message": "string"
}
]
}
],
"severity": "ok",
"warnings": [
{
"code": "EUNKNOWN",
"message": "string"
}
]
},
"severity": "ok",
"time": "2019-08-24T14:15:22Z",
"websocket": {
"body": "string",
"code": 0,
"dismissed": true,
"error": "string",
"healthy": true,
"severity": "ok",
"warnings": [
{
"code": "EUNKNOWN",
"message": "string"
}
]
},
"workspace_proxy": {
"dismissed": true,
"error": "string",
"healthy": true,
"severity": "ok",
"warnings": [
{
"code": "EUNKNOWN",
"message": "string"
}
],
"workspace_proxies": {
"regions": [
{
"created_at": "2019-08-24T14:15:22Z",
"deleted": true,
"derp_enabled": true,
"derp_only": true,
"display_name": "string",
"healthy": true,
"icon_url": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"name": "string",
"path_app_url": "string",
"status": {
"checked_at": "2019-08-24T14:15:22Z",
"report": {
"errors": ["string"],
"warnings": ["string"]
},
"status": "ok"
},
"updated_at": "2019-08-24T14:15:22Z",
"version": "string",
"wildcard_hostname": "string"
}
]
}
}
}
```
@@ -401,7 +401,7 @@ curl -X GET http://coder-server:8080/api/v2/debug/health/settings \
```json
{
"dismissed_healthchecks": ["DERP"]
"dismissed_healthchecks": ["DERP"]
}
```
@@ -431,7 +431,7 @@ curl -X PUT http://coder-server:8080/api/v2/debug/health/settings \
```json
{
"dismissed_healthchecks": ["DERP"]
"dismissed_healthchecks": ["DERP"]
}
```
@@ -447,7 +447,7 @@ curl -X PUT http://coder-server:8080/api/v2/debug/health/settings \
```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
{
"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
{
"detail": "string",
"message": "string",
"validations": [
{
"detail": "string",
"field": "string"
}
]
"detail": "string",
"message": "string",
"validations": [
{
"detail": "string",
"field": "string"
}
]
}
```
@@ -53,14 +53,14 @@ curl -X GET http://coder-server:8080/api/v2/buildinfo \
```json
{
"agent_api_version": "string",
"dashboard_url": "string",
"deployment_id": "string",
"external_url": "string",
"telemetry": true,
"upgrade_message": "string",
"version": "string",
"workspace_proxy": true
"agent_api_version": "string",
"dashboard_url": "string",
"deployment_id": "string",
"external_url": "string",
"telemetry": true,
"upgrade_message": "string",
"version": "string",
"workspace_proxy": true
}
```
@@ -87,7 +87,7 @@ curl -X POST http://coder-server:8080/api/v2/csp/reports \
```json
{
"csp-report": {}
"csp-report": {}
}
```
@@ -124,377 +124,377 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \
```json
{
"config": {
"access_url": {
"forceQuery": true,
"fragment": "string",
"host": "string",
"omitHost": true,
"opaque": "string",
"path": "string",
"rawFragment": "string",
"rawPath": "string",
"rawQuery": "string",
"scheme": "string",
"user": {}
},
"address": {
"host": "string",
"port": "string"
},
"agent_fallback_troubleshooting_url": {
"forceQuery": true,
"fragment": "string",
"host": "string",
"omitHost": true,
"opaque": "string",
"path": "string",
"rawFragment": "string",
"rawPath": "string",
"rawQuery": "string",
"scheme": "string",
"user": {}
},
"agent_stat_refresh_interval": 0,
"allow_workspace_renames": true,
"autobuild_poll_interval": 0,
"browser_only": true,
"cache_directory": "string",
"cli_upgrade_message": "string",
"config": "string",
"config_ssh": {
"deploymentName": "string",
"sshconfigOptions": ["string"]
},
"dangerous": {
"allow_all_cors": true,
"allow_path_app_sharing": true,
"allow_path_app_site_owner_access": true
},
"derp": {
"config": {
"block_direct": true,
"force_websockets": true,
"path": "string",
"url": "string"
},
"server": {
"enable": true,
"region_code": "string",
"region_id": 0,
"region_name": "string",
"relay_url": {
"forceQuery": true,
"fragment": "string",
"host": "string",
"omitHost": true,
"opaque": "string",
"path": "string",
"rawFragment": "string",
"rawPath": "string",
"rawQuery": "string",
"scheme": "string",
"user": {}
},
"stun_addresses": ["string"]
}
},
"disable_owner_workspace_exec": true,
"disable_password_auth": true,
"disable_path_apps": true,
"docs_url": {
"forceQuery": true,
"fragment": "string",
"host": "string",
"omitHost": true,
"opaque": "string",
"path": "string",
"rawFragment": "string",
"rawPath": "string",
"rawQuery": "string",
"scheme": "string",
"user": {}
},
"enable_terraform_debug_mode": true,
"experiments": ["string"],
"external_auth": {
"value": [
{
"app_install_url": "string",
"app_installations_url": "string",
"auth_url": "string",
"client_id": "string",
"device_code_url": "string",
"device_flow": true,
"display_icon": "string",
"display_name": "string",
"id": "string",
"no_refresh": true,
"regex": "string",
"scopes": ["string"],
"token_url": "string",
"type": "string",
"validate_url": "string"
}
]
},
"external_token_encryption_keys": ["string"],
"healthcheck": {
"refresh": 0,
"threshold_database": 0
},
"http_address": "string",
"in_memory_database": true,
"job_hang_detector_interval": 0,
"logging": {
"human": "string",
"json": "string",
"log_filter": ["string"],
"stackdriver": "string"
},
"metrics_cache_refresh_interval": 0,
"notifications": {
"dispatch_timeout": 0,
"email": {
"auth": {
"identity": "string",
"password": "string",
"password_file": "string",
"username": "string"
},
"force_tls": true,
"from": "string",
"hello": "string",
"smarthost": {
"host": "string",
"port": "string"
},
"tls": {
"ca_file": "string",
"cert_file": "string",
"insecure_skip_verify": true,
"key_file": "string",
"server_name": "string",
"start_tls": true
}
},
"fetch_interval": 0,
"lease_count": 0,
"lease_period": 0,
"max_send_attempts": 0,
"method": "string",
"retry_interval": 0,
"sync_buffer_size": 0,
"sync_interval": 0,
"webhook": {
"endpoint": {
"forceQuery": true,
"fragment": "string",
"host": "string",
"omitHost": true,
"opaque": "string",
"path": "string",
"rawFragment": "string",
"rawPath": "string",
"rawQuery": "string",
"scheme": "string",
"user": {}
}
}
},
"oauth2": {
"github": {
"allow_everyone": true,
"allow_signups": true,
"allowed_orgs": ["string"],
"allowed_teams": ["string"],
"client_id": "string",
"client_secret": "string",
"enterprise_base_url": "string"
}
},
"oidc": {
"allow_signups": true,
"auth_url_params": {},
"client_cert_file": "string",
"client_id": "string",
"client_key_file": "string",
"client_secret": "string",
"email_domain": ["string"],
"email_field": "string",
"group_allow_list": ["string"],
"group_auto_create": true,
"group_mapping": {},
"group_regex_filter": {},
"groups_field": "string",
"icon_url": {
"forceQuery": true,
"fragment": "string",
"host": "string",
"omitHost": true,
"opaque": "string",
"path": "string",
"rawFragment": "string",
"rawPath": "string",
"rawQuery": "string",
"scheme": "string",
"user": {}
},
"ignore_email_verified": true,
"ignore_user_info": true,
"issuer_url": "string",
"name_field": "string",
"scopes": ["string"],
"sign_in_text": "string",
"signups_disabled_text": "string",
"skip_issuer_checks": true,
"user_role_field": "string",
"user_role_mapping": {},
"user_roles_default": ["string"],
"username_field": "string"
},
"pg_auth": "string",
"pg_connection_url": "string",
"pprof": {
"address": {
"host": "string",
"port": "string"
},
"enable": true
},
"prometheus": {
"address": {
"host": "string",
"port": "string"
},
"aggregate_agent_stats_by": ["string"],
"collect_agent_stats": true,
"collect_db_metrics": true,
"enable": true
},
"provisioner": {
"daemon_poll_interval": 0,
"daemon_poll_jitter": 0,
"daemon_psk": "string",
"daemon_types": ["string"],
"daemons": 0,
"force_cancel_interval": 0
},
"proxy_health_status_interval": 0,
"proxy_trusted_headers": ["string"],
"proxy_trusted_origins": ["string"],
"rate_limit": {
"api": 0,
"disable_all": true
},
"redirect_to_access_url": true,
"scim_api_key": "string",
"secure_auth_cookie": true,
"session_lifetime": {
"default_duration": 0,
"disable_expiry_refresh": true,
"max_token_lifetime": 0
},
"ssh_keygen_algorithm": "string",
"strict_transport_security": 0,
"strict_transport_security_options": ["string"],
"support": {
"links": {
"value": [
{
"icon": "bug",
"name": "string",
"target": "string"
}
]
}
},
"swagger": {
"enable": true
},
"telemetry": {
"enable": true,
"trace": true,
"url": {
"forceQuery": true,
"fragment": "string",
"host": "string",
"omitHost": true,
"opaque": "string",
"path": "string",
"rawFragment": "string",
"rawPath": "string",
"rawQuery": "string",
"scheme": "string",
"user": {}
}
},
"terms_of_service_url": "string",
"tls": {
"address": {
"host": "string",
"port": "string"
},
"allow_insecure_ciphers": true,
"cert_file": ["string"],
"client_auth": "string",
"client_ca_file": "string",
"client_cert_file": "string",
"client_key_file": "string",
"enable": true,
"key_file": ["string"],
"min_version": "string",
"redirect_http": true,
"supported_ciphers": ["string"]
},
"trace": {
"capture_logs": true,
"data_dog": true,
"enable": true,
"honeycomb_api_key": "string"
},
"update_check": true,
"user_quiet_hours_schedule": {
"allow_user_custom": true,
"default_schedule": "string"
},
"verbose": true,
"web_terminal_renderer": "string",
"wgtunnel_host": "string",
"wildcard_access_url": "string",
"write_config": true
},
"options": [
{
"annotations": {
"property1": "string",
"property2": "string"
},
"default": "string",
"description": "string",
"env": "string",
"flag": "string",
"flag_shorthand": "string",
"group": {
"description": "string",
"name": "string",
"parent": {
"description": "string",
"name": "string",
"parent": {},
"yaml": "string"
},
"yaml": "string"
},
"hidden": true,
"name": "string",
"required": true,
"use_instead": [{}],
"value": null,
"value_source": "",
"yaml": "string"
}
]
"config": {
"access_url": {
"forceQuery": true,
"fragment": "string",
"host": "string",
"omitHost": true,
"opaque": "string",
"path": "string",
"rawFragment": "string",
"rawPath": "string",
"rawQuery": "string",
"scheme": "string",
"user": {}
},
"address": {
"host": "string",
"port": "string"
},
"agent_fallback_troubleshooting_url": {
"forceQuery": true,
"fragment": "string",
"host": "string",
"omitHost": true,
"opaque": "string",
"path": "string",
"rawFragment": "string",
"rawPath": "string",
"rawQuery": "string",
"scheme": "string",
"user": {}
},
"agent_stat_refresh_interval": 0,
"allow_workspace_renames": true,
"autobuild_poll_interval": 0,
"browser_only": true,
"cache_directory": "string",
"cli_upgrade_message": "string",
"config": "string",
"config_ssh": {
"deploymentName": "string",
"sshconfigOptions": ["string"]
},
"dangerous": {
"allow_all_cors": true,
"allow_path_app_sharing": true,
"allow_path_app_site_owner_access": true
},
"derp": {
"config": {
"block_direct": true,
"force_websockets": true,
"path": "string",
"url": "string"
},
"server": {
"enable": true,
"region_code": "string",
"region_id": 0,
"region_name": "string",
"relay_url": {
"forceQuery": true,
"fragment": "string",
"host": "string",
"omitHost": true,
"opaque": "string",
"path": "string",
"rawFragment": "string",
"rawPath": "string",
"rawQuery": "string",
"scheme": "string",
"user": {}
},
"stun_addresses": ["string"]
}
},
"disable_owner_workspace_exec": true,
"disable_password_auth": true,
"disable_path_apps": true,
"docs_url": {
"forceQuery": true,
"fragment": "string",
"host": "string",
"omitHost": true,
"opaque": "string",
"path": "string",
"rawFragment": "string",
"rawPath": "string",
"rawQuery": "string",
"scheme": "string",
"user": {}
},
"enable_terraform_debug_mode": true,
"experiments": ["string"],
"external_auth": {
"value": [
{
"app_install_url": "string",
"app_installations_url": "string",
"auth_url": "string",
"client_id": "string",
"device_code_url": "string",
"device_flow": true,
"display_icon": "string",
"display_name": "string",
"id": "string",
"no_refresh": true,
"regex": "string",
"scopes": ["string"],
"token_url": "string",
"type": "string",
"validate_url": "string"
}
]
},
"external_token_encryption_keys": ["string"],
"healthcheck": {
"refresh": 0,
"threshold_database": 0
},
"http_address": "string",
"in_memory_database": true,
"job_hang_detector_interval": 0,
"logging": {
"human": "string",
"json": "string",
"log_filter": ["string"],
"stackdriver": "string"
},
"metrics_cache_refresh_interval": 0,
"notifications": {
"dispatch_timeout": 0,
"email": {
"auth": {
"identity": "string",
"password": "string",
"password_file": "string",
"username": "string"
},
"force_tls": true,
"from": "string",
"hello": "string",
"smarthost": {
"host": "string",
"port": "string"
},
"tls": {
"ca_file": "string",
"cert_file": "string",
"insecure_skip_verify": true,
"key_file": "string",
"server_name": "string",
"start_tls": true
}
},
"fetch_interval": 0,
"lease_count": 0,
"lease_period": 0,
"max_send_attempts": 0,
"method": "string",
"retry_interval": 0,
"sync_buffer_size": 0,
"sync_interval": 0,
"webhook": {
"endpoint": {
"forceQuery": true,
"fragment": "string",
"host": "string",
"omitHost": true,
"opaque": "string",
"path": "string",
"rawFragment": "string",
"rawPath": "string",
"rawQuery": "string",
"scheme": "string",
"user": {}
}
}
},
"oauth2": {
"github": {
"allow_everyone": true,
"allow_signups": true,
"allowed_orgs": ["string"],
"allowed_teams": ["string"],
"client_id": "string",
"client_secret": "string",
"enterprise_base_url": "string"
}
},
"oidc": {
"allow_signups": true,
"auth_url_params": {},
"client_cert_file": "string",
"client_id": "string",
"client_key_file": "string",
"client_secret": "string",
"email_domain": ["string"],
"email_field": "string",
"group_allow_list": ["string"],
"group_auto_create": true,
"group_mapping": {},
"group_regex_filter": {},
"groups_field": "string",
"icon_url": {
"forceQuery": true,
"fragment": "string",
"host": "string",
"omitHost": true,
"opaque": "string",
"path": "string",
"rawFragment": "string",
"rawPath": "string",
"rawQuery": "string",
"scheme": "string",
"user": {}
},
"ignore_email_verified": true,
"ignore_user_info": true,
"issuer_url": "string",
"name_field": "string",
"scopes": ["string"],
"sign_in_text": "string",
"signups_disabled_text": "string",
"skip_issuer_checks": true,
"user_role_field": "string",
"user_role_mapping": {},
"user_roles_default": ["string"],
"username_field": "string"
},
"pg_auth": "string",
"pg_connection_url": "string",
"pprof": {
"address": {
"host": "string",
"port": "string"
},
"enable": true
},
"prometheus": {
"address": {
"host": "string",
"port": "string"
},
"aggregate_agent_stats_by": ["string"],
"collect_agent_stats": true,
"collect_db_metrics": true,
"enable": true
},
"provisioner": {
"daemon_poll_interval": 0,
"daemon_poll_jitter": 0,
"daemon_psk": "string",
"daemon_types": ["string"],
"daemons": 0,
"force_cancel_interval": 0
},
"proxy_health_status_interval": 0,
"proxy_trusted_headers": ["string"],
"proxy_trusted_origins": ["string"],
"rate_limit": {
"api": 0,
"disable_all": true
},
"redirect_to_access_url": true,
"scim_api_key": "string",
"secure_auth_cookie": true,
"session_lifetime": {
"default_duration": 0,
"disable_expiry_refresh": true,
"max_token_lifetime": 0
},
"ssh_keygen_algorithm": "string",
"strict_transport_security": 0,
"strict_transport_security_options": ["string"],
"support": {
"links": {
"value": [
{
"icon": "bug",
"name": "string",
"target": "string"
}
]
}
},
"swagger": {
"enable": true
},
"telemetry": {
"enable": true,
"trace": true,
"url": {
"forceQuery": true,
"fragment": "string",
"host": "string",
"omitHost": true,
"opaque": "string",
"path": "string",
"rawFragment": "string",
"rawPath": "string",
"rawQuery": "string",
"scheme": "string",
"user": {}
}
},
"terms_of_service_url": "string",
"tls": {
"address": {
"host": "string",
"port": "string"
},
"allow_insecure_ciphers": true,
"cert_file": ["string"],
"client_auth": "string",
"client_ca_file": "string",
"client_cert_file": "string",
"client_key_file": "string",
"enable": true,
"key_file": ["string"],
"min_version": "string",
"redirect_http": true,
"supported_ciphers": ["string"]
},
"trace": {
"capture_logs": true,
"data_dog": true,
"enable": true,
"honeycomb_api_key": "string"
},
"update_check": true,
"user_quiet_hours_schedule": {
"allow_user_custom": true,
"default_schedule": "string"
},
"verbose": true,
"web_terminal_renderer": "string",
"wgtunnel_host": "string",
"wildcard_access_url": "string",
"write_config": true
},
"options": [
{
"annotations": {
"property1": "string",
"property2": "string"
},
"default": "string",
"description": "string",
"env": "string",
"flag": "string",
"flag_shorthand": "string",
"group": {
"description": "string",
"name": "string",
"parent": {
"description": "string",
"name": "string",
"parent": {},
"yaml": "string"
},
"yaml": "string"
},
"hidden": true,
"name": "string",
"required": true,
"use_instead": [{}],
"value": null,
"value_source": "",
"yaml": "string"
}
]
}
```
@@ -525,11 +525,11 @@ curl -X GET http://coder-server:8080/api/v2/deployment/ssh \
```json
{
"hostname_prefix": "string",
"ssh_config_options": {
"property1": "string",
"property2": "string"
}
"hostname_prefix": "string",
"ssh_config_options": {
"property1": "string",
"property2": "string"
}
}
```
@@ -560,28 +560,28 @@ curl -X GET http://coder-server:8080/api/v2/deployment/stats \
```json
{
"aggregated_from": "2019-08-24T14:15:22Z",
"collected_at": "2019-08-24T14:15:22Z",
"next_update_at": "2019-08-24T14:15:22Z",
"session_count": {
"jetbrains": 0,
"reconnecting_pty": 0,
"ssh": 0,
"vscode": 0
},
"workspaces": {
"building": 0,
"connection_latency_ms": {
"p50": 0,
"p95": 0
},
"failed": 0,
"pending": 0,
"running": 0,
"rx_bytes": 0,
"stopped": 0,
"tx_bytes": 0
}
"aggregated_from": "2019-08-24T14:15:22Z",
"collected_at": "2019-08-24T14:15:22Z",
"next_update_at": "2019-08-24T14:15:22Z",
"session_count": {
"jetbrains": 0,
"reconnecting_pty": 0,
"ssh": 0,
"vscode": 0
},
"workspaces": {
"building": 0,
"connection_latency_ms": {
"p50": 0,
"p95": 0
},
"failed": 0,
"pending": 0,
"running": 0,
"rx_bytes": 0,
"stopped": 0,
"tx_bytes": 0
}
}
```
@@ -685,9 +685,9 @@ curl -X GET http://coder-server:8080/api/v2/updatecheck \
```json
{
"current": true,
"url": "string",
"version": "string"
"current": true,
"url": "string",
"version": "string"
}
```
@@ -722,7 +722,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/keys/tokens/tokenconfig
```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
{
"authenticated": true,
"created_at": "2019-08-24T14:15:22Z",
"expires": "2019-08-24T14:15:22Z",
"has_refresh_token": true,
"provider_id": "string",
"updated_at": "2019-08-24T14:15:22Z",
"validate_error": "string"
"authenticated": true,
"created_at": "2019-08-24T14:15:22Z",
"expires": "2019-08-24T14:15:22Z",
"has_refresh_token": true,
"provider_id": "string",
"updated_at": "2019-08-24T14:15:22Z",
"validate_error": "string"
}
```
@@ -62,31 +62,31 @@ curl -X GET http://coder-server:8080/api/v2/external-auth/{externalauth} \
```json
{
"app_install_url": "string",
"app_installable": true,
"authenticated": true,
"device": true,
"display_name": "string",
"installations": [
{
"account": {
"avatar_url": "string",
"id": 0,
"login": "string",
"name": "string",
"profile_url": "string"
},
"configure_url": "string",
"id": 0
}
],
"user": {
"avatar_url": "string",
"id": 0,
"login": "string",
"name": "string",
"profile_url": "string"
}
"app_install_url": "string",
"app_installable": true,
"authenticated": true,
"device": true,
"display_name": "string",
"installations": [
{
"account": {
"avatar_url": "string",
"id": 0,
"login": "string",
"name": "string",
"profile_url": "string"
},
"configure_url": "string",
"id": 0
}
],
"user": {
"avatar_url": "string",
"id": 0,
"login": "string",
"name": "string",
"profile_url": "string"
}
}
```
@@ -149,11 +149,11 @@ curl -X GET http://coder-server:8080/api/v2/external-auth/{externalauth}/device
```json
{
"device_code": "string",
"expires_in": 0,
"interval": 0,
"user_code": "string",
"verification_uri": "string"
"device_code": "string",
"expires_in": 0,
"interval": 0,
"user_code": "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
{
"entries": [
{
"amount": 0,
"date": "string"
}
],
"tz_hour_offset": 0
"entries": [
{
"amount": 0,
"date": "string"
}
],
"tz_hour_offset": 0
}
```
@@ -78,55 +78,55 @@ curl -X GET http://coder-server:8080/api/v2/insights/templates?start_time=2019-0
```json
{
"interval_reports": [
{
"active_users": 14,
"end_time": "2019-08-24T14:15:22Z",
"interval": "week",
"start_time": "2019-08-24T14:15:22Z",
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"]
}
],
"report": {
"active_users": 22,
"apps_usage": [
{
"display_name": "Visual Studio Code",
"icon": "string",
"seconds": 80500,
"slug": "vscode",
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"times_used": 2,
"type": "builtin"
}
],
"end_time": "2019-08-24T14:15:22Z",
"parameters_usage": [
{
"description": "string",
"display_name": "string",
"name": "string",
"options": [
{
"description": "string",
"icon": "string",
"name": "string",
"value": "string"
}
],
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"type": "string",
"values": [
{
"count": 0,
"value": "string"
}
]
}
],
"start_time": "2019-08-24T14:15:22Z",
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"]
}
"interval_reports": [
{
"active_users": 14,
"end_time": "2019-08-24T14:15:22Z",
"interval": "week",
"start_time": "2019-08-24T14:15:22Z",
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"]
}
],
"report": {
"active_users": 22,
"apps_usage": [
{
"display_name": "Visual Studio Code",
"icon": "string",
"seconds": 80500,
"slug": "vscode",
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"times_used": 2,
"type": "builtin"
}
],
"end_time": "2019-08-24T14:15:22Z",
"parameters_usage": [
{
"description": "string",
"display_name": "string",
"name": "string",
"options": [
{
"description": "string",
"icon": "string",
"name": "string",
"value": "string"
}
],
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"type": "string",
"values": [
{
"count": 0,
"value": "string"
}
]
}
],
"start_time": "2019-08-24T14:15:22Z",
"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
{
"report": {
"end_time": "2019-08-24T14:15:22Z",
"start_time": "2019-08-24T14:15:22Z",
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"users": [
{
"avatar_url": "http://example.com",
"seconds": 80500,
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5",
"username": "string"
}
]
}
"report": {
"end_time": "2019-08-24T14:15:22Z",
"start_time": "2019-08-24T14:15:22Z",
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"users": [
{
"avatar_url": "http://example.com",
"seconds": 80500,
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5",
"username": "string"
}
]
}
}
```
@@ -217,23 +217,23 @@ curl -X GET http://coder-server:8080/api/v2/insights/user-latency?start_time=201
```json
{
"report": {
"end_time": "2019-08-24T14:15:22Z",
"start_time": "2019-08-24T14:15:22Z",
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"users": [
{
"avatar_url": "http://example.com",
"latency_ms": {
"p50": 31.312,
"p95": 119.832
},
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5",
"username": "string"
}
]
}
"report": {
"end_time": "2019-08-24T14:15:22Z",
"start_time": "2019-08-24T14:15:22Z",
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"users": [
{
"avatar_url": "http://example.com",
"latency_ms": {
"p50": 31.312,
"p95": 119.832
},
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5",
"username": "string"
}
]
}
}
```
+227 -227
View File
@@ -25,30 +25,30 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/members
```json
[
{
"avatar_url": "string",
"created_at": "2019-08-24T14:15:22Z",
"email": "string",
"global_roles": [
{
"display_name": "string",
"name": "string",
"organization_id": "string"
}
],
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"roles": [
{
"display_name": "string",
"name": "string",
"organization_id": "string"
}
],
"updated_at": "2019-08-24T14:15:22Z",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5",
"username": "string"
}
{
"avatar_url": "string",
"created_at": "2019-08-24T14:15:22Z",
"email": "string",
"global_roles": [
{
"display_name": "string",
"name": "string",
"organization_id": "string"
}
],
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"roles": [
{
"display_name": "string",
"name": "string",
"organization_id": "string"
}
],
"updated_at": "2019-08-24T14:15:22Z",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5",
"username": "string"
}
]
```
@@ -106,34 +106,34 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/members
```json
[
{
"assignable": true,
"built_in": true,
"display_name": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"organization_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"site_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"user_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
]
}
{
"assignable": true,
"built_in": true,
"display_name": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"organization_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"site_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"user_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
]
}
]
```
@@ -229,29 +229,29 @@ curl -X PUT http://coder-server:8080/api/v2/organizations/{organization}/members
```json
{
"display_name": "string",
"name": "string",
"organization_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"site_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"user_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
]
"display_name": "string",
"name": "string",
"organization_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"site_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"user_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
]
}
```
@@ -268,32 +268,32 @@ curl -X PUT http://coder-server:8080/api/v2/organizations/{organization}/members
```json
[
{
"display_name": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"organization_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"site_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"user_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
]
}
{
"display_name": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"organization_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"site_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"user_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
]
}
]
```
@@ -387,29 +387,29 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member
```json
{
"display_name": "string",
"name": "string",
"organization_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"site_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"user_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
]
"display_name": "string",
"name": "string",
"organization_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"site_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"user_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
]
}
```
@@ -426,32 +426,32 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member
```json
[
{
"display_name": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"organization_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"site_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"user_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
]
}
{
"display_name": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"organization_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"site_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"user_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
]
}
]
```
@@ -553,32 +553,32 @@ curl -X DELETE http://coder-server:8080/api/v2/organizations/{organization}/memb
```json
[
{
"display_name": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"organization_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"site_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"user_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
]
}
{
"display_name": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"organization_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"site_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"user_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
]
}
]
```
@@ -680,17 +680,17 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member
```json
{
"created_at": "2019-08-24T14:15:22Z",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"roles": [
{
"display_name": "string",
"name": "string",
"organization_id": "string"
}
],
"updated_at": "2019-08-24T14:15:22Z",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
"created_at": "2019-08-24T14:15:22Z",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"roles": [
{
"display_name": "string",
"name": "string",
"organization_id": "string"
}
],
"updated_at": "2019-08-24T14:15:22Z",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
}
```
@@ -747,7 +747,7 @@ curl -X PUT http://coder-server:8080/api/v2/organizations/{organization}/members
```json
{
"roles": ["string"]
"roles": ["string"]
}
```
@@ -765,17 +765,17 @@ curl -X PUT http://coder-server:8080/api/v2/organizations/{organization}/members
```json
{
"created_at": "2019-08-24T14:15:22Z",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"roles": [
{
"display_name": "string",
"name": "string",
"organization_id": "string"
}
],
"updated_at": "2019-08-24T14:15:22Z",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
"created_at": "2019-08-24T14:15:22Z",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"roles": [
{
"display_name": "string",
"name": "string",
"organization_id": "string"
}
],
"updated_at": "2019-08-24T14:15:22Z",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
}
```
@@ -806,34 +806,34 @@ curl -X GET http://coder-server:8080/api/v2/users/roles \
```json
[
{
"assignable": true,
"built_in": true,
"display_name": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"organization_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"site_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"user_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
]
}
{
"assignable": true,
"built_in": true,
"display_name": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"organization_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"site_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"user_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
]
}
]
```
+31 -31
View File
@@ -19,10 +19,10 @@ curl -X GET http://coder-server:8080/api/v2/notifications/dispatch-methods \
```json
[
{
"available": ["string"],
"default": "string"
}
{
"available": ["string"],
"default": "string"
}
]
```
@@ -63,7 +63,7 @@ curl -X GET http://coder-server:8080/api/v2/notifications/settings \
```json
{
"notifier_paused": true
"notifier_paused": true
}
```
@@ -93,7 +93,7 @@ curl -X PUT http://coder-server:8080/api/v2/notifications/settings \
```json
{
"notifier_paused": true
"notifier_paused": true
}
```
@@ -109,7 +109,7 @@ curl -X PUT http://coder-server:8080/api/v2/notifications/settings \
```json
{
"notifier_paused": true
"notifier_paused": true
}
```
@@ -141,16 +141,16 @@ curl -X GET http://coder-server:8080/api/v2/notifications/templates/system \
```json
[
{
"actions": "string",
"body_template": "string",
"group": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"kind": "string",
"method": "string",
"name": "string",
"title_template": "string"
}
{
"actions": "string",
"body_template": "string",
"group": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"kind": "string",
"method": "string",
"name": "string",
"title_template": "string"
}
]
```
@@ -203,11 +203,11 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/notifications/preferenc
```json
[
{
"disabled": true,
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"updated_at": "2019-08-24T14:15:22Z"
}
{
"disabled": true,
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"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
{
"template_disabled_map": {
"property1": true,
"property2": true
}
"template_disabled_map": {
"property1": true,
"property2": true
}
}
```
@@ -268,11 +268,11 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/notifications/preferenc
```json
[
{
"disabled": true,
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"updated_at": "2019-08-24T14:15:22Z"
}
{
"disabled": true,
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"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
{
"license": "string"
"license": "string"
}
```
@@ -34,10 +34,10 @@ curl -X POST http://coder-server:8080/api/v2/licenses \
```json
{
"claims": {},
"id": 0,
"uploaded_at": "2019-08-24T14:15:22Z",
"uuid": "095be615-a8ad-4c33-8e9c-c7612fbf6c9f"
"claims": {},
"id": 0,
"uploaded_at": "2019-08-24T14:15:22Z",
"uuid": "095be615-a8ad-4c33-8e9c-c7612fbf6c9f"
}
```
@@ -68,14 +68,14 @@ curl -X POST http://coder-server:8080/api/v2/licenses/refresh-entitlements \
```json
{
"detail": "string",
"message": "string",
"validations": [
{
"detail": "string",
"field": "string"
}
]
"detail": "string",
"message": "string",
"validations": [
{
"detail": "string",
"field": "string"
}
]
}
```
@@ -106,16 +106,16 @@ curl -X GET http://coder-server:8080/api/v2/organizations \
```json
[
{
"created_at": "2019-08-24T14:15:22Z",
"description": "string",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_default": true,
"name": "string",
"updated_at": "2019-08-24T14:15:22Z"
}
{
"created_at": "2019-08-24T14:15:22Z",
"description": "string",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_default": true,
"name": "string",
"updated_at": "2019-08-24T14:15:22Z"
}
]
```
@@ -161,10 +161,10 @@ curl -X POST http://coder-server:8080/api/v2/organizations \
```json
{
"description": "string",
"display_name": "string",
"icon": "string",
"name": "string"
"description": "string",
"display_name": "string",
"icon": "string",
"name": "string"
}
```
@@ -180,14 +180,14 @@ curl -X POST http://coder-server:8080/api/v2/organizations \
```json
{
"created_at": "2019-08-24T14:15:22Z",
"description": "string",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_default": true,
"name": "string",
"updated_at": "2019-08-24T14:15:22Z"
"created_at": "2019-08-24T14:15:22Z",
"description": "string",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_default": true,
"name": "string",
"updated_at": "2019-08-24T14:15:22Z"
}
```
@@ -224,14 +224,14 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization} \
```json
{
"created_at": "2019-08-24T14:15:22Z",
"description": "string",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_default": true,
"name": "string",
"updated_at": "2019-08-24T14:15:22Z"
"created_at": "2019-08-24T14:15:22Z",
"description": "string",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_default": true,
"name": "string",
"updated_at": "2019-08-24T14:15:22Z"
}
```
@@ -268,14 +268,14 @@ curl -X DELETE http://coder-server:8080/api/v2/organizations/{organization} \
```json
{
"detail": "string",
"message": "string",
"validations": [
{
"detail": "string",
"field": "string"
}
]
"detail": "string",
"message": "string",
"validations": [
{
"detail": "string",
"field": "string"
}
]
}
```
@@ -305,10 +305,10 @@ curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization} \
```json
{
"description": "string",
"display_name": "string",
"icon": "string",
"name": "string"
"description": "string",
"display_name": "string",
"icon": "string",
"name": "string"
}
```
@@ -325,14 +325,14 @@ curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization} \
```json
{
"created_at": "2019-08-24T14:15:22Z",
"description": "string",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_default": true,
"name": "string",
"updated_at": "2019-08-24T14:15:22Z"
"created_at": "2019-08-24T14:15:22Z",
"description": "string",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_default": true,
"name": "string",
"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
{
"agent_name": "string",
"port": 0
"agent_name": "string",
"port": 0
}
```
@@ -55,10 +55,10 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/port-share \
```json
{
"agent_name": "string",
"port": 0,
"protocol": "http",
"share_level": "owner"
"agent_name": "string",
"port": 0,
"protocol": "http",
"share_level": "owner"
}
```
@@ -75,11 +75,11 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/port-share \
```json
{
"agent_name": "string",
"port": 0,
"protocol": "http",
"share_level": "owner",
"workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9"
"agent_name": "string",
"port": 0,
"protocol": "http",
"share_level": "owner",
"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
{
"count": 0,
"users": [
{
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "",
"name": "string",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
"display_name": "string",
"name": "string",
"organization_id": "string"
}
],
"status": "active",
"theme_preference": "string",
"updated_at": "2019-08-24T14:15:22Z",
"username": "string"
}
]
"count": 0,
"users": [
{
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "",
"name": "string",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
"display_name": "string",
"name": "string",
"organization_id": "string"
}
],
"status": "active",
"theme_preference": "string",
"updated_at": "2019-08-24T14:15:22Z",
"username": "string"
}
]
}
```
@@ -81,13 +81,13 @@ curl -X POST http://coder-server:8080/api/v2/users \
```json
{
"disable_login": true,
"email": "user@example.com",
"login_type": "",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"password": "string",
"username": "string"
"disable_login": true,
"email": "user@example.com",
"login_type": "",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"password": "string",
"username": "string"
}
```
@@ -103,25 +103,25 @@ curl -X POST http://coder-server:8080/api/v2/users \
```json
{
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "",
"name": "string",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
"display_name": "string",
"name": "string",
"organization_id": "string"
}
],
"status": "active",
"theme_preference": "string",
"updated_at": "2019-08-24T14:15:22Z",
"username": "string"
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "",
"name": "string",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
"display_name": "string",
"name": "string",
"organization_id": "string"
}
],
"status": "active",
"theme_preference": "string",
"updated_at": "2019-08-24T14:15:22Z",
"username": "string"
}
```
@@ -152,18 +152,18 @@ curl -X GET http://coder-server:8080/api/v2/users/authmethods \
```json
{
"github": {
"enabled": true
},
"oidc": {
"enabled": true,
"iconUrl": "string",
"signInText": "string"
},
"password": {
"enabled": true
},
"terms_of_service_url": "string"
"github": {
"enabled": true
},
"oidc": {
"enabled": true,
"iconUrl": "string",
"signInText": "string"
},
"password": {
"enabled": true
},
"terms_of_service_url": "string"
}
```
@@ -194,14 +194,14 @@ curl -X GET http://coder-server:8080/api/v2/users/first \
```json
{
"detail": "string",
"message": "string",
"validations": [
{
"detail": "string",
"field": "string"
}
]
"detail": "string",
"message": "string",
"validations": [
{
"detail": "string",
"field": "string"
}
]
}
```
@@ -231,20 +231,20 @@ curl -X POST http://coder-server:8080/api/v2/users/first \
```json
{
"email": "string",
"name": "string",
"password": "string",
"trial": true,
"trial_info": {
"company_name": "string",
"country": "string",
"developers": "string",
"first_name": "string",
"job_title": "string",
"last_name": "string",
"phone_number": "string"
},
"username": "string"
"email": "string",
"name": "string",
"password": "string",
"trial": true,
"trial_info": {
"company_name": "string",
"country": "string",
"developers": "string",
"first_name": "string",
"job_title": "string",
"last_name": "string",
"phone_number": "string"
},
"username": "string"
}
```
@@ -260,8 +260,8 @@ curl -X POST http://coder-server:8080/api/v2/users/first \
```json
{
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
}
```
@@ -292,14 +292,14 @@ curl -X POST http://coder-server:8080/api/v2/users/logout \
```json
{
"detail": "string",
"message": "string",
"validations": [
{
"detail": "string",
"field": "string"
}
]
"detail": "string",
"message": "string",
"validations": [
{
"detail": "string",
"field": "string"
}
]
}
```
@@ -376,25 +376,25 @@ curl -X GET http://coder-server:8080/api/v2/users/{user} \
```json
{
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "",
"name": "string",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
"display_name": "string",
"name": "string",
"organization_id": "string"
}
],
"status": "active",
"theme_preference": "string",
"updated_at": "2019-08-24T14:15:22Z",
"username": "string"
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "",
"name": "string",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
"display_name": "string",
"name": "string",
"organization_id": "string"
}
],
"status": "active",
"theme_preference": "string",
"updated_at": "2019-08-24T14:15:22Z",
"username": "string"
}
```
@@ -450,7 +450,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/appearance \
```json
{
"theme_preference": "string"
"theme_preference": "string"
}
```
@@ -467,25 +467,25 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/appearance \
```json
{
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "",
"name": "string",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
"display_name": "string",
"name": "string",
"organization_id": "string"
}
],
"status": "active",
"theme_preference": "string",
"updated_at": "2019-08-24T14:15:22Z",
"username": "string"
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "",
"name": "string",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
"display_name": "string",
"name": "string",
"organization_id": "string"
}
],
"status": "active",
"theme_preference": "string",
"updated_at": "2019-08-24T14:15:22Z",
"username": "string"
}
```
@@ -523,10 +523,10 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/autofill-parameters?tem
```json
[
{
"name": "string",
"value": "string"
}
{
"name": "string",
"value": "string"
}
]
```
@@ -573,10 +573,10 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/gitsshkey \
```json
{
"created_at": "2019-08-24T14:15:22Z",
"public_key": "string",
"updated_at": "2019-08-24T14:15:22Z",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
"created_at": "2019-08-24T14:15:22Z",
"public_key": "string",
"updated_at": "2019-08-24T14:15:22Z",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
}
```
@@ -613,10 +613,10 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/gitsshkey \
```json
{
"created_at": "2019-08-24T14:15:22Z",
"public_key": "string",
"updated_at": "2019-08-24T14:15:22Z",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
"created_at": "2019-08-24T14:15:22Z",
"public_key": "string",
"updated_at": "2019-08-24T14:15:22Z",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
}
```
@@ -653,7 +653,7 @@ curl -X POST http://coder-server:8080/api/v2/users/{user}/keys \
```json
{
"key": "string"
"key": "string"
}
```
@@ -690,18 +690,18 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/keys/tokens \
```json
[
{
"created_at": "2019-08-24T14:15:22Z",
"expires_at": "2019-08-24T14:15:22Z",
"id": "string",
"last_used": "2019-08-24T14:15:22Z",
"lifetime_seconds": 0,
"login_type": "password",
"scope": "all",
"token_name": "string",
"updated_at": "2019-08-24T14:15:22Z",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
}
{
"created_at": "2019-08-24T14:15:22Z",
"expires_at": "2019-08-24T14:15:22Z",
"id": "string",
"last_used": "2019-08-24T14:15:22Z",
"lifetime_seconds": 0,
"login_type": "password",
"scope": "all",
"token_name": "string",
"updated_at": "2019-08-24T14:15:22Z",
"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
{
"lifetime": 0,
"scope": "all",
"token_name": "string"
"lifetime": 0,
"scope": "all",
"token_name": "string"
}
```
@@ -779,7 +779,7 @@ curl -X POST http://coder-server:8080/api/v2/users/{user}/keys/tokens \
```json
{
"key": "string"
"key": "string"
}
```
@@ -817,16 +817,16 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/keys/tokens/{keyname} \
```json
{
"created_at": "2019-08-24T14:15:22Z",
"expires_at": "2019-08-24T14:15:22Z",
"id": "string",
"last_used": "2019-08-24T14:15:22Z",
"lifetime_seconds": 0,
"login_type": "password",
"scope": "all",
"token_name": "string",
"updated_at": "2019-08-24T14:15:22Z",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
"created_at": "2019-08-24T14:15:22Z",
"expires_at": "2019-08-24T14:15:22Z",
"id": "string",
"last_used": "2019-08-24T14:15:22Z",
"lifetime_seconds": 0,
"login_type": "password",
"scope": "all",
"token_name": "string",
"updated_at": "2019-08-24T14:15:22Z",
"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
{
"created_at": "2019-08-24T14:15:22Z",
"expires_at": "2019-08-24T14:15:22Z",
"id": "string",
"last_used": "2019-08-24T14:15:22Z",
"lifetime_seconds": 0,
"login_type": "password",
"scope": "all",
"token_name": "string",
"updated_at": "2019-08-24T14:15:22Z",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
"created_at": "2019-08-24T14:15:22Z",
"expires_at": "2019-08-24T14:15:22Z",
"id": "string",
"last_used": "2019-08-24T14:15:22Z",
"lifetime_seconds": 0,
"login_type": "password",
"scope": "all",
"token_name": "string",
"updated_at": "2019-08-24T14:15:22Z",
"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
{
"login_type": ""
"login_type": ""
}
```
@@ -974,16 +974,16 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/organizations \
```json
[
{
"created_at": "2019-08-24T14:15:22Z",
"description": "string",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_default": true,
"name": "string",
"updated_at": "2019-08-24T14:15:22Z"
}
{
"created_at": "2019-08-24T14:15:22Z",
"description": "string",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_default": true,
"name": "string",
"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
{
"created_at": "2019-08-24T14:15:22Z",
"description": "string",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_default": true,
"name": "string",
"updated_at": "2019-08-24T14:15:22Z"
"created_at": "2019-08-24T14:15:22Z",
"description": "string",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_default": true,
"name": "string",
"updated_at": "2019-08-24T14:15:22Z"
}
```
@@ -1073,8 +1073,8 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/password \
```json
{
"old_password": "string",
"password": "string"
"old_password": "string",
"password": "string"
}
```
@@ -1111,8 +1111,8 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/profile \
```json
{
"name": "string",
"username": "string"
"name": "string",
"username": "string"
}
```
@@ -1129,25 +1129,25 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/profile \
```json
{
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "",
"name": "string",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
"display_name": "string",
"name": "string",
"organization_id": "string"
}
],
"status": "active",
"theme_preference": "string",
"updated_at": "2019-08-24T14:15:22Z",
"username": "string"
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "",
"name": "string",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
"display_name": "string",
"name": "string",
"organization_id": "string"
}
],
"status": "active",
"theme_preference": "string",
"updated_at": "2019-08-24T14:15:22Z",
"username": "string"
}
```
@@ -1184,25 +1184,25 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/roles \
```json
{
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "",
"name": "string",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
"display_name": "string",
"name": "string",
"organization_id": "string"
}
],
"status": "active",
"theme_preference": "string",
"updated_at": "2019-08-24T14:15:22Z",
"username": "string"
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "",
"name": "string",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
"display_name": "string",
"name": "string",
"organization_id": "string"
}
],
"status": "active",
"theme_preference": "string",
"updated_at": "2019-08-24T14:15:22Z",
"username": "string"
}
```
@@ -1232,7 +1232,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/roles \
```json
{
"roles": ["string"]
"roles": ["string"]
}
```
@@ -1249,25 +1249,25 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/roles \
```json
{
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "",
"name": "string",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
"display_name": "string",
"name": "string",
"organization_id": "string"
}
],
"status": "active",
"theme_preference": "string",
"updated_at": "2019-08-24T14:15:22Z",
"username": "string"
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "",
"name": "string",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
"display_name": "string",
"name": "string",
"organization_id": "string"
}
],
"status": "active",
"theme_preference": "string",
"updated_at": "2019-08-24T14:15:22Z",
"username": "string"
}
```
@@ -1304,25 +1304,25 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/status/activate \
```json
{
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "",
"name": "string",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
"display_name": "string",
"name": "string",
"organization_id": "string"
}
],
"status": "active",
"theme_preference": "string",
"updated_at": "2019-08-24T14:15:22Z",
"username": "string"
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "",
"name": "string",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
"display_name": "string",
"name": "string",
"organization_id": "string"
}
],
"status": "active",
"theme_preference": "string",
"updated_at": "2019-08-24T14:15:22Z",
"username": "string"
}
```
@@ -1359,25 +1359,25 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/status/suspend \
```json
{
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "",
"name": "string",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
"display_name": "string",
"name": "string",
"organization_id": "string"
}
],
"status": "active",
"theme_preference": "string",
"updated_at": "2019-08-24T14:15:22Z",
"username": "string"
"avatar_url": "http://example.com",
"created_at": "2019-08-24T14:15:22Z",
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
"login_type": "",
"name": "string",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
"display_name": "string",
"name": "string",
"organization_id": "string"
}
],
"status": "active",
"theme_preference": "string",
"updated_at": "2019-08-24T14:15:22Z",
"username": "string"
}
```
+11 -11
View File
@@ -19,17 +19,17 @@ curl -X GET http://coder-server:8080/api/v2/regions \
```json
{
"regions": [
{
"display_name": "string",
"healthy": true,
"icon_url": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"name": "string",
"path_app_url": "string",
"wildcard_hostname": "string"
}
]
"regions": [
{
"display_name": "string",
"healthy": true,
"icon_url": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"name": "string",
"path_app_url": "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
{
"ts": "2022-02-28T20:29:38.038452202Z",
"level": "INFO",
"msg": "exec",
"fields": {
"labels": {
"user_email": "jessie@coder.com",
"user_id": "5e876e9a-121663f01ebd1522060d5270",
"username": "jessie",
"workspace_id": "621d2e52-a6987ef6c56210058ee2593c",
"workspace_name": "main"
},
"cmdline": "uname -a",
"event": {
"filename": "/usr/bin/uname",
"argv": ["uname", "-a"],
"truncated": false,
"pid": 920684,
"uid": 101000,
"gid": 101000,
"comm": "bash"
}
}
"ts": "2022-02-28T20:29:38.038452202Z",
"level": "INFO",
"msg": "exec",
"fields": {
"labels": {
"user_email": "jessie@coder.com",
"user_id": "5e876e9a-121663f01ebd1522060d5270",
"username": "jessie",
"workspace_id": "621d2e52-a6987ef6c56210058ee2593c",
"workspace_name": "main"
},
"cmdline": "uname -a",
"event": {
"filename": "/usr/bin/uname",
"argv": ["uname", "-a"],
"truncated": false,
"pid": 920684,
"uid": 101000,
"gid": 101000,
"comm": "bash"
}
}
}
```
+6 -6
View File
@@ -1,9 +1,9 @@
{
"name": "Develop Coder on Coder using Envbuilder",
"build": {
"dockerfile": "Dockerfile"
},
"name": "Develop Coder on Coder using Envbuilder",
"build": {
"dockerfile": "Dockerfile"
},
"features": {},
"runArgs": ["--cap-add=SYS_PTRACE"]
"features": {},
"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.
[
{
"id": "aws-devcontainer",
"url": "",
"name": "AWS EC2 (Devcontainer)",
"description": "Provision AWS EC2 VMs with a devcontainer as Coder workspaces",
"icon": "/icon/aws.svg",
"tags": [
"vm",
"linux",
"aws",
"persistent",
"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"
},
{
"id": "aws-linux",
"url": "",
"name": "AWS EC2 (Linux)",
"description": "Provision AWS EC2 VMs as Coder workspaces",
"icon": "/icon/aws.svg",
"tags": [
"vm",
"linux",
"aws",
"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"
},
{
"id": "aws-windows",
"url": "",
"name": "AWS EC2 (Windows)",
"description": "Provision AWS EC2 VMs as Coder workspaces",
"icon": "/icon/aws.svg",
"tags": [
"vm",
"windows",
"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"
},
{
"id": "azure-linux",
"url": "",
"name": "Azure VM (Linux)",
"description": "Provision Azure VMs as Coder workspaces",
"icon": "/icon/azure.png",
"tags": [
"vm",
"linux",
"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"
},
{
"id": "do-linux",
"url": "",
"name": "DigitalOcean Droplet (Linux)",
"description": "Provision DigitalOcean Droplets as Coder workspaces",
"icon": "/icon/do.png",
"tags": [
"vm",
"linux",
"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"
},
{
"id": "docker",
"url": "",
"name": "Docker Containers",
"description": "Provision Docker containers as Coder workspaces",
"icon": "/icon/docker.png",
"tags": [
"docker",
"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"
},
{
"id": "gcp-devcontainer",
"url": "",
"name": "Google Compute Engine (Devcontainer)",
"description": "Provision a Devcontainer on Google Compute Engine instances as Coder workspaces",
"icon": "/icon/gcp.png",
"tags": [
"vm",
"linux",
"gcp",
"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"
},
{
"id": "gcp-linux",
"url": "",
"name": "Google Compute Engine (Linux)",
"description": "Provision Google Compute Engine instances as Coder workspaces",
"icon": "/icon/gcp.png",
"tags": [
"vm",
"linux",
"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"
},
{
"id": "gcp-vm-container",
"url": "",
"name": "Google Compute Engine (VM Container)",
"description": "Provision Google Compute Engine instances as Coder workspaces",
"icon": "/icon/gcp.png",
"tags": [
"vm-container",
"linux",
"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"
},
{
"id": "gcp-windows",
"url": "",
"name": "Google Compute Engine (Windows)",
"description": "Provision Google Compute Engine instances as Coder workspaces",
"icon": "/icon/gcp.png",
"tags": [
"vm",
"windows",
"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"
},
{
"id": "kubernetes",
"url": "",
"name": "Kubernetes (Deployment)",
"description": "Provision Kubernetes Deployments as Coder workspaces",
"icon": "/icon/k8s.png",
"tags": [
"kubernetes",
"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"
},
{
"id": "nomad-docker",
"url": "",
"name": "Nomad",
"description": "Provision Nomad Jobs as Coder workspaces",
"icon": "/icon/nomad.svg",
"tags": [
"nomad",
"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"
},
{
"id": "scratch",
"url": "",
"name": "Scratch",
"description": "A minimal starter template for Coder",
"icon": "/emojis/1f4e6.png",
"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"
}
{
"id": "aws-devcontainer",
"url": "",
"name": "AWS EC2 (Devcontainer)",
"description": "Provision AWS EC2 VMs with a devcontainer as Coder workspaces",
"icon": "/icon/aws.svg",
"tags": [
"vm",
"linux",
"aws",
"persistent",
"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\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",
"url": "",
"name": "AWS EC2 (Linux)",
"description": "Provision AWS EC2 VMs as Coder workspaces",
"icon": "/icon/aws.svg",
"tags": [
"vm",
"linux",
"aws",
"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\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",
"url": "",
"name": "AWS EC2 (Windows)",
"description": "Provision AWS EC2 VMs as Coder workspaces",
"icon": "/icon/aws.svg",
"tags": [
"vm",
"windows",
"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\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",
"url": "",
"name": "Azure VM (Linux)",
"description": "Provision Azure VMs as Coder workspaces",
"icon": "/icon/azure.png",
"tags": [
"vm",
"linux",
"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"
},
{
"id": "do-linux",
"url": "",
"name": "DigitalOcean Droplet (Linux)",
"description": "Provision DigitalOcean Droplets as Coder workspaces",
"icon": "/icon/do.png",
"tags": [
"vm",
"linux",
"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"
},
{
"id": "docker",
"url": "",
"name": "Docker Containers",
"description": "Provision Docker containers as Coder workspaces",
"icon": "/icon/docker.png",
"tags": [
"docker",
"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"
},
{
"id": "gcp-devcontainer",
"url": "",
"name": "Google Compute Engine (Devcontainer)",
"description": "Provision a Devcontainer on Google Compute Engine instances as Coder workspaces",
"icon": "/icon/gcp.png",
"tags": [
"vm",
"linux",
"gcp",
"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"
},
{
"id": "gcp-linux",
"url": "",
"name": "Google Compute Engine (Linux)",
"description": "Provision Google Compute Engine instances as Coder workspaces",
"icon": "/icon/gcp.png",
"tags": [
"vm",
"linux",
"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"
},
{
"id": "gcp-vm-container",
"url": "",
"name": "Google Compute Engine (VM Container)",
"description": "Provision Google Compute Engine instances as Coder workspaces",
"icon": "/icon/gcp.png",
"tags": [
"vm-container",
"linux",
"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"
},
{
"id": "gcp-windows",
"url": "",
"name": "Google Compute Engine (Windows)",
"description": "Provision Google Compute Engine instances as Coder workspaces",
"icon": "/icon/gcp.png",
"tags": [
"vm",
"windows",
"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"
},
{
"id": "kubernetes",
"url": "",
"name": "Kubernetes (Deployment)",
"description": "Provision Kubernetes Deployments as Coder workspaces",
"icon": "/icon/k8s.png",
"tags": [
"kubernetes",
"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"
},
{
"id": "nomad-docker",
"url": "",
"name": "Nomad",
"description": "Provision Nomad Jobs as Coder workspaces",
"icon": "/icon/nomad.svg",
"tags": [
"nomad",
"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"
},
{
"id": "scratch",
"url": "",
"name": "Scratch",
"description": "A minimal starter template for Coder",
"icon": "/emojis/1f4e6.png",
"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"
}
]
File diff suppressed because it is too large Load Diff
+44 -44
View File
@@ -31,50 +31,50 @@ instances provisioned by Coder:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"ec2:GetDefaultCreditSpecification",
"ec2:DescribeIamInstanceProfileAssociations",
"ec2:DescribeTags",
"ec2:DescribeInstances",
"ec2:DescribeInstanceTypes",
"ec2:CreateTags",
"ec2:RunInstances",
"ec2:DescribeInstanceCreditSpecifications",
"ec2:DescribeImages",
"ec2:ModifyDefaultCreditSpecification",
"ec2:DescribeVolumes"
],
"Resource": "*"
},
{
"Sid": "CoderResources",
"Effect": "Allow",
"Action": [
"ec2:DescribeInstanceAttribute",
"ec2:UnmonitorInstances",
"ec2:TerminateInstances",
"ec2:StartInstances",
"ec2:StopInstances",
"ec2:DeleteTags",
"ec2:MonitorInstances",
"ec2:CreateTags",
"ec2:RunInstances",
"ec2:ModifyInstanceAttribute",
"ec2:ModifyInstanceCreditSpecification"
],
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringEquals": {
"aws:ResourceTag/Coder_Provisioned": "true"
}
}
}
]
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"ec2:GetDefaultCreditSpecification",
"ec2:DescribeIamInstanceProfileAssociations",
"ec2:DescribeTags",
"ec2:DescribeInstances",
"ec2:DescribeInstanceTypes",
"ec2:CreateTags",
"ec2:RunInstances",
"ec2:DescribeInstanceCreditSpecifications",
"ec2:DescribeImages",
"ec2:ModifyDefaultCreditSpecification",
"ec2:DescribeVolumes"
],
"Resource": "*"
},
{
"Sid": "CoderResources",
"Effect": "Allow",
"Action": [
"ec2:DescribeInstanceAttribute",
"ec2:UnmonitorInstances",
"ec2:TerminateInstances",
"ec2:StartInstances",
"ec2:StopInstances",
"ec2:DeleteTags",
"ec2:MonitorInstances",
"ec2:CreateTags",
"ec2:RunInstances",
"ec2:ModifyInstanceAttribute",
"ec2:ModifyInstanceCreditSpecification"
],
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringEquals": {
"aws:ResourceTag/Coder_Provisioned": "true"
}
}
}
]
}
```
+44 -44
View File
@@ -30,50 +30,50 @@ instances provisioned by Coder:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"ec2:GetDefaultCreditSpecification",
"ec2:DescribeIamInstanceProfileAssociations",
"ec2:DescribeTags",
"ec2:DescribeInstances",
"ec2:DescribeInstanceTypes",
"ec2:CreateTags",
"ec2:RunInstances",
"ec2:DescribeInstanceCreditSpecifications",
"ec2:DescribeImages",
"ec2:ModifyDefaultCreditSpecification",
"ec2:DescribeVolumes"
],
"Resource": "*"
},
{
"Sid": "CoderResources",
"Effect": "Allow",
"Action": [
"ec2:DescribeInstanceAttribute",
"ec2:UnmonitorInstances",
"ec2:TerminateInstances",
"ec2:StartInstances",
"ec2:StopInstances",
"ec2:DeleteTags",
"ec2:MonitorInstances",
"ec2:CreateTags",
"ec2:RunInstances",
"ec2:ModifyInstanceAttribute",
"ec2:ModifyInstanceCreditSpecification"
],
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringEquals": {
"aws:ResourceTag/Coder_Provisioned": "true"
}
}
}
]
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"ec2:GetDefaultCreditSpecification",
"ec2:DescribeIamInstanceProfileAssociations",
"ec2:DescribeTags",
"ec2:DescribeInstances",
"ec2:DescribeInstanceTypes",
"ec2:CreateTags",
"ec2:RunInstances",
"ec2:DescribeInstanceCreditSpecifications",
"ec2:DescribeImages",
"ec2:ModifyDefaultCreditSpecification",
"ec2:DescribeVolumes"
],
"Resource": "*"
},
{
"Sid": "CoderResources",
"Effect": "Allow",
"Action": [
"ec2:DescribeInstanceAttribute",
"ec2:UnmonitorInstances",
"ec2:TerminateInstances",
"ec2:StartInstances",
"ec2:StopInstances",
"ec2:DeleteTags",
"ec2:MonitorInstances",
"ec2:CreateTags",
"ec2:RunInstances",
"ec2:ModifyInstanceAttribute",
"ec2:ModifyInstanceCreditSpecification"
],
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringEquals": {
"aws:ResourceTag/Coder_Provisioned": "true"
}
}
}
]
}
```
+44 -44
View File
@@ -30,50 +30,50 @@ instances provisioned by Coder:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"ec2:GetDefaultCreditSpecification",
"ec2:DescribeIamInstanceProfileAssociations",
"ec2:DescribeTags",
"ec2:DescribeInstances",
"ec2:DescribeInstanceTypes",
"ec2:CreateTags",
"ec2:RunInstances",
"ec2:DescribeInstanceCreditSpecifications",
"ec2:DescribeImages",
"ec2:ModifyDefaultCreditSpecification",
"ec2:DescribeVolumes"
],
"Resource": "*"
},
{
"Sid": "CoderResources",
"Effect": "Allow",
"Action": [
"ec2:DescribeInstanceAttribute",
"ec2:UnmonitorInstances",
"ec2:TerminateInstances",
"ec2:StartInstances",
"ec2:StopInstances",
"ec2:DeleteTags",
"ec2:MonitorInstances",
"ec2:CreateTags",
"ec2:RunInstances",
"ec2:ModifyInstanceAttribute",
"ec2:ModifyInstanceCreditSpecification"
],
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringEquals": {
"aws:ResourceTag/Coder_Provisioned": "true"
}
}
}
]
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"ec2:GetDefaultCreditSpecification",
"ec2:DescribeIamInstanceProfileAssociations",
"ec2:DescribeTags",
"ec2:DescribeInstances",
"ec2:DescribeInstanceTypes",
"ec2:CreateTags",
"ec2:RunInstances",
"ec2:DescribeInstanceCreditSpecifications",
"ec2:DescribeImages",
"ec2:ModifyDefaultCreditSpecification",
"ec2:DescribeVolumes"
],
"Resource": "*"
},
{
"Sid": "CoderResources",
"Effect": "Allow",
"Action": [
"ec2:DescribeInstanceAttribute",
"ec2:UnmonitorInstances",
"ec2:TerminateInstances",
"ec2:StartInstances",
"ec2:StopInstances",
"ec2:DeleteTags",
"ec2:MonitorInstances",
"ec2:CreateTags",
"ec2:RunInstances",
"ec2:ModifyInstanceAttribute",
"ec2:ModifyInstanceCreditSpecification"
],
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringEquals": {
"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} */
const nextConfig = {
output: "export",
reactStrictMode: true,
trailingSlash: true,
output: "export",
reactStrictMode: true,
trailingSlash: true,
};
module.exports = nextConfig;
+43 -43
View File
@@ -1,45 +1,45 @@
{
"name": "coder-docs-generator",
"private": true,
"scripts": {
"dev": "pnpm copy-images && next dev",
"build": "next build",
"start": "next start",
"export": "pnpm copy-images && next build",
"copy-images": "sh ./scripts/copyImages.sh",
"lint": "pnpm run lint:types",
"lint:types": "tsc --noEmit",
"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}'"
},
"dependencies": {
"@chakra-ui/react": "2.8.2",
"@emotion/react": "11.11.4",
"@emotion/styled": "11.11.5",
"archiver": "6.0.2",
"framer-motion": "^10.17.6",
"front-matter": "4.0.2",
"lodash": "4.17.21",
"next": "14.2.4",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-icons": "4.12.0",
"react-markdown": "9.0.1",
"rehype-raw": "7.0.0",
"remark-gfm": "4.0.0"
},
"devDependencies": {
"@types/lodash": "4.14.196",
"@types/node": "18.19.0",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"eslint": "8.56.0",
"eslint-config-next": "14.0.1",
"prettier": "3.3.3",
"typescript": "5.3.2"
},
"engines": {
"npm": ">=9.0.0 <10.0.0",
"node": ">=18.0.0 <21.0.0"
}
"name": "coder-docs-generator",
"private": true,
"scripts": {
"dev": "pnpm copy-images && next dev",
"build": "next build",
"start": "next start",
"export": "pnpm copy-images && next build",
"copy-images": "sh ./scripts/copyImages.sh",
"lint": "pnpm run lint:types",
"lint:types": "tsc --noEmit",
"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}'"
},
"dependencies": {
"@chakra-ui/react": "2.8.2",
"@emotion/react": "11.11.4",
"@emotion/styled": "11.11.5",
"archiver": "6.0.2",
"framer-motion": "^10.17.6",
"front-matter": "4.0.2",
"lodash": "4.17.21",
"next": "14.2.4",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-icons": "4.12.0",
"react-markdown": "9.0.1",
"rehype-raw": "7.0.0",
"remark-gfm": "4.0.0"
},
"devDependencies": {
"@types/lodash": "4.14.196",
"@types/node": "18.19.0",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"eslint": "8.56.0",
"eslint-config-next": "14.0.1",
"prettier": "3.3.3",
"typescript": "5.3.2"
},
"engines": {
"npm": ">=9.0.0 <10.0.0",
"node": ">=18.0.0 <21.0.0"
}
}
+419 -419
View File
@@ -1,29 +1,29 @@
import {
Box,
Button,
Code,
Drawer,
DrawerBody,
DrawerCloseButton,
DrawerContent,
DrawerOverlay,
Flex,
Grid,
GridProps,
Heading,
Icon,
Img,
Link,
OrderedList,
Table,
TableContainer,
Td,
Text,
Th,
Thead,
Tr,
UnorderedList,
useDisclosure,
Box,
Button,
Code,
Drawer,
DrawerBody,
DrawerCloseButton,
DrawerContent,
DrawerOverlay,
Flex,
Grid,
GridProps,
Heading,
Icon,
Img,
Link,
OrderedList,
Table,
TableContainer,
Td,
Text,
Th,
Thead,
Tr,
UnorderedList,
useDisclosure,
} from "@chakra-ui/react";
import fm from "front-matter";
import { readFileSync } from "fs";
@@ -42,19 +42,19 @@ import remarkGfm from "remark-gfm";
type FilePath = string;
type UrlPath = string;
type Route = {
path: FilePath;
title: string;
description?: string;
children?: Route[];
path: FilePath;
title: string;
description?: string;
children?: Route[];
};
type Manifest = { versions: string[]; routes: Route[] };
type NavItem = { title: string; path: UrlPath; children?: NavItem[] };
type Nav = NavItem[];
const readContentFile = (filePath: string) => {
const baseDir = process.cwd();
const docsPath = path.join(baseDir, "..", "docs");
return readFileSync(path.join(docsPath, filePath), { encoding: "utf-8" });
const baseDir = process.cwd();
const docsPath = path.join(baseDir, "..", "docs");
return readFileSync(path.join(docsPath, filePath), { encoding: "utf-8" });
};
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 removeIndexFilename = (path: string) => {
if (path.endsWith("index")) {
path = path.replace("index", "");
}
if (path.endsWith("index")) {
path = path.replace("index", "");
}
return path;
return path;
};
const removeREADMEName = (path: string) => {
if (path.startsWith("README")) {
path = path.replace("README", "");
}
if (path.startsWith("README")) {
path = path.replace("README", "");
}
return path;
return path;
};
// 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 -> ../file-next-to-file = ../file-next-to-file
const transformLinkUriSource = (sourceFile: string) => {
return (href = "") => {
const isExternal = href.startsWith("http") || href.startsWith("https");
if (!isExternal) {
// Remove .md form the path
href = removeMkdExtension(href);
return (href = "") => {
const isExternal = href.startsWith("http") || href.startsWith("https");
if (!isExternal) {
// Remove .md form the path
href = removeMkdExtension(href);
// Add the extra '..' if not an index file.
sourceFile = removeMkdExtension(sourceFile);
if (!sourceFile.endsWith("index")) {
href = "../" + href;
}
// Add the extra '..' if not an index file.
sourceFile = removeMkdExtension(sourceFile);
if (!sourceFile.endsWith("index")) {
href = "../" + href;
}
// Remove the index path
href = removeIndexFilename(href);
href = removeREADMEName(href);
}
return href;
};
// Remove the index path
href = removeIndexFilename(href);
href = removeREADMEName(href);
}
return href;
};
};
const transformFilePathToUrlPath = (filePath: string) => {
// Remove markdown extension
let urlPath = removeMkdExtension(filePath);
// Remove markdown extension
let urlPath = removeMkdExtension(filePath);
// Remove relative path
if (urlPath.startsWith("./")) {
urlPath = urlPath.replace("./", "");
}
// Remove relative path
if (urlPath.startsWith("./")) {
urlPath = urlPath.replace("./", "");
}
// Remove index from the root file
urlPath = removeIndexFilename(urlPath);
urlPath = removeREADMEName(urlPath);
// Remove index from the root file
urlPath = removeIndexFilename(urlPath);
urlPath = removeREADMEName(urlPath);
// Remove trailing slash
if (urlPath.endsWith("/")) {
urlPath = removeTrailingSlash(urlPath);
}
// Remove trailing slash
if (urlPath.endsWith("/")) {
urlPath = removeTrailingSlash(urlPath);
}
return urlPath;
return urlPath;
};
const mapRoutes = (manifest: Manifest): Record<UrlPath, Route> => {
const paths: Record<UrlPath, Route> = {};
const paths: Record<UrlPath, Route> = {};
const addPaths = (routes: Route[]) => {
for (const route of routes) {
paths[transformFilePathToUrlPath(route.path)] = route;
const addPaths = (routes: Route[]) => {
for (const route of routes) {
paths[transformFilePathToUrlPath(route.path)] = route;
if (route.children) {
addPaths(route.children);
}
}
};
if (route.children) {
addPaths(route.children);
}
}
};
addPaths(manifest.routes);
addPaths(manifest.routes);
return paths;
return paths;
};
let manifest: Manifest | undefined;
const getManifest = () => {
if (manifest) {
return manifest;
}
if (manifest) {
return manifest;
}
const manifestContent = readContentFile("manifest.json");
manifest = JSON.parse(manifestContent) as Manifest;
return manifest;
const manifestContent = readContentFile("manifest.json");
manifest = JSON.parse(manifestContent) as Manifest;
return manifest;
};
let navigation: Nav | undefined;
const getNavigation = (manifest: Manifest): Nav => {
if (navigation) {
return navigation;
}
if (navigation) {
return navigation;
}
const getNavItem = (route: Route, parentPath?: UrlPath): NavItem => {
const path = parentPath
? `${parentPath}/${transformFilePathToUrlPath(route.path)}`
: transformFilePathToUrlPath(route.path);
const navItem: NavItem = {
title: route.title,
path,
};
const getNavItem = (route: Route, parentPath?: UrlPath): NavItem => {
const path = parentPath
? `${parentPath}/${transformFilePathToUrlPath(route.path)}`
: transformFilePathToUrlPath(route.path);
const navItem: NavItem = {
title: route.title,
path,
};
if (route.children) {
navItem.children = [];
if (route.children) {
navItem.children = [];
for (const childRoute of route.children) {
navItem.children.push(getNavItem(childRoute));
}
}
for (const childRoute of route.children) {
navItem.children.push(getNavItem(childRoute));
}
}
return navItem;
};
return navItem;
};
navigation = [];
navigation = [];
for (const route of manifest.routes) {
navigation.push(getNavItem(route));
}
for (const route of manifest.routes) {
navigation.push(getNavItem(route));
}
return navigation;
return navigation;
};
const removeHtmlComments = (string: string) => {
return string.replace(/<!--[\s\S]*?-->/g, "");
return string.replace(/<!--[\s\S]*?-->/g, "");
};
export const getStaticPaths: GetStaticPaths = () => {
const manifest = getManifest();
const routes = mapRoutes(manifest);
const paths = Object.keys(routes).map((urlPath) => ({
params: { slug: urlPath.split("/") },
}));
const manifest = getManifest();
const routes = mapRoutes(manifest);
const paths = Object.keys(routes).map((urlPath) => ({
params: { slug: urlPath.split("/") },
}));
return {
paths,
fallback: false,
};
return {
paths,
fallback: false,
};
};
export const getStaticProps: GetStaticProps = (context) => {
// 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
const { slug = [""] } = context.params as { slug: string[] };
const manifest = getManifest();
const routes = mapRoutes(manifest);
const urlPath = slug.join("/");
const route = routes[urlPath];
const { body } = fm(readContentFile(route.path));
// Serialize MDX to support custom components
const content = removeHtmlComments(body);
const navigation = getNavigation(manifest);
const version = manifest.versions[0];
// 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
const { slug = [""] } = context.params as { slug: string[] };
const manifest = getManifest();
const routes = mapRoutes(manifest);
const urlPath = slug.join("/");
const route = routes[urlPath];
const { body } = fm(readContentFile(route.path));
// Serialize MDX to support custom components
const content = removeHtmlComments(body);
const navigation = getNavigation(manifest);
const version = manifest.versions[0];
return {
props: {
content,
navigation,
route,
version,
},
};
return {
props: {
content,
navigation,
route,
version,
},
};
};
const SidebarNavItem: React.FC<{ item: NavItem; nav: Nav }> = ({
item,
nav,
item,
nav,
}) => {
const router = useRouter();
let isActive = router.asPath.startsWith(`/${item.path}`);
const router = useRouter();
let isActive = router.asPath.startsWith(`/${item.path}`);
// Special case to handle the home path
if (item.path === "") {
isActive = router.asPath === "/";
// Special case to handle the home path
if (item.path === "") {
isActive = router.asPath === "/";
// Special case to handle the home path children
const homeNav = nav.find((navItem) => navItem.path === "") as NavItem;
const homeNavPaths =
homeNav.children?.map((item) => `/${item.path}/`) ?? [];
if (homeNavPaths.includes(router.asPath)) {
isActive = true;
}
}
// Special case to handle the home path children
const homeNav = nav.find((navItem) => navItem.path === "") as NavItem;
const homeNavPaths =
homeNav.children?.map((item) => `/${item.path}/`) ?? [];
if (homeNavPaths.includes(router.asPath)) {
isActive = true;
}
}
return (
<Box>
<NextLink href={"/" + item.path} passHref legacyBehavior>
<Link
fontWeight={isActive ? 600 : 400}
color={isActive ? "gray.900" : "gray.700"}
>
{item.title}
</Link>
</NextLink>
return (
<Box>
<NextLink href={"/" + item.path} passHref legacyBehavior>
<Link
fontWeight={isActive ? 600 : 400}
color={isActive ? "gray.900" : "gray.700"}
>
{item.title}
</Link>
</NextLink>
{isActive && item.children && (
<Grid
as="nav"
pt={2}
pl={3}
maxW="sm"
autoFlow="row"
gap={2}
autoRows="min-content"
>
{item.children.map((subItem) => (
<SidebarNavItem key={subItem.path} item={subItem} nav={nav} />
))}
</Grid>
)}
</Box>
);
{isActive && item.children && (
<Grid
as="nav"
pt={2}
pl={3}
maxW="sm"
autoFlow="row"
gap={2}
autoRows="min-content"
>
{item.children.map((subItem) => (
<SidebarNavItem key={subItem.path} item={subItem} nav={nav} />
))}
</Grid>
)}
</Box>
);
};
const SidebarNav: React.FC<{ nav: Nav; version: string } & GridProps> = ({
nav,
version,
...gridProps
nav,
version,
...gridProps
}) => {
return (
<Grid
h="100vh"
overflowY="scroll"
as="nav"
p={8}
w="300px"
autoFlow="row"
gap={2}
autoRows="min-content"
bgColor="white"
borderRightWidth={1}
borderColor="gray.200"
borderStyle="solid"
{...gridProps}
>
<Box mb={6}>
<Img src="/logo.svg" alt="Coder logo" />
</Box>
return (
<Grid
h="100vh"
overflowY="scroll"
as="nav"
p={8}
w="300px"
autoFlow="row"
gap={2}
autoRows="min-content"
bgColor="white"
borderRightWidth={1}
borderColor="gray.200"
borderStyle="solid"
{...gridProps}
>
<Box mb={6}>
<Img src="/logo.svg" alt="Coder logo" />
</Box>
{nav.map((navItem) => (
<SidebarNavItem key={navItem.path} item={navItem} nav={nav} />
))}
</Grid>
);
{nav.map((navItem) => (
<SidebarNavItem key={navItem.path} item={navItem} nav={nav} />
))}
</Grid>
);
};
const MobileNavbar: React.FC<{ nav: Nav; version: string }> = ({
nav,
version,
nav,
version,
}) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<>
<Flex
bgColor="white"
px={6}
alignItems="center"
h={16}
borderBottomWidth={1}
>
<Img src="/logo.svg" alt="Coder logo" w={28} />
return (
<>
<Flex
bgColor="white"
px={6}
alignItems="center"
h={16}
borderBottomWidth={1}
>
<Img src="/logo.svg" alt="Coder logo" w={28} />
<Button variant="ghost" ml="auto" onClick={onOpen}>
<Icon as={MdMenu} fontSize="2xl" />
</Button>
</Flex>
<Button variant="ghost" ml="auto" onClick={onOpen}>
<Icon as={MdMenu} fontSize="2xl" />
</Button>
</Flex>
<Drawer onClose={onClose} isOpen={isOpen}>
<DrawerOverlay />
<DrawerContent>
<DrawerCloseButton />
<DrawerBody p={0}>
<SidebarNav nav={nav} version={version} border={0} />
</DrawerBody>
</DrawerContent>
</Drawer>
</>
);
<Drawer onClose={onClose} isOpen={isOpen}>
<DrawerOverlay />
<DrawerContent>
<DrawerCloseButton />
<DrawerBody p={0}>
<SidebarNav nav={nav} version={version} border={0} />
</DrawerBody>
</DrawerContent>
</Drawer>
</>
);
};
const slugifyTitle = (titleSource: ReactNode) => {
if (Array.isArray(titleSource) && typeof titleSource[0] === "string") {
return _.kebabCase(titleSource[0].toLowerCase());
}
if (Array.isArray(titleSource) && typeof titleSource[0] === "string") {
return _.kebabCase(titleSource[0].toLowerCase());
}
return undefined;
return undefined;
};
const getImageUrl = (src: string | undefined) => {
if (src === undefined) {
return "";
}
const assetPath = src.split("images/")[1];
return `/images/${assetPath}`;
if (src === undefined) {
return "";
}
const assetPath = src.split("images/")[1];
return `/images/${assetPath}`;
};
const DocsPage: NextPage<{
content: string;
navigation: Nav;
route: Route;
version: string;
content: string;
navigation: Nav;
route: Route;
version: string;
}> = ({ content, navigation, route, version }) => {
return (
<>
<Head>
<title>{route.title}</title>
<meta name="source" content={route.path} />
</Head>
<Box
display={{ md: "grid" }}
gridTemplateColumns="max-content 1fr"
fontSize="md"
color="gray.700"
>
<Box display={{ base: "none", md: "block" }}>
<SidebarNav nav={navigation} version={version} />
</Box>
return (
<>
<Head>
<title>{route.title}</title>
<meta name="source" content={route.path} />
</Head>
<Box
display={{ md: "grid" }}
gridTemplateColumns="max-content 1fr"
fontSize="md"
color="gray.700"
>
<Box display={{ base: "none", md: "block" }}>
<SidebarNav nav={navigation} version={version} />
</Box>
<Box display={{ base: "block", md: "none" }}>
<MobileNavbar nav={navigation} version={version} />
</Box>
<Box display={{ base: "block", md: "none" }}>
<MobileNavbar nav={navigation} version={version} />
</Box>
<Box
as="main"
w="full"
pb={20}
px={{ base: 6, md: 10 }}
pl={{ base: 6, md: 20 }}
h="100vh"
overflowY="auto"
>
<Box maxW="872">
<Box lineHeight="tall">
{/* Some docs don't have the title */}
<Heading
as="h1"
fontSize="4xl"
pt={10}
pb={2}
// Hide this title if the doc has the title already
sx={{ "& + h1": { display: "none" } }}
>
{route.title}
</Heading>
<Box
as="main"
w="full"
pb={20}
px={{ base: 6, md: 10 }}
pl={{ base: 6, md: 20 }}
h="100vh"
overflowY="auto"
>
<Box maxW="872">
<Box lineHeight="tall">
{/* Some docs don't have the title */}
<Heading
as="h1"
fontSize="4xl"
pt={10}
pb={2}
// Hide this title if the doc has the title already
sx={{ "& + h1": { display: "none" } }}
>
{route.title}
</Heading>
<ReactMarkdown
rehypePlugins={[rehypeRaw]}
remarkPlugins={[remarkGfm]}
urlTransform={transformLinkUriSource(route.path)}
components={{
h1: ({ children }) => (
<Heading
as="h1"
fontSize="4xl"
pt={10}
pb={2}
id={slugifyTitle(children)}
>
{children}
</Heading>
),
<ReactMarkdown
rehypePlugins={[rehypeRaw]}
remarkPlugins={[remarkGfm]}
urlTransform={transformLinkUriSource(route.path)}
components={{
h1: ({ children }) => (
<Heading
as="h1"
fontSize="4xl"
pt={10}
pb={2}
id={slugifyTitle(children)}
>
{children}
</Heading>
),
h2: ({ children }) => (
<Heading
as="h2"
fontSize="3xl"
pt={10}
pb={2}
id={slugifyTitle(children)}
>
{children}
</Heading>
),
h3: ({ children }) => (
<Heading
as="h3"
fontSize="2xl"
pt={10}
pb={2}
id={slugifyTitle(children)}
>
{children}
</Heading>
),
img: ({ src }) => (
<Img
src={getImageUrl(src)}
mb={2}
borderWidth={1}
borderColor="gray.200"
borderStyle="solid"
rounded="md"
height="auto"
/>
),
p: ({ children }) => (
<Text pt={2} pb={2}>
{children}
</Text>
),
ul: ({ children }) => (
<UnorderedList
mb={4}
display="grid"
gridAutoFlow="row"
gap={2}
>
{children}
</UnorderedList>
),
ol: ({ children }) => (
<OrderedList
mb={4}
display="grid"
gridAutoFlow="row"
gap={2}
>
{children}
</OrderedList>
),
a: ({ children, href = "" }) => {
const isExternal =
href.startsWith("http") || href.startsWith("https");
h2: ({ children }) => (
<Heading
as="h2"
fontSize="3xl"
pt={10}
pb={2}
id={slugifyTitle(children)}
>
{children}
</Heading>
),
h3: ({ children }) => (
<Heading
as="h3"
fontSize="2xl"
pt={10}
pb={2}
id={slugifyTitle(children)}
>
{children}
</Heading>
),
img: ({ src }) => (
<Img
src={getImageUrl(src)}
mb={2}
borderWidth={1}
borderColor="gray.200"
borderStyle="solid"
rounded="md"
height="auto"
/>
),
p: ({ children }) => (
<Text pt={2} pb={2}>
{children}
</Text>
),
ul: ({ children }) => (
<UnorderedList
mb={4}
display="grid"
gridAutoFlow="row"
gap={2}
>
{children}
</UnorderedList>
),
ol: ({ children }) => (
<OrderedList
mb={4}
display="grid"
gridAutoFlow="row"
gap={2}
>
{children}
</OrderedList>
),
a: ({ children, href = "" }) => {
const isExternal =
href.startsWith("http") || href.startsWith("https");
return (
<Link
href={href}
target={isExternal ? "_blank" : undefined}
fontWeight={500}
color="blue.600"
>
{children}
</Link>
);
},
code: ({ node, ...props }) => (
<Code {...props} bgColor="gray.100" />
),
pre: ({ children }) => (
<Box
as="pre"
w="full"
sx={{ "& > code": { w: "full", p: 4, rounded: "md" } }}
mb={2}
>
{children}
</Box>
),
table: ({ children }) => (
<TableContainer
mt={1}
mb={2}
bgColor="white"
rounded="md"
borderWidth={1}
borderColor="gray.100"
borderStyle="solid"
>
<Table variant="simple">{children}</Table>
</TableContainer>
),
thead: ({ children }) => <Thead>{children}</Thead>,
th: ({ children }) => <Th>{children}</Th>,
td: ({ children }) => <Td>{children}</Td>,
tr: ({ children }) => <Tr>{children}</Tr>,
}}
>
{content}
</ReactMarkdown>
</Box>
</Box>
</Box>
</Box>
</>
);
return (
<Link
href={href}
target={isExternal ? "_blank" : undefined}
fontWeight={500}
color="blue.600"
>
{children}
</Link>
);
},
code: ({ node, ...props }) => (
<Code {...props} bgColor="gray.100" />
),
pre: ({ children }) => (
<Box
as="pre"
w="full"
sx={{ "& > code": { w: "full", p: 4, rounded: "md" } }}
mb={2}
>
{children}
</Box>
),
table: ({ children }) => (
<TableContainer
mt={1}
mb={2}
bgColor="white"
rounded="md"
borderWidth={1}
borderColor="gray.100"
borderStyle="solid"
>
<Table variant="simple">{children}</Table>
</TableContainer>
),
thead: ({ children }) => <Thead>{children}</Thead>,
th: ({ children }) => <Th>{children}</Th>,
td: ({ children }) => <Td>{children}</Td>,
tr: ({ children }) => <Tr>{children}</Tr>,
}}
>
{content}
</ReactMarkdown>
</Box>
</Box>
</Box>
</Box>
</>
);
};
export default DocsPage;
+18 -18
View File
@@ -3,27 +3,27 @@ import type { AppProps } from "next/app";
import Head from "next/head";
const theme = extendTheme({
styles: {
global: {
body: {
bg: "gray.50",
},
},
},
styles: {
global: {
body: {
bg: "gray.50",
},
},
},
});
const MyApp: React.FC<AppProps> = ({ Component, pageProps }) => {
return (
<>
<Head>
<link rel="mask-icon" href="/favicon.svg" color="#000000" />
<link rel="alternate icon" type="image/png" href="/favicon.png" />
</Head>
<ChakraProvider theme={theme}>
<Component {...pageProps} />
</ChakraProvider>
</>
);
return (
<>
<Head>
<link rel="mask-icon" href="/favicon.svg" color="#000000" />
<link rel="alternate icon" type="image/png" href="/favicon.png" />
</Head>
<ChakraProvider theme={theme}>
<Component {...pageProps} />
</ChakraProvider>
</>
);
};
export default MyApp;
+18 -18
View File
@@ -1,20 +1,20 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules", "docs"]
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"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.",
"name": "coder",
"version": "0.0.0",
"packageManager": "pnpm@9.7.1+sha512.faf344af2d6ca65c4c5c8c2224ea77a81a5e8859cbc4e06b1511ddce2f0151512431dd19e6aff31f2c6a8f5f2aced9bd2273e1fed7dd4de1868984059d2c4247",
"scripts": {
"format": "prettier --cache --write '**/*.{css,html,json,md,yaml,yml}'",
"format:check": "prettier --cache --check '**/*.{css,html,json,md,yaml,yml}'",
"storybook": "pnpm run -C site/ storybook"
},
"devDependencies": {
"prettier": "3.3.3"
}
"_comment": "This version doesn't matter, it's just to allow importing from other repos.",
"name": "coder",
"version": "0.0.0",
"packageManager": "pnpm@9.7.1+sha512.faf344af2d6ca65c4c5c8c2224ea77a81a5e8859cbc4e06b1511ddce2f0151512431dd19e6aff31f2c6a8f5f2aced9bd2273e1fed7dd4de1868984059d2c4247",
"scripts": {
"format": "prettier --cache --write '**/*.{css,html,json,md,yaml,yml}'",
"format:check": "prettier --cache --check '**/*.{css,html,json,md,yaml,yml}'",
"storybook": "pnpm run -C site/ storybook"
},
"devDependencies": {
"prettier": "3.3.3"
}
}
File diff suppressed because it is too large Load Diff
+7 -7
View File
@@ -1,9 +1,9 @@
{
"dependencies": {
"widdershins": "^4.0.1"
},
"resolutions": {
"semver": "7.5.3",
"jsonpointer": "5.0.1"
}
"dependencies": {
"widdershins": "^4.0.1"
},
"resolutions": {
"semver": "7.5.3",
"jsonpointer": "5.0.1"
}
}
+1 -1
View File
@@ -39,7 +39,7 @@ var (
// CLI option types:
"github.com/coder/serpent",
}
indent = " "
indent = "\t"
)
func main() {
+4 -4
View File
@@ -1,17 +1,17 @@
// From codersdk/genericmap.go
export interface Buzz {
readonly foo: Foo
readonly bazz: string
readonly foo: Foo
readonly bazz: string
}
// From codersdk/genericmap.go
export interface Foo {
readonly bar: string
readonly bar: string
}
// From codersdk/genericmap.go
export interface FooBuzz<R extends Custom> {
readonly something: (readonly R[])
readonly something: (readonly R[])
}
// From codersdk/genericmap.go
+14 -14
View File
@@ -1,35 +1,35 @@
// From codersdk/generics.go
export interface Complex<C extends comparable, S extends Single, T extends Custom> {
readonly dynamic: Fields<C, boolean, string, S>
readonly order: FieldsDiffOrder<C, string, S, T>
readonly comparable: C
readonly single: S
readonly static: Static
readonly dynamic: Fields<C, boolean, string, S>
readonly order: FieldsDiffOrder<C, string, S, T>
readonly comparable: C
readonly single: S
readonly static: Static
}
// From codersdk/generics.go
export interface Dynamic<A extends any, S extends Single> {
readonly dynamic: Fields<boolean, A, string, S>
readonly comparable: boolean
readonly dynamic: Fields<boolean, A, string, S>
readonly comparable: boolean
}
// From codersdk/generics.go
export interface Fields<C extends comparable, A extends any, T extends Custom, S extends Single> {
readonly comparable: C
readonly any: A
readonly custom: T
readonly again: T
readonly single_constraint: S
readonly comparable: C
readonly any: A
readonly custom: T
readonly again: T
readonly single_constraint: S
}
// From codersdk/generics.go
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
export interface Static {
readonly static: Fields<string, number, number, string>
readonly static: Fields<string, number, number, string>
}
// From codersdk/generics.go
+3 -3
View File
@@ -1,10 +1,10 @@
// From codersdk/genericslice.go
export interface Bar {
readonly Bar: string
readonly Bar: string
}
// From codersdk/genericslice.go
export interface Foo<R extends any> {
readonly Slice: (readonly R[])
readonly TwoD: (readonly (readonly R[])[])
readonly Slice: (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.SetIndent("", " ")
enc.SetIndent("", "\t")
return enc.Encode(examples)
}
+39 -43
View File
@@ -1,45 +1,41 @@
{
"files": {
"ignore": ["**/*Generated.ts"]
},
"formatter": {
"indentStyle": "space",
"indentWidth": 2
},
"linter": {
"rules": {
"a11y": {
"noSvgWithoutTitle": { "level": "off" },
"useButtonType": { "level": "off" }
},
"style": {
"noNonNullAssertion": { "level": "off" },
"noParameterAssign": { "level": "off" },
"useDefaultParameterLast": { "level": "off" },
"useSelfClosingElements": { "level": "off" }
},
"suspicious": {
"noArrayIndexKey": { "level": "off" },
"noThenProperty": { "level": "off" }
},
"nursery": {
"noRestrictedImports": {
"level": "error",
"options": {
"paths": {
"@mui/material": "Use @mui/material/<name> instead. See: https://material-ui.com/guides/minimizing-bundle-size/.",
"@mui/icons-material": "Use @mui/icons-material/<name> instead. See: https://material-ui.com/guides/minimizing-bundle-size/.",
"@mui/material/Avatar": "Use components/Avatar/Avatar instead.",
"@mui/material/Alert": "Use components/Alert/Alert instead.",
"@mui/material/Popover": "Use components/Popover/Popover 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."
}
}
}
}
}
}
"files": {
"ignore": ["**/*Generated.ts"]
},
"linter": {
"rules": {
"a11y": {
"noSvgWithoutTitle": { "level": "off" },
"useButtonType": { "level": "off" }
},
"style": {
"noNonNullAssertion": { "level": "off" },
"noParameterAssign": { "level": "off" },
"useDefaultParameterLast": { "level": "off" },
"useSelfClosingElements": { "level": "off" }
},
"suspicious": {
"noArrayIndexKey": { "level": "off" },
"noThenProperty": { "level": "off" }
},
"nursery": {
"noRestrictedImports": {
"level": "error",
"options": {
"paths": {
"@mui/material": "Use @mui/material/<name> instead. See: https://material-ui.com/guides/minimizing-bundle-size/.",
"@mui/icons-material": "Use @mui/icons-material/<name> instead. See: https://material-ui.com/guides/minimizing-bundle-size/.",
"@mui/material/Avatar": "Use components/Avatar/Avatar instead.",
"@mui/material/Alert": "Use components/Alert/Alert instead.",
"@mui/material/Popover": "Use components/Popover/Popover 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;
export const setupApiCalls = async (page: Page) => {
try {
const token = await findSessionToken(page);
API.setSessionToken(token);
} catch {
// If this fails, we have an unauthenticated client.
}
try {
const token = await findSessionToken(page);
API.setSessionToken(token);
} catch {
// 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> => {
if (currentOrgId) {
return currentOrgId;
}
const currentUser = await API.getAuthenticatedUser();
currentOrgId = currentUser.organization_ids[0];
return currentOrgId;
if (currentOrgId) {
return currentOrgId;
}
const currentUser = await API.getAuthenticatedUser();
currentOrgId = currentUser.organization_ids[0];
return currentOrgId;
};
export const createUser = async (orgId: string) => {
const name = randomName();
const user = await API.createUser({
email: `${name}@coder.com`,
username: name,
name: name,
password: "s3cure&password!",
login_type: "password",
disable_login: false,
organization_id: orgId,
});
return user;
const name = randomName();
const user = await API.createUser({
email: `${name}@coder.com`,
username: name,
name: name,
password: "s3cure&password!",
login_type: "password",
disable_login: false,
organization_id: orgId,
});
return user;
};
export const createGroup = async (orgId: string) => {
const name = randomName();
const group = await API.createGroup(orgId, {
name,
display_name: `Display ${name}`,
avatar_url: "/emojis/1f60d.png",
quota_allowance: 0,
});
return group;
const name = randomName();
const group = await API.createGroup(orgId, {
name,
display_name: `Display ${name}`,
avatar_url: "/emojis/1f60d.png",
quota_allowance: 0,
});
return group;
};
export const createOrganization = async () => {
const name = randomName();
const org = await API.createOrganization({
name,
display_name: `Org ${name}`,
description: `Org description ${name}`,
icon: "/emojis/1f957.png",
});
return org;
const name = randomName();
const org = await API.createOrganization({
name,
display_name: `Org ${name}`,
description: `Org description ${name}`,
icon: "/emojis/1f957.png",
});
return org;
};
export async function verifyConfigFlagBoolean(
page: Page,
config: DeploymentConfig,
flag: string,
page: Page,
config: DeploymentConfig,
flag: string,
) {
const opt = findConfigOption(config, flag);
const type = opt.value ? "option-enabled" : "option-disabled";
const value = opt.value ? "Enabled" : "Disabled";
const opt = findConfigOption(config, flag);
const type = opt.value ? "option-enabled" : "option-disabled";
const value = opt.value ? "Enabled" : "Disabled";
const configOption = page.locator(
`div.options-table .option-${flag} .${type}`,
);
await expect(configOption).toHaveText(value);
const configOption = page.locator(
`div.options-table .option-${flag} .${type}`,
);
await expect(configOption).toHaveText(value);
}
export async function verifyConfigFlagNumber(
page: Page,
config: DeploymentConfig,
flag: string,
page: Page,
config: DeploymentConfig,
flag: string,
) {
const opt = findConfigOption(config, flag);
const configOption = page.locator(
`div.options-table .option-${flag} .option-value-number`,
);
await expect(configOption).toHaveText(String(opt.value));
const opt = findConfigOption(config, flag);
const configOption = page.locator(
`div.options-table .option-${flag} .option-value-number`,
);
await expect(configOption).toHaveText(String(opt.value));
}
export async function verifyConfigFlagString(
page: Page,
config: DeploymentConfig,
flag: string,
page: Page,
config: DeploymentConfig,
flag: string,
) {
const opt = findConfigOption(config, flag);
const opt = findConfigOption(config, flag);
const configOption = page.locator(
`div.options-table .option-${flag} .option-value-string`,
);
await expect(configOption).toHaveText(opt.value);
const configOption = page.locator(
`div.options-table .option-${flag} .option-value-string`,
);
await expect(configOption).toHaveText(opt.value);
}
export async function verifyConfigFlagEmpty(page: Page, flag: string) {
const configOption = page.locator(
`div.options-table .option-${flag} .option-value-empty`,
);
await expect(configOption).toHaveText("Not set");
const configOption = page.locator(
`div.options-table .option-${flag} .option-value-empty`,
);
await expect(configOption).toHaveText("Not set");
}
export async function verifyConfigFlagArray(
page: Page,
config: DeploymentConfig,
flag: string,
page: Page,
config: DeploymentConfig,
flag: string,
) {
const opt = findConfigOption(config, flag);
const configOption = page.locator(
`div.options-table .option-${flag} .option-array`,
);
const opt = findConfigOption(config, flag);
const configOption = page.locator(
`div.options-table .option-${flag} .option-array`,
);
// Verify array of options with simple dots
for (const item of opt.value) {
await expect(configOption.locator("li", { hasText: item })).toBeVisible();
}
// Verify array of options with simple dots
for (const item of opt.value) {
await expect(configOption.locator("li", { hasText: item })).toBeVisible();
}
}
export async function verifyConfigFlagEntries(
page: Page,
config: DeploymentConfig,
flag: string,
page: Page,
config: DeploymentConfig,
flag: string,
) {
const opt = findConfigOption(config, flag);
const configOption = page.locator(
`div.options-table .option-${flag} .option-array`,
);
const opt = findConfigOption(config, flag);
const configOption = page.locator(
`div.options-table .option-${flag} .option-array`,
);
// Verify array of options with green marks.
Object.entries(opt.value)
.sort((a, b) => a[0].localeCompare(b[0]))
.map(async ([item]) => {
await expect(
configOption.locator(`.option-array-item-${item}.option-enabled`, {
hasText: item,
}),
).toBeVisible();
});
// Verify array of options with green marks.
Object.entries(opt.value)
.sort((a, b) => a[0].localeCompare(b[0]))
.map(async ([item]) => {
await expect(
configOption.locator(`.option-array-item-${item}.option-enabled`, {
hasText: item,
}),
).toBeVisible();
});
}
export async function verifyConfigFlagDuration(
page: Page,
config: DeploymentConfig,
flag: string,
page: Page,
config: DeploymentConfig,
flag: string,
) {
const opt = findConfigOption(config, flag);
const configOption = page.locator(
`div.options-table .option-${flag} .option-value-string`,
);
await expect(configOption).toHaveText(
formatDuration(
// intervalToDuration takes ms, so convert nanoseconds to ms
intervalToDuration({
start: 0,
end: (opt.value as number) / 1e6,
}),
),
);
const opt = findConfigOption(config, flag);
const configOption = page.locator(
`div.options-table .option-${flag} .option-value-string`,
);
await expect(configOption).toHaveText(
formatDuration(
// intervalToDuration takes ms, so convert nanoseconds to ms
intervalToDuration({
start: 0,
end: (opt.value as number) / 1e6,
}),
),
);
}
export function findConfigOption(
config: DeploymentConfig,
flag: string,
config: DeploymentConfig,
flag: string,
): SerpentOption {
const opt = config.options.find((option) => option.flag === flag);
if (opt === undefined) {
// must be undefined as `false` is expected
throw new Error(`Option with env ${flag} has undefined value.`);
}
return opt;
const opt = config.options.find((option) => option.flag === flag);
if (opt === undefined) {
// must be undefined as `false` is expected
throw new Error(`Option with env ${flag} has undefined value.`);
}
return opt;
}
+15 -15
View File
@@ -4,8 +4,8 @@ export const coderMain = path.join(__dirname, "../../enterprise/cmd/coder");
// Default port from the server
export const coderPort = process.env.CODER_E2E_PORT
? Number(process.env.CODER_E2E_PORT)
: 3111;
? Number(process.env.CODER_E2E_PORT)
: 3111;
export const prometheusPort = 2114;
export const workspaceProxyPort = 3112;
@@ -19,23 +19,23 @@ export const password = "SomeSecurePassword!";
export const email = "admin@coder.com";
export const gitAuth = {
deviceProvider: "device",
webProvider: "web",
// These ports need to be hardcoded so that they can be
// used in `playwright.config.ts` to set the environment
// variables for the server.
devicePort: 50515,
webPort: 50516,
deviceProvider: "device",
webProvider: "web",
// These ports need to be hardcoded so that they can be
// used in `playwright.config.ts` to set the environment
// variables for the server.
devicePort: 50515,
webPort: 50516,
authPath: "/auth",
tokenPath: "/token",
codePath: "/code",
validatePath: "/validate",
installationsPath: "/installations",
authPath: "/auth",
tokenPath: "/token",
codePath: "/code",
validatePath: "/validate",
installationsPath: "/installations",
};
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 ?? "";
+30 -30
View File
@@ -3,35 +3,35 @@ import { type Page, expect } from "@playwright/test";
type PollingOptions = { timeout?: number; intervals?: number[] };
export const expectUrl = expect.extend({
/**
* toHavePathName is an alternative to `toHaveURL` that won't fail if the URL contains query parameters.
*/
async toHavePathName(page: Page, expected: string, options?: PollingOptions) {
let actual: string = new URL(page.url()).pathname;
let pass: boolean;
try {
await expect
.poll(() => {
actual = new URL(page.url()).pathname;
return actual;
}, options)
.toBe(expected);
pass = true;
} catch {
pass = false;
}
/**
* toHavePathName is an alternative to `toHaveURL` that won't fail if the URL contains query parameters.
*/
async toHavePathName(page: Page, expected: string, options?: PollingOptions) {
let actual: string = new URL(page.url()).pathname;
let pass: boolean;
try {
await expect
.poll(() => {
actual = new URL(page.url()).pathname;
return actual;
}, options)
.toBe(expected);
pass = true;
} catch {
pass = false;
}
return {
name: "toHavePathName",
pass,
actual,
expected,
message: () =>
`The page does not have the expected URL pathname.\nExpected: ${
this.isNot ? "not" : ""
}${this.utils.printExpected(
expected,
)}\nActual: ${this.utils.printReceived(actual)}`,
};
},
return {
name: "toHavePathName",
pass,
actual,
expected,
message: () =>
`The page does not have the expected URL pathname.\nExpected: ${
this.isNot ? "not" : ""
}${this.utils.printExpected(
expected,
)}\nActual: ${this.utils.printReceived(actual)}`,
};
},
});
+30 -30
View File
@@ -7,41 +7,41 @@ import { expectUrl } from "./expectUrl";
import { storageState } from "./playwright.config";
test("setup deployment", async ({ page }) => {
await page.goto("/", { waitUntil: "domcontentloaded" });
await setupApiCalls(page);
const exists = await API.hasFirstUser();
// 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 (exists) {
return;
}
await page.goto("/", { waitUntil: "domcontentloaded" });
await setupApiCalls(page);
const exists = await API.hasFirstUser();
// 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 (exists) {
return;
}
// Setup first user
await page.getByLabel(Language.usernameLabel).fill(constants.username);
await page.getByLabel(Language.emailLabel).fill(constants.email);
await page.getByLabel(Language.passwordLabel).fill(constants.password);
await page.getByTestId("create").click();
// Setup first user
await page.getByLabel(Language.usernameLabel).fill(constants.username);
await page.getByLabel(Language.emailLabel).fill(constants.email);
await page.getByLabel(Language.passwordLabel).fill(constants.password);
await page.getByTestId("create").click();
await expectUrl(page).toHavePathName("/workspaces");
await page.context().storageState({ path: storageState });
await expectUrl(page).toHavePathName("/workspaces");
await page.context().storageState({ path: storageState });
await page.getByTestId("button-select-template").isVisible();
await page.getByTestId("button-select-template").isVisible();
// Setup license
if (constants.requireEnterpriseTests || constants.enterpriseLicense) {
// Make sure that we have something that looks like a real license
expect(constants.enterpriseLicense).toBeTruthy();
expect(constants.enterpriseLicense.length).toBeGreaterThan(92); // the signature alone should be this long
expect(constants.enterpriseLicense.split(".").length).toBe(3); // otherwise it's invalid
// Setup license
if (constants.requireEnterpriseTests || constants.enterpriseLicense) {
// Make sure that we have something that looks like a real license
expect(constants.enterpriseLicense).toBeTruthy();
expect(constants.enterpriseLicense.length).toBeGreaterThan(92); // the signature alone should be this long
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.getByRole("textbox").fill(constants.enterpriseLicense);
await page.getByText("Upload License").click();
await page.getByText("Add a license").click();
await page.getByRole("textbox").fill(constants.enterpriseLicense);
await page.getByText("Upload License").click();
await expect(
page.getByText("You have successfully added a license"),
).toBeVisible();
}
await expect(
page.getByText("You have successfully added a license"),
).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";
export const beforeCoderTest = async (page: Page) => {
// eslint-disable-next-line no-console -- Show everything that was printed with console.log()
page.on("console", (msg) => console.log(`[onConsole] ${msg.text()}`));
// 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("request", (request) => {
if (!isApiCall(request.url())) {
return;
}
page.on("request", (request) => {
if (!isApiCall(request.url())) {
return;
}
// eslint-disable-next-line no-console -- Log HTTP requests for debugging purposes
console.log(
`[onRequest] method=${request.method()} url=${request.url()} postData=${
request.postData() ? request.postData() : ""
}`,
);
});
page.on("response", async (response) => {
if (!isApiCall(response.url())) {
return;
}
// eslint-disable-next-line no-console -- Log HTTP requests for debugging purposes
console.log(
`[onRequest] method=${request.method()} url=${request.url()} postData=${
request.postData() ? request.postData() : ""
}`,
);
});
page.on("response", async (response) => {
if (!isApiCall(response.url())) {
return;
}
const shouldLogResponse =
!response.url().endsWith("/api/v2/deployment/config") &&
!response.url().endsWith("/api/v2/debug/health?force=false");
const shouldLogResponse =
!response.url().endsWith("/api/v2/deployment/config") &&
!response.url().endsWith("/api/v2/debug/health?force=false");
let responseText = "";
try {
if (shouldLogResponse) {
const buffer = await response.body();
responseText = buffer.toString("utf-8");
responseText = responseText.replace(/\n$/g, "");
} else {
responseText = "skipped...";
}
} catch (error) {
responseText = "not_available";
}
let responseText = "";
try {
if (shouldLogResponse) {
const buffer = await response.body();
responseText = buffer.toString("utf-8");
responseText = responseText.replace(/\n$/g, "");
} else {
responseText = "skipped...";
}
} catch (error) {
responseText = "not_available";
}
// eslint-disable-next-line no-console -- Log HTTP requests for debugging purposes
console.log(
`[onResponse] url=${response.url()} status=${response.status()} body=${responseText}`,
);
});
// eslint-disable-next-line no-console -- Log HTTP requests for debugging purposes
console.log(
`[onResponse] url=${response.url()} status=${response.status()} body=${responseText}`,
);
});
};
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.
const cookies = await context.cookies();
const sessionCookie = cookies.find((c) => c.name === "coder_session_token");
const options = {
method: "DELETE",
hostname: "127.0.0.1",
port: coderPort,
path: `/api/v2/external-auth/${gitAuth.webProvider}?coder_session_token=${sessionCookie?.value}`,
};
// 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 sessionCookie = cookies.find((c) => c.name === "coder_session_token");
const options = {
method: "DELETE",
hostname: "127.0.0.1",
port: coderPort,
path: `/api/v2/external-auth/${gitAuth.webProvider}?coder_session_token=${sessionCookie?.value}`,
};
const req = http.request(options, (res) => {
let data = "";
res.on("data", (chunk) => {
data += chunk;
});
const req = http.request(options, (res) => {
let data = "";
res.on("data", (chunk) => {
data += chunk;
});
res.on("end", () => {
// Both 200 (key deleted successfully) and 500 (key was not found) are valid responses.
if (res.statusCode !== 200 && res.statusCode !== 500) {
console.error("failed to delete external auth link", data);
throw new Error(
`failed to delete external auth link: HTTP response ${res.statusCode}`,
);
}
});
});
res.on("end", () => {
// Both 200 (key deleted successfully) and 500 (key was not found) are valid responses.
if (res.statusCode !== 200 && res.statusCode !== 500) {
console.error("failed to delete external auth link", data);
throw new Error(
`failed to delete external auth link: HTTP response ${res.statusCode}`,
);
}
});
});
req.on("error", (err) => {
throw err.message;
});
req.on("error", (err) => {
throw err.message;
});
req.end();
req.end();
};
const isApiCall = (urlString: string): boolean => {
const url = new URL(urlString);
const apiPath = "/api/v2";
const url = new URL(urlString);
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
export const emptyParameter: RichParameter = {
name: "",
description: "",
type: "",
mutable: false,
defaultValue: "",
icon: "",
options: [],
validationRegex: "",
validationError: "",
validationMin: undefined,
validationMax: undefined,
validationMonotonic: "",
required: false,
displayName: "",
order: 0,
ephemeral: false,
name: "",
description: "",
type: "",
mutable: false,
defaultValue: "",
icon: "",
options: [],
validationRegex: "",
validationError: "",
validationMin: undefined,
validationMax: undefined,
validationMonotonic: "",
required: false,
displayName: "",
order: 0,
ephemeral: false,
};
// firstParameter is mutable string with a default value (parameter value not required).
export const firstParameter: RichParameter = {
...emptyParameter,
...emptyParameter,
name: "first_parameter",
displayName: "First parameter",
type: "number",
description: "This is first parameter.",
icon: "/emojis/1f310.png",
defaultValue: "123",
mutable: true,
order: 1,
name: "first_parameter",
displayName: "First parameter",
type: "number",
description: "This is first parameter.",
icon: "/emojis/1f310.png",
defaultValue: "123",
mutable: true,
order: 1,
};
// secondParameter is immutable string with a default value (parameter value not required).
export const secondParameter: RichParameter = {
...emptyParameter,
...emptyParameter,
name: "second_parameter",
displayName: "Second parameter",
type: "string",
description: "This is second parameter.",
defaultValue: "abc",
order: 2,
name: "second_parameter",
displayName: "Second parameter",
type: "string",
description: "This is second parameter.",
defaultValue: "abc",
order: 2,
};
// thirdParameter is mutable string with an empty default value (parameter value not required).
export const thirdParameter: RichParameter = {
...emptyParameter,
...emptyParameter,
name: "third_parameter",
type: "string",
description: "This is third parameter.",
defaultValue: "",
mutable: true,
order: 3,
name: "third_parameter",
type: "string",
description: "This is third parameter.",
defaultValue: "",
mutable: true,
order: 3,
};
// fourthParameter is immutable boolean with a default "true" value (parameter value not required).
export const fourthParameter: RichParameter = {
...emptyParameter,
...emptyParameter,
name: "fourth_parameter",
type: "bool",
description: "This is fourth parameter.",
defaultValue: "true",
order: 3,
name: "fourth_parameter",
type: "bool",
description: "This is fourth parameter.",
defaultValue: "true",
order: 3,
};
// fifthParameter is immutable "string with options", with a default option selected (parameter value not required).
export const fifthParameter: RichParameter = {
...emptyParameter,
...emptyParameter,
name: "fifth_parameter",
displayName: "Fifth parameter",
type: "string",
options: [
{
name: "ABC",
description: "This is ABC",
value: "abc",
icon: "",
},
{
name: "DEF",
description: "This is DEF",
value: "def",
icon: "",
},
{
name: "GHI",
description: "This is GHI",
value: "ghi",
icon: "",
},
],
description: "This is fifth parameter.",
defaultValue: "def",
order: 3,
name: "fifth_parameter",
displayName: "Fifth parameter",
type: "string",
options: [
{
name: "ABC",
description: "This is ABC",
value: "abc",
icon: "",
},
{
name: "DEF",
description: "This is DEF",
value: "def",
icon: "",
},
{
name: "GHI",
description: "This is GHI",
value: "ghi",
icon: "",
},
],
description: "This is fifth parameter.",
defaultValue: "def",
order: 3,
};
// sixthParameter is mutable string without a default value (parameter value is required).
export const sixthParameter: RichParameter = {
...emptyParameter,
...emptyParameter,
name: "sixth_parameter",
displayName: "Sixth parameter",
type: "number",
description: "This is sixth parameter.",
icon: "/emojis/1f310.png",
required: true,
mutable: true,
order: 1,
name: "sixth_parameter",
displayName: "Sixth parameter",
type: "number",
description: "This is sixth parameter.",
icon: "/emojis/1f310.png",
required: true,
mutable: true,
order: 1,
};
// seventhParameter is immutable string without a default value (parameter value is required).
export const seventhParameter: RichParameter = {
...emptyParameter,
...emptyParameter,
name: "seventh_parameter",
displayName: "Seventh parameter",
type: "string",
description: "This is seventh parameter.",
required: true,
order: 1,
name: "seventh_parameter",
displayName: "Seventh parameter",
type: "string",
description: "This is seventh parameter.",
required: true,
order: 1,
};
// randParamName returns a new parameter with a random name.
// It helps to avoid cross-test interference when user-auto-fill triggers on
// the same parameter name.
export const randParamName = (p: RichParameter): RichParameter => {
const name = `${p.name}_${Math.random().toString(36).substring(7)}`;
return { ...p, name: name };
const name = `${p.name}_${Math.random().toString(36).substring(7)}`;
return { ...p, name: name };
};
// Build options
export const firstBuildOption: RichParameter = {
...emptyParameter,
...emptyParameter,
name: "first_build_option",
displayName: "First build option",
type: "string",
description: "This is first build option.",
icon: "/emojis/1f310.png",
defaultValue: "ABCDEF",
mutable: true,
ephemeral: true,
name: "first_build_option",
displayName: "First build option",
type: "string",
description: "This is first build option.",
icon: "/emojis/1f310.png",
defaultValue: "ABCDEF",
mutable: true,
ephemeral: true,
};
export const secondBuildOption: RichParameter = {
...emptyParameter,
...emptyParameter,
name: "second_build_option",
displayName: "Second build option",
type: "bool",
description: "This is second build option.",
defaultValue: "false",
mutable: true,
ephemeral: true,
name: "second_build_option",
displayName: "Second build option",
type: "bool",
description: "This is second build option.",
defaultValue: "false",
mutable: true,
ephemeral: true,
};
+127 -127
View File
@@ -2,13 +2,13 @@ import { execSync } from "node:child_process";
import * as path from "node:path";
import { defineConfig } from "@playwright/test";
import {
coderMain,
coderPort,
coderdPProfPort,
e2eFakeExperiment1,
e2eFakeExperiment2,
gitAuth,
requireTerraformTests,
coderMain,
coderPort,
coderdPProfPort,
e2eFakeExperiment1,
e2eFakeExperiment2,
gitAuth,
requireTerraformTests,
} from "./constants";
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 hasDocker = false;
try {
execSync("terraform --version");
hasTerraform = true;
execSync("terraform --version");
hasTerraform = true;
} catch {
/* empty */
/* empty */
}
try {
execSync("docker --version");
hasDocker = true;
execSync("docker --version");
hasDocker = true;
} catch {
/* empty */
/* empty */
}
if (!hasTerraform || !hasDocker) {
const msg = `Terraform provisioners require docker & terraform binaries to function. \n${
hasTerraform
? ""
: "\tThe `terraform` executable is not present in the runtime environment.\n"
}${
hasDocker
? ""
: "\tThe `docker` executable is not present in the runtime environment.\n"
}`;
throw new Error(msg);
const msg = `Terraform provisioners require docker & terraform binaries to function. \n${
hasTerraform
? ""
: "\tThe `terraform` executable is not present in the runtime environment.\n"
}${
hasDocker
? ""
: "\tThe `docker` executable is not present in the runtime environment.\n"
}`;
throw new Error(msg);
}
const localURL = (port: number, path: string): string => {
return `http://localhost:${port}${path}`;
return `http://localhost:${port}${path}`;
};
export default defineConfig({
projects: [
{
name: "testsSetup",
testMatch: /global.setup\.ts/,
},
{
name: "tests",
testMatch: /.*\.spec\.ts/,
dependencies: ["testsSetup"],
use: { storageState },
timeout: 50_000,
},
],
reporter: [["./reporter.ts"]],
use: {
baseURL: `http://localhost:${coderPort}`,
video: "retain-on-failure",
...(wsEndpoint
? {
connectOptions: {
wsEndpoint: wsEndpoint,
},
}
: {
launchOptions: {
args: ["--disable-webgl"],
},
}),
},
webServer: {
url: `http://localhost:${coderPort}/api/v2/deployment/config`,
command: [
`go run -tags embed ${coderMain} server`,
"--global-config $(mktemp -d -t e2e-XXXXXXXXXX)",
`--access-url=http://localhost:${coderPort}`,
`--http-address=0.0.0.0:${coderPort}`,
"--in-memory",
"--telemetry=false",
"--dangerous-disable-rate-limits",
"--provisioner-daemons 10",
// TODO: Enable some terraform provisioners
`--provisioner-types=echo${requireTerraformTests ? ",terraform" : ""}`,
"--provisioner-daemons=10",
"--web-terminal-renderer=dom",
"--pprof-enable",
]
.filter(Boolean)
.join(" "),
env: {
...process.env,
// Otherwise, the runner fails on Mac with: could not determine kind of name for C.uuid_string_t
CGO_ENABLED: "0",
projects: [
{
name: "testsSetup",
testMatch: /global.setup\.ts/,
},
{
name: "tests",
testMatch: /.*\.spec\.ts/,
dependencies: ["testsSetup"],
use: { storageState },
timeout: 50_000,
},
],
reporter: [["./reporter.ts"]],
use: {
baseURL: `http://localhost:${coderPort}`,
video: "retain-on-failure",
...(wsEndpoint
? {
connectOptions: {
wsEndpoint: wsEndpoint,
},
}
: {
launchOptions: {
args: ["--disable-webgl"],
},
}),
},
webServer: {
url: `http://localhost:${coderPort}/api/v2/deployment/config`,
command: [
`go run -tags embed ${coderMain} server`,
"--global-config $(mktemp -d -t e2e-XXXXXXXXXX)",
`--access-url=http://localhost:${coderPort}`,
`--http-address=0.0.0.0:${coderPort}`,
"--in-memory",
"--telemetry=false",
"--dangerous-disable-rate-limits",
"--provisioner-daemons 10",
// TODO: Enable some terraform provisioners
`--provisioner-types=echo${requireTerraformTests ? ",terraform" : ""}`,
"--provisioner-daemons=10",
"--web-terminal-renderer=dom",
"--pprof-enable",
]
.filter(Boolean)
.join(" "),
env: {
...process.env,
// Otherwise, the runner fails on Mac with: could not determine kind of name for C.uuid_string_t
CGO_ENABLED: "0",
// This is the test provider for git auth with devices!
CODER_GITAUTH_0_ID: gitAuth.deviceProvider,
CODER_GITAUTH_0_TYPE: "github",
CODER_GITAUTH_0_CLIENT_ID: "client",
CODER_GITAUTH_0_CLIENT_SECRET: "secret",
CODER_GITAUTH_0_DEVICE_FLOW: "true",
CODER_GITAUTH_0_APP_INSTALL_URL:
"https://github.com/apps/coder/installations/new",
CODER_GITAUTH_0_APP_INSTALLATIONS_URL: localURL(
gitAuth.devicePort,
gitAuth.installationsPath,
),
CODER_GITAUTH_0_TOKEN_URL: localURL(
gitAuth.devicePort,
gitAuth.tokenPath,
),
CODER_GITAUTH_0_DEVICE_CODE_URL: localURL(
gitAuth.devicePort,
gitAuth.codePath,
),
CODER_GITAUTH_0_VALIDATE_URL: localURL(
gitAuth.devicePort,
gitAuth.validatePath,
),
// This is the test provider for git auth with devices!
CODER_GITAUTH_0_ID: gitAuth.deviceProvider,
CODER_GITAUTH_0_TYPE: "github",
CODER_GITAUTH_0_CLIENT_ID: "client",
CODER_GITAUTH_0_CLIENT_SECRET: "secret",
CODER_GITAUTH_0_DEVICE_FLOW: "true",
CODER_GITAUTH_0_APP_INSTALL_URL:
"https://github.com/apps/coder/installations/new",
CODER_GITAUTH_0_APP_INSTALLATIONS_URL: localURL(
gitAuth.devicePort,
gitAuth.installationsPath,
),
CODER_GITAUTH_0_TOKEN_URL: localURL(
gitAuth.devicePort,
gitAuth.tokenPath,
),
CODER_GITAUTH_0_DEVICE_CODE_URL: localURL(
gitAuth.devicePort,
gitAuth.codePath,
),
CODER_GITAUTH_0_VALIDATE_URL: localURL(
gitAuth.devicePort,
gitAuth.validatePath,
),
CODER_GITAUTH_1_ID: gitAuth.webProvider,
CODER_GITAUTH_1_TYPE: "github",
CODER_GITAUTH_1_CLIENT_ID: "client",
CODER_GITAUTH_1_CLIENT_SECRET: "secret",
CODER_GITAUTH_1_AUTH_URL: localURL(gitAuth.webPort, gitAuth.authPath),
CODER_GITAUTH_1_TOKEN_URL: localURL(gitAuth.webPort, gitAuth.tokenPath),
CODER_GITAUTH_1_DEVICE_CODE_URL: localURL(
gitAuth.webPort,
gitAuth.codePath,
),
CODER_GITAUTH_1_VALIDATE_URL: localURL(
gitAuth.webPort,
gitAuth.validatePath,
),
CODER_PPROF_ADDRESS: `127.0.0.1:${coderdPProfPort}`,
CODER_EXPERIMENTS: `multi-organization,${e2eFakeExperiment1},${e2eFakeExperiment2}`,
CODER_GITAUTH_1_ID: gitAuth.webProvider,
CODER_GITAUTH_1_TYPE: "github",
CODER_GITAUTH_1_CLIENT_ID: "client",
CODER_GITAUTH_1_CLIENT_SECRET: "secret",
CODER_GITAUTH_1_AUTH_URL: localURL(gitAuth.webPort, gitAuth.authPath),
CODER_GITAUTH_1_TOKEN_URL: localURL(gitAuth.webPort, gitAuth.tokenPath),
CODER_GITAUTH_1_DEVICE_CODE_URL: localURL(
gitAuth.webPort,
gitAuth.codePath,
),
CODER_GITAUTH_1_VALIDATE_URL: localURL(
gitAuth.webPort,
gitAuth.validatePath,
),
CODER_PPROF_ADDRESS: `127.0.0.1:${coderdPProfPort}`,
CODER_EXPERIMENTS: `multi-organization,${e2eFakeExperiment1},${e2eFakeExperiment2}`,
// Tests for Deployment / User Authentication / OIDC
CODER_OIDC_ISSUER_URL: "https://accounts.google.com",
CODER_OIDC_EMAIL_DOMAIN: "coder.com",
CODER_OIDC_CLIENT_ID: "1234567890",
CODER_OIDC_CLIENT_SECRET: "1234567890Secret",
CODER_OIDC_ALLOW_SIGNUPS: "false",
CODER_OIDC_SIGN_IN_TEXT: "Hello",
CODER_OIDC_ICON_URL: "/icon/google.svg",
},
reuseExistingServer: false,
},
// Tests for Deployment / User Authentication / OIDC
CODER_OIDC_ISSUER_URL: "https://accounts.google.com",
CODER_OIDC_EMAIL_DOMAIN: "coder.com",
CODER_OIDC_CLIENT_ID: "1234567890",
CODER_OIDC_CLIENT_SECRET: "1234567890Secret",
CODER_OIDC_ALLOW_SIGNUPS: "false",
CODER_OIDC_SIGN_IN_TEXT: "Hello",
CODER_OIDC_ICON_URL: "/icon/google.svg",
},
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";
export const startWorkspaceProxy = async (
token: string,
token: string,
): Promise<ChildProcess> => {
const cp = spawn("go", ["run", coderMain, "wsproxy", "server"], {
env: {
...process.env,
CODER_PRIMARY_ACCESS_URL: `http://127.0.0.1:${coderPort}`,
CODER_PROXY_SESSION_TOKEN: token,
CODER_HTTP_ADDRESS: `localhost:${workspaceProxyPort}`,
},
});
cp.stdout.on("data", (data: Buffer) => {
// eslint-disable-next-line no-console -- Log wsproxy activity
console.log(
`[wsproxy] [stdout] [onData] ${data.toString().replace(/\n$/g, "")}`,
);
});
cp.stderr.on("data", (data: Buffer) => {
// eslint-disable-next-line no-console -- Log wsproxy activity
console.log(
`[wsproxy] [stderr] [onData] ${data.toString().replace(/\n$/g, "")}`,
);
});
return cp;
const cp = spawn("go", ["run", coderMain, "wsproxy", "server"], {
env: {
...process.env,
CODER_PRIMARY_ACCESS_URL: `http://127.0.0.1:${coderPort}`,
CODER_PROXY_SESSION_TOKEN: token,
CODER_HTTP_ADDRESS: `localhost:${workspaceProxyPort}`,
},
});
cp.stdout.on("data", (data: Buffer) => {
// eslint-disable-next-line no-console -- Log wsproxy activity
console.log(
`[wsproxy] [stdout] [onData] ${data.toString().replace(/\n$/g, "")}`,
);
});
cp.stderr.on("data", (data: Buffer) => {
// eslint-disable-next-line no-console -- Log wsproxy activity
console.log(
`[wsproxy] [stderr] [onData] ${data.toString().replace(/\n$/g, "")}`,
);
});
return cp;
};
export const stopWorkspaceProxy = async (cp: ChildProcess, goRun = true) => {
exec(goRun ? `pkill -P ${cp.pid}` : `kill ${cp.pid}`, (error) => {
if (error) {
throw new Error(`exec error: ${JSON.stringify(error)}`);
}
});
await waitUntilUrlIsNotResponding(`http://127.0.0.1:${workspaceProxyPort}`);
exec(goRun ? `pkill -P ${cp.pid}` : `kill ${cp.pid}`, (error) => {
if (error) {
throw new Error(`exec error: ${JSON.stringify(error)}`);
}
});
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";
/* eslint-disable no-console -- Logging is sort of the whole point here */
import type {
FullConfig,
FullResult,
Reporter,
Suite,
TestCase,
TestError,
TestResult,
FullConfig,
FullResult,
Reporter,
Suite,
TestCase,
TestError,
TestResult,
} from "@playwright/test/reporter";
import { API } from "api/api";
import { coderdPProfPort, enterpriseLicense } from "./constants";
class CoderReporter implements Reporter {
config: FullConfig | null = null;
testOutput = new Map<string, Array<[Writable, string]>>();
passedCount = 0;
skippedCount = 0;
failedTests: TestCase[] = [];
timedOutTests: TestCase[] = [];
config: FullConfig | null = null;
testOutput = new Map<string, Array<[Writable, string]>>();
passedCount = 0;
skippedCount = 0;
failedTests: TestCase[] = [];
timedOutTests: TestCase[] = [];
onBegin(config: FullConfig, suite: Suite) {
this.config = config;
console.log(`==> Running ${suite.allTests().length} tests`);
}
onBegin(config: FullConfig, suite: Suite) {
this.config = config;
console.log(`==> Running ${suite.allTests().length} tests`);
}
onTestBegin(test: TestCase) {
this.testOutput.set(test.id, []);
console.log(`==> Starting test ${test.title}`);
}
onTestBegin(test: TestCase) {
this.testOutput.set(test.id, []);
console.log(`==> Starting test ${test.title}`);
}
onStdOut(chunk: string, test?: TestCase, _?: TestResult): void {
// If there's no associated test, just print it now
if (!test) {
for (const line of logLines(chunk)) {
console.log(`[stdout] ${line}`);
}
return;
}
// Will be printed if the test fails
this.testOutput.get(test.id)!.push([process.stdout, chunk]);
}
onStdOut(chunk: string, test?: TestCase, _?: TestResult): void {
// If there's no associated test, just print it now
if (!test) {
for (const line of logLines(chunk)) {
console.log(`[stdout] ${line}`);
}
return;
}
// Will be printed if the test fails
this.testOutput.get(test.id)!.push([process.stdout, chunk]);
}
onStdErr(chunk: string, test?: TestCase, _?: TestResult): void {
// If there's no associated test, just print it now
if (!test) {
for (const line of logLines(chunk)) {
console.error(`[stderr] ${line}`);
}
return;
}
// Will be printed if the test fails
this.testOutput.get(test.id)!.push([process.stderr, chunk]);
}
onStdErr(chunk: string, test?: TestCase, _?: TestResult): void {
// If there's no associated test, just print it now
if (!test) {
for (const line of logLines(chunk)) {
console.error(`[stderr] ${line}`);
}
return;
}
// Will be printed if the test fails
this.testOutput.get(test.id)!.push([process.stderr, chunk]);
}
async onTestEnd(test: TestCase, result: TestResult) {
try {
if (test.expectedStatus === "skipped") {
console.log(`==> Skipping test ${test.title}`);
this.skippedCount++;
return;
}
async onTestEnd(test: TestCase, result: TestResult) {
try {
if (test.expectedStatus === "skipped") {
console.log(`==> Skipping test ${test.title}`);
this.skippedCount++;
return;
}
console.log(`==> Finished test ${test.title}: ${result.status}`);
console.log(`==> Finished test ${test.title}: ${result.status}`);
if (result.status === "passed") {
this.passedCount++;
return;
}
if (result.status === "passed") {
this.passedCount++;
return;
}
if (result.status === "failed") {
this.failedTests.push(test);
}
if (result.status === "failed") {
this.failedTests.push(test);
}
if (result.status === "timedOut") {
this.timedOutTests.push(test);
}
if (result.status === "timedOut") {
this.timedOutTests.push(test);
}
const fsTestTitle = test.title.replaceAll(" ", "-");
const outputFile = `test-results/debug-pprof-goroutine-${fsTestTitle}.txt`;
await exportDebugPprof(outputFile);
const fsTestTitle = test.title.replaceAll(" ", "-");
const outputFile = `test-results/debug-pprof-goroutine-${fsTestTitle}.txt`;
await exportDebugPprof(outputFile);
console.log(`Data from pprof has been saved to ${outputFile}`);
console.log("==> Output");
const output = this.testOutput.get(test.id)!;
for (const [target, chunk] of output) {
target.write(`${chunk.replace(/\n$/g, "")}\n`);
}
console.log(`Data from pprof has been saved to ${outputFile}`);
console.log("==> Output");
const output = this.testOutput.get(test.id)!;
for (const [target, chunk] of output) {
target.write(`${chunk.replace(/\n$/g, "")}\n`);
}
if (result.errors.length > 0) {
console.log("==> Errors");
for (const error of result.errors) {
reportError(error);
}
}
if (result.errors.length > 0) {
console.log("==> Errors");
for (const error of result.errors) {
reportError(error);
}
}
if (result.attachments.length > 0) {
console.log("==> Attachments");
for (const attachment of result.attachments) {
console.log(attachment);
}
}
} finally {
this.testOutput.delete(test.id);
}
}
if (result.attachments.length > 0) {
console.log("==> Attachments");
for (const attachment of result.attachments) {
console.log(attachment);
}
}
} finally {
this.testOutput.delete(test.id);
}
}
onEnd(result: FullResult) {
console.log(`==> Tests ${result.status}`);
if (!enterpriseLicense) {
console.log(
"==> Enterprise tests were skipped, because no license was provided",
);
}
console.log(`${this.passedCount} passed`);
if (this.skippedCount > 0) {
console.log(`${this.skippedCount} skipped`);
}
if (this.failedTests.length > 0) {
console.log(`${this.failedTests.length} failed`);
for (const test of this.failedTests) {
console.log(` ${test.location.file} ${test.title}`);
}
}
if (this.timedOutTests.length > 0) {
console.log(`${this.timedOutTests.length} timed out`);
for (const test of this.timedOutTests) {
console.log(` ${test.location.file} ${test.title}`);
}
}
}
onEnd(result: FullResult) {
console.log(`==> Tests ${result.status}`);
if (!enterpriseLicense) {
console.log(
"==> Enterprise tests were skipped, because no license was provided",
);
}
console.log(`${this.passedCount} passed`);
if (this.skippedCount > 0) {
console.log(`${this.skippedCount} skipped`);
}
if (this.failedTests.length > 0) {
console.log(`${this.failedTests.length} failed`);
for (const test of this.failedTests) {
console.log(` ${test.location.file} ${test.title}`);
}
}
if (this.timedOutTests.length > 0) {
console.log(`${this.timedOutTests.length} timed out`);
for (const test of this.timedOutTests) {
console.log(` ${test.location.file} ${test.title}`);
}
}
}
}
const logLines = (chunk: string | Buffer): string[] => {
if (chunk instanceof Buffer) {
// 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.
return [chunk.toString()];
}
return chunk.trimEnd().split("\n");
if (chunk instanceof Buffer) {
// 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.
return [chunk.toString()];
}
return chunk.trimEnd().split("\n");
};
const exportDebugPprof = async (outputFile: string) => {
const axiosInstance = API.getAxiosInstance();
const response = await axiosInstance.get(
`http://127.0.0.1:${coderdPProfPort}/debug/pprof/goroutine?debug=1`,
);
const axiosInstance = API.getAxiosInstance();
const response = await axiosInstance.get(
`http://127.0.0.1:${coderdPProfPort}/debug/pprof/goroutine?debug=1`,
);
if (response.status !== 200) {
throw new Error(`Error: Received status code ${response.status}`);
}
if (response.status !== 200) {
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) => {
if (error.location) {
console.log(`${error.location.file}:${error.location.line}:`);
}
if (error.snippet) {
console.log(error.snippet);
}
if (error.location) {
console.log(`${error.location.file}:${error.location.line}:`);
}
if (error.snippet) {
console.log(error.snippet);
}
if (error.message) {
console.log(error.message);
} else {
console.log(error);
}
if (error.message) {
console.log(error.message);
} else {
console.log(error);
}
};
// 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 { test } from "@playwright/test";
import {
createTemplate,
createWorkspace,
startAgent,
stopAgent,
stopWorkspace,
createTemplate,
createWorkspace,
startAgent,
stopAgent,
stopWorkspace,
} from "../helpers";
import { beforeCoderTest } from "../hooks";
test.beforeEach(({ page }) => beforeCoderTest(page));
test("app", async ({ context, page }) => {
const appContent = "Hello World";
const token = randomUUID();
const srv = http
.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end(appContent);
})
.listen(0);
const addr = srv.address();
if (typeof addr !== "object" || !addr) {
throw new Error("Expected addr to be an object");
}
const appName = "test-app";
const template = await createTemplate(page, {
apply: [
{
apply: {
resources: [
{
agents: [
{
token,
apps: [
{
url: `http://localhost:${addr.port}`,
displayName: appName,
order: 0,
},
],
order: 0,
},
],
},
],
},
},
],
});
const workspaceName = await createWorkspace(page, template);
const agent = await startAgent(page, token);
const appContent = "Hello World";
const token = randomUUID();
const srv = http
.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end(appContent);
})
.listen(0);
const addr = srv.address();
if (typeof addr !== "object" || !addr) {
throw new Error("Expected addr to be an object");
}
const appName = "test-app";
const template = await createTemplate(page, {
apply: [
{
apply: {
resources: [
{
agents: [
{
token,
apps: [
{
url: `http://localhost:${addr.port}`,
displayName: appName,
order: 0,
},
],
order: 0,
},
],
},
],
},
},
],
});
const workspaceName = await createWorkspace(page, template);
const agent = await startAgent(page, token);
// Wait for the web terminal to open in a new tab
const pagePromise = context.waitForEvent("page");
await page.getByText(appName).click();
const app = await pagePromise;
await app.waitForLoadState("domcontentloaded");
await app.getByText(appContent).isVisible();
// Wait for the web terminal to open in a new tab
const pagePromise = context.waitForEvent("page");
await page.getByText(appName).click();
const app = await pagePromise;
await app.waitForLoadState("domcontentloaded");
await app.getByText(appContent).isVisible();
await stopWorkspace(page, workspaceName);
await stopAgent(agent);
await stopWorkspace(page, workspaceName);
await stopAgent(agent);
});
+52 -52
View File
@@ -1,69 +1,69 @@
import { expect, test } from "@playwright/test";
import {
createTemplate,
createWorkspace,
requiresEnterpriseLicense,
createTemplate,
createWorkspace,
requiresEnterpriseLicense,
} from "../helpers";
import { beforeCoderTest } from "../hooks";
test.beforeEach(({ page }) => beforeCoderTest(page));
test("inspecting and filtering audit logs", async ({ page }) => {
requiresEnterpriseLicense();
requiresEnterpriseLicense();
const userName = "admin";
// Do some stuff that should show up in the audit logs
const templateName = await createTemplate(page);
const workspaceName = await createWorkspace(page, templateName);
const userName = "admin";
// Do some stuff that should show up in the audit logs
const templateName = await createTemplate(page);
const workspaceName = await createWorkspace(page, templateName);
// Go to the audit history
await page.goto("/audit");
// Go to the audit history
await page.goto("/audit");
// Make sure those things we did all actually show up
await expect(page.getByText(`${userName} logged in`)).toBeVisible();
await expect(
page.getByText(`${userName} created template ${templateName}`),
).toBeVisible();
await expect(
page.getByText(`${userName} created workspace ${workspaceName}`),
).toBeVisible();
await expect(
page.getByText(`${userName} started workspace ${workspaceName}`),
).toBeVisible();
// Make sure those things we did all actually show up
await expect(page.getByText(`${userName} logged in`)).toBeVisible();
await expect(
page.getByText(`${userName} created template ${templateName}`),
).toBeVisible();
await expect(
page.getByText(`${userName} created workspace ${workspaceName}`),
).toBeVisible();
await expect(
page.getByText(`${userName} started workspace ${workspaceName}`),
).toBeVisible();
// Make sure we can inspect the details of the log item
const createdWorkspace = page.locator(".MuiTableRow-root", {
hasText: `${userName} created workspace ${workspaceName}`,
});
await createdWorkspace.getByLabel("open-dropdown").click();
await expect(
createdWorkspace.getByText(`automatic_updates: "never"`),
).toBeVisible();
await expect(
createdWorkspace.getByText(`name: "${workspaceName}"`),
).toBeVisible();
// Make sure we can inspect the details of the log item
const createdWorkspace = page.locator(".MuiTableRow-root", {
hasText: `${userName} created workspace ${workspaceName}`,
});
await createdWorkspace.getByLabel("open-dropdown").click();
await expect(
createdWorkspace.getByText(`automatic_updates: "never"`),
).toBeVisible();
await expect(
createdWorkspace.getByText(`name: "${workspaceName}"`),
).toBeVisible();
const startedWorkspaceMessage = `${userName} started workspace ${workspaceName}`;
const loginMessage = `${userName} logged in`;
const startedWorkspaceMessage = `${userName} started workspace ${workspaceName}`;
const loginMessage = `${userName} logged in`;
// Filter by resource type
await page.getByText("All resource types").click();
await page.getByRole("menu").getByText("Workspace Build").click();
// Our workspace build should be visible
await expect(page.getByText(startedWorkspaceMessage)).toBeVisible();
// Logins should no longer be visible
await expect(page.getByText(loginMessage)).not.toBeVisible();
// Filter by resource type
await page.getByText("All resource types").click();
await page.getByRole("menu").getByText("Workspace Build").click();
// Our workspace build should be visible
await expect(page.getByText(startedWorkspaceMessage)).toBeVisible();
// Logins should no longer be visible
await expect(page.getByText(loginMessage)).not.toBeVisible();
// Clear filters, everything should be visible again
await page.getByLabel("Clear filter").click();
await expect(page.getByText(startedWorkspaceMessage)).toBeVisible();
await expect(page.getByText(loginMessage)).toBeVisible();
// Clear filters, everything should be visible again
await page.getByLabel("Clear filter").click();
await expect(page.getByText(startedWorkspaceMessage)).toBeVisible();
await expect(page.getByText(loginMessage)).toBeVisible();
// Filter by action type
await page.getByText("All actions").click();
await page.getByRole("menu").getByText("Login").click();
// Logins should be visible
await expect(page.getByText(loginMessage)).toBeVisible();
// Our workspace build should no longer be visible
await expect(page.getByText(startedWorkspaceMessage)).not.toBeVisible();
// Filter by action type
await page.getByText("All actions").click();
await page.getByRole("menu").getByText("Login").click();
// Logins should be visible
await expect(page.getByText(loginMessage)).toBeVisible();
// Our workspace build should no longer be visible
await expect(page.getByText(startedWorkspaceMessage)).not.toBeVisible();
});
+53 -53
View File
@@ -3,80 +3,80 @@ import { expectUrl } from "../../expectUrl";
import { randomName, requiresEnterpriseLicense } from "../../helpers";
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
const form = page.locator("form", { hasText: "Application name" });
await form
.getByLabel("Application name", { exact: true })
.fill(applicationName);
await form.getByRole("button", { name: "Submit" }).click();
// Fill out the form
const form = page.locator("form", { hasText: "Application name" });
await form
.getByLabel("Application name", { exact: true })
.fill(applicationName);
await form.getByRole("button", { name: "Submit" }).click();
// Open a new session without cookies to see the login page
const browser = await chromium.launch();
const incognitoContext = await browser.newContext();
await incognitoContext.clearCookies();
const incognitoPage = await incognitoContext.newPage();
await incognitoPage.goto("/", { waitUntil: "domcontentloaded" });
// Open a new session without cookies to see the login page
const browser = await chromium.launch();
const incognitoContext = await browser.newContext();
await incognitoContext.clearCookies();
const incognitoPage = await incognitoContext.newPage();
await incognitoPage.goto("/", { waitUntil: "domcontentloaded" });
// Verify the application name
const name = incognitoPage.locator("h1", { hasText: applicationName });
await expect(name).toBeVisible();
// Verify the application name
const name = incognitoPage.locator("h1", { hasText: applicationName });
await expect(name).toBeVisible();
// Shut down browser
await incognitoPage.close();
await browser.close();
// Shut down browser
await incognitoPage.close();
await browser.close();
});
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
const form = page.locator("form", { hasText: "Logo URL" });
await form.getByLabel("Logo URL", { exact: true }).fill(imageLink);
await form.getByRole("button", { name: "Submit" }).click();
// Fill out the form
const form = page.locator("form", { hasText: "Logo URL" });
await form.getByLabel("Logo URL", { exact: true }).fill(imageLink);
await form.getByRole("button", { name: "Submit" }).click();
// Open a new session without cookies to see the login page
const browser = await chromium.launch();
const incognitoContext = await browser.newContext();
await incognitoContext.clearCookies();
const incognitoPage = await incognitoContext.newPage();
await incognitoPage.goto("/", { waitUntil: "domcontentloaded" });
// Open a new session without cookies to see the login page
const browser = await chromium.launch();
const incognitoContext = await browser.newContext();
await incognitoContext.clearCookies();
const incognitoPage = await incognitoContext.newPage();
await incognitoPage.goto("/", { waitUntil: "domcontentloaded" });
// Verify banner
const logo = incognitoPage.locator("img.application-logo");
await expect(logo).toHaveAttribute("src", imageLink);
// Verify banner
const logo = incognitoPage.locator("img.application-logo");
await expect(logo).toHaveAttribute("src", imageLink);
// Shut down browser
await incognitoPage.close();
await browser.close();
// Shut down browser
await incognitoPage.close();
await browser.close();
});
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
const form = page.locator("form", { hasText: "Service Banner" });
await form.getByLabel("Enabled", { exact: true }).check();
await form.getByLabel("Message", { exact: true }).fill(message);
await form.getByRole("button", { name: "Submit" }).click();
// Fill out the form
const form = page.locator("form", { hasText: "Service Banner" });
await form.getByLabel("Enabled", { exact: true }).check();
await form.getByLabel("Message", { exact: true }).fill(message);
await form.getByRole("button", { name: "Submit" }).click();
// Verify service banner
await page.goto("/workspaces", { waitUntil: "domcontentloaded" });
await expectUrl(page).toHavePathName("/workspaces");
// Verify service banner
await page.goto("/workspaces", { waitUntil: "domcontentloaded" });
await expectUrl(page).toHavePathName("/workspaces");
const bar = page.locator("div.service-banner", { hasText: message });
await expect(bar).toBeVisible();
const bar = page.locator("div.service-banner", { hasText: message });
await expect(bar).toBeVisible();
});
+27 -27
View File
@@ -4,36 +4,36 @@ import { setupApiCalls } from "../../api";
import { e2eFakeExperiment1, e2eFakeExperiment2 } from "../../constants";
test("experiments", async ({ page }) => {
await setupApiCalls(page);
await setupApiCalls(page);
// Load experiments from backend API
const availableExperiments = await API.getAvailableExperiments();
// Load experiments from backend API
const availableExperiments = await API.getAvailableExperiments();
// Verify if the site lists the same experiments
await page.goto("/deployment/general", { waitUntil: "networkidle" });
// Verify if the site lists the same experiments
await page.goto("/deployment/general", { waitUntil: "networkidle" });
const experimentsLocator = page.locator(
"div.options-table tr.option-experiments ul.option-array",
);
await expect(experimentsLocator).toBeVisible();
const experimentsLocator = page.locator(
"div.options-table tr.option-experiments ul.option-array",
);
await expect(experimentsLocator).toBeVisible();
// Firstly, check if all enabled experiments are listed
expect(
experimentsLocator.locator(
`li.option-array-item-${e2eFakeExperiment1}.option-enabled`,
),
).toBeVisible;
expect(
experimentsLocator.locator(
`li.option-array-item-${e2eFakeExperiment2}.option-enabled`,
),
).toBeVisible;
// Firstly, check if all enabled experiments are listed
expect(
experimentsLocator.locator(
`li.option-array-item-${e2eFakeExperiment1}.option-enabled`,
),
).toBeVisible;
expect(
experimentsLocator.locator(
`li.option-array-item-${e2eFakeExperiment2}.option-enabled`,
),
).toBeVisible;
// Secondly, check if available experiments are listed
for (const experiment of availableExperiments.safe) {
const experimentLocator = experimentsLocator.locator(
`li.option-array-item-${experiment}`,
);
await expect(experimentLocator).toBeVisible();
}
// Secondly, check if available experiments are listed
for (const experiment of availableExperiments.safe) {
const experimentLocator = experimentsLocator.locator(
`li.option-array-item-${experiment}`,
);
await expect(experimentLocator).toBeVisible();
}
});
+20 -20
View File
@@ -2,29 +2,29 @@ import { expect, test } from "@playwright/test";
import { requiresEnterpriseLicense } from "../../helpers";
test("license was added successfully", async ({ page }) => {
requiresEnterpriseLicense();
requiresEnterpriseLicense();
await page.goto("/deployment/licenses", { waitUntil: "domcontentloaded" });
const firstLicense = page.locator(".licenses > .license-card", {
hasText: "#1",
});
await expect(firstLicense).toBeVisible();
await page.goto("/deployment/licenses", { waitUntil: "domcontentloaded" });
const firstLicense = page.locator(".licenses > .license-card", {
hasText: "#1",
});
await expect(firstLicense).toBeVisible();
// Trial vs. Enterprise?
const accountType = firstLicense.locator(".account-type");
await expect(accountType).toHaveText("Enterprise");
// Trial vs. Enterprise?
const accountType = firstLicense.locator(".account-type");
await expect(accountType).toHaveText("Enterprise");
// User limit 1/1
const userLimit = firstLicense.locator(".user-limit");
await expect(userLimit).toHaveText("1 / 1");
// User limit 1/1
const userLimit = firstLicense.locator(".user-limit");
await expect(userLimit).toHaveText("1 / 1");
// License should not be expired yet
const licenseExpires = firstLicense.locator(".license-expires");
const licenseExpiresDate = new Date(await licenseExpires.innerText());
const now = new Date();
expect(licenseExpiresDate.getTime()).toBeGreaterThan(now.getTime());
// License should not be expired yet
const licenseExpires = firstLicense.locator(".license-expires");
const licenseExpiresDate = new Date(await licenseExpires.innerText());
const now = new Date();
expect(licenseExpiresDate.getTime()).toBeGreaterThan(now.getTime());
// "Remove" button should be visible
const removeButton = firstLicense.locator(".remove-button");
await expect(removeButton).toBeVisible();
// "Remove" button should be visible
const removeButton = firstLicense.locator(".remove-button");
await expect(removeButton).toBeVisible();
});
+31 -31
View File
@@ -1,40 +1,40 @@
import { test } from "@playwright/test";
import { API } from "api/api";
import {
setupApiCalls,
verifyConfigFlagArray,
verifyConfigFlagBoolean,
verifyConfigFlagDuration,
verifyConfigFlagNumber,
verifyConfigFlagString,
setupApiCalls,
verifyConfigFlagArray,
verifyConfigFlagBoolean,
verifyConfigFlagDuration,
verifyConfigFlagNumber,
verifyConfigFlagString,
} from "../../api";
test("enabled network settings", async ({ page }) => {
await setupApiCalls(page);
const config = await API.getDeploymentConfig();
await setupApiCalls(page);
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 verifyConfigFlagBoolean(page, config, "block-direct-connections");
await verifyConfigFlagBoolean(page, config, "browser-only");
await verifyConfigFlagBoolean(page, config, "derp-force-websockets");
await verifyConfigFlagBoolean(page, config, "derp-server-enable");
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 verifyConfigFlagString(page, config, "derp-server-region-name");
await verifyConfigFlagArray(page, config, "derp-server-stun-addresses");
await verifyConfigFlagBoolean(page, config, "disable-password-auth");
await verifyConfigFlagBoolean(page, config, "disable-session-expiry-refresh");
await verifyConfigFlagDuration(page, config, "max-token-lifetime");
await verifyConfigFlagDuration(page, config, "proxy-health-interval");
await verifyConfigFlagBoolean(page, config, "redirect-to-access-url");
await verifyConfigFlagBoolean(page, config, "secure-auth-cookie");
await verifyConfigFlagDuration(page, config, "session-duration");
await verifyConfigFlagString(page, config, "tls-address");
await verifyConfigFlagBoolean(page, config, "tls-allow-insecure-ciphers");
await verifyConfigFlagString(page, config, "tls-client-auth");
await verifyConfigFlagBoolean(page, config, "tls-enable");
await verifyConfigFlagString(page, config, "tls-min-version");
await verifyConfigFlagString(page, config, "access-url");
await verifyConfigFlagBoolean(page, config, "block-direct-connections");
await verifyConfigFlagBoolean(page, config, "browser-only");
await verifyConfigFlagBoolean(page, config, "derp-force-websockets");
await verifyConfigFlagBoolean(page, config, "derp-server-enable");
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 verifyConfigFlagString(page, config, "derp-server-region-name");
await verifyConfigFlagArray(page, config, "derp-server-stun-addresses");
await verifyConfigFlagBoolean(page, config, "disable-password-auth");
await verifyConfigFlagBoolean(page, config, "disable-session-expiry-refresh");
await verifyConfigFlagDuration(page, config, "max-token-lifetime");
await verifyConfigFlagDuration(page, config, "proxy-health-interval");
await verifyConfigFlagBoolean(page, config, "redirect-to-access-url");
await verifyConfigFlagBoolean(page, config, "secure-auth-cookie");
await verifyConfigFlagDuration(page, config, "session-duration");
await verifyConfigFlagString(page, config, "tls-address");
await verifyConfigFlagBoolean(page, config, "tls-allow-insecure-ciphers");
await verifyConfigFlagString(page, config, "tls-client-auth");
await verifyConfigFlagBoolean(page, config, "tls-enable");
await verifyConfigFlagString(page, config, "tls-min-version");
});
+30 -30
View File
@@ -1,39 +1,39 @@
import { test } from "@playwright/test";
import { API } from "api/api";
import {
setupApiCalls,
verifyConfigFlagArray,
verifyConfigFlagBoolean,
verifyConfigFlagDuration,
verifyConfigFlagEmpty,
verifyConfigFlagString,
setupApiCalls,
verifyConfigFlagArray,
verifyConfigFlagBoolean,
verifyConfigFlagDuration,
verifyConfigFlagEmpty,
verifyConfigFlagString,
} from "../../api";
test("enabled observability settings", async ({ page }) => {
await setupApiCalls(page);
const config = await API.getDeploymentConfig();
await setupApiCalls(page);
const config = await API.getDeploymentConfig();
await page.goto("/deployment/observability", {
waitUntil: "domcontentloaded",
});
await page.goto("/deployment/observability", {
waitUntil: "domcontentloaded",
});
await verifyConfigFlagBoolean(page, config, "trace-logs");
await verifyConfigFlagBoolean(page, config, "enable-terraform-debug-mode");
await verifyConfigFlagBoolean(page, config, "enable-terraform-debug-mode");
await verifyConfigFlagDuration(page, config, "health-check-refresh");
await verifyConfigFlagEmpty(page, "health-check-threshold-database");
await verifyConfigFlagString(page, config, "log-human");
await verifyConfigFlagString(page, config, "prometheus-address");
await verifyConfigFlagArray(
page,
config,
"prometheus-aggregate-agent-stats-by",
);
await verifyConfigFlagBoolean(page, config, "prometheus-collect-agent-stats");
await verifyConfigFlagBoolean(page, config, "prometheus-collect-db-metrics");
await verifyConfigFlagBoolean(page, config, "prometheus-enable");
await verifyConfigFlagBoolean(page, config, "trace-datadog");
await verifyConfigFlagBoolean(page, config, "trace");
await verifyConfigFlagBoolean(page, config, "verbose");
await verifyConfigFlagBoolean(page, config, "pprof-enable");
await verifyConfigFlagBoolean(page, config, "trace-logs");
await verifyConfigFlagBoolean(page, config, "enable-terraform-debug-mode");
await verifyConfigFlagBoolean(page, config, "enable-terraform-debug-mode");
await verifyConfigFlagDuration(page, config, "health-check-refresh");
await verifyConfigFlagEmpty(page, "health-check-threshold-database");
await verifyConfigFlagString(page, config, "log-human");
await verifyConfigFlagString(page, config, "prometheus-address");
await verifyConfigFlagArray(
page,
config,
"prometheus-aggregate-agent-stats-by",
);
await verifyConfigFlagBoolean(page, config, "prometheus-collect-agent-stats");
await verifyConfigFlagBoolean(page, config, "prometheus-collect-db-metrics");
await verifyConfigFlagBoolean(page, config, "prometheus-enable");
await verifyConfigFlagBoolean(page, config, "trace-datadog");
await verifyConfigFlagBoolean(page, config, "trace");
await verifyConfigFlagBoolean(page, config, "verbose");
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 { API, type DeploymentConfig } from "api/api";
import {
findConfigOption,
setupApiCalls,
verifyConfigFlagBoolean,
verifyConfigFlagNumber,
verifyConfigFlagString,
findConfigOption,
setupApiCalls,
verifyConfigFlagBoolean,
verifyConfigFlagNumber,
verifyConfigFlagString,
} from "../../api";
test("enabled security settings", async ({ page }) => {
await setupApiCalls(page);
const config = await API.getDeploymentConfig();
await setupApiCalls(page);
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 verifyConfigFlagBoolean(page, config, "secure-auth-cookie");
await verifyConfigFlagBoolean(page, config, "disable-owner-workspace-access");
await verifyConfigFlagString(page, config, "ssh-keygen-algorithm");
await verifyConfigFlagBoolean(page, config, "secure-auth-cookie");
await verifyConfigFlagBoolean(page, config, "disable-owner-workspace-access");
await verifyConfigFlagBoolean(page, config, "tls-redirect-http-to-https");
await verifyStrictTransportSecurity(page, config);
await verifyConfigFlagString(page, config, "tls-address");
await verifyConfigFlagBoolean(page, config, "tls-allow-insecure-ciphers");
await verifyConfigFlagString(page, config, "tls-client-auth");
await verifyConfigFlagBoolean(page, config, "tls-enable");
await verifyConfigFlagString(page, config, "tls-min-version");
await verifyConfigFlagBoolean(page, config, "tls-redirect-http-to-https");
await verifyStrictTransportSecurity(page, config);
await verifyConfigFlagString(page, config, "tls-address");
await verifyConfigFlagBoolean(page, config, "tls-allow-insecure-ciphers");
await verifyConfigFlagString(page, config, "tls-client-auth");
await verifyConfigFlagBoolean(page, config, "tls-enable");
await verifyConfigFlagString(page, config, "tls-min-version");
});
async function verifyStrictTransportSecurity(
page: Page,
config: DeploymentConfig,
page: Page,
config: DeploymentConfig,
) {
const flag = "strict-transport-security";
const opt = findConfigOption(config, flag);
if (opt.value !== 0) {
await verifyConfigFlagNumber(page, config, flag);
return;
}
const flag = "strict-transport-security";
const opt = findConfigOption(config, flag);
if (opt.value !== 0) {
await verifyConfigFlagNumber(page, config, flag);
return;
}
const configOption = page.locator(
`div.options-table .option-${flag} .option-value-string`,
);
await expect(configOption).toHaveText("Disabled");
const configOption = page.locator(
`div.options-table .option-${flag} .option-value-string`,
);
await expect(configOption).toHaveText("Disabled");
}
+24 -24
View File
@@ -1,33 +1,33 @@
import { test } from "@playwright/test";
import { API } from "api/api";
import {
setupApiCalls,
verifyConfigFlagArray,
verifyConfigFlagBoolean,
verifyConfigFlagEntries,
verifyConfigFlagString,
setupApiCalls,
verifyConfigFlagArray,
verifyConfigFlagBoolean,
verifyConfigFlagEntries,
verifyConfigFlagString,
} from "../../api";
test("login with OIDC", async ({ page }) => {
await setupApiCalls(page);
const config = await API.getDeploymentConfig();
await setupApiCalls(page);
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-allow-signups");
await verifyConfigFlagEntries(page, config, "oidc-auth-url-params");
await verifyConfigFlagString(page, config, "oidc-client-id");
await verifyConfigFlagArray(page, config, "oidc-email-domain");
await verifyConfigFlagString(page, config, "oidc-email-field");
await verifyConfigFlagEntries(page, config, "oidc-group-mapping");
await verifyConfigFlagBoolean(page, config, "oidc-ignore-email-verified");
await verifyConfigFlagBoolean(page, config, "oidc-ignore-userinfo");
await verifyConfigFlagString(page, config, "oidc-issuer-url");
await verifyConfigFlagString(page, config, "oidc-group-regex-filter");
await verifyConfigFlagArray(page, config, "oidc-scopes");
await verifyConfigFlagEntries(page, config, "oidc-user-role-mapping");
await verifyConfigFlagString(page, config, "oidc-username-field");
await verifyConfigFlagString(page, config, "oidc-sign-in-text");
await verifyConfigFlagString(page, config, "oidc-icon-url");
await verifyConfigFlagBoolean(page, config, "oidc-group-auto-create");
await verifyConfigFlagBoolean(page, config, "oidc-allow-signups");
await verifyConfigFlagEntries(page, config, "oidc-auth-url-params");
await verifyConfigFlagString(page, config, "oidc-client-id");
await verifyConfigFlagArray(page, config, "oidc-email-domain");
await verifyConfigFlagString(page, config, "oidc-email-field");
await verifyConfigFlagEntries(page, config, "oidc-group-mapping");
await verifyConfigFlagBoolean(page, config, "oidc-ignore-email-verified");
await verifyConfigFlagBoolean(page, config, "oidc-ignore-userinfo");
await verifyConfigFlagString(page, config, "oidc-issuer-url");
await verifyConfigFlagString(page, config, "oidc-group-regex-filter");
await verifyConfigFlagArray(page, config, "oidc-scopes");
await verifyConfigFlagEntries(page, config, "oidc-user-role-mapping");
await verifyConfigFlagString(page, config, "oidc-username-field");
await verifyConfigFlagString(page, config, "oidc-sign-in-text");
await verifyConfigFlagString(page, config, "oidc-icon-url");
});
@@ -6,100 +6,100 @@ import { randomName, requiresEnterpriseLicense } from "../../helpers";
import { startWorkspaceProxy, stopWorkspaceProxy } from "../../proxy";
test("default proxy is online", async ({ page }) => {
requiresEnterpriseLicense();
await setupApiCalls(page);
requiresEnterpriseLicense();
await setupApiCalls(page);
await page.goto("/deployment/workspace-proxies", {
waitUntil: "domcontentloaded",
});
await page.goto("/deployment/workspace-proxies", {
waitUntil: "domcontentloaded",
});
// Verify if the default proxy is healthy
const workspaceProxyPrimary = page.locator(
`table.MuiTable-root tr[data-testid="primary"]`,
);
// Verify if the default proxy is healthy
const workspaceProxyPrimary = page.locator(
`table.MuiTable-root tr[data-testid="primary"]`,
);
const workspaceProxyName = workspaceProxyPrimary.locator("td.name span");
const workspaceProxyURL = workspaceProxyPrimary.locator("td.url");
const workspaceProxyStatus = workspaceProxyPrimary.locator("td.status span");
const workspaceProxyName = workspaceProxyPrimary.locator("td.name span");
const workspaceProxyURL = workspaceProxyPrimary.locator("td.url");
const workspaceProxyStatus = workspaceProxyPrimary.locator("td.status span");
await expect(workspaceProxyName).toHaveText("Default");
await expect(workspaceProxyURL).toHaveText(`http://localhost:${coderPort}`);
await expect(workspaceProxyStatus).toHaveText("Healthy");
await expect(workspaceProxyName).toHaveText("Default");
await expect(workspaceProxyURL).toHaveText(`http://localhost:${coderPort}`);
await expect(workspaceProxyStatus).toHaveText("Healthy");
});
test("custom proxy is online", async ({ page }) => {
requiresEnterpriseLicense();
await setupApiCalls(page);
requiresEnterpriseLicense();
await setupApiCalls(page);
const proxyName = randomName();
const proxyName = randomName();
// Register workspace proxy
const proxyResponse = await API.createWorkspaceProxy({
name: proxyName,
display_name: "",
icon: "/emojis/1f1e7-1f1f7.png",
});
expect(proxyResponse.proxy_token).toBeDefined();
// Register workspace proxy
const proxyResponse = await API.createWorkspaceProxy({
name: proxyName,
display_name: "",
icon: "/emojis/1f1e7-1f1f7.png",
});
expect(proxyResponse.proxy_token).toBeDefined();
// Start "wsproxy server"
const proxyServer = await startWorkspaceProxy(proxyResponse.proxy_token);
await waitUntilWorkspaceProxyIsHealthy(page, proxyName);
// Start "wsproxy server"
const proxyServer = await startWorkspaceProxy(proxyResponse.proxy_token);
await waitUntilWorkspaceProxyIsHealthy(page, proxyName);
// Verify if custom proxy is healthy
await page.goto("/deployment/workspace-proxies", {
waitUntil: "domcontentloaded",
});
// Verify if custom proxy is healthy
await page.goto("/deployment/workspace-proxies", {
waitUntil: "domcontentloaded",
});
const workspaceProxy = page.locator("table.MuiTable-root tr", {
hasText: proxyName,
});
const workspaceProxy = page.locator("table.MuiTable-root tr", {
hasText: proxyName,
});
const workspaceProxyName = workspaceProxy.locator("td.name span");
const workspaceProxyURL = workspaceProxy.locator("td.url");
const workspaceProxyStatus = workspaceProxy.locator("td.status span");
const workspaceProxyName = workspaceProxy.locator("td.name span");
const workspaceProxyURL = workspaceProxy.locator("td.url");
const workspaceProxyStatus = workspaceProxy.locator("td.status span");
await expect(workspaceProxyName).toHaveText(proxyName);
await expect(workspaceProxyURL).toHaveText(
`http://127.0.0.1:${workspaceProxyPort}`,
);
await expect(workspaceProxyStatus).toHaveText("Healthy");
await expect(workspaceProxyName).toHaveText(proxyName);
await expect(workspaceProxyURL).toHaveText(
`http://127.0.0.1:${workspaceProxyPort}`,
);
await expect(workspaceProxyStatus).toHaveText("Healthy");
// Tear down the proxy
await stopWorkspaceProxy(proxyServer);
// Tear down the proxy
await stopWorkspaceProxy(proxyServer);
});
const waitUntilWorkspaceProxyIsHealthy = async (
page: Page,
proxyName: string,
page: Page,
proxyName: string,
) => {
await page.goto("/deployment/workspace-proxies", {
waitUntil: "domcontentloaded",
});
await page.goto("/deployment/workspace-proxies", {
waitUntil: "domcontentloaded",
});
const maxRetries = 30;
const retryIntervalMs = 1000;
let retries = 0;
while (retries < maxRetries) {
await page.reload();
const maxRetries = 30;
const retryIntervalMs = 1000;
let retries = 0;
while (retries < maxRetries) {
await page.reload();
const workspaceProxy = page.locator("table.MuiTable-root tr", {
hasText: proxyName,
});
const workspaceProxyStatus = workspaceProxy.locator("td.status span");
const workspaceProxy = page.locator("table.MuiTable-root tr", {
hasText: proxyName,
});
const workspaceProxyStatus = workspaceProxy.locator("td.status span");
try {
await expect(workspaceProxyStatus).toHaveText("Healthy", {
timeout: 1_000,
});
return; // healthy!
} catch {
retries++;
await new Promise((resolve) => setTimeout(resolve, retryIntervalMs));
}
}
throw new Error(
`Workspace proxy "${proxyName}" is unhealthy after ${
maxRetries * retryIntervalMs
}ms`,
);
try {
await expect(workspaceProxyStatus).toHaveText("Healthy", {
timeout: 1_000,
});
return; // healthy!
} catch {
retries++;
await new Promise((resolve) => setTimeout(resolve, retryIntervalMs));
}
}
throw new Error(
`Workspace proxy "${proxyName}" is unhealthy after ${
maxRetries * retryIntervalMs
}ms`,
);
};
+131 -131
View File
@@ -3,32 +3,32 @@ import { test } from "@playwright/test";
import type { ExternalAuthDevice } from "api/typesGenerated";
import { gitAuth } from "../constants";
import {
Awaiter,
createServer,
createTemplate,
createWorkspace,
echoResponsesWithExternalAuth,
Awaiter,
createServer,
createTemplate,
createWorkspace,
echoResponsesWithExternalAuth,
} from "../helpers";
import { beforeCoderTest, resetExternalAuthKey } from "../hooks";
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!
srv.use(gitAuth.validatePath, (req, res) => {
res.write(JSON.stringify(ghUser));
res.end();
});
srv.use(gitAuth.tokenPath, (req, res) => {
const r = (Math.random() + 1).toString(36).substring(7);
res.write(JSON.stringify({ access_token: r }));
res.end();
});
srv.use(gitAuth.authPath, (req, res) => {
res.redirect(
`${baseURL}/external-auth/${gitAuth.webProvider}/callback?code=1234&state=${req.query.state}`,
);
});
// The GitHub validate endpoint returns the currently authenticated user!
srv.use(gitAuth.validatePath, (req, res) => {
res.write(JSON.stringify(ghUser));
res.end();
});
srv.use(gitAuth.tokenPath, (req, res) => {
const r = (Math.random() + 1).toString(36).substring(7);
res.write(JSON.stringify({ access_token: r }));
res.end();
});
srv.use(gitAuth.authPath, (req, res) => {
res.redirect(
`${baseURL}/external-auth/${gitAuth.webProvider}/callback?code=1234&state=${req.query.state}`,
);
});
});
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!
test("external auth device", async ({ page }) => {
const device: ExternalAuthDevice = {
device_code: "1234",
user_code: "1234-5678",
expires_in: 900,
interval: 1,
verification_uri: "",
};
const device: ExternalAuthDevice = {
device_code: "1234",
user_code: "1234-5678",
expires_in: 900,
interval: 1,
verification_uri: "",
};
// Start a server to mock the GitHub API.
const srv = await createServer(gitAuth.devicePort);
srv.use(gitAuth.validatePath, (req, res) => {
res.write(JSON.stringify(ghUser));
res.end();
});
srv.use(gitAuth.codePath, (req, res) => {
res.write(JSON.stringify(device));
res.end();
});
srv.use(gitAuth.installationsPath, (req, res) => {
res.write(JSON.stringify(ghInstall));
res.end();
});
// Start a server to mock the GitHub API.
const srv = await createServer(gitAuth.devicePort);
srv.use(gitAuth.validatePath, (req, res) => {
res.write(JSON.stringify(ghUser));
res.end();
});
srv.use(gitAuth.codePath, (req, res) => {
res.write(JSON.stringify(device));
res.end();
});
srv.use(gitAuth.installationsPath, (req, res) => {
res.write(JSON.stringify(ghInstall));
res.end();
});
const token = {
access_token: "",
error: "authorization_pending",
error_description: "",
};
// First we send a result from the API that the token hasn't been
// authorized yet to ensure the UI reacts properly.
const sentPending = new Awaiter();
srv.use(gitAuth.tokenPath, (req, res) => {
res.write(JSON.stringify(token));
res.end();
sentPending.done();
});
const token = {
access_token: "",
error: "authorization_pending",
error_description: "",
};
// First we send a result from the API that the token hasn't been
// authorized yet to ensure the UI reacts properly.
const sentPending = new Awaiter();
srv.use(gitAuth.tokenPath, (req, res) => {
res.write(JSON.stringify(token));
res.end();
sentPending.done();
});
await page.goto(`/external-auth/${gitAuth.deviceProvider}`, {
waitUntil: "domcontentloaded",
});
await page.getByText(device.user_code).isVisible();
await sentPending.wait();
// Update the token to be valid and ensure the UI updates!
token.error = "";
token.access_token = "hello-world";
await page.waitForSelector("text=1 organization authorized");
await page.goto(`/external-auth/${gitAuth.deviceProvider}`, {
waitUntil: "domcontentloaded",
});
await page.getByText(device.user_code).isVisible();
await sentPending.wait();
// Update the token to be valid and ensure the UI updates!
token.error = "";
token.access_token = "hello-world";
await page.waitForSelector("text=1 organization authorized");
});
test("external auth web", async ({ page }) => {
await page.goto(`/external-auth/${gitAuth.webProvider}`, {
waitUntil: "domcontentloaded",
});
// This endpoint doesn't have the installations URL set intentionally!
await page.waitForSelector("text=You've authenticated with GitHub!");
await page.goto(`/external-auth/${gitAuth.webProvider}`, {
waitUntil: "domcontentloaded",
});
// This endpoint doesn't have the installations URL set intentionally!
await page.waitForSelector("text=You've authenticated with GitHub!");
});
test("successful external auth from workspace", async ({ page }) => {
const templateName = await createTemplate(
page,
echoResponsesWithExternalAuth([
{ id: gitAuth.webProvider, optional: false },
]),
);
const templateName = await createTemplate(
page,
echoResponsesWithExternalAuth([
{ id: gitAuth.webProvider, optional: false },
]),
);
await createWorkspace(page, templateName, [], [], gitAuth.webProvider);
await createWorkspace(page, templateName, [], [], gitAuth.webProvider);
});
const ghUser: Endpoints["GET /user"]["response"]["data"] = {
login: "kylecarbs",
id: 7122116,
node_id: "MDQ6VXNlcjcxMjIxMTY=",
avatar_url: "https://avatars.githubusercontent.com/u/7122116?v=4",
gravatar_id: "",
url: "https://api.github.com/users/kylecarbs",
html_url: "https://github.com/kylecarbs",
followers_url: "https://api.github.com/users/kylecarbs/followers",
following_url:
"https://api.github.com/users/kylecarbs/following{/other_user}",
gists_url: "https://api.github.com/users/kylecarbs/gists{/gist_id}",
starred_url: "https://api.github.com/users/kylecarbs/starred{/owner}{/repo}",
subscriptions_url: "https://api.github.com/users/kylecarbs/subscriptions",
organizations_url: "https://api.github.com/users/kylecarbs/orgs",
repos_url: "https://api.github.com/users/kylecarbs/repos",
events_url: "https://api.github.com/users/kylecarbs/events{/privacy}",
received_events_url: "https://api.github.com/users/kylecarbs/received_events",
type: "User",
site_admin: false,
name: "Kyle Carberry",
company: "@coder",
blog: "https://carberry.com",
location: "Austin, TX",
email: "kyle@carberry.com",
hireable: null,
bio: "hey there",
twitter_username: "kylecarbs",
public_repos: 52,
public_gists: 9,
followers: 208,
following: 31,
created_at: "2014-04-01T02:24:41Z",
updated_at: "2023-06-26T13:03:09Z",
login: "kylecarbs",
id: 7122116,
node_id: "MDQ6VXNlcjcxMjIxMTY=",
avatar_url: "https://avatars.githubusercontent.com/u/7122116?v=4",
gravatar_id: "",
url: "https://api.github.com/users/kylecarbs",
html_url: "https://github.com/kylecarbs",
followers_url: "https://api.github.com/users/kylecarbs/followers",
following_url:
"https://api.github.com/users/kylecarbs/following{/other_user}",
gists_url: "https://api.github.com/users/kylecarbs/gists{/gist_id}",
starred_url: "https://api.github.com/users/kylecarbs/starred{/owner}{/repo}",
subscriptions_url: "https://api.github.com/users/kylecarbs/subscriptions",
organizations_url: "https://api.github.com/users/kylecarbs/orgs",
repos_url: "https://api.github.com/users/kylecarbs/repos",
events_url: "https://api.github.com/users/kylecarbs/events{/privacy}",
received_events_url: "https://api.github.com/users/kylecarbs/received_events",
type: "User",
site_admin: false,
name: "Kyle Carberry",
company: "@coder",
blog: "https://carberry.com",
location: "Austin, TX",
email: "kyle@carberry.com",
hireable: null,
bio: "hey there",
twitter_username: "kylecarbs",
public_repos: 52,
public_gists: 9,
followers: 208,
following: 31,
created_at: "2014-04-01T02:24:41Z",
updated_at: "2023-06-26T13:03:09Z",
};
const ghInstall: Endpoints["GET /user/installations"]["response"]["data"] = {
installations: [
{
id: 1,
access_tokens_url: "",
account: ghUser,
app_id: 1,
app_slug: "coder",
created_at: "2014-04-01T02:24:41Z",
events: [],
html_url: "",
permissions: {},
repositories_url: "",
repository_selection: "all",
single_file_name: "",
suspended_at: null,
suspended_by: null,
target_id: 1,
target_type: "",
updated_at: "2023-06-26T13:03:09Z",
},
],
total_count: 1,
installations: [
{
id: 1,
access_tokens_url: "",
account: ghUser,
app_id: 1,
app_slug: "coder",
created_at: "2014-04-01T02:24:41Z",
events: [],
html_url: "",
permissions: {},
repositories_url: "",
repository_selection: "all",
single_file_name: "",
suspended_at: null,
suspended_by: null,
target_id: 1,
target_type: "",
updated_at: "2023-06-26T13:03:09Z",
},
],
total_count: 1,
};
+22 -22
View File
@@ -1,9 +1,9 @@
import { expect, test } from "@playwright/test";
import {
createGroup,
createUser,
getCurrentOrgId,
setupApiCalls,
createGroup,
createUser,
getCurrentOrgId,
setupApiCalls,
} from "../../api";
import { requiresEnterpriseLicense } from "../../helpers";
import { beforeCoderTest } from "../../hooks";
@@ -11,24 +11,24 @@ import { beforeCoderTest } from "../../hooks";
test.beforeEach(async ({ page }) => await beforeCoderTest(page));
test("add members", async ({ page, baseURL }) => {
requiresEnterpriseLicense();
await setupApiCalls(page);
const orgId = await getCurrentOrgId();
const group = await createGroup(orgId);
const numberOfMembers = 3;
const users = await Promise.all(
Array.from({ length: numberOfMembers }, () => createUser(orgId)),
);
requiresEnterpriseLicense();
await setupApiCalls(page);
const orgId = await getCurrentOrgId();
const group = await createGroup(orgId);
const numberOfMembers = 3;
const users = await Promise.all(
Array.from({ length: numberOfMembers }, () => createUser(orgId)),
);
await page.goto(`${baseURL}/groups/${group.name}`, {
waitUntil: "domcontentloaded",
});
await expect(page).toHaveTitle(`${group.display_name} - Coder`);
await page.goto(`${baseURL}/groups/${group.name}`, {
waitUntil: "domcontentloaded",
});
await expect(page).toHaveTitle(`${group.display_name} - Coder`);
for (const user of users) {
await page.getByPlaceholder("User email or username").fill(user.username);
await page.getByRole("option", { name: user.email }).click();
await page.getByRole("button", { name: "Add user" }).click();
await expect(page.getByRole("row", { name: user.username })).toBeVisible();
}
for (const user of users) {
await page.getByPlaceholder("User email or username").fill(user.username);
await page.getByRole("option", { name: user.email }).click();
await page.getByRole("button", { name: "Add user" }).click();
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";
test(`Every user should be automatically added to the default '${DEFAULT_GROUP_NAME}' group upon creation`, async ({
page,
baseURL,
page,
baseURL,
}) => {
requiresEnterpriseLicense();
await setupApiCalls(page);
const orgId = await getCurrentOrgId();
const numberOfMembers = 3;
const users = await Promise.all(
Array.from({ length: numberOfMembers }, () => createUser(orgId)),
);
requiresEnterpriseLicense();
await setupApiCalls(page);
const orgId = await getCurrentOrgId();
const numberOfMembers = 3;
const users = await Promise.all(
Array.from({ length: numberOfMembers }, () => createUser(orgId)),
);
await page.goto(`${baseURL}/groups`, { waitUntil: "domcontentloaded" });
await expect(page).toHaveTitle("Groups - Coder");
await page.goto(`${baseURL}/groups`, { waitUntil: "domcontentloaded" });
await expect(page).toHaveTitle("Groups - Coder");
const groupRow = page.getByRole("row", { name: DEFAULT_GROUP_NAME });
await groupRow.click();
await expect(page).toHaveTitle(`${DEFAULT_GROUP_NAME} - Coder`);
const groupRow = page.getByRole("row", { name: DEFAULT_GROUP_NAME });
await groupRow.click();
await expect(page).toHaveTitle(`${DEFAULT_GROUP_NAME} - Coder`);
for (const user of users) {
await expect(page.getByRole("row", { name: user.username })).toBeVisible();
}
for (const user of users) {
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("create group", async ({ page, baseURL }) => {
requiresEnterpriseLicense();
await page.goto(`${baseURL}/groups`, { waitUntil: "domcontentloaded" });
await expect(page).toHaveTitle("Groups - Coder");
requiresEnterpriseLicense();
await page.goto(`${baseURL}/groups`, { waitUntil: "domcontentloaded" });
await expect(page).toHaveTitle("Groups - Coder");
await page.getByText("Create group").click();
await expect(page).toHaveTitle("Create Group - Coder");
await page.getByText("Create group").click();
await expect(page).toHaveTitle("Create Group - Coder");
const name = randomName();
const groupValues = {
name: name,
displayName: `Display Name for ${name}`,
avatarURL: "/emojis/1f60d.png",
};
const name = randomName();
const groupValues = {
name: name,
displayName: `Display Name for ${name}`,
avatarURL: "/emojis/1f60d.png",
};
await page.getByLabel("Name", { exact: true }).fill(groupValues.name);
await page.getByLabel("Display Name").fill(groupValues.displayName);
await page.getByLabel("Avatar URL").fill(groupValues.avatarURL);
await page.getByRole("button", { name: "Submit" }).click();
await page.getByLabel("Name", { exact: true }).fill(groupValues.name);
await page.getByLabel("Display Name").fill(groupValues.displayName);
await page.getByLabel("Avatar URL").fill(groupValues.avatarURL);
await page.getByRole("button", { name: "Submit" }).click();
await expect(page).toHaveTitle(`${groupValues.displayName} - Coder`);
await expect(page.getByText(groupValues.displayName)).toBeVisible();
await expect(page.getByText("No members yet")).toBeVisible();
await expect(page).toHaveTitle(`${groupValues.displayName} - Coder`);
await expect(page.getByText(groupValues.displayName)).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("navigate to group page", async ({ page, baseURL }) => {
requiresEnterpriseLicense();
await setupApiCalls(page);
const orgId = await getCurrentOrgId();
const group = await createGroup(orgId);
requiresEnterpriseLicense();
await setupApiCalls(page);
const orgId = await getCurrentOrgId();
const group = await createGroup(orgId);
await page.goto(`${baseURL}/users`, { waitUntil: "domcontentloaded" });
await expect(page).toHaveTitle("Users - Coder");
await page.goto(`${baseURL}/users`, { waitUntil: "domcontentloaded" });
await expect(page).toHaveTitle("Users - Coder");
await page.getByRole("link", { name: "Groups" }).click();
await expect(page).toHaveTitle("Groups - Coder");
await page.getByRole("link", { name: "Groups" }).click();
await expect(page).toHaveTitle("Groups - Coder");
const groupRow = page.getByRole("row", { name: group.display_name });
await groupRow.click();
await expect(page).toHaveTitle(`${group.display_name} - Coder`);
const groupRow = page.getByRole("row", { name: group.display_name });
await groupRow.click();
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("remove group", async ({ page, baseURL }) => {
requiresEnterpriseLicense();
await setupApiCalls(page);
const orgId = await getCurrentOrgId();
const group = await createGroup(orgId);
requiresEnterpriseLicense();
await setupApiCalls(page);
const orgId = await getCurrentOrgId();
const group = await createGroup(orgId);
await page.goto(`${baseURL}/groups/${group.name}`, {
waitUntil: "domcontentloaded",
});
await expect(page).toHaveTitle(`${group.display_name} - Coder`);
await page.goto(`${baseURL}/groups/${group.name}`, {
waitUntil: "domcontentloaded",
});
await expect(page).toHaveTitle(`${group.display_name} - Coder`);
await page.getByRole("button", { name: "Delete" }).click();
const dialog = page.getByTestId("dialog");
await dialog.getByLabel("Name of the group to delete").fill(group.name);
await dialog.getByRole("button", { name: "Delete" }).click();
await expect(page.getByText("Group deleted successfully.")).toBeVisible();
await page.getByRole("button", { name: "Delete" }).click();
const dialog = page.getByTestId("dialog");
await dialog.getByLabel("Name of the group to delete").fill(group.name);
await dialog.getByRole("button", { name: "Delete" }).click();
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 { API } from "api/api";
import {
createGroup,
createUser,
getCurrentOrgId,
setupApiCalls,
createGroup,
createUser,
getCurrentOrgId,
setupApiCalls,
} from "../../api";
import { requiresEnterpriseLicense } from "../../helpers";
import { beforeCoderTest } from "../../hooks";
@@ -12,25 +12,25 @@ import { beforeCoderTest } from "../../hooks";
test.beforeEach(async ({ page }) => await beforeCoderTest(page));
test("remove member", async ({ page, baseURL }) => {
requiresEnterpriseLicense();
await setupApiCalls(page);
const orgId = await getCurrentOrgId();
const [group, member] = await Promise.all([
createGroup(orgId),
createUser(orgId),
]);
await API.addMember(group.id, member.id);
requiresEnterpriseLicense();
await setupApiCalls(page);
const orgId = await getCurrentOrgId();
const [group, member] = await Promise.all([
createGroup(orgId),
createUser(orgId),
]);
await API.addMember(group.id, member.id);
await page.goto(`${baseURL}/groups/${group.name}`, {
waitUntil: "domcontentloaded",
});
await expect(page).toHaveTitle(`${group.display_name} - Coder`);
await page.goto(`${baseURL}/groups/${group.name}`, {
waitUntil: "domcontentloaded",
});
await expect(page).toHaveTitle(`${group.display_name} - Coder`);
const userRow = page.getByRole("row", { name: member.username });
await userRow.getByRole("button", { name: "More options" }).click();
const userRow = page.getByRole("row", { name: member.username });
await userRow.getByRole("button", { name: "More options" }).click();
const menu = page.locator("#more-options");
await menu.getByText("Remove").click({ timeout: 1_000 });
const menu = page.locator("#more-options");
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";
test.beforeEach(async ({ page }) => {
await beforeCoderTest(page);
await setupApiCalls(page);
await beforeCoderTest(page);
await setupApiCalls(page);
});
test("create and delete organization", async ({ page, baseURL }) => {
requiresEnterpriseLicense();
requiresEnterpriseLicense();
// Create an organization
await page.goto(`${baseURL}/organizations/new`, {
waitUntil: "domcontentloaded",
});
// Create an organization
await page.goto(`${baseURL}/organizations/new`, {
waitUntil: "domcontentloaded",
});
await page.getByLabel("Name", { exact: true }).fill("floop");
await page.getByLabel("Display name").fill("Floop");
await page.getByLabel("Description").fill("Org description floop");
await page.getByLabel("Icon", { exact: true }).fill("/emojis/1f957.png");
await page.getByLabel("Name", { exact: true }).fill("floop");
await page.getByLabel("Display name").fill("Floop");
await page.getByLabel("Description").fill("Org description floop");
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
await expectUrl(page).toHavePathName("/organizations/floop");
await expect(page.getByText("Organization created.")).toBeVisible();
// Expect to be redirected to the new organization
await expectUrl(page).toHavePathName("/organizations/floop");
await expect(page.getByText("Organization created.")).toBeVisible();
await page.getByRole("button", { name: "Delete this organization" }).click();
const dialog = page.getByTestId("dialog");
await dialog.getByLabel("Name").fill("floop");
await dialog.getByRole("button", { name: "Delete" }).click();
await expect(page.getByText("Organization deleted.")).toBeVisible();
await page.getByRole("button", { name: "Delete this organization" }).click();
const dialog = page.getByTestId("dialog");
await dialog.getByLabel("Name").fill("floop");
await dialog.getByRole("button", { name: "Delete" }).click();
await expect(page.getByText("Organization deleted.")).toBeVisible();
});
+48 -48
View File
@@ -1,13 +1,13 @@
import { randomUUID } from "node:crypto";
import { test } from "@playwright/test";
import {
createTemplate,
createWorkspace,
downloadCoderVersion,
sshIntoWorkspace,
startAgentWithCommand,
stopAgent,
stopWorkspace,
createTemplate,
createWorkspace,
downloadCoderVersion,
sshIntoWorkspace,
startAgentWithCommand,
stopAgent,
stopWorkspace,
} from "../helpers";
import { beforeCoderTest } from "../hooks";
@@ -17,48 +17,48 @@ const agentVersion = "v2.12.1";
test.beforeEach(({ page }) => beforeCoderTest(page));
test(`ssh with agent ${agentVersion}`, async ({ page }) => {
test.setTimeout(40_000); // This is a slow test, 20s may not be enough on Mac.
test.setTimeout(40_000); // This is a slow test, 20s may not be enough on Mac.
const token = randomUUID();
const template = await createTemplate(page, {
apply: [
{
apply: {
resources: [
{
agents: [
{
token,
order: 0,
},
],
},
],
},
},
],
});
const workspaceName = await createWorkspace(page, template);
const binaryPath = await downloadCoderVersion(agentVersion);
const agent = await startAgentWithCommand(page, token, binaryPath);
const token = randomUUID();
const template = await createTemplate(page, {
apply: [
{
apply: {
resources: [
{
agents: [
{
token,
order: 0,
},
],
},
],
},
},
],
});
const workspaceName = await createWorkspace(page, template);
const binaryPath = await downloadCoderVersion(agentVersion);
const agent = await startAgentWithCommand(page, token, binaryPath);
const client = await sshIntoWorkspace(page, workspaceName);
await new Promise<void>((resolve, reject) => {
// We just exec a command to be certain the agent is running!
client.exec("exit 0", (err, stream) => {
if (err) {
return reject(err);
}
stream.on("exit", (code) => {
if (code !== 0) {
return reject(new Error(`Command exited with code ${code}`));
}
client.end();
resolve();
});
});
});
const client = await sshIntoWorkspace(page, workspaceName);
await new Promise<void>((resolve, reject) => {
// We just exec a command to be certain the agent is running!
client.exec("exit 0", (err, stream) => {
if (err) {
return reject(err);
}
stream.on("exit", (code) => {
if (code !== 0) {
return reject(new Error(`Command exited with code ${code}`));
}
client.end();
resolve();
});
});
});
await stopWorkspace(page, workspaceName);
await stopAgent(agent, false);
await stopWorkspace(page, workspaceName);
await stopAgent(agent, false);
});
+47 -47
View File
@@ -1,13 +1,13 @@
import { randomUUID } from "node:crypto";
import { test } from "@playwright/test";
import {
createTemplate,
createWorkspace,
downloadCoderVersion,
sshIntoWorkspace,
startAgent,
stopAgent,
stopWorkspace,
createTemplate,
createWorkspace,
downloadCoderVersion,
sshIntoWorkspace,
startAgent,
stopAgent,
stopWorkspace,
} from "../helpers";
import { beforeCoderTest } from "../hooks";
@@ -17,46 +17,46 @@ const clientVersion = "v0.27.0";
test.beforeEach(({ page }) => beforeCoderTest(page));
test(`ssh with client ${clientVersion}`, async ({ page }) => {
const token = randomUUID();
const template = await createTemplate(page, {
apply: [
{
apply: {
resources: [
{
agents: [
{
token,
order: 0,
},
],
},
],
},
},
],
});
const workspaceName = await createWorkspace(page, template);
const agent = await startAgent(page, token);
const binaryPath = await downloadCoderVersion(clientVersion);
const token = randomUUID();
const template = await createTemplate(page, {
apply: [
{
apply: {
resources: [
{
agents: [
{
token,
order: 0,
},
],
},
],
},
},
],
});
const workspaceName = await createWorkspace(page, template);
const agent = await startAgent(page, token);
const binaryPath = await downloadCoderVersion(clientVersion);
const client = await sshIntoWorkspace(page, workspaceName, binaryPath);
await new Promise<void>((resolve, reject) => {
// We just exec a command to be certain the agent is running!
client.exec("exit 0", (err, stream) => {
if (err) {
return reject(err);
}
stream.on("exit", (code) => {
if (code !== 0) {
return reject(new Error(`Command exited with code ${code}`));
}
client.end();
resolve();
});
});
});
const client = await sshIntoWorkspace(page, workspaceName, binaryPath);
await new Promise<void>((resolve, reject) => {
// We just exec a command to be certain the agent is running!
client.exec("exit 0", (err, stream) => {
if (err) {
return reject(err);
}
stream.on("exit", (code) => {
if (code !== 0) {
return reject(new Error(`Command exited with code ${code}`));
}
client.end();
resolve();
});
});
});
await stopWorkspace(page, workspaceName);
await stopAgent(agent);
await stopWorkspace(page, workspaceName);
await stopAgent(agent);
});
@@ -4,6 +4,6 @@ import { beforeCoderTest } from "../../hooks";
test.beforeEach(({ page }) => beforeCoderTest(page));
test("list templates", async ({ page, baseURL }) => {
await page.goto(`${baseURL}/templates`, { waitUntil: "domcontentloaded" });
await expect(page).toHaveTitle("Templates - Coder");
await page.goto(`${baseURL}/templates`, { waitUntil: "domcontentloaded" });
await expect(page).toHaveTitle("Templates - Coder");
});
@@ -6,40 +6,40 @@ import { beforeCoderTest } from "../../hooks";
test.beforeEach(({ page }) => beforeCoderTest(page));
test("update template schedule settings without override other settings", async ({
page,
baseURL,
page,
baseURL,
}) => {
await setupApiCalls(page);
const orgId = await getCurrentOrgId();
const templateVersion = await API.createTemplateVersion(orgId, {
storage_method: "file" as const,
provisioner: "echo",
user_variable_values: [],
example_id: "docker",
tags: {},
});
const template = await API.createTemplate(orgId, {
name: "test-template",
display_name: "Test Template",
template_version_id: templateVersion.id,
disable_everyone_group_access: false,
require_active_version: true,
});
await setupApiCalls(page);
const orgId = await getCurrentOrgId();
const templateVersion = await API.createTemplateVersion(orgId, {
storage_method: "file" as const,
provisioner: "echo",
user_variable_values: [],
example_id: "docker",
tags: {},
});
const template = await API.createTemplate(orgId, {
name: "test-template",
display_name: "Test Template",
template_version_id: templateVersion.id,
disable_everyone_group_access: false,
require_active_version: true,
});
await page.goto(`${baseURL}/templates/${template.name}/settings/schedule`, {
waitUntil: "domcontentloaded",
});
await page.getByLabel("Default autostop (hours)").fill("48");
await page.getByRole("button", { name: "Submit" }).click();
await expect(page.getByText("Template updated successfully")).toBeVisible();
await page.goto(`${baseURL}/templates/${template.name}/settings/schedule`, {
waitUntil: "domcontentloaded",
});
await page.getByLabel("Default autostop (hours)").fill("48");
await page.getByRole("button", { name: "Submit" }).click();
await expect(page.getByText("Template updated successfully")).toBeVisible();
const updatedTemplate = await API.getTemplate(template.id);
// Validate that the template data remains consistent, with the exception of
// the 'default_ttl_ms' field (updated during the test) and the 'updated at'
// field (automatically updated by the backend).
expect({
...template,
default_ttl_ms: 48 * 60 * 60 * 1000,
updated_at: updatedTemplate.updated_at,
}).toStrictEqual(updatedTemplate);
const updatedTemplate = await API.getTemplate(template.id);
// Validate that the template data remains consistent, with the exception of
// the 'default_ttl_ms' field (updated during the test) and the 'updated at'
// field (automatically updated by the backend).
expect({
...template,
default_ttl_ms: 48 * 60 * 60 * 1000,
updated_at: updatedTemplate.updated_at,
}).toStrictEqual(updatedTemplate);
});
+47 -47
View File
@@ -1,73 +1,73 @@
import { expect, test } from "@playwright/test";
import { expectUrl } from "../expectUrl";
import {
createGroup,
createTemplate,
requiresEnterpriseLicense,
updateTemplateSettings,
createGroup,
createTemplate,
requiresEnterpriseLicense,
updateTemplateSettings,
} from "../helpers";
import { beforeCoderTest } from "../hooks";
test.beforeEach(({ page }) => beforeCoderTest(page));
test("template update with new name redirects on successful submit", async ({
page,
page,
}) => {
const templateName = await createTemplate(page);
const templateName = await createTemplate(page);
await updateTemplateSettings(page, templateName, {
name: "new-name",
});
await updateTemplateSettings(page, templateName, {
name: "new-name",
});
});
test("add and remove a group", async ({ page }) => {
requiresEnterpriseLicense();
requiresEnterpriseLicense();
const templateName = await createTemplate(page);
const groupName = await createGroup(page);
const templateName = await createTemplate(page);
const groupName = await createGroup(page);
await page.goto(`/templates/${templateName}/settings/permissions`, {
waitUntil: "domcontentloaded",
});
await expectUrl(page).toHavePathName(
`/templates/${templateName}/settings/permissions`,
);
await page.goto(`/templates/${templateName}/settings/permissions`, {
waitUntil: "domcontentloaded",
});
await expectUrl(page).toHavePathName(
`/templates/${templateName}/settings/permissions`,
);
// Type the first half of the group name
await page
.getByPlaceholder("Search for user or group", { exact: true })
.fill(groupName.slice(0, 4));
// Type the first half of the group name
await page
.getByPlaceholder("Search for user or group", { exact: true })
.fill(groupName.slice(0, 4));
// Select the group from the list and add it
await page.getByText(groupName).click();
await page.getByText("Add member").click();
const row = page.locator(".MuiTableRow-root", { hasText: groupName });
await expect(row).toBeVisible();
// Select the group from the list and add it
await page.getByText(groupName).click();
await page.getByText("Add member").click();
const row = page.locator(".MuiTableRow-root", { hasText: groupName });
await expect(row).toBeVisible();
// Now remove the group
await row.getByLabel("More options").click();
await page.getByText("Remove").click();
await expect(page.getByText("Group removed successfully!")).toBeVisible();
await expect(row).not.toBeVisible();
// Now remove the group
await row.getByLabel("More options").click();
await page.getByText("Remove").click();
await expect(page.getByText("Group removed successfully!")).toBeVisible();
await expect(row).not.toBeVisible();
});
test("require latest version", async ({ page }) => {
requiresEnterpriseLicense();
requiresEnterpriseLicense();
const templateName = await createTemplate(page);
const templateName = await createTemplate(page);
await page.goto(`/templates/${templateName}/settings`, {
waitUntil: "domcontentloaded",
});
await expectUrl(page).toHavePathName(`/templates/${templateName}/settings`);
let checkbox = await page.waitForSelector("#require_active_version");
await checkbox.click();
await page.getByTestId("form-submit").click();
await page.goto(`/templates/${templateName}/settings`, {
waitUntil: "domcontentloaded",
});
await expectUrl(page).toHavePathName(`/templates/${templateName}/settings`);
let checkbox = await page.waitForSelector("#require_active_version");
await checkbox.click();
await page.getByTestId("form-submit").click();
await page.goto(`/templates/${templateName}/settings`, {
waitUntil: "domcontentloaded",
});
checkbox = await page.waitForSelector("#require_active_version");
await checkbox.scrollIntoViewIfNeeded();
expect(await checkbox.isChecked()).toBe(true);
await page.goto(`/templates/${templateName}/settings`, {
waitUntil: "domcontentloaded",
});
checkbox = await page.waitForSelector("#require_active_version");
await checkbox.scrollIntoViewIfNeeded();
expect(await checkbox.isChecked()).toBe(true);
});
@@ -5,63 +5,63 @@ import { beforeCoderTest } from "../../hooks";
test.beforeEach(async ({ page }) => await beforeCoderTest(page));
test("create user with password", async ({ page, baseURL }) => {
await page.goto(`${baseURL}/users`, { waitUntil: "domcontentloaded" });
await expect(page).toHaveTitle("Users - Coder");
await page.goto(`${baseURL}/users`, { waitUntil: "domcontentloaded" });
await expect(page).toHaveTitle("Users - Coder");
await page.getByRole("button", { name: "Create user" }).click();
await expect(page).toHaveTitle("Create User - Coder");
await page.getByRole("button", { name: "Create user" }).click();
await expect(page).toHaveTitle("Create User - Coder");
const name = randomName();
const userValues = {
username: name,
name: name,
email: `${name}@coder.com`,
loginType: "password",
password: "s3cure&password!",
};
const name = randomName();
const userValues = {
username: name,
name: name,
email: `${name}@coder.com`,
loginType: "password",
password: "s3cure&password!",
};
await page.getByLabel("Username").fill(userValues.username);
await page.getByLabel("Full name").fill(userValues.username);
await page.getByLabel("Email").fill(userValues.email);
await page.getByLabel("Login Type").click();
await page.getByRole("option", { name: "Password", exact: false }).click();
// Using input[name=password] due to the select element utilizing 'password'
// as the label for the currently active option.
const passwordField = page.locator("input[name=password]");
await passwordField.fill(userValues.password);
await page.getByRole("button", { name: "Create user" }).click();
await expect(page.getByText("Successfully created user.")).toBeVisible();
await page.getByLabel("Username").fill(userValues.username);
await page.getByLabel("Full name").fill(userValues.username);
await page.getByLabel("Email").fill(userValues.email);
await page.getByLabel("Login Type").click();
await page.getByRole("option", { name: "Password", exact: false }).click();
// Using input[name=password] due to the select element utilizing 'password'
// as the label for the currently active option.
const passwordField = page.locator("input[name=password]");
await passwordField.fill(userValues.password);
await page.getByRole("button", { name: "Create user" }).click();
await expect(page.getByText("Successfully created user.")).toBeVisible();
await expect(page).toHaveTitle("Users - Coder");
await expect(page.locator("tr", { hasText: userValues.email })).toBeVisible();
await expect(page).toHaveTitle("Users - Coder");
await expect(page.locator("tr", { hasText: userValues.email })).toBeVisible();
});
test("create user without full name is optional", async ({ page, baseURL }) => {
await page.goto(`${baseURL}/users`, { waitUntil: "domcontentloaded" });
await expect(page).toHaveTitle("Users - Coder");
await page.goto(`${baseURL}/users`, { waitUntil: "domcontentloaded" });
await expect(page).toHaveTitle("Users - Coder");
await page.getByRole("button", { name: "Create user" }).click();
await expect(page).toHaveTitle("Create User - Coder");
await page.getByRole("button", { name: "Create user" }).click();
await expect(page).toHaveTitle("Create User - Coder");
const name = randomName();
const userValues = {
username: name,
email: `${name}@coder.com`,
loginType: "password",
password: "s3cure&password!",
};
const name = randomName();
const userValues = {
username: name,
email: `${name}@coder.com`,
loginType: "password",
password: "s3cure&password!",
};
await page.getByLabel("Username").fill(userValues.username);
await page.getByLabel("Email").fill(userValues.email);
await page.getByLabel("Login Type").click();
await page.getByRole("option", { name: "Password", exact: false }).click();
// Using input[name=password] due to the select element utilizing 'password'
// as the label for the currently active option.
const passwordField = page.locator("input[name=password]");
await passwordField.fill(userValues.password);
await page.getByRole("button", { name: "Create user" }).click();
await expect(page.getByText("Successfully created user.")).toBeVisible();
await page.getByLabel("Username").fill(userValues.username);
await page.getByLabel("Email").fill(userValues.email);
await page.getByLabel("Login Type").click();
await page.getByRole("option", { name: "Password", exact: false }).click();
// Using input[name=password] due to the select element utilizing 'password'
// as the label for the currently active option.
const passwordField = page.locator("input[name=password]");
await passwordField.fill(userValues.password);
await page.getByRole("button", { name: "Create user" }).click();
await expect(page.getByText("Successfully created user.")).toBeVisible();
await expect(page).toHaveTitle("Users - Coder");
await expect(page.locator("tr", { hasText: userValues.email })).toBeVisible();
await expect(page).toHaveTitle("Users - Coder");
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("remove user", async ({ page, baseURL }) => {
await setupApiCalls(page);
const orgId = await getCurrentOrgId();
const user = await createUser(orgId);
await setupApiCalls(page);
const orgId = await getCurrentOrgId();
const user = await createUser(orgId);
await page.goto(`${baseURL}/users`, { waitUntil: "domcontentloaded" });
await expect(page).toHaveTitle("Users - Coder");
await page.goto(`${baseURL}/users`, { waitUntil: "domcontentloaded" });
await expect(page).toHaveTitle("Users - Coder");
const userRow = page.getByRole("row", { name: user.email });
await userRow.getByRole("button", { name: "More options" }).click();
const menu = page.locator("#more-options");
await menu.getByText("Delete").click();
const userRow = page.getByRole("row", { name: user.email });
await userRow.getByRole("button", { name: "More options" }).click();
const menu = page.locator("#more-options");
await menu.getByText("Delete").click();
const dialog = page.getByTestId("dialog");
await dialog.getByLabel("Name of the user to delete").fill(user.username);
await dialog.getByRole("button", { name: "Delete" }).click();
const dialog = page.getByTestId("dialog");
await dialog.getByLabel("Name of the user to delete").fill(user.username);
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 { test } from "@playwright/test";
import {
createTemplate,
createWorkspace,
openTerminalWindow,
startAgent,
stopAgent,
createTemplate,
createWorkspace,
openTerminalWindow,
startAgent,
stopAgent,
} from "../helpers";
import { beforeCoderTest } from "../hooks";
test.beforeEach(({ page }) => beforeCoderTest(page));
test("web terminal", async ({ context, page }) => {
const token = randomUUID();
const template = await createTemplate(page, {
apply: [
{
apply: {
resources: [
{
agents: [
{
token,
displayApps: {
webTerminal: true,
},
order: 0,
},
],
},
],
},
},
],
});
const workspaceName = await createWorkspace(page, template);
const agent = await startAgent(page, token);
const terminal = await openTerminalWindow(page, context, workspaceName);
const token = randomUUID();
const template = await createTemplate(page, {
apply: [
{
apply: {
resources: [
{
agents: [
{
token,
displayApps: {
webTerminal: true,
},
order: 0,
},
],
},
],
},
},
],
});
const workspaceName = await createWorkspace(page, template);
const agent = await startAgent(page, token);
const terminal = await openTerminalWindow(page, context, workspaceName);
await terminal.waitForSelector("div.xterm-rows", {
state: "visible",
});
await terminal.waitForSelector("div.xterm-rows", {
state: "visible",
});
// Workaround: delay next steps as "div.xterm-rows" can be recreated/reattached
// after a couple of milliseconds.
await terminal.waitForTimeout(2000);
// Workaround: delay next steps as "div.xterm-rows" can be recreated/reattached
// after a couple of milliseconds.
await terminal.waitForTimeout(2000);
// Ensure that we can type in it
await terminal.keyboard.type("echo he${justabreak}llo123456");
await terminal.keyboard.press("Enter");
// Ensure that we can type in it
await terminal.keyboard.type("echo he${justabreak}llo123456");
await terminal.keyboard.press("Enter");
// 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 {
await terminal.waitForSelector(
'div.xterm-rows span:text-matches("hello123456")',
{
state: "visible",
timeout: 10 * 1000,
},
);
} catch (error) {
const pageContent = await terminal.content();
// eslint-disable-next-line no-console -- Let's see what is inside of xterm-rows
console.log("Unable to find echoed text:", pageContent);
throw error;
}
// 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 {
await terminal.waitForSelector(
'div.xterm-rows span:text-matches("hello123456")',
{
state: "visible",
timeout: 10 * 1000,
},
);
} catch (error) {
const pageContent = await terminal.content();
// eslint-disable-next-line no-console -- Let's see what is inside of xterm-rows
console.log("Unable to find echoed text:", pageContent);
throw error;
}
await stopAgent(agent);
await stopAgent(agent);
});
@@ -1,65 +1,65 @@
import { expect, test } from "@playwright/test";
import { username } from "../../constants";
import {
createTemplate,
createWorkspace,
echoResponsesWithParameters,
createTemplate,
createWorkspace,
echoResponsesWithParameters,
} from "../../helpers";
import { emptyParameter } from "../../parameters";
import type { RichParameter } from "../../provisionerGenerated";
test("create workspace in auto mode", async ({ page }) => {
const richParameters: RichParameter[] = [
{ ...emptyParameter, name: "repo", type: "string" },
];
const template = await createTemplate(
page,
echoResponsesWithParameters(richParameters),
);
const name = "test-workspace";
await page.goto(
`/templates/${template}/workspace?mode=auto&param.repo=example&name=${name}`,
{
waitUntil: "domcontentloaded",
},
);
await expect(page).toHaveTitle(`${username}/${name} - Coder`);
const richParameters: RichParameter[] = [
{ ...emptyParameter, name: "repo", type: "string" },
];
const template = await createTemplate(
page,
echoResponsesWithParameters(richParameters),
);
const name = "test-workspace";
await page.goto(
`/templates/${template}/workspace?mode=auto&param.repo=example&name=${name}`,
{
waitUntil: "domcontentloaded",
},
);
await expect(page).toHaveTitle(`${username}/${name} - Coder`);
});
test("use an existing workspace that matches the `match` parameter instead of creating a new one", async ({
page,
page,
}) => {
const richParameters: RichParameter[] = [
{ ...emptyParameter, name: "repo", type: "string" },
];
const template = await createTemplate(
page,
echoResponsesWithParameters(richParameters),
);
const prevWorkspace = await createWorkspace(page, template);
await page.goto(
`/templates/${template}/workspace?mode=auto&param.repo=example&name=new-name&match=name:${prevWorkspace}`,
{
waitUntil: "domcontentloaded",
},
);
await expect(page).toHaveTitle(`${username}/${prevWorkspace} - Coder`);
const richParameters: RichParameter[] = [
{ ...emptyParameter, name: "repo", type: "string" },
];
const template = await createTemplate(
page,
echoResponsesWithParameters(richParameters),
);
const prevWorkspace = await createWorkspace(page, template);
await page.goto(
`/templates/${template}/workspace?mode=auto&param.repo=example&name=new-name&match=name:${prevWorkspace}`,
{
waitUntil: "domcontentloaded",
},
);
await expect(page).toHaveTitle(`${username}/${prevWorkspace} - Coder`);
});
test("show error if `match` parameter is invalid", async ({ page }) => {
const richParameters: RichParameter[] = [
{ ...emptyParameter, name: "repo", type: "string" },
];
const template = await createTemplate(
page,
echoResponsesWithParameters(richParameters),
);
const prevWorkspace = await createWorkspace(page, template);
await page.goto(
`/templates/${template}/workspace?mode=auto&param.repo=example&name=new-name&match=not-valid-query:${prevWorkspace}`,
{
waitUntil: "domcontentloaded",
},
);
await expect(page.getByText("Invalid match value")).toBeVisible();
const richParameters: RichParameter[] = [
{ ...emptyParameter, name: "repo", type: "string" },
];
const template = await createTemplate(
page,
echoResponsesWithParameters(richParameters),
);
const prevWorkspace = await createWorkspace(page, template);
await page.goto(
`/templates/${template}/workspace?mode=auto&param.repo=example&name=new-name&match=not-valid-query:${prevWorkspace}`,
{
waitUntil: "domcontentloaded",
},
);
await expect(page.getByText("Invalid match value")).toBeVisible();
});
+151 -151
View File
@@ -1,191 +1,191 @@
import { expect, test } from "@playwright/test";
import {
StarterTemplates,
createTemplate,
createWorkspace,
echoResponsesWithParameters,
openTerminalWindow,
requireTerraformProvisioner,
verifyParameters,
StarterTemplates,
createTemplate,
createWorkspace,
echoResponsesWithParameters,
openTerminalWindow,
requireTerraformProvisioner,
verifyParameters,
} from "../../helpers";
import { beforeCoderTest } from "../../hooks";
import {
fifthParameter,
firstParameter,
fourthParameter,
randParamName,
secondParameter,
seventhParameter,
sixthParameter,
thirdParameter,
fifthParameter,
firstParameter,
fourthParameter,
randParamName,
secondParameter,
seventhParameter,
sixthParameter,
thirdParameter,
} from "../../parameters";
import type { RichParameter } from "../../provisionerGenerated";
test.beforeEach(({ page }) => beforeCoderTest(page));
test("create workspace", async ({ page }) => {
const template = await createTemplate(page, {
apply: [
{
apply: {
resources: [
{
name: "example",
},
],
},
},
],
});
await createWorkspace(page, template);
const template = await createTemplate(page, {
apply: [
{
apply: {
resources: [
{
name: "example",
},
],
},
},
],
});
await createWorkspace(page, template);
});
test("create workspace with default immutable parameters", async ({ page }) => {
const richParameters: RichParameter[] = [
secondParameter,
fourthParameter,
fifthParameter,
];
const template = await createTemplate(
page,
echoResponsesWithParameters(richParameters),
);
const workspaceName = await createWorkspace(page, template);
await verifyParameters(page, workspaceName, richParameters, [
{ name: secondParameter.name, value: secondParameter.defaultValue },
{ name: fourthParameter.name, value: fourthParameter.defaultValue },
{ name: fifthParameter.name, value: fifthParameter.defaultValue },
]);
const richParameters: RichParameter[] = [
secondParameter,
fourthParameter,
fifthParameter,
];
const template = await createTemplate(
page,
echoResponsesWithParameters(richParameters),
);
const workspaceName = await createWorkspace(page, template);
await verifyParameters(page, workspaceName, richParameters, [
{ name: secondParameter.name, value: secondParameter.defaultValue },
{ name: fourthParameter.name, value: fourthParameter.defaultValue },
{ name: fifthParameter.name, value: fifthParameter.defaultValue },
]);
});
test("create workspace with default mutable parameters", async ({ page }) => {
const richParameters: RichParameter[] = [firstParameter, thirdParameter];
const template = await createTemplate(
page,
echoResponsesWithParameters(richParameters),
);
const workspaceName = await createWorkspace(page, template);
await verifyParameters(page, workspaceName, richParameters, [
{ name: firstParameter.name, value: firstParameter.defaultValue },
{ name: thirdParameter.name, value: thirdParameter.defaultValue },
]);
const richParameters: RichParameter[] = [firstParameter, thirdParameter];
const template = await createTemplate(
page,
echoResponsesWithParameters(richParameters),
);
const workspaceName = await createWorkspace(page, template);
await verifyParameters(page, workspaceName, richParameters, [
{ name: firstParameter.name, value: firstParameter.defaultValue },
{ name: thirdParameter.name, value: thirdParameter.defaultValue },
]);
});
test("create workspace with default and required parameters", async ({
page,
page,
}) => {
const richParameters: RichParameter[] = [
secondParameter,
fourthParameter,
sixthParameter,
seventhParameter,
];
const buildParameters = [
{ name: sixthParameter.name, value: "12345" },
{ name: seventhParameter.name, value: "abcdef" },
];
const template = await createTemplate(
page,
echoResponsesWithParameters(richParameters),
);
const workspaceName = await createWorkspace(
page,
template,
richParameters,
buildParameters,
);
await verifyParameters(page, workspaceName, richParameters, [
// user values:
...buildParameters,
// default values:
{ name: secondParameter.name, value: secondParameter.defaultValue },
{ name: fourthParameter.name, value: fourthParameter.defaultValue },
]);
const richParameters: RichParameter[] = [
secondParameter,
fourthParameter,
sixthParameter,
seventhParameter,
];
const buildParameters = [
{ name: sixthParameter.name, value: "12345" },
{ name: seventhParameter.name, value: "abcdef" },
];
const template = await createTemplate(
page,
echoResponsesWithParameters(richParameters),
);
const workspaceName = await createWorkspace(
page,
template,
richParameters,
buildParameters,
);
await verifyParameters(page, workspaceName, richParameters, [
// user values:
...buildParameters,
// default values:
{ name: secondParameter.name, value: secondParameter.defaultValue },
{ name: fourthParameter.name, value: fourthParameter.defaultValue },
]);
});
test("create workspace and overwrite default parameters", async ({ page }) => {
// We use randParamName to prevent the new values from corrupting user_history
// and thus affecting other tests.
const richParameters: RichParameter[] = [
randParamName(secondParameter),
randParamName(fourthParameter),
];
// We use randParamName to prevent the new values from corrupting user_history
// and thus affecting other tests.
const richParameters: RichParameter[] = [
randParamName(secondParameter),
randParamName(fourthParameter),
];
const buildParameters = [
{ name: richParameters[0].name, value: "AAAAA" },
{ name: richParameters[1].name, value: "false" },
];
const template = await createTemplate(
page,
echoResponsesWithParameters(richParameters),
);
const buildParameters = [
{ name: richParameters[0].name, value: "AAAAA" },
{ name: richParameters[1].name, value: "false" },
];
const template = await createTemplate(
page,
echoResponsesWithParameters(richParameters),
);
const workspaceName = await createWorkspace(
page,
template,
richParameters,
buildParameters,
);
await verifyParameters(page, workspaceName, richParameters, buildParameters);
const workspaceName = await createWorkspace(
page,
template,
richParameters,
buildParameters,
);
await verifyParameters(page, workspaceName, richParameters, buildParameters);
});
test("create workspace with disable_param search params", async ({ page }) => {
const richParameters: RichParameter[] = [
firstParameter, // mutable
secondParameter, //immutable
];
const richParameters: RichParameter[] = [
firstParameter, // mutable
secondParameter, //immutable
];
const templateName = await createTemplate(
page,
echoResponsesWithParameters(richParameters),
);
const templateName = await createTemplate(
page,
echoResponsesWithParameters(richParameters),
);
await page.goto(
`/templates/${templateName}/workspace?disable_params=first_parameter,second_parameter`,
{
waitUntil: "domcontentloaded",
},
);
await page.goto(
`/templates/${templateName}/workspace?disable_params=first_parameter,second_parameter`,
{
waitUntil: "domcontentloaded",
},
);
await expect(page.getByLabel(/First parameter/i)).toBeDisabled();
await expect(page.getByLabel(/Second parameter/i)).toBeDisabled();
await expect(page.getByLabel(/First parameter/i)).toBeDisabled();
await expect(page.getByLabel(/Second parameter/i)).toBeDisabled();
});
test("create docker workspace", async ({ context, page }) => {
test.skip(
true,
"creating docker containers is currently leaky. They are not cleaned up when the tests are over.",
);
requireTerraformProvisioner();
const template = await createTemplate(page, StarterTemplates.STARTER_DOCKER);
test.skip(
true,
"creating docker containers is currently leaky. They are not cleaned up when the tests are over.",
);
requireTerraformProvisioner();
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.
await page.waitForSelector(
`//div[@role="status"][@data-testid="agent-status-ready"]`,
{
state: "visible",
},
);
// The workspace agents must be ready before we try to interact with the workspace.
await page.waitForSelector(
`//div[@role="status"][@data-testid="agent-status-ready"]`,
{
state: "visible",
},
);
// Wait for the terminal button to be visible, and click it.
const terminalButton =
"//a[@data-testid='terminal'][normalize-space()='Terminal']";
await page.waitForSelector(terminalButton, {
state: "visible",
});
// Wait for the terminal button to be visible, and click it.
const terminalButton =
"//a[@data-testid='terminal'][normalize-space()='Terminal']";
await page.waitForSelector(terminalButton, {
state: "visible",
});
const terminal = await openTerminalWindow(
page,
context,
workspaceName,
"main",
);
await terminal.waitForSelector(
`//textarea[contains(@class,"xterm-helper-textarea")]`,
{
state: "visible",
},
);
const terminal = await openTerminalWindow(
page,
context,
workspaceName,
"main",
);
await terminal.waitForSelector(
`//textarea[contains(@class,"xterm-helper-textarea")]`,
{
state: "visible",
},
);
});
@@ -1,10 +1,10 @@
import { test } from "@playwright/test";
import {
buildWorkspaceWithParameters,
createTemplate,
createWorkspace,
echoResponsesWithParameters,
verifyParameters,
buildWorkspaceWithParameters,
createTemplate,
createWorkspace,
echoResponsesWithParameters,
verifyParameters,
} from "../../helpers";
import { beforeCoderTest } from "../../hooks";
import { firstBuildOption, secondBuildOption } from "../../parameters";
@@ -13,35 +13,35 @@ import type { RichParameter } from "../../provisionerGenerated";
test.beforeEach(({ page }) => beforeCoderTest(page));
test("restart workspace with ephemeral parameters", async ({ page }) => {
const richParameters: RichParameter[] = [firstBuildOption, secondBuildOption];
const template = await createTemplate(
page,
echoResponsesWithParameters(richParameters),
);
const workspaceName = await createWorkspace(page, template);
const richParameters: RichParameter[] = [firstBuildOption, secondBuildOption];
const template = await createTemplate(
page,
echoResponsesWithParameters(richParameters),
);
const workspaceName = await createWorkspace(page, template);
// Verify that build options are default (not selected).
await verifyParameters(page, workspaceName, richParameters, [
{ name: richParameters[0].name, value: firstBuildOption.defaultValue },
{ name: richParameters[1].name, value: secondBuildOption.defaultValue },
]);
// Verify that build options are default (not selected).
await verifyParameters(page, workspaceName, richParameters, [
{ name: richParameters[0].name, value: firstBuildOption.defaultValue },
{ name: richParameters[1].name, value: secondBuildOption.defaultValue },
]);
// Now, restart the workspace with ephemeral parameters selected.
const buildParameters = [
{ name: richParameters[0].name, value: "AAAAA" },
{ name: richParameters[1].name, value: "true" },
];
await buildWorkspaceWithParameters(
page,
workspaceName,
richParameters,
buildParameters,
true,
);
// Now, restart the workspace with ephemeral parameters selected.
const buildParameters = [
{ name: richParameters[0].name, value: "AAAAA" },
{ name: richParameters[1].name, value: "true" },
];
await buildWorkspaceWithParameters(
page,
workspaceName,
richParameters,
buildParameters,
true,
);
// Verify that build options are default (not selected).
await verifyParameters(page, workspaceName, richParameters, [
{ name: richParameters[0].name, value: firstBuildOption.defaultValue },
{ name: richParameters[1].name, value: secondBuildOption.defaultValue },
]);
// Verify that build options are default (not selected).
await verifyParameters(page, workspaceName, richParameters, [
{ name: richParameters[0].name, value: firstBuildOption.defaultValue },
{ name: richParameters[1].name, value: secondBuildOption.defaultValue },
]);
});
@@ -1,11 +1,11 @@
import { test } from "@playwright/test";
import {
buildWorkspaceWithParameters,
createTemplate,
createWorkspace,
echoResponsesWithParameters,
stopWorkspace,
verifyParameters,
buildWorkspaceWithParameters,
createTemplate,
createWorkspace,
echoResponsesWithParameters,
stopWorkspace,
verifyParameters,
} from "../../helpers";
import { beforeCoderTest } from "../../hooks";
import { firstBuildOption, secondBuildOption } from "../../parameters";
@@ -14,38 +14,38 @@ import type { RichParameter } from "../../provisionerGenerated";
test.beforeEach(({ page }) => beforeCoderTest(page));
test("start workspace with ephemeral parameters", async ({ page }) => {
const richParameters: RichParameter[] = [firstBuildOption, secondBuildOption];
const template = await createTemplate(
page,
echoResponsesWithParameters(richParameters),
);
const workspaceName = await createWorkspace(page, template);
const richParameters: RichParameter[] = [firstBuildOption, secondBuildOption];
const template = await createTemplate(
page,
echoResponsesWithParameters(richParameters),
);
const workspaceName = await createWorkspace(page, template);
// Verify that build options are default (not selected).
await verifyParameters(page, workspaceName, richParameters, [
{ name: richParameters[0].name, value: firstBuildOption.defaultValue },
{ name: richParameters[1].name, value: secondBuildOption.defaultValue },
]);
// Verify that build options are default (not selected).
await verifyParameters(page, workspaceName, richParameters, [
{ name: richParameters[0].name, value: firstBuildOption.defaultValue },
{ name: richParameters[1].name, value: secondBuildOption.defaultValue },
]);
// Stop the workspace
await stopWorkspace(page, workspaceName);
// Stop the workspace
await stopWorkspace(page, workspaceName);
// Now, start the workspace with ephemeral parameters selected.
const buildParameters = [
{ name: richParameters[0].name, value: "AAAAA" },
{ name: richParameters[1].name, value: "true" },
];
// Now, start the workspace with ephemeral parameters selected.
const buildParameters = [
{ name: richParameters[0].name, value: "AAAAA" },
{ name: richParameters[1].name, value: "true" },
];
await buildWorkspaceWithParameters(
page,
workspaceName,
richParameters,
buildParameters,
);
await buildWorkspaceWithParameters(
page,
workspaceName,
richParameters,
buildParameters,
);
// Verify that build options are default (not selected).
await verifyParameters(page, workspaceName, richParameters, [
{ name: richParameters[0].name, value: firstBuildOption.defaultValue },
{ name: richParameters[1].name, value: secondBuildOption.defaultValue },
]);
// Verify that build options are default (not selected).
await verifyParameters(page, workspaceName, richParameters, [
{ name: richParameters[0].name, value: firstBuildOption.defaultValue },
{ name: richParameters[1].name, value: secondBuildOption.defaultValue },
]);
});
@@ -1,132 +1,132 @@
import { test } from "@playwright/test";
import {
createTemplate,
createWorkspace,
echoResponsesWithParameters,
updateTemplate,
updateWorkspace,
updateWorkspaceParameters,
verifyParameters,
createTemplate,
createWorkspace,
echoResponsesWithParameters,
updateTemplate,
updateWorkspace,
updateWorkspaceParameters,
verifyParameters,
} from "../../helpers";
import { beforeCoderTest } from "../../hooks";
import {
fifthParameter,
firstParameter,
secondBuildOption,
secondParameter,
sixthParameter,
fifthParameter,
firstParameter,
secondBuildOption,
secondParameter,
sixthParameter,
} from "../../parameters";
import type { RichParameter } from "../../provisionerGenerated";
test.beforeEach(({ page }) => beforeCoderTest(page));
test("update workspace, new optional, immutable parameter added", async ({
page,
page,
}) => {
const richParameters: RichParameter[] = [firstParameter, secondParameter];
const template = await createTemplate(
page,
echoResponsesWithParameters(richParameters),
);
const richParameters: RichParameter[] = [firstParameter, secondParameter];
const template = await createTemplate(
page,
echoResponsesWithParameters(richParameters),
);
const workspaceName = await createWorkspace(page, template);
const workspaceName = await createWorkspace(page, template);
// Verify that parameter values are default.
await verifyParameters(page, workspaceName, richParameters, [
{ name: firstParameter.name, value: firstParameter.defaultValue },
{ name: secondParameter.name, value: secondParameter.defaultValue },
]);
// Verify that parameter values are default.
await verifyParameters(page, workspaceName, richParameters, [
{ name: firstParameter.name, value: firstParameter.defaultValue },
{ name: secondParameter.name, value: secondParameter.defaultValue },
]);
// Push updated template.
const updatedRichParameters = [...richParameters, fifthParameter];
await updateTemplate(
page,
template,
echoResponsesWithParameters(updatedRichParameters),
);
// Push updated template.
const updatedRichParameters = [...richParameters, fifthParameter];
await updateTemplate(
page,
template,
echoResponsesWithParameters(updatedRichParameters),
);
// Now, update the workspace, and select the value for immutable parameter.
await updateWorkspace(page, workspaceName, updatedRichParameters, [
{ name: fifthParameter.name, value: fifthParameter.options[0].value },
]);
// Now, update the workspace, and select the value for immutable parameter.
await updateWorkspace(page, workspaceName, updatedRichParameters, [
{ name: fifthParameter.name, value: fifthParameter.options[0].value },
]);
// Verify parameter values.
await verifyParameters(page, workspaceName, updatedRichParameters, [
{ name: firstParameter.name, value: firstParameter.defaultValue },
{ name: secondParameter.name, value: secondParameter.defaultValue },
{ name: fifthParameter.name, value: fifthParameter.options[0].value },
]);
// Verify parameter values.
await verifyParameters(page, workspaceName, updatedRichParameters, [
{ name: firstParameter.name, value: firstParameter.defaultValue },
{ name: secondParameter.name, value: secondParameter.defaultValue },
{ name: fifthParameter.name, value: fifthParameter.options[0].value },
]);
});
test("update workspace, new required, mutable parameter added", async ({
page,
page,
}) => {
const richParameters: RichParameter[] = [firstParameter, secondParameter];
const template = await createTemplate(
page,
echoResponsesWithParameters(richParameters),
);
const richParameters: RichParameter[] = [firstParameter, secondParameter];
const template = await createTemplate(
page,
echoResponsesWithParameters(richParameters),
);
const workspaceName = await createWorkspace(page, template);
const workspaceName = await createWorkspace(page, template);
// Verify that parameter values are default.
await verifyParameters(page, workspaceName, richParameters, [
{ name: firstParameter.name, value: firstParameter.defaultValue },
{ name: secondParameter.name, value: secondParameter.defaultValue },
]);
// Verify that parameter values are default.
await verifyParameters(page, workspaceName, richParameters, [
{ name: firstParameter.name, value: firstParameter.defaultValue },
{ name: secondParameter.name, value: secondParameter.defaultValue },
]);
// Push updated template.
const updatedRichParameters = [...richParameters, sixthParameter];
await updateTemplate(
page,
template,
echoResponsesWithParameters(updatedRichParameters),
);
// Push updated template.
const updatedRichParameters = [...richParameters, sixthParameter];
await updateTemplate(
page,
template,
echoResponsesWithParameters(updatedRichParameters),
);
// Now, update the workspace, and provide the parameter value.
const buildParameters = [{ name: sixthParameter.name, value: "99" }];
await updateWorkspace(
page,
workspaceName,
updatedRichParameters,
buildParameters,
);
// Now, update the workspace, and provide the parameter value.
const buildParameters = [{ name: sixthParameter.name, value: "99" }];
await updateWorkspace(
page,
workspaceName,
updatedRichParameters,
buildParameters,
);
// Verify parameter values.
await verifyParameters(page, workspaceName, updatedRichParameters, [
{ name: firstParameter.name, value: firstParameter.defaultValue },
{ name: secondParameter.name, value: secondParameter.defaultValue },
...buildParameters,
]);
// Verify parameter values.
await verifyParameters(page, workspaceName, updatedRichParameters, [
{ name: firstParameter.name, value: firstParameter.defaultValue },
{ name: secondParameter.name, value: secondParameter.defaultValue },
...buildParameters,
]);
});
test("update workspace with ephemeral parameter enabled", async ({ page }) => {
const richParameters: RichParameter[] = [firstParameter, secondBuildOption];
const template = await createTemplate(
page,
echoResponsesWithParameters(richParameters),
);
const richParameters: RichParameter[] = [firstParameter, secondBuildOption];
const template = await createTemplate(
page,
echoResponsesWithParameters(richParameters),
);
const workspaceName = await createWorkspace(page, template);
const workspaceName = await createWorkspace(page, template);
// Verify that parameter values are default.
await verifyParameters(page, workspaceName, richParameters, [
{ name: firstParameter.name, value: firstParameter.defaultValue },
{ name: secondBuildOption.name, value: secondBuildOption.defaultValue },
]);
// Verify that parameter values are default.
await verifyParameters(page, workspaceName, richParameters, [
{ name: firstParameter.name, value: firstParameter.defaultValue },
{ name: secondBuildOption.name, value: secondBuildOption.defaultValue },
]);
// Now, update the workspace, and select the value for ephemeral parameter.
const buildParameters = [{ name: secondBuildOption.name, value: "true" }];
await updateWorkspaceParameters(
page,
workspaceName,
richParameters,
buildParameters,
);
// Now, update the workspace, and select the value for ephemeral parameter.
const buildParameters = [{ name: secondBuildOption.name, value: "true" }];
await updateWorkspaceParameters(
page,
workspaceName,
richParameters,
buildParameters,
);
// Verify that parameter values are default.
await verifyParameters(page, workspaceName, richParameters, [
{ name: firstParameter.name, value: firstParameter.defaultValue },
{ name: secondBuildOption.name, value: secondBuildOption.defaultValue },
]);
// Verify that parameter values are default.
await verifyParameters(page, workspaceName, richParameters, [
{ name: firstParameter.name, value: firstParameter.defaultValue },
{ name: secondBuildOption.name, value: secondBuildOption.defaultValue },
]);
});

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