Skip to content

Commit

Permalink
Add exec nodes
Browse files Browse the repository at this point in the history
  • Loading branch information
Dartoxian committed Feb 14, 2024
1 parent 9aeba87 commit ad8c026
Show file tree
Hide file tree
Showing 12 changed files with 235 additions and 44 deletions.
4 changes: 2 additions & 2 deletions docs/docs/api-reference/starlark-reference/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -557,15 +557,15 @@ 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

service_two = plan.add_service(
...,
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
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const EnclaveBuilderModal = (props: EnclaveBuilderModalProps) => {
edges: Edge<any>[];
data: Record<string, KurtosisNodeData>;
} => {
variableContextKey.current += 1;
const parseResult = getInitialGraphStateFromEnclave<KurtosisNodeData>(props.existingEnclave);
if (parseResult.isErr) {
setError(parseResult.error);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,41 @@
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";

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 (
<KurtosisNode
id={id}
selected={selected}
name={(data[id] as KurtosisArtifactNodeData).artifactName}
name={nodeData.artifactName}
color={"yellow.900"}
minWidth={300}
maxWidth={800}
>
<KurtosisFormControl<KurtosisArtifactNodeData> name={"artifactName"} label={"Artifact Name"} isRequired>
<StringArgumentInput size={"sm"} name={"artifactName"} isRequired />
<StringArgumentInput size={"sm"} name={"artifactName"} isRequired validate={validateName} />
</KurtosisFormControl>
<KurtosisFormControl name={"files"} label={"Files"}>
<FileTreeArgumentInput name={"files"} />
</KurtosisFormControl>
</KurtosisNode>
);
},
(oldProps, newProps) => oldProps.id !== newProps.id && oldProps.selected !== newProps.selected,
(oldProps, newProps) => oldProps.id !== newProps.id || oldProps.selected !== newProps.selected,
);
Original file line number Diff line number Diff line change
@@ -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 (
<KurtosisNode
id={id}
selected={selected}
name={nodeData.execName}
color={"purple.900"}
minWidth={300}
maxWidth={800}
>
<KurtosisFormControl<KurtosisExecNodeData> name={"execName"} label={"Exec Name"} isRequired>
<StringArgumentInput size={"sm"} name={"execName"} isRequired validate={validateName} />
</KurtosisFormControl>
<Tabs>
<TabList>
<Tab>Config</Tab>
<Tab>Advanced</Tab>
</TabList>
<TabPanels>
<TabPanel>
{" "}
<KurtosisFormControl<KurtosisServiceNodeData>
name={"serviceName"}
label={"Service"}
helperText={"Choose which service to run this command in."}
isRequired
>
<SelectArgumentInput<KurtosisServiceNodeData>
options={serviceVariableOptions}
isRequired
size={"sm"}
placeholder={"Select a Service"}
name={`serviceName`}
/>
</KurtosisFormControl>
<KurtosisFormControl<KurtosisExecNodeData> name={"command"} label={"Command"} isRequired>
<MentionStringArgumentInput size={"sm"} name={"command"} isRequired />
</KurtosisFormControl>
</TabPanel>
<TabPanel>
<KurtosisFormControl<KurtosisExecNodeData>
name={"acceptableCodes"}
label={"Acceptable Exit Codes"}
isRequired
>
<ListArgumentInput<KurtosisExecNodeData>
FieldComponent={AcceptableCodeInput}
size={"sm"}
name={"acceptableCodes"}
createNewValue={() => ({ value: 0 })}
isRequired
/>
</KurtosisFormControl>
</TabPanel>
</TabPanels>
</Tabs>
</KurtosisNode>
);
},
(oldProps, newProps) => oldProps.id !== newProps.id || oldProps.selected !== newProps.selected,
);

