diff --git a/CHANGELOG.md b/CHANGELOG.md index 14c8427bfe..07ed901418 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## [0.86.22](https://github.com/kurtosis-tech/kurtosis/compare/0.86.21...0.86.22) (2024-02-15) + + +### Bug Fixes + +* allow building images in arm64 ([#2161](https://github.com/kurtosis-tech/kurtosis/issues/2161)) ([acd884f](https://github.com/kurtosis-tech/kurtosis/commit/acd884fb1a8292b450e66c8f48156f4cef52a082)) + +## [0.86.21](https://github.com/kurtosis-tech/kurtosis/compare/0.86.20...0.86.21) (2024-02-15) + + +### Bug Fixes + +* Core image builds for arm64 under CI ([#2149](https://github.com/kurtosis-tech/kurtosis/issues/2149)) ([807ddae](https://github.com/kurtosis-tech/kurtosis/commit/807ddae12b9274819f53160a8771da7541f4a4c1)) + +## [0.86.20](https://github.com/kurtosis-tech/kurtosis/compare/0.86.19...0.86.20) (2024-02-14) + + +### Features + +* support `run_sh` and `exec` in enclave builder ([#2158](https://github.com/kurtosis-tech/kurtosis/issues/2158)) ([f784eaf](https://github.com/kurtosis-tech/kurtosis/commit/f784eaf7a24ae282aa470d22e6a9ad721d04cc05)) + ## [0.86.19](https://github.com/kurtosis-tech/kurtosis/compare/0.86.18...0.86.19) (2024-02-09) diff --git a/LICENSE.md b/LICENSE.md index 40946a206a..871c36fbb8 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -3,7 +3,7 @@ Business Source License 1.1 Parameters Licensor: Kurtosis Technologies, Inc. -Licensed Work: Kurtosis 0.86.19 +Licensed Work: Kurtosis 0.86.22 The Licensed Work is (c) 2024 Kurtosis Technologies, Inc. Additional Use Grant: You may make use of the Licensed Work, provided that you may not use the Licensed Work for an Environment Orchestration Service. @@ -12,7 +12,7 @@ you may not use the Licensed Work for an Environment Orchestration Service. allows third parties (other than your employees and contractors) to create distributed system environments. -Change Date: 2028-02-09 +Change Date: 2028-02-15 Change License: Apache 2.0 (Apache License, Version 2.0) diff --git a/README.md b/README.md index 7b6da21925..a2a01ce152 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,40 @@ +[![Follow us on X, formerly Twitter](https://img.shields.io/twitter/follow/KurtosisTech?style=social)](https://twitter.com/Kurtosistech) +[![Number of GitHub stars](https://img.shields.io/github/stars/kurtosis-tech/kurtosis)](https://github.com/kurtosis-tech/kurtosis/stargazers) + ---- What is Kurtosis? ================= -[Kurtosis](https://www.kurtosis.com) is a platform for packaging and launching environments of containerized services ("distributed applications") with a focus on approachability for the average developer. What Docker did for shipping binaries, Kurtosis aims to do even better for distributed applications. + +Have you ever tried to build on top of a colleague's work, or contribute to an open source project, just to get stuck on the first steps of spinning up a stack to play with? [Kurtosis](https://www.kurtosis.com) handles the complexity of spinning up ephemeral dev or test stacks so you can focus on developing, not configuring. Kurtosis is formed of: +- A packaging system for distributing backend stack definitions, which can run on docker or on kubernetes +- A runtime with a per-stack file management system for reproducibly initializing the state of your stack +- A set of tools to enable devs to interact with their stacks, like they do on docker or k8s -- A language for declaring a distributed application in Python syntax ([Starlark](https://github.com/google/starlark-go/blob/master/doc/spec.md)) -- A packaging system for sharing and reusing distributed application components -- A runtime that makes a Kurtosis app Just Work, independent of whether it's running on Docker or Kubernetes, local or in the cloud -- A set of tools to ease common distributed app development needs (e.g. a log aggregator to ease log-diving, automatic port-forwarding to ease connectivity, a `kurtosis service shell` command to ease container filesystem exploration, etc.) +Why use Kurtosis? +========================= -Why should I use Kurtosis? -========================== -Kurtosis shines when creating, working with, and destroying self-contained distributed application environments. Currently, our users report this to be most useful when: +Kurtosis is best for: -- You're developing on your application and you need to rapidly iterate on it -- You want to try someone's containerized service or distributed application without setting up an environment, dependencies, etc. -- You want to spin up your distributed application in ephemeral environments as part of your integration tests -- You want to ad-hoc test your application on a big cloud cluster -- You're the author of a containerized service or distributed application and you want to give your users a one-liner to try it -- You want to get an instance of your application running in the cloud without provisioning or administering a Kubernetes cluster +- Reusing the logic in your stack definitions for all of: local dev, scheduled testing in CI, and ad-hoc larger-scale testing on k8s clusters +- Giving other devs a way to spin up your application, and commonly used variations of it, with one-liners, via Kurtosis' packaging and parameterization systems +- Handling complex setup logic in your backend stack, like passing arbitrary data between services as they start up, and enforcing arbitrary wait conditions -If you're in web3, we have even more specific web3 usecases [here](https://web3.kurtosis.com). +How is Kurtosis different than Docker Compose or Helm? +========================== -Check out an introductory demo video here: +Kurtosis operates at a level higher than Docker Compose or Helm, and produces stacks running on either of the underlying engines (the Docker engine, or Kubernetes). +Because of this additional layer of abstraction, we are able to introduce several features to improve the experience of spinning up ephemeral stacks: - +- A per-stack file management system that enables portable state initialization for dev or test stacks +- Stack-level parameterizability; users have a powerful and flexible way (beyond messing with env vars) to affect modifications in their stacks +- First-class plug-and-play composability; it's expected for users to import stack definitions into larger stacks, and this experience is optimized +- The ability to get all of the above, but running over _either_ the docker engine or k8s, at your election How do I get going? =================== @@ -53,11 +58,7 @@ If you have an issue or feature request, we'd love to hear about it through one ### Going further -To try more Kurtosis packages just like this one, check out the [`awesome-kurtosis` repo][awesome-kurtosis] or one of these packages: - -- [Ethereum](https://github.com/kurtosis-tech/ethereum-package): fully functional private Ethereum network in Kurtosis with Flashbots MEV-boost, any EL and CL client combination, and a collection of network monitoring tools. -- [DIVE](https://github.com/HugoByte/DIVE): A CLI + Kurtosis package by [Hugobyte](https://hugobyte.com) for the ICON ecosystem that can spin up EVM, Cosmos, or JVM networks with a bridge between them. -- [NEAR](https://github.com/kurtosis-tech/near-package): A private NEAR network in Kurtosis. +To try more Kurtosis packages just like this one, check out the [`awesome-kurtosis` repo][awesome-kurtosis]! To learn about how to write Kurtosis packages, check out our [quickstart][quickstart-reference]. @@ -67,40 +68,6 @@ To see where we're going with the product, check out the roadmap [here](https:// Got more questions? Drop them in our [Github Discussions](https://github.com/kurtosis-tech/kurtosis/discussions/new?category=q-a) where we, or other community members, can help answer. -Why Kurtosis over Compose, Helm, or Terraform? -============================================== -These tools have been around for over a decade, yet most developers still struggle to build distributed applications. Why? In a sentence: building distributed applications is hard, and these tools still haven't made it easy enough for the average developer. - -Some of our observations: - -- No tool works across the whole software lifecycle: Compose is oriented around quick local environments rather than Prod environments, while Helm and Terraform are the opposite. This often means a dedicated DevOps team handles Prod deployment, leading to the same "throw it across the wall" problem the DevOps movement was founded around. -- Compose, Helm, and Terraform use fully declarative paradigms, making difficult the sequential "first this, then this" logic necessary for many prototyping workflows. -- The inherently declarative nature of all three make [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) difficult, leading to frequent copy-pasting. -- All three tend to leave resources hanging around that the developer needs to manually clean up. -- Compose and Helm favor "run it and see what happens" over validation & error-checking, resulting in debugging time and longer dev cycles. -- A significant percentage of developers don't understand how Docker works, and [most don't understand Kubernetes or Terraform][stackoverflow-2022-developer-survey--other-tools]. - -Here's what our users tell us they like about Kurtosis: - -- **It's understandable:** you write code in Python syntax, and you get your distributed application the other side. Variables and functions keep your code DRY. -- **It's portable:** your application runs with a one-liner independent of where you run it. You can build your application on your local Docker, and in seconds get the same thing on your friend's laptop or a Kubernetes cluster in the cloud. -- **It can handle sequential dependencies:** for example, "first generate these files, then use them when starting a service". -- **It's reliable and reproducible:** Kurtosis started as a testing tool and is built to be safe: deterministic execution order, validation to catch errors before runtime, built-in support for inter-service dependencies and readiness checks, etc. Your distributed app should spin up the same way, every time. -- **It abstracts away complexity while being configurable:** instantiating a distributed application is as simple as calling its function with the parameters you want. For example, instantiating a Postgres server with modified username and password: - - On the CLI... - ```bash - kurtosis run github.com/kurtosis-tech/postgres-package '{"user": "bobmarley", "password": "buffalosoldier"}' - ``` - - Inside an environment definition... - ```python - postgres = import_module("github.com/kurtosis-tech/postgres-package/main.star") - - def run(plan): - postgres.run(plan, user = "bobmarley", password = "buffalosoldier") - ``` - Contributing to Kurtosis ======================== diff --git a/api/golang/kurtosis_version/kurtosis_version.go b/api/golang/kurtosis_version/kurtosis_version.go index 7ff326bd39..b9a79d1b75 100644 --- a/api/golang/kurtosis_version/kurtosis_version.go +++ b/api/golang/kurtosis_version/kurtosis_version.go @@ -9,6 +9,6 @@ const ( // !!!!!!!!!!! DO NOT UPDATE! WILL BE MANUALLY UPDATED DURING THE RELEASE PROCESS !!!!!!!!!!!!!!!!!!!!!! // This is necessary so that Kurt Core consumers will know if they're compatible with the currently-running // API container - KurtosisVersion = "0.86.19" + KurtosisVersion = "0.86.22" // !!!!!!!!!!! DO NOT UPDATE! WILL BE MANUALLY UPDATED DURING THE RELEASE PROCESS !!!!!!!!!!!!!!!!!!!!!! ) diff --git a/api/rust/Cargo.toml b/api/rust/Cargo.toml index ee1dab1ca3..cd7eae102b 100644 --- a/api/rust/Cargo.toml +++ b/api/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kurtosis-sdk" -version = "0.86.19" +version = "0.86.22" license = "BUSL-1.1" description = "Rust SDK for Kurtosis" edition = "2021" diff --git a/api/typescript/package.json b/api/typescript/package.json index 17bba4117c..45a5b3a29b 100644 --- a/api/typescript/package.json +++ b/api/typescript/package.json @@ -1,7 +1,7 @@ { "name": "kurtosis-sdk", "//": "NOTE: DO NOT UPDATE THIS VERSION MANUALLY - IT WILL BE UPDATED DURING THE RELEASE PROCESS!", - "version": "0.86.19", + "version": "0.86.22", "main": "./build/index", "description": "This repo contains a Typescript client for communicating with the Kurtosis Engine server, which is responsible for creating, managing and destroying Kurtosis Enclaves.", "types": "./build/index", diff --git a/api/typescript/src/kurtosis_version/kurtosis_version.ts b/api/typescript/src/kurtosis_version/kurtosis_version.ts index e467c095ff..4034304cf5 100644 --- a/api/typescript/src/kurtosis_version/kurtosis_version.ts +++ b/api/typescript/src/kurtosis_version/kurtosis_version.ts @@ -1,5 +1,5 @@ // !!!!!!!!!!! DO NOT UPDATE! WILL BE MANUALLY UPDATED DURING THE RELEASE PROCESS !!!!!!!!!!!!!!!!!!!!!! // This is necessary so that Kurt Core consumers (e.g. modules) will know if they're compatible with the currently-running // API container -export const KURTOSIS_VERSION: string = "0.86.19" +export const KURTOSIS_VERSION: string = "0.86.22" // !!!!!!!!!!! DO NOT UPDATE! WILL BE MANUALLY UPDATED DURING THE RELEASE PROCESS !!!!!!!!!!!!!!!!!!!!!! diff --git a/core/server/Dockerfile b/core/server/Dockerfile index cd57de82c6..f601298e67 100644 --- a/core/server/Dockerfile +++ b/core/server/Dockerfile @@ -2,7 +2,12 @@ FROM alpine:3.17 # We need protobut-dev to run protobuf compiler against startosis .proto files RUN apk update && apk add --no-cache bash protobuf-dev sudo shadow curl xz -RUN sh <(curl -L https://nixos.org/nix/install) --daemon --yes + +# Install Nix +# We need to set filter-syscalls to false to allow Nix install to work properly inside a container with cross platform emulation +# via QEMU: https://github.com/NixOS/nix/issues/5258 and use a more flexible installer https://github.com/DeterminateSystems/nix-installer +# with a workaround on the same issue: https://github.com/DeterminateSystems/nix-installer/issues/324) +RUN curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install linux --no-confirm --init none --extra-conf "filter-syscalls = false" ARG TARGETARCH diff --git a/core/server/Dockerfile.debug b/core/server/Dockerfile.debug index 1aceb832e5..a224ddcf48 100644 --- a/core/server/Dockerfile.debug +++ b/core/server/Dockerfile.debug @@ -2,7 +2,12 @@ FROM alpine:3.19 # We need protobut-dev to run protobuf compiler against startosis .proto files RUN apk update && apk add --no-cache bash protobuf-dev sudo shadow curl xz -RUN sh <(curl -L https://nixos.org/nix/install) --daemon --yes + +# Install Nix +# We need to set filter-syscalls to false to allow Nix install to work properly inside a container with cross platform emulation +# via QEMU: https://github.com/NixOS/nix/issues/5258 and use a more flexible installer https://github.com/DeterminateSystems/nix-installer +# with a workaround on the same issue: https://github.com/DeterminateSystems/nix-installer/issues/324) +RUN curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install linux --no-confirm --init none --extra-conf "filter-syscalls = false" # Make sure that you changed the port inside the APIC's code before changing it here EXPOSE 50103 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/docs/docs/api-reference/starlark-reference/service-config.md b/docs/docs/api-reference/starlark-reference/service-config.md index b2dab77535..11afaa6592 100644 --- a/docs/docs/api-reference/starlark-reference/service-config.md +++ b/docs/docs/api-reference/starlark-reference/service-config.md @@ -73,8 +73,10 @@ config = ServiceConfig( flake_output = "containerImage", ) - # The ports that the container should listen on, identified by a user-friendly ID that can be used to select the port again in the future. - # If no ports are provided, no ports will be exposed on the host machine, unless there is an EXPOSE in the Dockerfile + # The ports that the container should listen on, identified by a user-friendly ID that can be used to select the port again in the future. + # Kurtosis will automatically perform a check to ensure all declared UDP and TCP ports are open and ready for traffic and connections upon startup. + # You may specify a custom wait timeout duration or disable the feature entirely, learn more via PortSpec docs + # If no ports are provided, no ports will be exposed on the host machine, unless there is an EXPOSE in the Dockerfile. # OPTIONAL (Default: {}) ports = { "grpc": PortSpec( diff --git a/docs/docs/guides/running-docker-compose.md b/docs/docs/guides/running-docker-compose.md index 0561d99d96..a0f5402c13 100644 --- a/docs/docs/guides/running-docker-compose.md +++ b/docs/docs/guides/running-docker-compose.md @@ -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: diff --git a/docs/yarn.lock b/docs/yarn.lock index 3d3f411a12..4b50e3a37e 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -4147,9 +4147,9 @@ flux@^4.0.1: fbjs "^3.0.1" follow-redirects@^1.0.0, follow-redirects@^1.14.7: - version "1.15.0" - resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.0.tgz" - integrity sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ== + version "1.15.4" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf" + integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw== fork-ts-checker-webpack-plugin@^6.5.0: version "6.5.2" diff --git a/enclave-manager/web/lerna.json b/enclave-manager/web/lerna.json index ab4b306ef9..284ae64f52 100644 --- a/enclave-manager/web/lerna.json +++ b/enclave-manager/web/lerna.json @@ -1,6 +1,6 @@ { "packages": ["packages/*"], - "version": "0.86.19", + "version": "0.86.22", "npmClient": "yarn", "$schema": "node_modules/lerna/schemas/lerna-schema.json", "useNx": false, diff --git a/enclave-manager/web/packages/app/package.json b/enclave-manager/web/packages/app/package.json index 9a579db5fe..197166c175 100644 --- a/enclave-manager/web/packages/app/package.json +++ b/enclave-manager/web/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@kurtosis/emui-app", - "version": "0.86.19", + "version": "0.86.22", "private": true, "homepage": ".", "dependencies": { @@ -10,7 +10,7 @@ "html-react-parser": "^4.2.2", "js-cookie": "^3.0.5", "kurtosis-cloud-indexer-sdk": "^0.0.2", - "kurtosis-ui-components": "0.86.19", + "kurtosis-ui-components": "0.86.22", "react-error-boundary": "^4.0.11", "react-hook-form": "^7.47.0", "react-mentions": "^4.4.10", diff --git a/enclave-manager/web/packages/app/src/emui/enclaves/components/form/CodeEditorInput.tsx b/enclave-manager/web/packages/app/src/emui/enclaves/components/form/CodeEditorInput.tsx new file mode 100644 index 0000000000..1e0bc2815b --- /dev/null +++ b/enclave-manager/web/packages/app/src/emui/enclaves/components/form/CodeEditorInput.tsx @@ -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 = KurtosisFormInputProps & { + fileName: string; +}; + +export const CodeEditorInput = (props: CodeEditorInputProps) => { + return ( + } + 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 = FieldPath, +> = { + field: ControllerRenderProps; + fileName: string; +}; + +const CodeEditorInputImpl = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + field, + fileName, +}: CodeEditorImplProps) => { + return ; +}; diff --git a/enclave-manager/web/packages/app/src/emui/enclaves/components/form/ListArgumentInput.tsx b/enclave-manager/web/packages/app/src/emui/enclaves/components/form/ListArgumentInput.tsx index c71fc4da1e..6fa774def5 100644 --- a/enclave-manager/web/packages/app/src/emui/enclaves/components/form/ListArgumentInput.tsx +++ b/enclave-manager/web/packages/app/src/emui/enclaves/components/form/ListArgumentInput.tsx @@ -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 = KurtosisFormInputProps & { - FieldComponent: (props: KurtosisFormInputProps) => ReactElement; + FieldComponent: FC>; createNewValue: () => object; }; 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 5947f2c1db..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 @@ -1,5 +1,4 @@ import { - Box, Button, ButtonGroup, Flex, @@ -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; @@ -70,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); @@ -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); @@ -210,7 +182,6 @@ const EnclaveBuilderModalImpl = ({ Close - ); }; - -const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); - -const getLayoutedElements = (nodes: Node[], edges: Edge[]) => { - 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[]; - initialEdges: Edge[]; - existingEnclave?: RemoveFunctions; -}; - -const Visualiser = forwardRef( - ({ 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 ( - - - - - - - - (insertOffset.current = 1)} - onNodesChange={onNodesChange} - onEdgesChange={onEdgesChange} - nodeTypes={nodeTypes} - fitView - > - - - - - - ); - }, -); 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 ea3722fd38..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,109 +1,41 @@ -import { Flex, IconButton, Text } from "@chakra-ui/react"; +import { isDefined } from "kurtosis-ui-components"; import { memo } from "react"; -import { FormProvider, useForm } from "react-hook-form"; -import { FiTrash } from "react-icons/fi"; -import { RxCornerBottomRight } from "react-icons/rx"; -import { Handle, NodeProps, NodeResizeControl, Position, useReactFlow } from "reactflow"; +import { NodeProps } from "reactflow"; import { KurtosisFormControl } from "../../form/KurtosisFormControl"; import { StringArgumentInput } from "../../form/StringArgumentInput"; import { FileTreeArgumentInput } from "./input/FileTreeArgumentInput"; -import { KurtosisArtifactNodeData, useVariableContext } from "./VariableContextProvider"; +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, updateData, removeData } = useVariableContext(); - const formMethods = useForm({ - defaultValues: (data[id] as KurtosisArtifactNodeData) || {}, - mode: "onBlur", - shouldFocusError: false, - }); + const { data } = useVariableContext(); + const nodeData = data[id] as KurtosisArtifactNodeData; - const { deleteElements } = useReactFlow(); - - const handleDeleteNode = (e: React.MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - deleteElements({ nodes: [{ id }] }); - removeData(id); - }; - - const handleBlur = async () => { - const isValid = await formMethods.trigger(); - updateData(id, { ...formMethods.getValues(), isValid }); - }; + if (!isDefined(nodeData)) { + // Node has probably been deleted. + return null; + } return ( - - - - - - - - - - - {(data[id] as KurtosisArtifactNodeData)?.artifactName || Unnamed Artifact} - - } - colorScheme={"red"} - variant={"ghost"} - size={"sm"} - onClick={handleDeleteNode} - /> - - - name={"artifactName"} label={"Artifact Name"} isRequired> - - - - - - - - + + name={"artifactName"} label={"Artifact Name"} isRequired> + + + + + + ); }, - (oldProps, newProps) => { - return 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/KurtosisNode.tsx b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/KurtosisNode.tsx new file mode 100644 index 0000000000..1a41c7334b --- /dev/null +++ b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/KurtosisNode.tsx @@ -0,0 +1,141 @@ +import { Flex, IconButton, Text, useToken } from "@chakra-ui/react"; +import { debounce } from "lodash"; +import { memo, PropsWithChildren, useEffect, useMemo } from "react"; +import { DefaultValues, FormProvider, useForm } from "react-hook-form"; +import { FiTrash } from "react-icons/fi"; +import { RxCornerBottomRight } from "react-icons/rx"; +import { Handle, NodeResizeControl, Position, useReactFlow } from "reactflow"; +import { KurtosisNodeData } from "./types"; +import { useVariableContext } from "./VariableContextProvider"; + +type KurtosisNodeProps = PropsWithChildren<{ + id: string; + name: string; + selected: boolean; + minWidth: number; + maxWidth: number; + color: string; +}>; + +export const KurtosisNode = memo( + ({ + id, + name, + selected, + minWidth, + maxWidth, + children, + color, + }: KurtosisNodeProps) => { + const chakraColor = useToken("colors", color); + const { data, updateData, removeData } = useVariableContext(); + const formMethods = useForm({ + defaultValues: (data[id] as DefaultValues) || {}, + mode: "onBlur", + shouldFocusError: false, + }); + + const { deleteElements, zoomOut, zoomIn } = useReactFlow(); + + const handleDeleteNode = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + deleteElements({ nodes: [{ id }] }); + removeData(id); + }; + + const handleChange = useMemo( + () => + debounce(async () => { + const isValid = await formMethods.trigger(); + updateData(id, { ...formMethods.getValues(), isValid }); + }, 500), + [updateData, formMethods, id], + ); + + useEffect(() => { + const watcher = formMethods.watch(handleChange); + return () => watcher.unsubscribe(); + }, [formMethods, handleChange]); + + const handleScroll = (e: React.WheelEvent) => { + if (e.currentTarget.scrollTop === 0 && e.deltaY < 0) { + zoomIn(); + } + if ( + Math.abs(e.currentTarget.scrollHeight - e.currentTarget.clientHeight - e.currentTarget.scrollTop) <= 1 && + e.deltaY > 0 + ) { + zoomOut(); + } + }; + + return ( + + + + + + + + + + {name || Unnamed} + } + colorScheme={"red"} + variant={"ghost"} + size={"sm"} + onClick={handleDeleteNode} + /> + + + {children} + + + + ); + }, +); 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 303d603787..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 @@ -1,270 +1,90 @@ -import { Flex, Grid, GridItem, IconButton, Tab, TabList, TabPanel, TabPanels, Tabs, Text } from "@chakra-ui/react"; -import { memo, useMemo } from "react"; -import { FormProvider, useForm } from "react-hook-form"; -import { FiTrash } from "react-icons/fi"; -import { RxCornerBottomRight } from "react-icons/rx"; -import { Handle, NodeProps, NodeResizeControl, Position, useReactFlow } from "reactflow"; +import { Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react"; +import { memo } from "react"; +import { NodeProps } from "reactflow"; import { DictArgumentInput } from "../../form/DictArgumentInput"; -import { IntegerArgumentInput } from "../../form/IntegerArgumentInput"; import { KurtosisFormControl } from "../../form/KurtosisFormControl"; import { ListArgumentInput } from "../../form/ListArgumentInput"; -import { OptionsArgumentInput } from "../../form/OptionArgumentInput"; -import { SelectArgumentInput, SelectOption } from "../../form/SelectArgumentInput"; import { StringArgumentInput } from "../../form/StringArgumentInput"; import { MentionStringArgumentInput } from "./input/MentionStringArgumentInput"; -import { - KurtosisFileMount, - KurtosisPort, - KurtosisServiceNodeData, - useVariableContext, -} from "./VariableContextProvider"; +import { MountArtifactFileInput } from "./input/MountArtifactFileInput"; +import { PortConfigurationField } from "./input/PortConfigurationInput"; +import { validateDockerLocator, validateName } from "./input/validators"; +import { KurtosisNode } from "./KurtosisNode"; +import { KurtosisFileMount, KurtosisPort, KurtosisServiceNodeData } from "./types"; +import { useVariableContext } from "./VariableContextProvider"; export const KurtosisServiceNode = memo( ({ id, selected }: NodeProps) => { - const { data, updateData, removeData, variables } = useVariableContext(); - const artifactVariableOptions = useMemo((): SelectOption[] => { - return variables - .filter((variable) => variable.id.startsWith("artifact")) - .map((variable) => ({ display: variable.displayName, value: `{{${variable.id}}}` })); - }, [variables]); - const formMethods = useForm({ - defaultValues: (data[id] as KurtosisServiceNodeData) || {}, - mode: "onBlur", - shouldFocusError: false, - }); - - const { deleteElements } = useReactFlow(); - - const handleDeleteNode = (e: React.MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - deleteElements({ nodes: [{ id }] }); - removeData(id); - }; - - const handleBlur = async () => { - const isValid = await formMethods.trigger(); - updateData(id, { ...formMethods.getValues(), isValid }); - }; + const { data } = useVariableContext(); return ( - - - - - - - + + + name={"serviceName"} label={"Service Name"} isRequired> + + + name={"image"} label={"Container Image"} isRequired> + + + + + + Environment + Ports + Files + - - - {(data[id] as KurtosisServiceNodeData)?.serviceName || Unnamed Service} - - } - colorScheme={"red"} - variant={"ghost"} - size={"sm"} - onClick={handleDeleteNode} - /> - - - - name={"serviceName"} label={"Service Name"} isRequired> - { - if (typeof val !== "string") { - return "Value should be a string"; - } - if (!val.match(/^[a-z]([-a-z0-9]{0,61}[a-z0-9])?$/)) { - return ( - "Service names must adhere to the RFC 1035 standard, specifically implementing this regex and" + - " be 1-63 characters long: ^[a-z]([-a-z0-9]{0,61}[a-z0-9])?$. This means the service name must " + - "only contain lowercase alphanumeric characters or '-', and must start with a lowercase alphabet " + - "and end with a lowercase alphanumeric" - ); - } - }} + + + name={"env"} label={"Environment Variables"}> + + name={"env"} + KeyFieldComponent={StringArgumentInput} + ValueFieldComponent={MentionStringArgumentInput} /> - 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"; - } - }} + + + name={"ports"} label={"Ports"}> + ({ + portName: "", + applicationProtocol: "", + transportProtocol: "TCP", + port: 0, + })} /> - - - - Environment Variables - Ports - Files - - - - - name={"env"} label={"Environment Variables"}> - - name={"env"} - KeyFieldComponent={StringArgumentInput} - ValueFieldComponent={MentionStringArgumentInput} - /> - - - - name={"ports"} label={"Ports"}> - ( - - - - {...props} - size={"sm"} - placeholder={"Port Name (eg postgres)"} - name={`${props.name as `ports.${number}`}.portName`} - /> - - - - {...props} - size={"sm"} - placeholder={"Application Protocol (eg postgresql)"} - name={`${props.name as `ports.${number}`}.applicationProtocol`} - validate={(val) => { - if (typeof val !== "string") { - return "Value should be a string"; - } - if (val.includes(" ")) { - return "Application protocol cannot include spaces"; - } - }} - /> - - - - {...props} - options={["TCP", "UDP"]} - name={`${props.name as `ports.${number}`}.transportProtocol`} - /> - - - - {...props} - name={`${props.name as `ports.${number}`}.port`} - size={"sm"} - /> - - - )} - createNewValue={(): KurtosisPort => ({ - portName: "", - applicationProtocol: "", - transportProtocol: "TCP", - port: 0, - })} - /> - - - - - - name={"files"} - label={"Files"} - helperText={"Choose where to mount artifacts on this services filesystem"} - > - ( - - - - {...props} - size={"sm"} - placeholder={"/some/path"} - name={`${props.name as `files.${number}`}.mountPoint`} - /> - - - - options={artifactVariableOptions} - {...props} - size={"sm"} - placeholder={"Select an Artifact"} - name={`${props.name as `files.${number}`}.artifactName`} - /> - - - )} - createNewValue={(): KurtosisFileMount => ({ - mountPoint: "", - artifactName: "", - })} - /> - - - - - - - + + + + name={"files"} + label={"Files"} + helperText={"Choose where to mount artifacts on this services filesystem"} + > + ({ + mountPoint: "", + artifactName: "", + })} + /> + + + + + ); }, - (oldProps, newProps) => { - return 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/KurtosisShellNode.tsx b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/KurtosisShellNode.tsx new file mode 100644 index 0000000000..f354bb5a6e --- /dev/null +++ b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/KurtosisShellNode.tsx @@ -0,0 +1,135 @@ +import { Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react"; +import { isDefined } from "kurtosis-ui-components"; +import { memo } from "react"; +import { NodeProps } from "reactflow"; +import { BooleanArgumentInput } from "../../form/BooleanArgumentInput"; +import { CodeEditorInput } from "../../form/CodeEditorInput"; +import { DictArgumentInput } from "../../form/DictArgumentInput"; +import { KurtosisFormControl } from "../../form/KurtosisFormControl"; +import { ListArgumentInput } from "../../form/ListArgumentInput"; +import { StringArgumentInput } from "../../form/StringArgumentInput"; +import { MentionStringArgumentInput } from "./input/MentionStringArgumentInput"; +import { MountArtifactFileInput } from "./input/MountArtifactFileInput"; +import { validateDockerLocator, validateDurationString, validateName } from "./input/validators"; +import { KurtosisNode } from "./KurtosisNode"; +import { KurtosisFileMount, KurtosisShellNodeData } from "./types"; +import { useVariableContext } from "./VariableContextProvider"; + +export const KurtosisShellNode = memo( + ({ id, selected }: NodeProps) => { + const { data } = useVariableContext(); + const nodeData = data[id] as KurtosisShellNodeData; + + if (!isDefined(nodeData)) { + // Node has probably been deleted. + return null; + } + + return ( + + + name={"shellName"} label={"Shell Name"} isRequired> + + + name={"image"} label={"Container Image"}> + + + + + + Script + Environment + Files + Advanced + + + + + name={"command"} label={"Script to run"} isRequired> + + + + + name={"env"} label={"Environment Variables"}> + + name={"env"} + KeyFieldComponent={StringArgumentInput} + ValueFieldComponent={MentionStringArgumentInput} + /> + + + + + name={"files"} + label={"Input Files"} + helperText={"Choose where to mount artifacts on this execution tasks filesystem"} + > + ({ + mountPoint: "", + artifactName: "", + })} + /> + + + 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"} + placeholder={"/some/output/location"} + isRequired + /> + + + + + + name={"wait_enabled"} + label={"Wait enabled"} + isRequired + helperText={"Whether kurtosis should wait a preset time for this step to complete."} + > + name={"wait_enabled"} /> + + + name={"wait"} + label={"Wait"} + isDisabled={nodeData.wait_enabled === "false"} + helperText={"Whether kurtosis should wait a preset time for this step to complete."} + > + + name={"wait"} + isDisabled={nodeData.wait_enabled === "false"} + size={"sm"} + placeholder={"180s"} + validate={nodeData.wait_enabled === "false" ? undefined : validateDurationString} + /> + + + + + + + ); + }, + (oldProps, newProps) => oldProps.id !== newProps.id || oldProps.selected !== newProps.selected, +); diff --git a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/VariableContextProvider.tsx b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/VariableContextProvider.tsx index d5e3d742cc..f071f4f782 100644 --- a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/VariableContextProvider.tsx +++ b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/VariableContextProvider.tsx @@ -1,38 +1,7 @@ import { createContext, PropsWithChildren, useCallback, useContext, useMemo, useState } from "react"; -import { Variable } from "./types"; +import { KurtosisNodeData, Variable } from "./types"; import { getVariablesFromNodes } from "./utils"; -export type KurtosisPort = { - portName: string; - port: number; - transportProtocol: "TCP" | "UDP"; - applicationProtocol: string; -}; - -export type KurtosisFileMount = { - mountPoint: string; - artifactName: string; -}; - -export type KurtosisServiceNodeData = { - type: "service"; - serviceName: string; - image: string; - env: { key: string; value: string }[]; - ports: KurtosisPort[]; - files: KurtosisFileMount[]; - isValid: boolean; -}; - -export type KurtosisArtifactNodeData = { - type: "artifact"; - artifactName: string; - files: Record; - isValid: boolean; -}; - -export type KurtosisNodeData = KurtosisArtifactNodeData | KurtosisServiceNodeData; - type VariableContextState = { data: Record; variables: Variable[]; 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 new file mode 100644 index 0000000000..ece3844b1b --- /dev/null +++ b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/Visualiser.tsx @@ -0,0 +1,260 @@ +import { Box, Button, ButtonGroup, Flex } from "@chakra-ui/react"; +import Dagre from "@dagrejs/dagre"; +import { RemoveFunctions } from "kurtosis-ui-components"; +import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from "react"; +import { FiPlusCircle } from "react-icons/fi"; +import { + Background, + BackgroundVariant, + Controls, + Edge, + Node, + ReactFlow, + useEdgesState, + useNodesState, + useReactFlow, + XYPosition, +} from "reactflow"; +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"; +import { useVariableContext } from "./VariableContextProvider"; + +const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); +const getLayoutedElements = (nodes: Node[], edges: Edge[]) => { + 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, + shellNode: KurtosisShellNode, + execNode: KurtosisExecNode, +}; + +export type VisualiserImperativeAttributes = { + getStarlark: () => string; +}; +type VisualiserProps = { + initialNodes: Node[]; + initialEdges: Edge[]; + existingEnclave?: RemoveFunctions; +}; +export const Visualiser = forwardRef( + ({ 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: 400, + style: { width: "400px" }, + type: "artifactNode", + data: {}, + }); + }; + + const handleAddShellNode = () => { + const id = uuidv4(); + updateData(id, { + type: "shell", + shellName: "", + command: "", + image: "", + env: [], + files: [], + store: "", + wait_enabled: "true", + wait: "", + isValid: false, + }); + addNodes({ + id, + position: getNewNodePosition(), + width: 650, + style: { width: "650px" }, + type: "shellNode", + data: {}, + }); + }; + + 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 }); + }, + [fitView], + ); + + 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 ( + + + + + + + + + + (insertOffset.current = 1)} + onNodesChange={onNodesChange} + onEdgesChange={onEdgesChange} + onNodeDoubleClick={handleNodeDoubleClick} + nodeTypes={nodeTypes} + fitView + > + + + + + + ); + }, +); +Visualiser.displayName = "ForwardRef Visualiser"; diff --git a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/input/MentionStringArgumentInput.tsx b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/input/MentionStringArgumentInput.tsx index 7684098336..83db6c565c 100644 --- a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/input/MentionStringArgumentInput.tsx +++ b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/input/MentionStringArgumentInput.tsx @@ -27,7 +27,9 @@ export const MentionStringArgumentInput = ({ } 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 new file mode 100644 index 0000000000..8f0631044b --- /dev/null +++ b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/input/MountArtifactFileInput.tsx @@ -0,0 +1,38 @@ +import { Grid, GridItem } from "@chakra-ui/react"; +import { useMemo } from "react"; +import { SelectArgumentInput, SelectOption } from "../../../form/SelectArgumentInput"; +import { StringArgumentInput } from "../../../form/StringArgumentInput"; +import { KurtosisFormInputProps } from "../../../form/types"; +import { KurtosisServiceNodeData } from "../types"; +import { useVariableContext } from "../VariableContextProvider"; + +export const MountArtifactFileInput = (props: KurtosisFormInputProps) => { + const { variables } = useVariableContext(); + const artifactVariableOptions = useMemo((): SelectOption[] => { + return variables + .filter((variable) => variable.id.match(/^(?:artifact|shell)\.[^.]+$/)) + .map((variable) => ({ display: variable.displayName, value: `{{${variable.id}}}` })); + }, [variables]); + + return ( + + + + {...props} + size={"sm"} + placeholder={"/some/path"} + name={`${props.name as `files.${number}`}.mountPoint`} + /> + + + + options={artifactVariableOptions} + {...props} + size={"sm"} + placeholder={"Select an Artifact"} + name={`${props.name as `files.${number}`}.artifactName`} + /> + + + ); +}; diff --git a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/input/PortConfigurationInput.tsx b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/input/PortConfigurationInput.tsx new file mode 100644 index 0000000000..b9a8760ac3 --- /dev/null +++ b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/input/PortConfigurationInput.tsx @@ -0,0 +1,49 @@ +import { Grid, GridItem } from "@chakra-ui/react"; +import { IntegerArgumentInput } from "../../../form/IntegerArgumentInput"; +import { OptionsArgumentInput } from "../../../form/OptionArgumentInput"; +import { StringArgumentInput } from "../../../form/StringArgumentInput"; +import { KurtosisFormInputProps } from "../../../form/types"; +import { KurtosisServiceNodeData } from "../types"; + +export const PortConfigurationField = (props: KurtosisFormInputProps) => ( + + + + {...props} + size={"sm"} + placeholder={"Port Name (eg postgres)"} + name={`${props.name as `ports.${number}`}.portName`} + /> + + + + {...props} + size={"sm"} + placeholder={"Application Protocol (eg postgresql)"} + name={`${props.name as `ports.${number}`}.applicationProtocol`} + validate={(val) => { + if (typeof val !== "string") { + return "Value should be a string"; + } + if (val.includes(" ")) { + return "Application protocol cannot include spaces"; + } + }} + /> + + + + {...props} + options={["TCP", "UDP"]} + name={`${props.name as `ports.${number}`}.transportProtocol`} + /> + + + + {...props} + name={`${props.name as `ports.${number}`}.port`} + size={"sm"} + /> + + +); 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 new file mode 100644 index 0000000000..528f1226d7 --- /dev/null +++ b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/input/validators.ts @@ -0,0 +1,48 @@ +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"; + } + if (value === "") { + return; + } + + if ( + !value.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"; + } +} + +export function validateDurationString(value?: string) { + if (typeof value !== "string") { + return "Value should be a string"; + } + if (value === "") { + return; + } + + if (!value.match(/^\d+[msd]?$/)) { + 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 e17cd84d48..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 @@ -3,3 +3,65 @@ export type Variable = { displayName: string; value: string; }; + +export type KurtosisPort = { + portName: string; + port: number; + transportProtocol: "TCP" | "UDP"; + applicationProtocol: string; +}; + +export type KurtosisEnvironmentVar = { key: string; value: string }; + +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; + image: string; + env: KurtosisEnvironmentVar[]; + ports: KurtosisPort[]; + files: KurtosisFileMount[]; + isValid: boolean; +}; +export type KurtosisArtifactNodeData = { + type: "artifact"; + artifactName: string; + files: Record; + isValid: boolean; +}; + +export type KurtosisShellNodeData = { + type: "shell"; + shellName: string; + command: string; + image: string; + env: KurtosisEnvironmentVar[]; + files: KurtosisFileMount[]; + store: string; + wait_enabled: "true" | "false"; + wait: string; + isValid: boolean; +}; + +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 4ee26fec42..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 @@ -2,8 +2,7 @@ import { isDefined, RemoveFunctions, stringifyError } from "kurtosis-ui-componen import { Edge, Node } from "reactflow"; import { Result } from "true-myth"; import { EnclaveFullInfo } from "../../../types"; -import { Variable } from "./types"; -import { KurtosisNodeData, KurtosisServiceNodeData } from "./VariableContextProvider"; +import { KurtosisNodeData, KurtosisServiceNodeData, Variable } from "./types"; export const EMUI_BUILD_STATE_KEY = "EMUI_BUILD_STATE"; @@ -36,62 +35,102 @@ export function getInitialGraphStateFromEnclave( } } +export function getNodeName(kurtosisNodeData: KurtosisNodeData): string { + if (kurtosisNodeData.type === "service") { + return kurtosisNodeData.serviceName; + } + if (kurtosisNodeData.type === "artifact") { + return kurtosisNodeData.artifactName; + } + if (kurtosisNodeData.type === "shell") { + return kurtosisNodeData.shellName; + } + if (kurtosisNodeData.type === "exec") { + return kurtosisNodeData.execName; + } + throw new Error(`Unknown node type.`); +} + function normaliseNameToStarlarkVariable(name: string) { return name.replace(/\s|-/g, "_").toLowerCase(); } -const variablePattern = /\{\{((?:service|artifact).([^.]+)\.?.*)}}/; +function escapeString(value: string): string { + return value.replaceAll(/(["\\])/g, "\\$1"); +} + +const variablePattern = /\{\{((?:service|artifact|shell).([^.]+)\.?.*)}}/; export function getVariablesFromNodes(nodes: Record): Variable[] { - return Object.entries(nodes).flatMap(([id, data]) => - data.type === "service" - ? [ + return Object.entries(nodes).flatMap(([id, data]) => { + if (data.type === "service") { + return [ + { + id: `service.${id}.name`, + displayName: `service.${data.serviceName}.name`, + value: `${normaliseNameToStarlarkVariable(data.serviceName)}.name`, + }, + { + id: `service.${id}.hostname`, + displayName: `service.${data.serviceName}.hostname`, + value: `${normaliseNameToStarlarkVariable(data.serviceName)}.hostname`, + }, + ...data.ports.flatMap((port, i) => [ { - id: `service.${id}.name`, - displayName: `service.${data.serviceName}.name`, - value: `${normaliseNameToStarlarkVariable(data.serviceName)}.name`, + id: `service.${id}.port.${i}`, + displayName: `service.${data.serviceName}.port.${port.portName}`, + value: `"{}://{}:{}".format(${normaliseNameToStarlarkVariable(data.serviceName)}.ports["${ + port.portName + }"].application_protocol, ${normaliseNameToStarlarkVariable( + data.serviceName, + )}.hostname, ${normaliseNameToStarlarkVariable(data.serviceName)}.ports["${port.portName}"].number)`, }, { - id: `service.${id}.hostname`, - displayName: `service.${data.serviceName}.hostname`, - value: `${normaliseNameToStarlarkVariable(data.serviceName)}.hostname`, + id: `service.${id}.port.${i}.port`, + displayName: `service.${data.serviceName}.port.${port.portName}.port`, + value: `${normaliseNameToStarlarkVariable(data.serviceName)}.ports["${port.portName}"].number`, }, - ...data.ports.flatMap((port, i) => [ - { - id: `service.${id}.port.${i}`, - displayName: `service.${data.serviceName}.port.${port.portName}`, - value: `"{}://{}:{}".format(${normaliseNameToStarlarkVariable(data.serviceName)}.ports["${ - port.portName - }"].application_protocol, ${normaliseNameToStarlarkVariable( - data.serviceName, - )}.hostname, ${normaliseNameToStarlarkVariable(data.serviceName)}.ports["${port.portName}"].number)`, - }, - { - id: `service.${id}.port.${i}.port`, - displayName: `service.${data.serviceName}.port.${port.portName}.port`, - value: `${normaliseNameToStarlarkVariable(data.serviceName)}.ports["${port.portName}"].number`, - }, - { - id: `service.${id}.port.${i}.applicationProtocol`, - displayName: `service.${data.serviceName}.port.${port.portName}.application_protocol`, - value: `${normaliseNameToStarlarkVariable(data.serviceName)}.ports["${ - port.portName - }"].application_protocol`, - }, - ]), - ...data.env.map((env, i) => ({ - id: `service.${id}.env.${i}`, - displayName: `service.${data.serviceName}.env.${env.key}`, - value: `"${env.value}"`, - })), - ] - : [ { - id: `artifact.${id}`, - displayName: `artifact.${data.artifactName}`, - value: `${normaliseNameToStarlarkVariable(data.artifactName)}`, + id: `service.${id}.port.${i}.applicationProtocol`, + displayName: `service.${data.serviceName}.port.${port.portName}.application_protocol`, + value: `${normaliseNameToStarlarkVariable(data.serviceName)}.ports["${ + port.portName + }"].application_protocol`, }, - ], - ); + ]), + ...data.env.map((env, i) => ({ + id: `service.${id}.env.${i}`, + displayName: `service.${data.serviceName}.env.${env.key}`, + value: `"${env.value}"`, + })), + ]; + } + if (data.type === "artifact") { + return [ + { + id: `artifact.${id}`, + displayName: `artifact.${data.artifactName}`, + value: `${normaliseNameToStarlarkVariable(data.artifactName)}`, + }, + ]; + } + + if (data.type === "shell") { + return [ + { + id: `shell.${id}`, + displayName: `shell.${data.shellName}`, + value: `${normaliseNameToStarlarkVariable(data.shellName)}.files_artifacts[0]`, + }, + ...data.env.map((env, i) => ({ + id: `shell.${id}.env.${i}`, + displayName: `shell.${data.shellName}.env.${env.key}`, + value: `"${env.value}"`, + })), + ]; + } + + return []; + }); } export function getNodeDependencies(nodes: Record): Record> { @@ -127,6 +166,38 @@ export function getNodeDependencies(nodes: Record): Re } }); } + if (data.type === "shell") { + const nameMatches = data.shellName.match(variablePattern); + if (nameMatches) { + getDependenciesFor(id).add(nameMatches[2]); + } + data.env.forEach((env) => { + const envMatches = env.key.match(variablePattern) || env.value.match(variablePattern); + if (envMatches) { + getDependenciesFor(id).add(envMatches[2]); + } + }); + data.files.forEach((file) => { + const fileMatches = file.mountPoint.match(variablePattern) || file.artifactName.match(variablePattern); + if (fileMatches) { + getDependenciesFor(id).add(fileMatches[2]); + } + }); + } + 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; } @@ -215,13 +286,54 @@ export function generateStarlarkFromGraph( starlark += ` config = {\n`; for (const [fileName, fileText] of Object.entries(nodeData.files)) { starlark += ` "${fileName}": struct(\n`; - starlark += ` template="""${fileText}""",\n`; + starlark += ` template="""${escapeString(fileText)}""",\n`; starlark += ` data={},\n`; starlark += ` ),\n`; } starlark += ` },\n`; starlark += ` )\n\n`; } + + if (nodeData.type === "shell") { + const shellName = normaliseNameToStarlarkVariable(nodeData.shellName); + starlark += ` ${shellName} = plan.run_sh(\n`; + starlark += ` run = """${escapeString(nodeData.command)}""",\n`; + const image = interpolateValue(nodeData.image); + if (image !== '""') { + starlark += ` image = ${image},\n`; + } + starlark += ` env_vars = {\n`; + for (const { key, value } of nodeData.env) { + starlark += ` ${interpolateValue(key)}: ${interpolateValue(value)},\n`; + } + starlark += ` },\n`; + starlark += ` files = {\n`; + for (const { mountPoint, artifactName } of nodeData.files) { + starlark += ` ${interpolateValue(mountPoint)}: ${interpolateValue(artifactName)},\n`; + } + starlark += ` },\n`; + starlark += ` store = [\n`; + starlark += ` StoreSpec(src = ${interpolateValue(nodeData.store)}, name="${shellName}"),\n`; + starlark += ` ],\n`; + const wait = interpolateValue(nodeData.wait); + if (nodeData.wait_enabled === "false" || wait !== '""') { + starlark += ` wait=${nodeData.wait_enabled === "true" ? wait : "None"},\n`; + } + 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 diff --git a/enclave-manager/web/packages/components/package.json b/enclave-manager/web/packages/components/package.json index 5489cb0d28..d46ba89132 100644 --- a/enclave-manager/web/packages/components/package.json +++ b/enclave-manager/web/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "kurtosis-ui-components", - "version": "0.86.19", + "version": "0.86.22", "private": false, "main": "build/index", "description": "This repo contains components used by Kurtosis UI applications.", diff --git a/version.txt b/version.txt index 4c134a2757..bb774beb79 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.86.19 +0.86.22