From ad8c026de698e65a9a7f6afe3ac6659db1fa6d42 Mon Sep 17 00:00:00 2001 From: Ben Gazzard Date: Wed, 14 Feb 2024 18:08:44 +0000 Subject: [PATCH] Add exec nodes --- .../api-reference/starlark-reference/plan.md | 4 +- .../components/modals/EnclaveBuilderModal.tsx | 1 + .../enclaveBuilder/KurtosisArtifactNode.tsx | 14 ++- .../enclaveBuilder/KurtosisExecNode.tsx | 105 ++++++++++++++++++ .../enclaveBuilder/KurtosisServiceNode.tsx | 22 +--- .../enclaveBuilder/KurtosisShellNode.tsx | 21 ++-- .../modals/enclaveBuilder/Visualiser.tsx | 27 ++++- .../input/MentionStringArgumentInput.tsx | 6 +- .../input/MountArtifactFileInput.tsx | 2 +- .../modals/enclaveBuilder/input/validators.ts | 19 ++++ .../components/modals/enclaveBuilder/types.ts | 22 +++- .../components/modals/enclaveBuilder/utils.ts | 36 +++++- 12 files changed, 235 insertions(+), 44 deletions(-) create mode 100644 enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/KurtosisExecNode.tsx diff --git a/docs/docs/api-reference/starlark-reference/plan.md b/docs/docs/api-reference/starlark-reference/plan.md index 34d18ee71e..99b570c1ec 100644 --- a/docs/docs/api-reference/starlark-reference/plan.md +++ b/docs/docs/api-reference/starlark-reference/plan.md @@ -557,7 +557,7 @@ The instruction returns a `struct` with [future references][future-references-re ..., config=ServiceConfig( name="service_one", - files={"/src": results.file_artifacts[0]}, # copies the directory task into service_one + files={"/src": result.file_artifacts[0]}, # copies the directory task into service_one ) ) # the path to the file will look like: /src/task/test.txt @@ -565,7 +565,7 @@ The instruction returns a `struct` with [future references][future-references-re ..., config=ServiceConfig( name="service_two", - files={"/src": results.file_artifacts[1]}, # copies the file test.txt into service_two + files={"/src": result.file_artifacts[1]}, # copies the file test.txt into service_two ), ) # the path to the file will look like: /src/test.txt ``` diff --git a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/EnclaveBuilderModal.tsx b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/EnclaveBuilderModal.tsx index 56de1295e9..f9a5844698 100644 --- a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/EnclaveBuilderModal.tsx +++ b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/EnclaveBuilderModal.tsx @@ -46,6 +46,7 @@ export const EnclaveBuilderModal = (props: EnclaveBuilderModalProps) => { edges: Edge[]; data: Record; } => { + variableContextKey.current += 1; const parseResult = getInitialGraphStateFromEnclave(props.existingEnclave); if (parseResult.isErr) { setError(parseResult.error); diff --git a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/KurtosisArtifactNode.tsx b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/KurtosisArtifactNode.tsx index 0855d43f09..84fb5d78b1 100644 --- a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/KurtosisArtifactNode.tsx +++ b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/KurtosisArtifactNode.tsx @@ -1,8 +1,10 @@ +import { isDefined } from "kurtosis-ui-components"; import { memo } from "react"; import { NodeProps } from "reactflow"; import { KurtosisFormControl } from "../../form/KurtosisFormControl"; import { StringArgumentInput } from "../../form/StringArgumentInput"; import { FileTreeArgumentInput } from "./input/FileTreeArgumentInput"; +import { validateName } from "./input/validators"; import { KurtosisNode } from "./KurtosisNode"; import { KurtosisArtifactNodeData } from "./types"; import { useVariableContext } from "./VariableContextProvider"; @@ -10,18 +12,24 @@ import { useVariableContext } from "./VariableContextProvider"; export const KurtosisArtifactNode = memo( ({ id, selected }: NodeProps) => { const { data } = useVariableContext(); + const nodeData = data[id] as KurtosisArtifactNodeData; + + if (!isDefined(nodeData)) { + // Node has probably been deleted. + return null; + } return ( name={"artifactName"} label={"Artifact Name"} isRequired> - + @@ -29,5 +37,5 @@ export const KurtosisArtifactNode = memo( ); }, - (oldProps, newProps) => oldProps.id !== newProps.id && oldProps.selected !== newProps.selected, + (oldProps, newProps) => oldProps.id !== newProps.id || oldProps.selected !== newProps.selected, ); diff --git a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/KurtosisExecNode.tsx b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/KurtosisExecNode.tsx new file mode 100644 index 0000000000..d24ff1b9f3 --- /dev/null +++ b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/KurtosisExecNode.tsx @@ -0,0 +1,105 @@ +import { Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react"; +import { isDefined } from "kurtosis-ui-components"; +import { memo, useMemo } from "react"; +import { NodeProps } from "reactflow"; +import { IntegerArgumentInput } from "../../form/IntegerArgumentInput"; +import { KurtosisFormControl } from "../../form/KurtosisFormControl"; +import { ListArgumentInput } from "../../form/ListArgumentInput"; +import { SelectArgumentInput, SelectOption } from "../../form/SelectArgumentInput"; +import { StringArgumentInput } from "../../form/StringArgumentInput"; +import { KurtosisFormInputProps } from "../../form/types"; +import { MentionStringArgumentInput } from "./input/MentionStringArgumentInput"; +import { validateName } from "./input/validators"; +import { KurtosisNode } from "./KurtosisNode"; +import { KurtosisExecNodeData, KurtosisServiceNodeData } from "./types"; +import { useVariableContext } from "./VariableContextProvider"; + +export const KurtosisExecNode = memo( + ({ id, selected }: NodeProps) => { + const { data, variables } = useVariableContext(); + const nodeData = data[id] as KurtosisExecNodeData; + + const serviceVariableOptions = useMemo((): SelectOption[] => { + return variables + .filter((variable) => variable.id.match(/^service\.[^.]+\.name+$/)) + .map((variable) => ({ + display: variable.displayName.replace(/service\.(.*)\.name/, "$1"), + value: `{{${variable.id}}}`, + })); + }, [variables]); + + if (!isDefined(nodeData)) { + // Node has probably been deleted. + return null; + } + + return ( + + name={"execName"} label={"Exec Name"} isRequired> + + + + + Config + Advanced + + + + {" "} + + name={"serviceName"} + label={"Service"} + helperText={"Choose which service to run this command in."} + isRequired + > + + options={serviceVariableOptions} + isRequired + size={"sm"} + placeholder={"Select a Service"} + name={`serviceName`} + /> + + name={"command"} label={"Command"} isRequired> + + + + + + name={"acceptableCodes"} + label={"Acceptable Exit Codes"} + isRequired + > + + FieldComponent={AcceptableCodeInput} + size={"sm"} + name={"acceptableCodes"} + createNewValue={() => ({ value: 0 })} + isRequired + /> + + + + + + ); + }, + (oldProps, newProps) => oldProps.id !== newProps.id || oldProps.selected !== newProps.selected, +); + +const AcceptableCodeInput = (props: KurtosisFormInputProps) => { + return ( + + {...props} + size={"sm"} + name={`${props.name as `acceptableCodes.${number}`}.value`} + /> + ); +}; diff --git a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/KurtosisServiceNode.tsx b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/KurtosisServiceNode.tsx index 20bf341087..5ddb6abb4b 100644 --- a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/KurtosisServiceNode.tsx +++ b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/KurtosisServiceNode.tsx @@ -8,7 +8,7 @@ import { StringArgumentInput } from "../../form/StringArgumentInput"; import { MentionStringArgumentInput } from "./input/MentionStringArgumentInput"; import { MountArtifactFileInput } from "./input/MountArtifactFileInput"; import { PortConfigurationField } from "./input/PortConfigurationInput"; -import { validateDockerLocator } from "./input/validators"; +import { validateDockerLocator, validateName } from "./input/validators"; import { KurtosisNode } from "./KurtosisNode"; import { KurtosisFileMount, KurtosisPort, KurtosisServiceNodeData } from "./types"; import { useVariableContext } from "./VariableContextProvider"; @@ -28,26 +28,10 @@ export const KurtosisServiceNode = memo( > name={"serviceName"} label={"Service Name"} isRequired> - + name={"image"} label={"Container Image"} isRequired> - { - if (typeof val !== "string") { - return "Value should be a string"; - } - if ( - !val.match( - /^(?[\w.\-_]+((?::\d+|)(?=\/[a-z0-9._-]+\/[a-z0-9._-]+))|)(?:\/|)(?[a-z0-9.\-_]+(?:\/[a-z0-9.\-_]+|))(:(?[\w.\-_]{1,127})|)$/gim, - ) - ) { - return "Value does not look like a docker image"; - } - }} - /> + diff --git a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/KurtosisShellNode.tsx b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/KurtosisShellNode.tsx index 023e2a3d0e..3282c761c6 100644 --- a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/KurtosisShellNode.tsx +++ b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/KurtosisShellNode.tsx @@ -12,7 +12,7 @@ import { StringArgumentInput } from "../../form/StringArgumentInput"; import { KurtosisFormInputProps } from "../../form/types"; import { MentionStringArgumentInput } from "./input/MentionStringArgumentInput"; import { MountArtifactFileInput } from "./input/MountArtifactFileInput"; -import { validateDockerLocator, validateDurationString } from "./input/validators"; +import { validateDockerLocator, validateDurationString, validateName } from "./input/validators"; import { KurtosisNode } from "./KurtosisNode"; import { KurtosisFileMount, KurtosisShellNodeData } from "./types"; import { useVariableContext } from "./VariableContextProvider"; @@ -55,7 +55,7 @@ export const KurtosisShellNode = memo( > name={"shellName"} label={"Shell Name"} isRequired> - + name={"image"} label={"Container Image"}> - name={"files"} - label={"Output Files"} - helperText={"Choose which files to expose from this execution task"} + name={"store"} + label={"Output File/Directory"} + helperText={ + "Choose which files to expose from this execution task. You can use either an absolute path, a directory, or a glob." + } + isRequired > - + name={"store"} - FieldComponent={MemodMentionStringValueArgumentInput} - createNewValue={() => ({ - value: "", - })} + placeholder={"/some/output/location"} + isRequired /> diff --git a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/Visualiser.tsx b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/Visualiser.tsx index cd9ef68321..07b2f2c6cd 100644 --- a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/Visualiser.tsx +++ b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/Visualiser.tsx @@ -18,6 +18,7 @@ import { import { v4 as uuidv4 } from "uuid"; import { EnclaveFullInfo } from "../../../types"; import { KurtosisArtifactNode } from "./KurtosisArtifactNode"; +import { KurtosisExecNode } from "./KurtosisExecNode"; import { KurtosisServiceNode } from "./KurtosisServiceNode"; import { KurtosisShellNode } from "./KurtosisShellNode"; import { generateStarlarkFromGraph, getNodeDependencies } from "./utils"; @@ -51,6 +52,7 @@ const nodeTypes = { serviceNode: KurtosisServiceNode, artifactNode: KurtosisArtifactNode, shellNode: KurtosisShellNode, + execNode: KurtosisExecNode, }; export type VisualiserImperativeAttributes = { @@ -129,7 +131,7 @@ export const Visualiser = forwardRef { + const id = uuidv4(); + updateData(id, { + type: "exec", + execName: "", + serviceName: "", + command: "", + acceptableCodes: [], + isValid: false, + }); + addNodes({ + id, + position: getNewNodePosition(), + width: 400, + style: { width: "400px" }, + type: "execNode", + data: {}, + }); + }; + const handleNodeDoubleClick = useCallback((e: React.MouseEvent, node: Node) => { fitView({ nodes: [node], maxZoom: 1, duration: 500 }); }, []); @@ -206,6 +228,9 @@ export const Visualiser = forwardRef} onClick={handleAddShellNode}> Add Shell Node + ({ } const suggestions = variables.map((v) => ({ display: v.displayName, id: v.id })); const queryTerms = query.toLowerCase().split(/\s+|\./); - return suggestions.filter((variable) => queryTerms.every((term) => variable.display.includes(term))); + return suggestions.filter((variable) => + queryTerms.every((term) => variable.display.toLowerCase().includes(term)), + ); }, [variables], ); @@ -56,7 +58,7 @@ export const MentionStringArgumentInput = ({ > diff --git a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/input/MountArtifactFileInput.tsx b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/input/MountArtifactFileInput.tsx index 631b9d84cc..8f0631044b 100644 --- a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/input/MountArtifactFileInput.tsx +++ b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/input/MountArtifactFileInput.tsx @@ -10,7 +10,7 @@ export const MountArtifactFileInput = (props: KurtosisFormInputProps { return variables - .filter((variable) => variable.id.startsWith("artifact")) + .filter((variable) => variable.id.match(/^(?:artifact|shell)\.[^.]+$/)) .map((variable) => ({ display: variable.displayName, value: `{{${variable.id}}}` })); }, [variables]); diff --git a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/input/validators.ts b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/input/validators.ts index d2c1475ca4..528f1226d7 100644 --- a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/input/validators.ts +++ b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/input/validators.ts @@ -1,3 +1,11 @@ +export function validateName(value?: string) { + if (typeof value !== "string") { + return "Value should be a string"; + } + if (value.match(/^\d+/)) { + return "Value cannot start with numbers"; + } +} export function validateDockerLocator(value?: string) { if (typeof value !== "string") { return "Value should be a string"; @@ -27,3 +35,14 @@ export function validateDurationString(value?: string) { return "Value should be a custom wait duration with like '10s' or '3m'."; } } + +export function combineValidators(...validators: ((v?: string) => string | void)[]): (v?: string) => string | void { + return function (v?: string) { + for (const validator of validators) { + const r = validator(v); + if (r) { + return r; + } + } + }; +} diff --git a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/types.ts b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/types.ts index 52632d1d7a..b11df47b8e 100644 --- a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/types.ts +++ b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/types.ts @@ -17,6 +17,20 @@ export type KurtosisFileMount = { mountPoint: string; artifactName: string; }; + +export type KurtosisAcceptableCode = { + value: number; +}; + +export type KurtosisExecNodeData = { + type: "exec"; + execName: string; + serviceName: string; + command: string; + acceptableCodes: KurtosisAcceptableCode[]; + isValid: boolean; +}; + export type KurtosisServiceNodeData = { type: "service"; serviceName: string; @@ -40,10 +54,14 @@ export type KurtosisShellNodeData = { image: string; env: KurtosisEnvironmentVar[]; files: KurtosisFileMount[]; - store: { value: string }[]; + store: string; wait_enabled: "true" | "false"; wait: string; isValid: boolean; }; -export type KurtosisNodeData = KurtosisArtifactNodeData | KurtosisServiceNodeData | KurtosisShellNodeData; +export type KurtosisNodeData = + | KurtosisArtifactNodeData + | KurtosisServiceNodeData + | KurtosisShellNodeData + | KurtosisExecNodeData; diff --git a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/utils.ts b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/utils.ts index 2eded3eb8f..87e53645a2 100644 --- a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/utils.ts +++ b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/utils.ts @@ -45,6 +45,9 @@ export function getNodeName(kurtosisNodeData: KurtosisNodeData): string { if (kurtosisNodeData.type === "shell") { return kurtosisNodeData.shellName; } + if (kurtosisNodeData.type === "exec") { + return kurtosisNodeData.execName; + } throw new Error(`Unknown node type.`); } @@ -116,7 +119,7 @@ export function getVariablesFromNodes(nodes: Record): { id: `shell.${id}`, displayName: `shell.${data.shellName}`, - value: `${normaliseNameToStarlarkVariable(data.shellName)}`, + value: `${normaliseNameToStarlarkVariable(data.shellName)}.files_artifacts[0]`, }, ...data.env.map((env, i) => ({ id: `shell.${id}.env.${i}`, @@ -181,6 +184,20 @@ export function getNodeDependencies(nodes: Record): Re } }); } + if (data.type === "exec") { + const nameMatches = data.execName.match(variablePattern); + if (nameMatches) { + getDependenciesFor(id).add(nameMatches[2]); + } + const serviceMatches = data.serviceName.match(variablePattern); + if (serviceMatches) { + getDependenciesFor(id).add(serviceMatches[2]); + } + const commandMatches = data.command.match(variablePattern); + if (commandMatches) { + getDependenciesFor(id).add(commandMatches[2]); + } + } }); return dependencies; } @@ -296,9 +313,7 @@ export function generateStarlarkFromGraph( } starlark += ` },\n`; starlark += ` store = [\n`; - for (const store of nodeData.store) { - starlark += ` ${interpolateValue(store.value)},\n`; - } + starlark += ` StoreSpec(src = ${interpolateValue(nodeData.store)}, name="${shellName}"),\n`; starlark += ` ],\n`; const wait = interpolateValue(nodeData.wait); if (nodeData.wait_enabled === "false" || wait !== '""') { @@ -306,6 +321,19 @@ export function generateStarlarkFromGraph( } starlark += ` )\n\n`; } + + if (nodeData.type === "exec") { + const execName = normaliseNameToStarlarkVariable(nodeData.execName); + starlark += ` ${execName} = plan.exec(\n`; + starlark += ` service_name = ${interpolateValue(nodeData.serviceName)},\n`; + starlark += ` recipe = ExecRecipe(\n`; + starlark += ` command = [${nodeData.command.split(" ").map(interpolateValue).join(", ")}],`; + starlark += ` ),\n`; + if (nodeData.acceptableCodes.length > 0) { + starlark += ` acceptable_codes = [${nodeData.acceptableCodes.map(({ value }) => value).join(", ")}],\n`; + } + starlark += ` )\n\n`; + } } // Delete any services from any existing enclave that aren't defined anymore