const AcceptableCodeInput = (props: KurtosisFormInputProps<KurtosisExecNodeData>) => {
return (
<IntegerArgumentInput<KurtosisExecNodeData>
{...props}
size={"sm"}
name={`${props.name as `acceptableCodes.${number}`}.value`}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -28,26 +28,10 @@ export const KurtosisServiceNode = memo(
>
<Flex gap={"16px"}>
<KurtosisFormControl<KurtosisServiceNodeData> name={"serviceName"} label={"Service Name"} isRequired>
<StringArgumentInput name={"serviceName"} size={"sm"} isRequired validate={validateDockerLocator} />
<StringArgumentInput name={"serviceName"} size={"sm"} isRequired validate={validateName} />
</KurtosisFormControl>
<KurtosisFormControl<KurtosisServiceNodeData> name={"image"} label={"Container Image"} isRequired>
<StringArgumentInput
size={"sm"}
name={"image"}
isRequired
validate={(val) => {
if (typeof val !== "string") {
return "Value should be a string";
}
if (
!val.match(
/^(?<repository>[\w.\-_]+((?::\d+|)(?=\/[a-z0-9._-]+\/[a-z0-9._-]+))|)(?:\/|)(?<image>[a-z0-9.\-_]+(?:\/[a-z0-9.\-_]+|))(:(?<tag>[\w.\-_]{1,127})|)$/gim,
)
) {
return "Value does not look like a docker image";
}
}}
/>
<StringArgumentInput size={"sm"} name={"image"} isRequired validate={validateDockerLocator} />
</KurtosisFormControl>
</Flex>
<Tabs>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -55,7 +55,7 @@ export const KurtosisShellNode = memo(
>
<Flex gap={"16px"}>
<KurtosisFormControl<KurtosisShellNodeData> name={"shellName"} label={"Shell Name"} isRequired>
<StringArgumentInput name={"shellName"} size={"sm"} isRequired />
<StringArgumentInput name={"shellName"} size={"sm"} isRequired validate={validateName} />
</KurtosisFormControl>
<KurtosisFormControl<KurtosisShellNodeData> name={"image"} label={"Container Image"}>
<StringArgumentInput
Expand Down Expand Up @@ -105,16 +105,17 @@ export const KurtosisShellNode = memo(
/>
</KurtosisFormControl>
<KurtosisFormControl<KurtosisShellNodeData>
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
>
<ListArgumentInput<KurtosisShellNodeData>
<MentionStringArgumentInput<KurtosisShellNodeData>
name={"store"}
FieldComponent={MemodMentionStringValueArgumentInput}
createNewValue={() => ({
value: "",
})}
placeholder={"/some/output/location"}
isRequired
/>
</KurtosisFormControl>
</TabPanel>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -51,6 +52,7 @@ const nodeTypes = {
serviceNode: KurtosisServiceNode,
artifactNode: KurtosisArtifactNode,
shellNode: KurtosisShellNode,
execNode: KurtosisExecNode,
};

export type VisualiserImperativeAttributes = {
Expand Down Expand Up @@ -129,7 +131,7 @@ export const Visualiser = forwardRef<VisualiserImperativeAttributes, VisualiserP
image: "",
env: [],
files: [],
store: [],
store: "",
wait_enabled: "true",
wait: "",
isValid: false,
Expand All @@ -144,6 +146,26 @@ export const Visualiser = forwardRef<VisualiserImperativeAttributes, VisualiserP
});
};

const handleAddExecNode = () => {
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 });
}, []);
Expand Down Expand Up @@ -206,6 +228,9 @@ export const Visualiser = forwardRef<VisualiserImperativeAttributes, VisualiserP
<Button leftIcon={<FiPlusCircle />} onClick={handleAddShellNode}>
Add Shell Node
</Button>
<Button leftIcon={<FiPlusCircle />} onClick={handleAddExecNode}>
Add Exec Node
</Button>
</ButtonGroup>
<Box bg={"gray.900"} flex={"1"}>
<ReactFlow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ export const MentionStringArgumentInput = <DataModel extends object>({
}
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],
);
Expand Down Expand Up @@ -56,7 +58,7 @@ export const MentionStringArgumentInput = <DataModel extends object>({
>
<Mention
className={"mentions__mention"}
trigger={/((?:@)?(\S\S.*))$/}
trigger={/(?<=^|.*\s)((\S\S+))$/}
markup={"{{__id__}}"}
data={handleQuery}
displayTransform={(id) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const MountArtifactFileInput = (props: KurtosisFormInputProps<KurtosisSer
const { variables } = useVariableContext();
const artifactVariableOptions = useMemo((): SelectOption[] => {
return variables
.filter((variable) => variable.id.startsWith("artifact"))
.filter((variable) => variable.id.match(/^(?:artifact|shell)\.[^.]+$/))
.map((variable) => ({ display: variable.displayName, value: `{{${variable.id}}}` }));
}, [variables]);

Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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;
}
}
};
}
Loading

0 comments on commit ad8c026

Please sign in to comment.