feat: add cohesive e2e tests for the web terminal, apps, and workspaces (#8140)

* feat: add cohesive e2e tests for the web terminal, apps, and workspaces

* Fix web terminal flake
This commit is contained in:
Kyle Carberry 2023-06-21 19:21:40 -05:00 committed by GitHub
parent 2a492b7008
commit d434181941
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1538 additions and 159 deletions

1
.gitignore vendored
View File

@ -27,6 +27,7 @@ site/storybook-static/
site/test-results/*
site/e2e/test-results/*
site/e2e/states/*.json
site/e2e/.auth.json
site/playwright-report/*
site/.swc
site/dist/

View File

@ -30,6 +30,7 @@ site/storybook-static/
site/test-results/*
site/e2e/test-results/*
site/e2e/states/*.json
site/e2e/.auth.json
site/playwright-report/*
site/.swc
site/dist/
@ -74,3 +75,6 @@ helm/templates/*.yaml
# Testdata shouldn't be formatted.
scripts/apitypings/testdata/**/*.ts
# Generated files shouldn't be formatted.
site/e2e/provisionerGenerated.ts

View File

@ -8,3 +8,6 @@ helm/templates/*.yaml
# Testdata shouldn't be formatted.
scripts/apitypings/testdata/**/*.ts
# Generated files shouldn't be formatted.
site/e2e/provisionerGenerated.ts

View File

@ -821,7 +821,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
for i := int64(0); i < cfg.Provisioner.Daemons.Value(); i++ {
daemonCacheDir := filepath.Join(cacheDir, fmt.Sprintf("provisioner-%d", i))
daemon, err := newProvisionerDaemon(
ctx, coderAPI, provisionerdMetrics, logger, cfg, daemonCacheDir, errCh, false, &provisionerdWaitGroup,
ctx, coderAPI, provisionerdMetrics, logger, cfg, daemonCacheDir, errCh, &provisionerdWaitGroup,
)
if err != nil {
return xerrors.Errorf("create provisioner daemon: %w", err)
@ -1177,7 +1177,6 @@ func newProvisionerDaemon(
cfg *codersdk.DeploymentValues,
cacheDir string,
errCh chan error,
dev bool,
wg *sync.WaitGroup,
) (srv *provisionerd.Server, err error) {
ctx, cancel := context.WithCancel(ctx)
@ -1192,53 +1191,14 @@ func newProvisionerDaemon(
return nil, xerrors.Errorf("mkdir %q: %w", cacheDir, err)
}
tfDir := filepath.Join(cacheDir, "tf")
err = os.MkdirAll(tfDir, 0o700)
if err != nil {
return nil, xerrors.Errorf("mkdir terraform dir: %w", err)
}
tracer := coderAPI.TracerProvider.Tracer(tracing.TracerName)
terraformClient, terraformServer := provisionersdk.MemTransportPipe()
wg.Add(1)
go func() {
defer wg.Done()
<-ctx.Done()
_ = terraformClient.Close()
_ = terraformServer.Close()
}()
wg.Add(1)
go func() {
defer wg.Done()
defer cancel()
err := terraform.Serve(ctx, &terraform.ServeOptions{
ServeOptions: &provisionersdk.ServeOptions{
Listener: terraformServer,
},
CachePath: tfDir,
Logger: logger,
Tracer: tracer,
})
if err != nil && !xerrors.Is(err, context.Canceled) {
select {
case errCh <- err:
default:
}
}
}()
workDir := filepath.Join(cacheDir, "work")
err = os.MkdirAll(workDir, 0o700)
if err != nil {
return nil, xerrors.Errorf("mkdir work dir: %w", err)
}
provisioners := provisionerd.Provisioners{
string(database.ProvisionerTypeTerraform): sdkproto.NewDRPCProvisionerClient(terraformClient),
}
// include echo provisioner when in dev mode
if dev {
provisioners := provisionerd.Provisioners{}
if cfg.Provisioner.DaemonsEcho {
echoClient, echoServer := provisionersdk.MemTransportPipe()
wg.Add(1)
go func() {
@ -1261,7 +1221,46 @@ func newProvisionerDaemon(
}
}()
provisioners[string(database.ProvisionerTypeEcho)] = sdkproto.NewDRPCProvisionerClient(echoClient)
} else {
tfDir := filepath.Join(cacheDir, "tf")
err = os.MkdirAll(tfDir, 0o700)
if err != nil {
return nil, xerrors.Errorf("mkdir terraform dir: %w", err)
}
tracer := coderAPI.TracerProvider.Tracer(tracing.TracerName)
terraformClient, terraformServer := provisionersdk.MemTransportPipe()
wg.Add(1)
go func() {
defer wg.Done()
<-ctx.Done()
_ = terraformClient.Close()
_ = terraformServer.Close()
}()
wg.Add(1)
go func() {
defer wg.Done()
defer cancel()
err := terraform.Serve(ctx, &terraform.ServeOptions{
ServeOptions: &provisionersdk.ServeOptions{
Listener: terraformServer,
},
CachePath: tfDir,
Logger: logger,
Tracer: tracer,
})
if err != nil && !xerrors.Is(err, context.Canceled) {
select {
case errCh <- err:
default:
}
}
}()
provisioners[string(database.ProvisionerTypeTerraform)] = sdkproto.NewDRPCProvisionerClient(terraformClient)
}
debounce := time.Second
return provisionerd.New(func(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) {
// This debounces calls to listen every second. Read the comment

View File

@ -288,6 +288,10 @@ provisioning:
# state for a long time, consider increasing this.
# (default: 3, type: int)
daemons: 3
# Whether to use echo provisioner daemons instead of Terraform. This is for E2E
# tests.
# (default: false, type: bool)
daemonsEcho: false
# Time to wait before polling for a new job.
# (default: 1s, type: duration)
daemonPollInterval: 1s

3
coderd/apidoc/docs.go generated
View File

@ -7945,6 +7945,9 @@ const docTemplate = `{
"daemons": {
"type": "integer"
},
"daemons_echo": {
"type": "boolean"
},
"force_cancel_interval": {
"type": "integer"
}

View File

@ -7120,6 +7120,9 @@
"daemons": {
"type": "integer"
},
"daemons_echo": {
"type": "boolean"
},
"force_cancel_interval": {
"type": "integer"
}

View File

@ -310,6 +310,7 @@ type GitAuthConfig struct {
type ProvisionerConfig struct {
Daemons clibase.Int64 `json:"daemons" typescript:",notnull"`
DaemonsEcho clibase.Bool `json:"daemons_echo" typescript:",notnull"`
DaemonPollInterval clibase.Duration `json:"daemon_poll_interval" typescript:",notnull"`
DaemonPollJitter clibase.Duration `json:"daemon_poll_jitter" typescript:",notnull"`
ForceCancelInterval clibase.Duration `json:"force_cancel_interval" typescript:",notnull"`
@ -1093,6 +1094,17 @@ when required by your organization's security policy.`,
Group: &deploymentGroupProvisioning,
YAML: "daemons",
},
{
Name: "Echo Provisioner",
Description: "Whether to use echo provisioner daemons instead of Terraform. This is for E2E tests.",
Flag: "provisioner-daemons-echo",
Env: "CODER_PROVISIONER_DAEMONS_ECHO",
Hidden: true,
Default: "false",
Value: &c.Provisioner.DaemonsEcho,
Group: &deploymentGroupProvisioning,
YAML: "daemonsEcho",
},
{
Name: "Poll Interval",
Description: "Time to wait before polling for a new job.",

View File

@ -283,6 +283,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \
"daemon_poll_interval": 0,
"daemon_poll_jitter": 0,
"daemons": 0,
"daemons_echo": true,
"force_cancel_interval": 0
},
"proxy_health_status_interval": 0,

View File

@ -1960,6 +1960,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"daemon_poll_interval": 0,
"daemon_poll_jitter": 0,
"daemons": 0,
"daemons_echo": true,
"force_cancel_interval": 0
},
"proxy_health_status_interval": 0,
@ -2289,6 +2290,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"daemon_poll_interval": 0,
"daemon_poll_jitter": 0,
"daemons": 0,
"daemons_echo": true,
"force_cancel_interval": 0
},
"proxy_health_status_interval": 0,
@ -3125,6 +3127,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"daemon_poll_interval": 0,
"daemon_poll_jitter": 0,
"daemons": 0,
"daemons_echo": true,
"force_cancel_interval": 0
}
```
@ -3136,6 +3139,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| `daemon_poll_interval` | integer | false | | |
| `daemon_poll_jitter` | integer | false | | |
| `daemons` | integer | false | | |
| `daemons_echo` | boolean | false | | |
| `force_cancel_interval` | integer | false | | |
## codersdk.ProvisionerDaemon

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "~> 0.7.0"
version = "~> 0.8.3"
}
docker = {
source = "kreuzwerker/docker"

View File

@ -30,6 +30,7 @@ storybook-static/
test-results/*
e2e/test-results/*
e2e/states/*.json
e2e/.auth.json
playwright-report/*
.swc
dist/
@ -74,3 +75,6 @@ stats/
# Testdata shouldn't be formatted.
../scripts/apitypings/testdata/**/*.ts
# Generated files shouldn't be formatted.
e2e/provisionerGenerated.ts

View File

@ -30,6 +30,7 @@ storybook-static/
test-results/*
e2e/test-results/*
e2e/states/*.json
e2e/.auth.json
playwright-report/*
.swc
dist/
@ -74,3 +75,6 @@ stats/
# Testdata shouldn't be formatted.
../scripts/apitypings/testdata/**/*.ts
# Generated files shouldn't be formatted.
e2e/provisionerGenerated.ts

18
site/e2e/global.setup.ts Normal file
View File

@ -0,0 +1,18 @@
import { test, expect } from "@playwright/test"
import * as constants from "./constants"
import { STORAGE_STATE } from "./playwright.config"
import { Language } from "../src/components/CreateUserForm/CreateUserForm"
test("create first user", async ({ page }) => {
await page.goto("/", { waitUntil: "networkidle" })
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("trial").click()
await page.getByTestId("create").click()
await expect(page).toHaveURL("/workspaces")
await page.context().storageState({ path: STORAGE_STATE })
})

View File

@ -1,35 +0,0 @@
import axios from "axios"
import { request } from "playwright"
import { createFirstUser } from "../src/api/api"
import * as constants from "./constants"
import { getStatePath } from "./helpers"
const globalSetup = async (): Promise<void> => {
axios.defaults.baseURL = `http://localhost:${constants.defaultPort}`
// Create first user
await createFirstUser({
email: constants.email,
username: constants.username,
password: constants.password,
trial: false,
})
// Authenticated storage
const authenticatedRequestContext = await request.newContext()
await authenticatedRequestContext.post(
`http://localhost:${constants.defaultPort}/api/v2/users/login`,
{
data: {
email: constants.email,
password: constants.password,
},
},
)
await authenticatedRequestContext.storageState({
path: getStatePath("authState"),
})
await authenticatedRequestContext.dispose()
}
export default globalSetup

View File

@ -1,31 +1,229 @@
import { Page } from "@playwright/test"
import { expect, Page } from "@playwright/test"
import { spawn } from "child_process"
import { randomUUID } from "crypto"
import path from "path"
import { TarWriter } from "utils/tar"
import {
Agent,
App,
AppSharingLevel,
Parse_Complete,
Parse_Response,
Provision_Complete,
Provision_Response,
Resource,
} from "./provisionerGenerated"
import { port } from "./playwright.config"
export const buttons = {
starterTemplates: "Starter Templates",
dockerTemplate: "Develop in Docker",
useTemplate: "Create Workspace",
createTemplate: "Create Template",
createWorkspace: "Create Workspace",
submitCreateWorkspace: "Create Workspace",
stopWorkspace: "Stop",
startWorkspace: "Start",
}
export const clickButton = async (page: Page, name: string): Promise<void> => {
await page.getByRole("button", { name, exact: true }).click()
}
export const fillInput = async (
// createWorkspace creates a workspace for a template.
// It does not wait for it to be running, but it does navigate to the page.
export const createWorkspace = async (
page: Page,
label: string,
value: string,
): Promise<void> => {
await page.fill(`text=${label}`, value)
templateName: string,
): Promise<string> => {
await page.goto("/templates/" + templateName + "/workspace", {
waitUntil: "networkidle",
})
const name = randomName()
await page.getByLabel("name").fill(name)
await page.getByTestId("form-submit").click()
await expect(page).toHaveURL("/@admin/" + name)
await page.getByTestId("build-status").isVisible()
return name
}
const statesDir = path.join(__dirname, "./states")
export const getStatePath = (name: string): string => {
return path.join(statesDir, `${name}.json`)
// createTemplate navigates to the /templates/new page and uploads a template
// with the resources provided in the responses argument.
export const createTemplate = async (
page: Page,
responses?: EchoProvisionerResponses,
): Promise<string> => {
// Required to have templates submit their provisioner type as echo!
await page.addInitScript({
content: "window.playwright = true",
})
await page.goto("/templates/new", { waitUntil: "networkidle" })
await page.getByTestId("file-upload").setInputFiles({
buffer: await createTemplateVersionTar(responses),
mimeType: "application/x-tar",
name: "template.tar",
})
const name = randomName()
await page.getByLabel("Name *").fill(name)
await page.getByTestId("form-submit").click()
await expect(page).toHaveURL("/templates/" + name, {
timeout: 30000,
})
return name
}
// startAgent runs the coder agent with the provided token.
// It awaits the agent to be ready before returning.
export const startAgent = async (page: Page, token: string): Promise<void> => {
const coderMain = path.join(
__dirname,
"..",
"..",
"enterprise",
"cmd",
"coder",
"main.go",
)
const cp = spawn("go", ["run", coderMain, "agent", "--no-reap"], {
env: {
...process.env,
CODER_AGENT_URL: "http://localhost:" + port,
CODER_AGENT_TOKEN: token,
},
})
let buffer = Buffer.of()
cp.stderr.on("data", (data: Buffer) => {
buffer = Buffer.concat([buffer, data])
})
try {
await page.getByTestId("agent-status-ready").isVisible()
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- The error is a string
} catch (ex: any) {
throw new Error(ex.toString() + "\n" + buffer.toString())
}
}
// Allows users to more easily define properties they want for agents and resources!
type RecursivePartial<T> = {
[P in keyof T]?: T[P] extends (infer U)[]
? RecursivePartial<U>[]
: T[P] extends object | undefined
? RecursivePartial<T[P]>
: T[P]
}
interface EchoProvisionerResponses {
// parse is for observing any Terraform variables
parse?: RecursivePartial<Parse_Response>[]
// plan occurs when the template is imported
plan?: RecursivePartial<Provision_Response>[]
// apply occurs when the workspace is built
apply?: RecursivePartial<Provision_Response>[]
}
// createTemplateVersionTar consumes a series of echo provisioner protobufs and
// converts it into an uploadable tar file.
const createTemplateVersionTar = async (
responses?: EchoProvisionerResponses,
): Promise<Buffer> => {
if (!responses) {
responses = {}
}
if (!responses.parse) {
responses.parse = [{}]
}
if (!responses.apply) {
responses.apply = [{}]
}
if (!responses.plan) {
responses.plan = responses.apply
}
const tar = new TarWriter()
responses.parse.forEach((response, index) => {
response.complete = {
templateVariables: [],
...response.complete,
} as Parse_Complete
tar.addFile(
`${index}.parse.protobuf`,
Parse_Response.encode(response as Parse_Response).finish(),
)
})
const fillProvisionResponse = (
response: RecursivePartial<Provision_Response>,
) => {
response.complete = {
error: "",
state: new Uint8Array(),
resources: [],
parameters: [],
gitAuthProviders: [],
plan: new Uint8Array(),
...response.complete,
} as Provision_Complete
response.complete.resources = response.complete.resources?.map(
(resource) => {
if (resource.agents) {
resource.agents = resource.agents?.map((agent) => {
if (agent.apps) {
agent.apps = agent.apps?.map((app) => {
return {
command: "",
displayName: "example",
external: false,
icon: "",
sharingLevel: AppSharingLevel.PUBLIC,
slug: "example",
subdomain: false,
url: "",
...app,
} as App
})
}
return {
apps: [],
architecture: "amd64",
connectionTimeoutSeconds: 300,
directory: "",
env: {},
id: randomUUID(),
metadata: [],
motdFile: "",
name: "dev",
operatingSystem: "linux",
shutdownScript: "",
shutdownScriptTimeoutSeconds: 0,
startupScript: "",
startupScriptBehavior: "",
startupScriptTimeoutSeconds: 300,
troubleshootingUrl: "",
token: randomUUID(),
...agent,
} as Agent
})
}
return {
agents: [],
dailyCost: 0,
hide: false,
icon: "",
instanceType: "",
metadata: [],
name: "dev",
type: "echo",
...resource,
} as Resource
},
)
}
responses.apply.forEach((response, index) => {
fillProvisionResponse(response)
tar.addFile(
`${index}.provision.apply.protobuf`,
Provision_Response.encode(response as Provision_Response).finish(),
)
})
responses.plan.forEach((response, index) => {
fillProvisionResponse(response)
tar.addFile(
`${index}.provision.plan.protobuf`,
Provision_Response.encode(response as Provision_Response).finish(),
)
})
return Buffer.from((await tar.write()) as ArrayBuffer)
}
const randomName = () => {
return randomUUID().slice(0, 8)
}

View File

@ -1,25 +1,47 @@
import { PlaywrightTestConfig } from "@playwright/test"
import { defineConfig } from "@playwright/test"
import path from "path"
import { defaultPort } from "./constants"
const port = process.env.CODER_E2E_PORT
export const port = process.env.CODER_E2E_PORT
? Number(process.env.CODER_E2E_PORT)
: defaultPort
const coderMain = path.join(__dirname, "../../enterprise/cmd/coder/main.go")
const config: PlaywrightTestConfig = {
testDir: "tests",
globalSetup: require.resolve("./globalSetup"),
export const STORAGE_STATE = path.join(__dirname, ".auth.json")
const config = defineConfig({
projects: [
{
name: "setup",
testMatch: /global.setup\.ts/,
},
{
name: "tests",
testMatch: /.*\.spec\.ts/,
dependencies: ["setup"],
use: {
storageState: STORAGE_STATE,
},
},
],
use: {
baseURL: `http://localhost:${port}`,
video: "retain-on-failure",
},
webServer: {
command: `go run -tags embed ${coderMain} server --global-config $(mktemp -d -t e2e-XXXXXXXXXX)`,
command:
`go run -tags embed ${coderMain} server ` +
`--global-config $(mktemp -d -t e2e-XXXXXXXXXX) ` +
`--access-url=http://localhost:${port} ` +
`--http-address=localhost:${port} ` +
`--in-memory --telemetry=false ` +
`--provisioner-daemons 10 ` +
`--provisioner-daemons-echo ` +
`--provisioner-daemon-poll-interval 50ms`,
port,
reuseExistingServer: false,
},
}
})
export default config

View File

@ -0,0 +1,876 @@
/* eslint-disable */
import * as _m0 from "protobufjs/minimal"
import { Observable } from "rxjs"
export const protobufPackage = "provisioner"
/** LogLevel represents severity of the log. */
export enum LogLevel {
TRACE = 0,
DEBUG = 1,
INFO = 2,
WARN = 3,
ERROR = 4,
UNRECOGNIZED = -1,
}
export enum AppSharingLevel {
OWNER = 0,
AUTHENTICATED = 1,
PUBLIC = 2,
UNRECOGNIZED = -1,
}
export enum WorkspaceTransition {
START = 0,
STOP = 1,
DESTROY = 2,
UNRECOGNIZED = -1,
}
/** Empty indicates a successful request/response. */
export interface Empty {}
/** TemplateVariable represents a Terraform variable. */
export interface TemplateVariable {
name: string
description: string
type: string
defaultValue: string
required: boolean
sensitive: boolean
}
/** RichParameterOption represents a singular option that a parameter may expose. */
export interface RichParameterOption {
name: string
description: string
value: string
icon: string
}
/** RichParameter represents a variable that is exposed. */
export interface RichParameter {
name: string
description: string
type: string
mutable: boolean
defaultValue: string
icon: string
options: RichParameterOption[]
validationRegex: string
validationError: string
validationMin?: number | undefined
validationMax?: number | undefined
validationMonotonic: string
required: boolean
legacyVariableName: string
displayName: string
}
/** RichParameterValue holds the key/value mapping of a parameter. */
export interface RichParameterValue {
name: string
value: string
}
/** VariableValue holds the key/value mapping of a Terraform variable. */
export interface VariableValue {
name: string
value: string
sensitive: boolean
}
/** Log represents output from a request. */
export interface Log {
level: LogLevel
output: string
}
export interface InstanceIdentityAuth {
instanceId: string
}
export interface GitAuthProvider {
id: string
accessToken: string
}
/** Agent represents a running agent on the workspace. */
export interface Agent {
id: string
name: string
env: { [key: string]: string }
startupScript: string
operatingSystem: string
architecture: string
directory: string
apps: App[]
token?: string | undefined
instanceId?: string | undefined
connectionTimeoutSeconds: number
troubleshootingUrl: string
motdFile: string
/** Field 14 was bool login_before_ready = 14, now removed. */
startupScriptTimeoutSeconds: number
shutdownScript: string
shutdownScriptTimeoutSeconds: number
metadata: Agent_Metadata[]
startupScriptBehavior: string
}
export interface Agent_Metadata {
key: string
displayName: string
script: string
interval: number
timeout: number
}
export interface Agent_EnvEntry {
key: string
value: string
}
/** App represents a dev-accessible application on the workspace. */
export interface App {
/**
* slug is the unique identifier for the app, usually the name from the
* template. It must be URL-safe and hostname-safe.
*/
slug: string
displayName: string
command: string
url: string
icon: string
subdomain: boolean
healthcheck: Healthcheck | undefined
sharingLevel: AppSharingLevel
external: boolean
}
/** Healthcheck represents configuration for checking for app readiness. */
export interface Healthcheck {
url: string
interval: number
threshold: number
}
/** Resource represents created infrastructure. */
export interface Resource {
name: string
type: string
agents: Agent[]
metadata: Resource_Metadata[]
hide: boolean
icon: string
instanceType: string
dailyCost: number
}
export interface Resource_Metadata {
key: string
value: string
sensitive: boolean
isNull: boolean
}
/** Parse consumes source-code from a directory to produce inputs. */
export interface Parse {}
export interface Parse_Request {
directory: string
}
export interface Parse_Complete {
templateVariables: TemplateVariable[]
}
export interface Parse_Response {
log?: Log | undefined
complete?: Parse_Complete | undefined
}
/**
* Provision consumes source-code from a directory to produce resources.
* Exactly one of Plan or Apply must be provided in a single session.
*/
export interface Provision {}
export interface Provision_Metadata {
coderUrl: string
workspaceTransition: WorkspaceTransition
workspaceName: string
workspaceOwner: string
workspaceId: string
workspaceOwnerId: string
workspaceOwnerEmail: string
templateName: string
templateVersion: string
workspaceOwnerOidcAccessToken: string
workspaceOwnerSessionToken: string
}
/**
* Config represents execution configuration shared by both Plan and
* Apply commands.
*/
export interface Provision_Config {
directory: string
state: Uint8Array
metadata: Provision_Metadata | undefined
provisionerLogLevel: string
}
export interface Provision_Plan {
config: Provision_Config | undefined
richParameterValues: RichParameterValue[]
variableValues: VariableValue[]
gitAuthProviders: GitAuthProvider[]
}
export interface Provision_Apply {
config: Provision_Config | undefined
plan: Uint8Array
}
export interface Provision_Cancel {}
export interface Provision_Request {
plan?: Provision_Plan | undefined
apply?: Provision_Apply | undefined
cancel?: Provision_Cancel | undefined
}
export interface Provision_Complete {
state: Uint8Array
error: string
resources: Resource[]
parameters: RichParameter[]
gitAuthProviders: string[]
plan: Uint8Array
}
export interface Provision_Response {
log?: Log | undefined
complete?: Provision_Complete | undefined
}
export const Empty = {
encode(_: Empty, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
return writer
},
}
export const TemplateVariable = {
encode(
message: TemplateVariable,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.name !== "") {
writer.uint32(10).string(message.name)
}
if (message.description !== "") {
writer.uint32(18).string(message.description)
}
if (message.type !== "") {
writer.uint32(26).string(message.type)
}
if (message.defaultValue !== "") {
writer.uint32(34).string(message.defaultValue)
}
if (message.required === true) {
writer.uint32(40).bool(message.required)
}
if (message.sensitive === true) {
writer.uint32(48).bool(message.sensitive)
}
return writer
},
}
export const RichParameterOption = {
encode(
message: RichParameterOption,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.name !== "") {
writer.uint32(10).string(message.name)
}
if (message.description !== "") {
writer.uint32(18).string(message.description)
}
if (message.value !== "") {
writer.uint32(26).string(message.value)
}
if (message.icon !== "") {
writer.uint32(34).string(message.icon)
}
return writer
},
}
export const RichParameter = {
encode(
message: RichParameter,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.name !== "") {
writer.uint32(10).string(message.name)
}
if (message.description !== "") {
writer.uint32(18).string(message.description)
}
if (message.type !== "") {
writer.uint32(26).string(message.type)
}
if (message.mutable === true) {
writer.uint32(32).bool(message.mutable)
}
if (message.defaultValue !== "") {
writer.uint32(42).string(message.defaultValue)
}
if (message.icon !== "") {
writer.uint32(50).string(message.icon)
}
for (const v of message.options) {
RichParameterOption.encode(v!, writer.uint32(58).fork()).ldelim()
}
if (message.validationRegex !== "") {
writer.uint32(66).string(message.validationRegex)
}
if (message.validationError !== "") {
writer.uint32(74).string(message.validationError)
}
if (message.validationMin !== undefined) {
writer.uint32(80).int32(message.validationMin)
}
if (message.validationMax !== undefined) {
writer.uint32(88).int32(message.validationMax)
}
if (message.validationMonotonic !== "") {
writer.uint32(98).string(message.validationMonotonic)
}
if (message.required === true) {
writer.uint32(104).bool(message.required)
}
if (message.legacyVariableName !== "") {
writer.uint32(114).string(message.legacyVariableName)
}
if (message.displayName !== "") {
writer.uint32(122).string(message.displayName)
}
return writer
},
}
export const RichParameterValue = {
encode(
message: RichParameterValue,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.name !== "") {
writer.uint32(10).string(message.name)
}
if (message.value !== "") {
writer.uint32(18).string(message.value)
}
return writer
},
}
export const VariableValue = {
encode(
message: VariableValue,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.name !== "") {
writer.uint32(10).string(message.name)
}
if (message.value !== "") {
writer.uint32(18).string(message.value)
}
if (message.sensitive === true) {
writer.uint32(24).bool(message.sensitive)
}
return writer
},
}
export const Log = {
encode(message: Log, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.level !== 0) {
writer.uint32(8).int32(message.level)
}
if (message.output !== "") {
writer.uint32(18).string(message.output)
}
return writer
},
}
export const InstanceIdentityAuth = {
encode(
message: InstanceIdentityAuth,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.instanceId !== "") {
writer.uint32(10).string(message.instanceId)
}
return writer
},
}
export const GitAuthProvider = {
encode(
message: GitAuthProvider,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.id !== "") {
writer.uint32(10).string(message.id)
}
if (message.accessToken !== "") {
writer.uint32(18).string(message.accessToken)
}
return writer
},
}
export const Agent = {
encode(message: Agent, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.id !== "") {
writer.uint32(10).string(message.id)
}
if (message.name !== "") {
writer.uint32(18).string(message.name)
}
Object.entries(message.env).forEach(([key, value]) => {
Agent_EnvEntry.encode(
{ key: key as any, value },
writer.uint32(26).fork(),
).ldelim()
})
if (message.startupScript !== "") {
writer.uint32(34).string(message.startupScript)
}
if (message.operatingSystem !== "") {
writer.uint32(42).string(message.operatingSystem)
}
if (message.architecture !== "") {
writer.uint32(50).string(message.architecture)
}
if (message.directory !== "") {
writer.uint32(58).string(message.directory)
}
for (const v of message.apps) {
App.encode(v!, writer.uint32(66).fork()).ldelim()
}
if (message.token !== undefined) {
writer.uint32(74).string(message.token)
}
if (message.instanceId !== undefined) {
writer.uint32(82).string(message.instanceId)
}
if (message.connectionTimeoutSeconds !== 0) {
writer.uint32(88).int32(message.connectionTimeoutSeconds)
}
if (message.troubleshootingUrl !== "") {
writer.uint32(98).string(message.troubleshootingUrl)
}
if (message.motdFile !== "") {
writer.uint32(106).string(message.motdFile)
}
if (message.startupScriptTimeoutSeconds !== 0) {
writer.uint32(120).int32(message.startupScriptTimeoutSeconds)
}
if (message.shutdownScript !== "") {
writer.uint32(130).string(message.shutdownScript)
}
if (message.shutdownScriptTimeoutSeconds !== 0) {
writer.uint32(136).int32(message.shutdownScriptTimeoutSeconds)
}
for (const v of message.metadata) {
Agent_Metadata.encode(v!, writer.uint32(146).fork()).ldelim()
}
if (message.startupScriptBehavior !== "") {
writer.uint32(154).string(message.startupScriptBehavior)
}
return writer
},
}
export const Agent_Metadata = {
encode(
message: Agent_Metadata,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.key !== "") {
writer.uint32(10).string(message.key)
}
if (message.displayName !== "") {
writer.uint32(18).string(message.displayName)
}
if (message.script !== "") {
writer.uint32(26).string(message.script)
}
if (message.interval !== 0) {
writer.uint32(32).int64(message.interval)
}
if (message.timeout !== 0) {
writer.uint32(40).int64(message.timeout)
}
return writer
},
}
export const Agent_EnvEntry = {
encode(
message: Agent_EnvEntry,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.key !== "") {
writer.uint32(10).string(message.key)
}
if (message.value !== "") {
writer.uint32(18).string(message.value)
}
return writer
},
}
export const App = {
encode(message: App, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.slug !== "") {
writer.uint32(10).string(message.slug)
}
if (message.displayName !== "") {
writer.uint32(18).string(message.displayName)
}
if (message.command !== "") {
writer.uint32(26).string(message.command)
}
if (message.url !== "") {
writer.uint32(34).string(message.url)
}
if (message.icon !== "") {
writer.uint32(42).string(message.icon)
}
if (message.subdomain === true) {
writer.uint32(48).bool(message.subdomain)
}
if (message.healthcheck !== undefined) {
Healthcheck.encode(message.healthcheck, writer.uint32(58).fork()).ldelim()
}
if (message.sharingLevel !== 0) {
writer.uint32(64).int32(message.sharingLevel)
}
if (message.external === true) {
writer.uint32(72).bool(message.external)
}
return writer
},
}
export const Healthcheck = {
encode(
message: Healthcheck,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.url !== "") {
writer.uint32(10).string(message.url)
}
if (message.interval !== 0) {
writer.uint32(16).int32(message.interval)
}
if (message.threshold !== 0) {
writer.uint32(24).int32(message.threshold)
}
return writer
},
}
export const Resource = {
encode(
message: Resource,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.name !== "") {
writer.uint32(10).string(message.name)
}
if (message.type !== "") {
writer.uint32(18).string(message.type)
}
for (const v of message.agents) {
Agent.encode(v!, writer.uint32(26).fork()).ldelim()
}
for (const v of message.metadata) {
Resource_Metadata.encode(v!, writer.uint32(34).fork()).ldelim()
}
if (message.hide === true) {
writer.uint32(40).bool(message.hide)
}
if (message.icon !== "") {
writer.uint32(50).string(message.icon)
}
if (message.instanceType !== "") {
writer.uint32(58).string(message.instanceType)
}
if (message.dailyCost !== 0) {
writer.uint32(64).int32(message.dailyCost)
}
return writer
},
}
export const Resource_Metadata = {
encode(
message: Resource_Metadata,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.key !== "") {
writer.uint32(10).string(message.key)
}
if (message.value !== "") {
writer.uint32(18).string(message.value)
}
if (message.sensitive === true) {
writer.uint32(24).bool(message.sensitive)
}
if (message.isNull === true) {
writer.uint32(32).bool(message.isNull)
}
return writer
},
}
export const Parse = {
encode(_: Parse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
return writer
},
}
export const Parse_Request = {
encode(
message: Parse_Request,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.directory !== "") {
writer.uint32(10).string(message.directory)
}
return writer
},
}
export const Parse_Complete = {
encode(
message: Parse_Complete,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
for (const v of message.templateVariables) {
TemplateVariable.encode(v!, writer.uint32(10).fork()).ldelim()
}
return writer
},
}
export const Parse_Response = {
encode(
message: Parse_Response,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.log !== undefined) {
Log.encode(message.log, writer.uint32(10).fork()).ldelim()
}
if (message.complete !== undefined) {
Parse_Complete.encode(message.complete, writer.uint32(18).fork()).ldelim()
}
return writer
},
}
export const Provision = {
encode(_: Provision, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
return writer
},
}
export const Provision_Metadata = {
encode(
message: Provision_Metadata,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.coderUrl !== "") {
writer.uint32(10).string(message.coderUrl)
}
if (message.workspaceTransition !== 0) {
writer.uint32(16).int32(message.workspaceTransition)
}
if (message.workspaceName !== "") {
writer.uint32(26).string(message.workspaceName)
}
if (message.workspaceOwner !== "") {
writer.uint32(34).string(message.workspaceOwner)
}
if (message.workspaceId !== "") {
writer.uint32(42).string(message.workspaceId)
}
if (message.workspaceOwnerId !== "") {
writer.uint32(50).string(message.workspaceOwnerId)
}
if (message.workspaceOwnerEmail !== "") {
writer.uint32(58).string(message.workspaceOwnerEmail)
}
if (message.templateName !== "") {
writer.uint32(66).string(message.templateName)
}
if (message.templateVersion !== "") {
writer.uint32(74).string(message.templateVersion)
}
if (message.workspaceOwnerOidcAccessToken !== "") {
writer.uint32(82).string(message.workspaceOwnerOidcAccessToken)
}
if (message.workspaceOwnerSessionToken !== "") {
writer.uint32(90).string(message.workspaceOwnerSessionToken)
}
return writer
},
}
export const Provision_Config = {
encode(
message: Provision_Config,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.directory !== "") {
writer.uint32(10).string(message.directory)
}
if (message.state.length !== 0) {
writer.uint32(18).bytes(message.state)
}
if (message.metadata !== undefined) {
Provision_Metadata.encode(
message.metadata,
writer.uint32(26).fork(),
).ldelim()
}
if (message.provisionerLogLevel !== "") {
writer.uint32(34).string(message.provisionerLogLevel)
}
return writer
},
}
export const Provision_Plan = {
encode(
message: Provision_Plan,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.config !== undefined) {
Provision_Config.encode(message.config, writer.uint32(10).fork()).ldelim()
}
for (const v of message.richParameterValues) {
RichParameterValue.encode(v!, writer.uint32(26).fork()).ldelim()
}
for (const v of message.variableValues) {
VariableValue.encode(v!, writer.uint32(34).fork()).ldelim()
}
for (const v of message.gitAuthProviders) {
GitAuthProvider.encode(v!, writer.uint32(42).fork()).ldelim()
}
return writer
},
}
export const Provision_Apply = {
encode(
message: Provision_Apply,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.config !== undefined) {
Provision_Config.encode(message.config, writer.uint32(10).fork()).ldelim()
}
if (message.plan.length !== 0) {
writer.uint32(18).bytes(message.plan)
}
return writer
},
}
export const Provision_Cancel = {
encode(
_: Provision_Cancel,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
return writer
},
}
export const Provision_Request = {
encode(
message: Provision_Request,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.plan !== undefined) {
Provision_Plan.encode(message.plan, writer.uint32(10).fork()).ldelim()
}
if (message.apply !== undefined) {
Provision_Apply.encode(message.apply, writer.uint32(18).fork()).ldelim()
}
if (message.cancel !== undefined) {
Provision_Cancel.encode(message.cancel, writer.uint32(26).fork()).ldelim()
}
return writer
},
}
export const Provision_Complete = {
encode(
message: Provision_Complete,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.state.length !== 0) {
writer.uint32(10).bytes(message.state)
}
if (message.error !== "") {
writer.uint32(18).string(message.error)
}
for (const v of message.resources) {
Resource.encode(v!, writer.uint32(26).fork()).ldelim()
}
for (const v of message.parameters) {
RichParameter.encode(v!, writer.uint32(34).fork()).ldelim()
}
for (const v of message.gitAuthProviders) {
writer.uint32(42).string(v!)
}
if (message.plan.length !== 0) {
writer.uint32(50).bytes(message.plan)
}
return writer
},
}
export const Provision_Response = {
encode(
message: Provision_Response,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.log !== undefined) {
Log.encode(message.log, writer.uint32(10).fork()).ldelim()
}
if (message.complete !== undefined) {
Provision_Complete.encode(
message.complete,
writer.uint32(18).fork(),
).ldelim()
}
return writer
},
}
export interface Provisioner {
Parse(request: Parse_Request): Observable<Parse_Response>
Provision(
request: Observable<Provision_Request>,
): Observable<Provision_Response>
}

