mirror of https://github.com/coder/coder.git
358 lines
9.7 KiB
TypeScript
358 lines
9.7 KiB
TypeScript
import {
|
|
ProvisionerJobLog,
|
|
ProvisionerJobStatus,
|
|
TemplateVersion,
|
|
UploadResponse,
|
|
WorkspaceResource,
|
|
} from "api/typesGenerated"
|
|
import { assign, createMachine } from "xstate"
|
|
import * as API from "api/api"
|
|
import { FileTree, traverse } from "util/filetree"
|
|
import { isAllowedFile } from "util/templateVersion"
|
|
import { TarReader, TarWriter } from "util/tar"
|
|
import { PublishVersionData } from "pages/TemplateVersionPage/TemplateVersionEditorPage/types"
|
|
|
|
export interface CreateVersionData {
|
|
file: File
|
|
}
|
|
|
|
export interface TemplateVersionEditorMachineContext {
|
|
orgId: string
|
|
templateId?: string
|
|
fileTree?: FileTree
|
|
uploadResponse?: UploadResponse
|
|
version?: TemplateVersion
|
|
resources?: WorkspaceResource[]
|
|
buildLogs?: ProvisionerJobLog[]
|
|
tarReader?: TarReader
|
|
publishingError?: unknown
|
|
}
|
|
|
|
export const templateVersionEditorMachine = createMachine(
|
|
{
|
|
predictableActionArguments: true,
|
|
id: "templateVersionEditor",
|
|
schema: {
|
|
context: {} as TemplateVersionEditorMachineContext,
|
|
events: {} as
|
|
| { type: "INITIALIZE"; tarReader: TarReader }
|
|
| {
|
|
type: "CREATE_VERSION"
|
|
fileTree: FileTree
|
|
templateId: string
|
|
}
|
|
| { type: "CANCEL_VERSION" }
|
|
| { type: "ADD_BUILD_LOG"; log: ProvisionerJobLog }
|
|
| { type: "PUBLISH" }
|
|
| ({ type: "CONFIRM_PUBLISH" } & PublishVersionData)
|
|
| { type: "CANCEL_PUBLISH" },
|
|
|
|
services: {} as {
|
|
uploadTar: {
|
|
data: UploadResponse
|
|
}
|
|
createBuild: {
|
|
data: TemplateVersion
|
|
}
|
|
cancelBuild: {
|
|
data: void
|
|
}
|
|
fetchVersion: {
|
|
data: TemplateVersion
|
|
}
|
|
getResources: {
|
|
data: WorkspaceResource[]
|
|
}
|
|
publishingVersion: {
|
|
data: void
|
|
}
|
|
},
|
|
},
|
|
tsTypes: {} as import("./templateVersionEditorXService.typegen").Typegen0,
|
|
initial: "initializing",
|
|
states: {
|
|
initializing: {
|
|
on: {
|
|
INITIALIZE: {
|
|
actions: ["assignTarReader"],
|
|
target: "idle",
|
|
},
|
|
},
|
|
},
|
|
idle: {
|
|
on: {
|
|
CREATE_VERSION: {
|
|
actions: ["assignCreateBuild"],
|
|
target: "cancelingBuild",
|
|
},
|
|
PUBLISH: {
|
|
target: "askPublishParameters",
|
|
},
|
|
},
|
|
},
|
|
askPublishParameters: {
|
|
on: {
|
|
CANCEL_PUBLISH: "idle",
|
|
CONFIRM_PUBLISH: "publishingVersion",
|
|
},
|
|
},
|
|
publishingVersion: {
|
|
tags: "loading",
|
|
entry: ["clearPublishingError"],
|
|
invoke: {
|
|
id: "publishingVersion",
|
|
src: "publishingVersion",
|
|
onDone: {
|
|
actions: ["onPublish"],
|
|
},
|
|
onError: {
|
|
actions: ["assignPublishingError"],
|
|
target: "askPublishParameters",
|
|
},
|
|
},
|
|
},
|
|
cancelingBuild: {
|
|
tags: "loading",
|
|
invoke: {
|
|
id: "cancelBuild",
|
|
src: "cancelBuild",
|
|
onDone: {
|
|
target: "uploadTar",
|
|
},
|
|
},
|
|
},
|
|
uploadTar: {
|
|
tags: "loading",
|
|
invoke: {
|
|
id: "uploadTar",
|
|
src: "uploadTar",
|
|
onDone: {
|
|
target: "creatingBuild",
|
|
actions: "assignUploadResponse",
|
|
},
|
|
},
|
|
},
|
|
creatingBuild: {
|
|
tags: "loading",
|
|
invoke: {
|
|
id: "createBuild",
|
|
src: "createBuild",
|
|
onDone: {
|
|
actions: "assignBuild",
|
|
target: "watchingBuildLogs",
|
|
},
|
|
},
|
|
},
|
|
watchingBuildLogs: {
|
|
tags: "loading",
|
|
invoke: {
|
|
id: "watchBuildLogs",
|
|
src: "watchBuildLogs",
|
|
onDone: {
|
|
target: "fetchingVersion",
|
|
},
|
|
},
|
|
on: {
|
|
ADD_BUILD_LOG: {
|
|
actions: "addBuildLog",
|
|
},
|
|
CANCEL_VERSION: {
|
|
target: "cancelingBuild",
|
|
},
|
|
CREATE_VERSION: {
|
|
actions: ["assignCreateBuild"],
|
|
target: "uploadTar",
|
|
},
|
|
},
|
|
},
|
|
fetchingVersion: {
|
|
tags: "loading",
|
|
invoke: {
|
|
id: "fetchVersion",
|
|
src: "fetchVersion",
|
|
onDone: {
|
|
actions: ["assignBuild"],
|
|
target: "fetchResources",
|
|
},
|
|
},
|
|
},
|
|
fetchResources: {
|
|
tags: "loading",
|
|
invoke: {
|
|
id: "getResources",
|
|
src: "getResources",
|
|
onDone: {
|
|
actions: ["assignResources"],
|
|
target: "idle",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
actions: {
|
|
assignCreateBuild: assign({
|
|
fileTree: (_, event) => event.fileTree,
|
|
templateId: (_, event) => event.templateId,
|
|
buildLogs: (_, _1) => [],
|
|
resources: (_, _1) => [],
|
|
}),
|
|
assignResources: assign({
|
|
resources: (_, event) => event.data,
|
|
}),
|
|
assignUploadResponse: assign({
|
|
uploadResponse: (_, event) => event.data,
|
|
}),
|
|
assignBuild: assign({
|
|
version: (_, event) => event.data,
|
|
}),
|
|
addBuildLog: assign({
|
|
buildLogs: (context, event) => {
|
|
const previousLogs = context.buildLogs ?? []
|
|
return [...previousLogs, event.log]
|
|
},
|
|
// Instead of periodically fetching the version,
|
|
// we just assume the state is running after the first log.
|
|
//
|
|
// The machine fetches the version after the log stream ends anyways!
|
|
version: (context) => {
|
|
if (!context.version || context.buildLogs?.length !== 0) {
|
|
return context.version
|
|
}
|
|
return {
|
|
...context.version,
|
|
job: {
|
|
...context.version.job,
|
|
status: "running" as ProvisionerJobStatus,
|
|
},
|
|
}
|
|
},
|
|
}),
|
|
assignTarReader: assign({
|
|
tarReader: (_, { tarReader }) => tarReader,
|
|
}),
|
|
assignPublishingError: assign({
|
|
publishingError: (_, event) => event.data,
|
|
}),
|
|
clearPublishingError: assign({ publishingError: (_) => undefined }),
|
|
},
|
|
services: {
|
|
uploadTar: async ({ fileTree, tarReader }) => {
|
|
if (!fileTree) {
|
|
throw new Error("file tree must to be set")
|
|
}
|
|
if (!tarReader) {
|
|
throw new Error("tar reader must to be set")
|
|
}
|
|
const tar = new TarWriter()
|
|
|
|
// Add previous non editable files
|
|
for (const file of tarReader.fileInfo) {
|
|
if (!isAllowedFile(file.name)) {
|
|
if (file.type === "5") {
|
|
tar.addFolder(file.name, {
|
|
mode: file.mode, // https://github.com/beatgammit/tar-js/blob/master/lib/tar.js#L42
|
|
mtime: file.mtime,
|
|
user: file.user,
|
|
group: file.group,
|
|
})
|
|
} else {
|
|
tar.addFile(
|
|
file.name,
|
|
tarReader.getTextFile(file.name) as string,
|
|
{
|
|
mode: file.mode, // https://github.com/beatgammit/tar-js/blob/master/lib/tar.js#L42
|
|
mtime: file.mtime,
|
|
user: file.user,
|
|
group: file.group,
|
|
},
|
|
)
|
|
}
|
|
}
|
|
}
|
|
// Add the editable files
|
|
traverse(fileTree, (content, _filename, fullPath) => {
|
|
// When a file is deleted. Don't add it to the tar.
|
|
if (content === undefined) {
|
|
return
|
|
}
|
|
|
|
if (typeof content === "string") {
|
|
tar.addFile(fullPath, content)
|
|
return
|
|
}
|
|
|
|
tar.addFolder(fullPath)
|
|
})
|
|
const blob = await tar.write()
|
|
return API.uploadTemplateFile(new File([blob], "template.tar"))
|
|
},
|
|
createBuild: (ctx) => {
|
|
if (!ctx.uploadResponse) {
|
|
throw new Error("no upload response")
|
|
}
|
|
return API.createTemplateVersion(ctx.orgId, {
|
|
provisioner: "terraform",
|
|
storage_method: "file",
|
|
tags: {},
|
|
template_id: ctx.templateId,
|
|
file_id: ctx.uploadResponse.hash,
|
|
})
|
|
},
|
|
fetchVersion: (ctx) => {
|
|
if (!ctx.version) {
|
|
throw new Error("template version must be set")
|
|
}
|
|
return API.getTemplateVersion(ctx.version.id)
|
|
},
|
|
watchBuildLogs:
|
|
({ version }) =>
|
|
async (callback) => {
|
|
if (!version) {
|
|
throw new Error("version must be set")
|
|
}
|
|
|
|
return API.watchBuildLogs(version.id, (log) => {
|
|
callback({ type: "ADD_BUILD_LOG", log })
|
|
})
|
|
},
|
|
getResources: (ctx) => {
|
|
if (!ctx.version) {
|
|
throw new Error("template version must be set")
|
|
}
|
|
return API.getTemplateVersionResources(ctx.version.id)
|
|
},
|
|
cancelBuild: async (ctx) => {
|
|
if (!ctx.version) {
|
|
return
|
|
}
|
|
if (ctx.version.job.status === "running") {
|
|
await API.cancelTemplateVersionBuild(ctx.version.id)
|
|
}
|
|
},
|
|
publishingVersion: async (
|
|
{ version, templateId },
|
|
{ name, isActiveVersion },
|
|
) => {
|
|
if (!version) {
|
|
throw new Error("Version is not set")
|
|
}
|
|
if (!templateId) {
|
|
throw new Error("Template is not set")
|
|
}
|
|
await Promise.all([
|
|
// Only do a patch if the name is different
|
|
name !== version.name
|
|
? API.patchTemplateVersion(version.id, { name })
|
|
: Promise.resolve(),
|
|
isActiveVersion
|
|
? API.updateActiveTemplateVersion(templateId, {
|
|
id: version.id,
|
|
})
|
|
: Promise.resolve(),
|
|
])
|
|
},
|
|
},
|
|
},
|
|
)
|