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 (
-
-
-
- } onClick={handleAddServiceNode}>
- Add Service Node
-
- } onClick={handleAddArtifactNode}>
- Add Files Node
-
-
-
- (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 (
+
+
+
+ } onClick={handleAddServiceNode}>
+ Add Service Node
+
+ } onClick={handleAddArtifactNode}>
+ Add Files Node
+
+ } onClick={handleAddShellNode}>
+ Add Shell Node
+
+ } onClick={handleAddExecNode}>
+ Add Exec Node
+
+
+
+ (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