View File

@ -0,0 +1,52 @@
import { test } from "@playwright/test"
import { randomUUID } from "crypto"
import * as http from "http"
import { createTemplate, createWorkspace, startAgent } from "../helpers"
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: [
{
complete: {
resources: [
{
agents: [
{
token,
apps: [
{
url: "http://localhost:" + addr.port,
displayName: appName,
},
],
},
],
},
],
},
},
],
})
await createWorkspace(page, template)
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("networkidle")
await app.getByText(appContent).isVisible()
})

View File

@ -0,0 +1,19 @@
import { test } from "@playwright/test"
import { createTemplate, createWorkspace } from "../helpers"
test("create workspace", async ({ page }) => {
const template = await createTemplate(page, {
apply: [
{
complete: {
resources: [
{
name: "example",
},
],
},
},
],
})
await createWorkspace(page, template)
})

View File

@ -1,7 +1,4 @@
import { test, expect } from "@playwright/test"
import { getStatePath } from "../helpers"
test.use({ storageState: getStatePath("authState") })
test("list templates", async ({ page, baseURL }) => {
await page.goto(`${baseURL}/templates`, { waitUntil: "networkidle" })

View File

@ -1,17 +0,0 @@
import { test, expect } from "@playwright/test"
import { getStatePath } from "../helpers"
test.use({ storageState: getStatePath("authState") })
test("signing out redirects to login page", async ({ page, baseURL }) => {
await page.goto(`${baseURL}/`, { waitUntil: "networkidle" })
await page.getByTestId("user-dropdown-trigger").click()
await page.getByRole("menuitem", { name: "Sign Out" }).click()
await expect(
page.getByRole("heading", { name: "Sign in to Coder" }),
).toBeVisible()
expect(page.url()).toMatch(/\/login$/) // ensure we're on the login page with no query params
})

View File

@ -0,0 +1,47 @@
import { test } from "@playwright/test"
import { createTemplate, createWorkspace, startAgent } from "../helpers"
import { randomUUID } from "crypto"
test("web terminal", async ({ context, page }) => {
const token = randomUUID()
const template = await createTemplate(page, {
apply: [
{
complete: {
resources: [
{
agents: [
{
token,
},
],
},
],
},
},
],
})
await createWorkspace(page, template)
await startAgent(page, token)
// Wait for the web terminal to open in a new tab
const pagePromise = context.waitForEvent("page")
await page.getByTestId("terminal").click()
const terminal = await pagePromise
await terminal.waitForLoadState("networkidle")
// Ensure that we can type in it
await terminal.keyboard.type("echo hello")
await terminal.keyboard.press("Enter")
const locator = terminal.locator("text=hello")
for (let i = 0; i < 10; i++) {
const items = await locator.all()
// Make sure the text came back
if (items.length === 2) {
break
}
await new Promise((r) => setTimeout(r, 250))
}
})

View File

@ -19,6 +19,7 @@
"lint:types": "tsc --noEmit",
"playwright:install": "playwright install --with-deps chromium",
"playwright:test": "playwright test --config=e2e/playwright.config.ts",
"gen:provisioner": "protoc --plugin=./node_modules/.bin/protoc-gen-ts-proto --ts_proto_out=./e2e/ --ts_proto_opt=outputJsonMethods=false,outputEncodeMethods=encode-no-creation,outputClientImpl=false,nestJs=false,outputPartialMethods=false,fileSuffix=Generated,suffix=hey -I ../provisionersdk/proto ../provisionersdk/proto/provisioner.proto && prettier --cache --write './e2e/provisionerGenerated.ts'",
"storybook": "STORYBOOK=true storybook dev -p 6006",
"storybook:build": "storybook build",
"test": "jest --selectProjects test",
@ -32,9 +33,9 @@
"dependencies": {
"@emoji-mart/data": "1.0.5",
"@emoji-mart/react": "1.0.1",
"@fastly/performance-observer-polyfill": "2.0.0",
"@emotion/react": "11.10.8",
"@emotion/styled": "11.11.0",
"@fastly/performance-observer-polyfill": "2.0.0",
"@fontsource/ibm-plex-mono": "4.5.10",
"@fontsource/inter": "5.0.2",
"@monaco-editor/react": "4.5.0",
@ -69,7 +70,6 @@
"jest-location-mock": "1.0.9",
"just-debounce-it": "3.1.1",
"lodash": "4.17.21",
"playwright": "1.29.2",
"react": "18.2.0",
"react-chartjs-2": "4.3.1",
"react-color": "2.19.3",
@ -99,7 +99,7 @@
"yup": "0.32.11"
},
"devDependencies": {
"@playwright/test": "1.29.2",
"@playwright/test": "1.35.1",
"@storybook/addon-actions": "7.0.4",
"@storybook/addon-essentials": "7.0.4",
"@storybook/addon-links": "7.0.4",
@ -150,6 +150,7 @@
"semver": "7.3.7",
"storybook": "7.0.4",
"storybook-react-context": "0.6.0",
"ts-proto": "1.150.0",
"typescript": "4.8.2",
"vite-plugin-checker": "0.6.0"
},

View File

@ -614,6 +614,7 @@ export interface PrometheusConfig {
// From codersdk/deployment.go
export interface ProvisionerConfig {
readonly daemons: number
readonly daemons_echo: boolean
readonly daemon_poll_interval: number
readonly daemon_poll_jitter: number
readonly force_cancel_interval: number

View File

@ -117,6 +117,7 @@ export const FileUpload: FC<FileUploadProps> = ({
<input
type="file"
data-testid="file-upload"
ref={inputRef}
className={styles.input}
accept={extension}

View File

@ -36,6 +36,7 @@ export const FormFooter: FC<FormFooterProps> = ({
color="primary"
type="submit"
disabled={submitDisabled}
data-testid="form-submit"
>
{submitLabel}
</LoadingButton>

View File

@ -26,6 +26,7 @@ const ReadyLifecycle = () => {
return (
<div
role="status"
data-testid="agent-status-ready"
aria-label={t("agentStatus.connected.ready") || "Ready"}
className={combineClasses([styles.status, styles.connected])}
/>

View File

@ -44,6 +44,7 @@ export const TerminalLink: FC<React.PropsWithChildren<TerminalLinkProps>> = ({
"width=900,height=600",
)
}}
data-testid="terminal"
>
<SecondaryAgentButton>{Language.linkText}</SecondaryAgentButton>
</Link>

View File

@ -51,6 +51,7 @@ export const WorkspaceStatusText: FC<
<Cond>
<span
role="status"
data-testid="build-status"
className={combineClasses([
className,
styles.root,

View File

@ -96,6 +96,7 @@ export const SetupPageView: React.FC<SetupPageViewProps> = ({
defaultChecked
value={form.values.trial}
onChange={form.handleChange}
data-testid="trial"
/>
</div>
@ -110,7 +111,12 @@ export const SetupPageView: React.FC<SetupPageViewProps> = ({
</Box>
</Box>
</div>
<LoadingButton fullWidth loading={isLoading} type="submit">
<LoadingButton
fullWidth
loading={isLoading}
type="submit"
data-testid="create"
>
{Language.create}
</LoadingButton>
</Stack>

View File

@ -15,7 +15,7 @@ test("tar", async () => {
group: "codergroup",
mode: parseInt("777", 8),
})
const blob = await writer.write()
const blob = (await writer.write()) as Blob
// Read
const reader = new TarReader()

View File

@ -232,7 +232,12 @@ export class TarWriter {
view.set(data, offset + 512)
offset += 512 + 512 * Math.floor((item.size + 511) / 512)
}
return new Blob([this.buffer], { type: "application/x-tar" })
// Required so it works in the browser and node.
if (typeof Blob !== "undefined") {
return new Blob([this.buffer], { type: "application/x-tar" })
} else {
return this.buffer
}
}
private writeString(str: string, offset: number, size: number) {

View File

@ -11,6 +11,7 @@ import {
import {
ProvisionerJob,
ProvisionerJobLog,
ProvisionerType,
Template,
TemplateExample,
TemplateVersion,
@ -33,6 +34,10 @@ import { assign, createMachine } from "xstate"
// 5.create template with the successful template version ID
// https://github.com/coder/coder/blob/b6703b11c6578b2f91a310d28b6a7e57f0069be6/cli/templatecreate.go#L169-L170
const provisioner: ProvisionerType =
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Playwright needs to use a different provisioner type!
typeof (window as any).playwright !== "undefined" ? "echo" : "terraform"
export interface CreateTemplateData {
name: string
display_name: string
@ -356,7 +361,7 @@ export const createTemplateMachine =
return createTemplateVersion(organizationId, {
storage_method: "file",
example_id: exampleId,
provisioner: "terraform",
provisioner: provisioner,
tags: {},
})
}
@ -371,7 +376,7 @@ export const createTemplateMachine =
return createTemplateVersion(organizationId, {
storage_method: "file",
file_id: version.job.file_id,
provisioner: "terraform",
provisioner: provisioner,
tags: {},
})
}
@ -380,7 +385,7 @@ export const createTemplateMachine =
return createTemplateVersion(organizationId, {
storage_method: "file",
file_id: uploadResponse.hash,
provisioner: "terraform",
provisioner: provisioner,
tags: {},
})
}
@ -402,7 +407,7 @@ export const createTemplateMachine =
return createTemplateVersion(organizationId, {
storage_method: "file",
file_id: version.job.file_id,
provisioner: "terraform",
provisioner: provisioner,
user_variable_values: templateData.user_variable_values,
tags: {},
})

View File

@ -326,7 +326,7 @@ export const templateVersionEditorMachine = createMachine(
tar.addFolder(fullPath)
})
const blob = await tar.write()
const blob = (await tar.write()) as Blob
return API.uploadTemplateFile(new File([blob], "template.tar"))
},
createBuild: (ctx) => {

View File

@ -1919,19 +1919,74 @@
tiny-glob "^0.2.9"
tslib "^2.4.0"
"@playwright/test@1.29.2":
version "1.29.2"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.29.2.tgz#c48184721d0f0b7627a886e2ec42f1efb2be339d"
integrity sha512-+3/GPwOgcoF0xLz/opTnahel1/y42PdcgZ4hs+BZGIUjtmEFSXGg+nFoaH3NSmuc7a6GSFwXDJ5L7VXpqzigNg==
"@playwright/test@1.35.1":
version "1.35.1"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.35.1.tgz#a596b61e15b980716696f149cc7a2002f003580c"
integrity sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==
dependencies:
"@types/node" "*"
playwright-core "1.29.2"
playwright-core "1.35.1"
optionalDependencies:
fsevents "2.3.2"
"@popperjs/core@^2.11.7":
version "2.11.7"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.7.tgz#ccab5c8f7dc557a52ca3288c10075c9ccd37fff7"
integrity sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==
"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==
"@protobufjs/base64@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735"
integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==
"@protobufjs/codegen@^2.0.4":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb"
integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==
"@protobufjs/eventemitter@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70"
integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==
"@protobufjs/fetch@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45"
integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==
dependencies:
"@protobufjs/aspromise" "^1.1.1"
"@protobufjs/inquire" "^1.1.0"
"@protobufjs/float@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1"
integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==
"@protobufjs/inquire@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089"
integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==
"@protobufjs/path@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d"
integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==
"@protobufjs/pool@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54"
integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==
"@protobufjs/utf8@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==
"@remix-run/router@1.6.3":
version "1.6.3"
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.6.3.tgz#8205baf6e17ef93be35bf62c37d2d594e9be0dad"
@ -3218,6 +3273,11 @@
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa"
integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==
"@types/long@^4.0.1":
version "4.0.2"
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a"
integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==
"@types/mdast@^3.0.0":
version "3.0.10"
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af"
@ -3268,6 +3328,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.22.tgz#fd2a15dca290fc9ad565b672fde746191cd0c6e6"
integrity sha512-qzaYbXVzin6EPjghf/hTdIbnVW1ErMx8rPzwRNJhlbyJhu2SyqlvjGOY/tbUt6VFyzg56lROcOeSQRInpt63Yw==
"@types/node@>=13.7.0":
version "20.3.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.1.tgz#e8a83f1aa8b649377bb1fb5d7bac5cb90e784dfe"
integrity sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==
"@types/node@^13.7.0":
version "13.13.52"
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.52.tgz#03c13be70b9031baaed79481c0c0cfb0045e53f7"
@ -3288,6 +3353,11 @@
resolved "https://registry.yarnpkg.com/@types/npmlog/-/npmlog-4.1.4.tgz#30eb872153c7ead3e8688c476054ddca004115f6"
integrity sha512-WKG4gTr8przEZBiJ5r3s8ZIAoMXNbOgQ+j/d5O4X3x6kZJRLNvyUJuUK/KoG3+8BaOHPhp2m7WC6JKKeovDSzQ==
"@types/object-hash@^3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@types/object-hash/-/object-hash-3.0.2.tgz#f3656433e6c6049571fc3fb3fb42f389af96c0eb"
integrity sha512-tfyXl1JPCf2hzIDK29gO7qGqJjThKBzg/Cn3bA68R9NmWdOx+f7k5mm4to/n43BHspCwcoUC6FU4NpUoK/h9bQ==
"@types/parse-json@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
@ -4513,6 +4583,11 @@ canvas@2.11.0:
nan "^2.17.0"
simple-get "^3.0.3"
case-anything@^2.1.10:
version "2.1.13"
resolved "https://registry.yarnpkg.com/case-anything/-/case-anything-2.1.13.tgz#0cdc16278cb29a7fcdeb072400da3f342ba329e9"
integrity sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==
ccount@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5"
@ -5041,6 +5116,11 @@ data-urls@^3.0.2:
whatwg-mimetype "^3.0.0"
whatwg-url "^11.0.0"
dataloader@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-1.4.0.tgz#bca11d867f5d3f1b9ed9f737bd15970c65dff5c8"
integrity sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==
date-fns@2.30.0:
version "2.30.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0"
@ -5225,6 +5305,11 @@ detect-indent@^6.1.0:
resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6"
integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==
detect-libc@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==
detect-libc@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd"
@ -5338,6 +5423,13 @@ dotenv@^16.0.0:
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07"
integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==
dprint-node@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/dprint-node/-/dprint-node-1.0.7.tgz#f571eaf61affb3a696cff1bdde78a021875ba540"
integrity sha512-NTZOW9A7ipb0n7z7nC3wftvsbceircwVHSgzobJsEQa+7RnOMbhrfX5IflA6CtC4GA63DSAiHYXa4JKEy9F7cA==
dependencies:
detect-libc "^1.0.3"
duplexify@^3.5.0, duplexify@^3.6.0:
version "3.7.1"
resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309"
@ -6320,7 +6412,7 @@ fs.realpath@^1.0.0:
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
fsevents@^2.3.2, fsevents@~2.3.2:
fsevents@2.3.2, fsevents@^2.3.2, fsevents@~2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
@ -8261,6 +8353,11 @@ log-symbols@^4.1.0:
chalk "^4.1.0"
is-unicode-supported "^0.1.0"
long@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==
longest-streak@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4"
@ -9545,17 +9642,10 @@ pkg-dir@^5.0.0:
dependencies:
find-up "^5.0.0"
playwright-core@1.29.2:
version "1.29.2"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.29.2.tgz#2e8347e7e8522409f22b244e600e703b64022406"
integrity sha512-94QXm4PMgFoHAhlCuoWyaBYKb92yOcGVHdQLoxQ7Wjlc7Flg4aC/jbFW7xMR52OfXMVkWicue4WXE7QEegbIRA==
playwright@1.29.2:
version "1.29.2"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.29.2.tgz#d6a0a3e8e44f023f7956ed19ffa8af915a042769"
integrity sha512-hKBYJUtdmYzcjdhYDkP9WGtORwwZBBKAW8+Lz7sr0ZMxtJr04ASXVzH5eBWtDkdb0c3LLFsehfPBTRfvlfKJOA==
dependencies:
playwright-core "1.29.2"
playwright-core@1.35.1:
version "1.35.1"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.35.1.tgz#52c1e6ffaa6a8c29de1a5bdf8cce0ce290ffb81d"
integrity sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==
pluralize@^7.0.0:
version "7.0.0"
@ -9727,6 +9817,25 @@ property-information@^6.0.0:
resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.2.0.tgz#b74f522c31c097b5149e3c3cb8d7f3defd986a1d"
integrity sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg==
protobufjs@^6.11.3, protobufjs@^6.8.8:
version "6.11.3"
resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.3.tgz#637a527205a35caa4f3e2a9a4a13ddffe0e7af74"
integrity sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==
dependencies:
"@protobufjs/aspromise" "^1.1.2"
"@protobufjs/base64" "^1.1.2"
"@protobufjs/codegen" "^2.0.4"
"@protobufjs/eventemitter" "^1.1.0"
"@protobufjs/fetch" "^1.1.0"
"@protobufjs/float" "^1.0.2"
"@protobufjs/inquire" "^1.1.0"
"@protobufjs/path" "^1.1.2"
"@protobufjs/pool" "^1.1.0"
"@protobufjs/utf8" "^1.1.0"
"@types/long" "^4.0.1"
"@types/node" ">=13.7.0"
long "^4.0.0"
proxy-addr@~2.0.7:
version "2.0.7"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
@ -11330,6 +11439,34 @@ ts-morph@^13.0.1:
"@ts-morph/common" "~0.12.3"
code-block-writer "^11.0.0"
ts-poet@^6.4.1:
version "6.4.1"
resolved "https://registry.yarnpkg.com/ts-poet/-/ts-poet-6.4.1.tgz#e68d314a07cf9c0d568a3bfd87023ec91ff77964"
integrity sha512-AjZEs4h2w4sDfwpHMxQKHrTlNh2wRbM5NRXmLz0RiH+yPGtSQFbe9hBpNocU8vqVNgfh0BIOiXR80xDz3kKxUQ==
dependencies:
dprint-node "^1.0.7"
ts-proto-descriptors@1.9.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/ts-proto-descriptors/-/ts-proto-descriptors-1.9.0.tgz#0ed5631f11851846c8de21be2bff346719edce71"
integrity sha512-Ui8zA5Q4Jnq6JIGRraUWvECrqixxtwwin8GkhIkvwCpR+JcSPsxWe8HfTj5eHfyruGYI6Zjf96XlC87hTakHfQ==
dependencies:
long "^4.0.0"
protobufjs "^6.8.8"
ts-proto@1.150.0:
version "1.150.0"
resolved "https://registry.yarnpkg.com/ts-proto/-/ts-proto-1.150.0.tgz#41b9a737caa5bc242274eda01749e4ecfc1a4fa2"
integrity sha512-EYnKWkNkWRmnK2nG5D9J1zN959YD/gff+e8/nqpOw7kdrfsnCe1dr/+EYxiRhPnq/umVpBLwI/KaSDg9G+9KPA==
dependencies:
"@types/object-hash" "^3.0.2"
case-anything "^2.1.10"
dataloader "^1.4.0"
object-hash "^3.0.0"
protobufjs "^6.11.3"
ts-poet "^6.4.1"
ts-proto-descriptors "1.9.0"
ts-prune@0.10.3:
version "0.10.3"
resolved "https://registry.yarnpkg.com/ts-prune/-/ts-prune-0.10.3.tgz#b6c71a525543b38dcf947a7d3adfb7f9e8b91f38"