Skip to content

Commit

Permalink
feat: support run_sh and exec in enclave builder (#2158)
Browse files Browse the repository at this point in the history
## Description:
This PR adds support for `run_sh` and `exec` operations in the emui
enclave builder. The allows users to complete the 'getting started with
starlark' flow.

## Is this change user facing?
YES (experimental flag)

## References (if applicable):
* https://docs.kurtosis.com/api-reference/starlark-reference/plan
  • Loading branch information
Dartoxian authored Feb 14, 2024
1 parent da8b0b9 commit f784eaf
Show file tree
Hide file tree
Showing 18 changed files with 1,159 additions and 639 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
2 changes: 1 addition & 1 deletion docs/docs/guides/running-docker-compose.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ kurtosis run .
```
OR using github link:
```
kurtosis run github.com/awesome-compose/nextcloud-redis-mariadb
kurtosis run github.com/docker/awesome-compose/nextcloud-redis-mariadb
```

Behind the scenes, Kurtosis will interpret your Docker Compose setup as a Kurtosis [package](../get-started/basic-concepts.md#package) and convert it into [starlark](../advanced-concepts/starlark.md) that is executed on an [enclave](../get-started/basic-concepts.md#enclave). The output will look like this:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { CodeEditor } from "kurtosis-ui-components";
import { Controller } from "react-hook-form";
import { FieldPath, FieldValues } from "react-hook-form/dist/types";
import { ControllerRenderProps } from "react-hook-form/dist/types/controller";

import { KurtosisFormInputProps } from "./types";

type CodeEditorInputProps<DataModel extends object> = KurtosisFormInputProps<DataModel> & {
fileName: string;
};

export const CodeEditorInput = <DataModel extends object>(props: CodeEditorInputProps<DataModel>) => {
return (
<Controller
render={({ field }) => <CodeEditorInputImpl field={field} fileName={props.fileName} />}
name={props.name}
defaultValue={"" as any}
rules={{
required: props.isRequired,
validate: props.validate,
}}
disabled={props.disabled}
/>
);
};

type CodeEditorImplProps<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
field: ControllerRenderProps<TFieldValues, TName>;
fileName: string;
};

const CodeEditorInputImpl = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
field,
fileName,
}: CodeEditorImplProps<TFieldValues, TName>) => {
return <CodeEditor text={field.value} onTextChange={field.onChange} fileName={fileName} />;
};
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { Button, ButtonGroup, Flex, useToast } from "@chakra-ui/react";

import { CopyButton, PasteButton, stringifyError } from "kurtosis-ui-components";
import { ReactElement } from "react";
import { FC } from "react";
import { useFieldArray, useFormContext } from "react-hook-form";
import { FiDelete, FiPlus } from "react-icons/fi";
import { KurtosisSubtypeFormControl } from "./KurtosisFormControl";
import { KurtosisFormInputProps } from "./types";

type ListArgumentInputProps<DataModel extends object> = KurtosisFormInputProps<DataModel> & {
FieldComponent: (props: KurtosisFormInputProps<DataModel>) => ReactElement;
FieldComponent: FC<KurtosisFormInputProps<DataModel>>;
createNewValue: () => object;
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
Box,
Button,
ButtonGroup,
Flex,
Expand All @@ -15,41 +14,18 @@ import {
Tooltip,
UnorderedList,
} from "@chakra-ui/react";
import Dagre from "@dagrejs/dagre";
import { isDefined, KurtosisAlert, KurtosisAlertModal, RemoveFunctions, stringifyError } from "kurtosis-ui-components";
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
import { FiPlusCircle } from "react-icons/fi";
import { useEffect, useMemo, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import {
Background,
BackgroundVariant,
Controls,
Edge,
Node,
ReactFlow,
ReactFlowProvider,
useEdgesState,
useNodesState,
useReactFlow,
XYPosition,
} from "reactflow";
import { Edge, Node, ReactFlowProvider } from "reactflow";
import "reactflow/dist/style.css";
import { v4 as uuidv4 } from "uuid";
import { useEnclavesContext } from "../../EnclavesContext";
import { EnclaveFullInfo } from "../../types";
import { KurtosisArtifactNode } from "./enclaveBuilder/KurtosisArtifactNode";
import { KurtosisServiceNode } from "./enclaveBuilder/KurtosisServiceNode";
import { ViewStarlarkModal } from "./enclaveBuilder/modals/ViewStarlarkModal";
import {
generateStarlarkFromGraph,
getInitialGraphStateFromEnclave,
getNodeDependencies,
} from "./enclaveBuilder/utils";
import {
KurtosisNodeData,
useVariableContext,
VariableContextProvider,
} from "./enclaveBuilder/VariableContextProvider";
import { KurtosisNodeData } from "./enclaveBuilder/types";
import { getInitialGraphStateFromEnclave, getNodeName } from "./enclaveBuilder/utils";
import { useVariableContext, VariableContextProvider } from "./enclaveBuilder/VariableContextProvider";
import { Visualiser, VisualiserImperativeAttributes } from "./enclaveBuilder/Visualiser";

type EnclaveBuilderModalProps = {
isOpen: boolean;
Expand All @@ -70,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 Expand Up @@ -129,12 +106,7 @@ const EnclaveBuilderModalImpl = ({
() =>
Object.values(data)
.filter((nodeData) => !nodeData.isValid)
.map(
(nodeData) =>
`${nodeData.type} ${
(nodeData.type === "artifact" ? nodeData.artifactName : nodeData.serviceName) || "with no name"
} has invalid data`,
),
.map((nodeData) => `${nodeData.type} ${getNodeName(nodeData)} has invalid data`),
[data],
);
const [isLoading, setIsLoading] = useState(false);
Expand Down Expand Up @@ -210,7 +182,6 @@ const EnclaveBuilderModalImpl = ({
Close
</Button>
<Button onClick={handlePreview}>Preview</Button>
<Button onClick={handlePreview}>Preview</Button>
<Tooltip
label={
dataIssues.length === 0 ? undefined : (
Expand Down Expand Up @@ -246,170 +217,3 @@ const EnclaveBuilderModalImpl = ({
</Modal>
);
};

const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));

const getLayoutedElements = <T extends object>(nodes: Node<T>[], edges: Edge<any>[]) => {
if (nodes.length === 0) {
return { nodes, edges };
}
g.setGraph({ rankdir: "LR", ranksep: 100 });

edges.forEach((edge) => g.setEdge(edge.source, edge.target));
nodes.forEach((node) =>
g.setNode(node.id, node as Node<{ label: string }, string | undefined> & { width?: number; height?: number }),
);

Dagre.layout(g);

return {
nodes: nodes.map((node) => {
const { x, y } = g.node(node.id);

return { ...node, position: { x, y } };
}),
edges,
};
};

const nodeTypes = { serviceNode: KurtosisServiceNode, artifactNode: KurtosisArtifactNode };

type VisualiserImperativeAttributes = {
getStarlark: () => string;
};

type VisualiserProps = {
initialNodes: Node<any>[];
initialEdges: Edge<any>[];
existingEnclave?: RemoveFunctions<EnclaveFullInfo>;
};

const Visualiser = forwardRef<VisualiserImperativeAttributes, VisualiserProps>(
({ initialNodes, initialEdges, existingEnclave }, ref) => {
const { data, updateData } = useVariableContext();
const insertOffset = useRef(0);
const { fitView, addNodes, getViewport } = useReactFlow();
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes || []);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges || []);

const onLayout = useCallback(() => {
const layouted = getLayoutedElements(nodes, edges);

setNodes([...layouted.nodes]);
setEdges([...layouted.edges]);

window.requestAnimationFrame(() => {
fitView();
});
}, [nodes, edges, fitView, setEdges, setNodes]);

const getNewNodePosition = (): XYPosition => {
const viewport = getViewport();
insertOffset.current += 1;
return { x: -viewport.x + insertOffset.current * 20 + 400, y: -viewport.y + insertOffset.current * 20 };
};

const handleAddServiceNode = () => {
const id = uuidv4();
updateData(id, { type: "service", serviceName: "", image: "", ports: [], env: [], files: [], isValid: false });
addNodes({
id,
position: getNewNodePosition(),
width: 650,
style: { width: "650px" },
type: "serviceNode",
data: {},
});
};

const handleAddArtifactNode = () => {
const id = uuidv4();
updateData(id, { type: "artifact", artifactName: "", files: {}, isValid: false });
addNodes({
id,
position: getNewNodePosition(),
width: 600,
style: { width: "400px" },
type: "artifactNode",
data: {},
});
};

useEffect(() => {
setEdges((prevState) => {
return Object.entries(getNodeDependencies(data)).flatMap(([to, froms]) =>
[...froms].map((from) => ({
id: `${from}-${to}`,
source: from,
target: to,
animated: true,
style: { strokeWidth: "3px" },
})),
);
});
}, [setEdges, data]);

// Remove the resizeObserver error
useEffect(() => {
const errorHandler = (e: any) => {
if (
e.message.includes(
"ResizeObserver loop completed with undelivered notifications" || "ResizeObserver loop limit exceeded",
)
) {
const resizeObserverErr = document.getElementById("webpack-dev-server-client-overlay");
if (resizeObserverErr) {
resizeObserverErr.style.display = "none";
}
}
};
window.addEventListener("error", errorHandler);

return () => {
window.removeEventListener("error", errorHandler);
};
}, []);

useImperativeHandle(
ref,
() => ({
getStarlark: () => {
return generateStarlarkFromGraph(nodes, edges, data, existingEnclave);
},
}),
[nodes, edges, data, existingEnclave],
);

return (
<Flex flexDirection={"column"} h={"100%"} gap={"8px"}>
<ButtonGroup paddingInline={6}>
<Button onClick={onLayout}>Do Layout</Button>
<Button leftIcon={<FiPlusCircle />} onClick={handleAddServiceNode}>
Add Service Node
</Button>
<Button leftIcon={<FiPlusCircle />} onClick={handleAddArtifactNode}>
Add Files Node
</Button>
</ButtonGroup>
<Box bg={"gray.900"} flex={"1"}>
<ReactFlow
minZoom={0.1}
maxZoom={1}
nodeDragThreshold={3}
nodes={nodes}
edges={edges}
proOptions={{ hideAttribution: true }}
onMove={() => (insertOffset.current = 1)}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
nodeTypes={nodeTypes}
fitView
>
<Controls />
<Background variant={BackgroundVariant.Dots} gap={12} size={1} />
</ReactFlow>
</Box>
</Flex>
);
},
);
Loading

0 comments on commit f784eaf

Please sign in to comment.