diff --git a/apps/deploy-web/src/components/deployments/LeaseRow.tsx b/apps/deploy-web/src/components/deployments/LeaseRow.tsx index 828cd7997..ec2d49046 100644 --- a/apps/deploy-web/src/components/deployments/LeaseRow.tsx +++ b/apps/deploy-web/src/components/deployments/LeaseRow.tsx @@ -1,13 +1,15 @@ "use client"; -import React, { SetStateAction, useCallback } from "react"; +import React, { SetStateAction, useCallback, useMemo } from "react"; import { useEffect, useState } from "react"; import { Alert, Badge, Button, Card, CardContent, CardHeader, CustomTooltip, Spinner } from "@akashnetwork/ui/components"; import { Check, Copy, InfoCircle, OpenInWindow } from "iconoir-react"; import yaml from "js-yaml"; +import get from "lodash/get"; import Link from "next/link"; import { useSnackbar } from "notistack"; import { AuditorButton } from "@src/components/providers/AuditorButton"; +import { CodeSnippet } from "@src/components/shared/CodeSnippet"; import { FavoriteButton } from "@src/components/shared/FavoriteButton"; import { LabelValueOld } from "@src/components/shared/LabelValueOld"; import { LinkTo } from "@src/components/shared/LinkTo"; @@ -28,6 +30,7 @@ import { copyTextToClipboard } from "@src/utils/copyClipboard"; import { deploymentData } from "@src/utils/deploymentData"; import { getGpusFromAttributes, sendManifestToProvider } from "@src/utils/deploymentUtils"; import { udenomToDenom } from "@src/utils/mathHelpers"; +import { sshVmImages } from "@src/utils/sdl/data"; import { cn } from "@src/utils/styleUtils"; import { UrlService } from "@src/utils/urlUtils"; import { ManifestErrorSnackbar } from "../shared/ManifestErrorSnackbar"; @@ -92,6 +95,8 @@ export const LeaseRow = React.forwardRef(({ lease, setActi } }, [isLeaseActive, provider, localCert, getLeaseStatus, getProviderStatus]); + const parsedManifest = useMemo(() => yaml.load(deploymentManifest), [deploymentManifest]); + const checkIfServicesAreAvailable = leaseStatus => { const servicesNames = leaseStatus ? Object.keys(leaseStatus.services) : []; const isServicesAvailable = @@ -117,7 +122,7 @@ export const LeaseRow = React.forwardRef(({ lease, setActi async function sendManifest() { setIsSendingManifest(true); try { - const doc = yaml.load(deploymentManifest); + const doc = parsedManifest; const manifest = deploymentData.getManifest(doc, true); await sendManifestToProvider(provider as ApiProviderList, manifest, dseq, localCert as LocalCert); @@ -142,6 +147,28 @@ export const LeaseRow = React.forwardRef(({ lease, setActi const gpuModels = bid && bid.bid.resources_offer.flatMap(x => getGpusFromAttributes(x.resources.gpu.attributes)); + const sshInstructions = useMemo(() => { + return servicesNames.reduce((acc, serviceName) => { + if (!sshVmImages.has(get(parsedManifest, ["services", serviceName, "image"]))) { + return acc; + } + + const exposes = leaseStatus.forwarded_ports[serviceName]; + + return exposes.reduce((exposesAcc, expose) => { + if (expose.port !== 22) { + return exposesAcc; + } + + if (exposesAcc) { + exposesAcc += "\n"; + } + + return exposesAcc.concat(`ssh root@${expose.host} -p ${expose.externalPort} -i ~/.ssh/id_rsa`); + }, acc); + }, ""); + }, [parsedManifest, servicesNames, leaseStatus]); + return ( @@ -421,6 +448,17 @@ export const LeaseRow = React.forwardRef(({ lease, setActi )} + + {sshInstructions && ( +
+
SSH Instructions:
+

+ Use this command in your terminal to access your VM via SSH. Make sure to pass the correct path to the private key corresponding to the public one + provided during this deployment creation. +

+ +
+ )}
); diff --git a/apps/deploy-web/src/components/new-deployment/ManifestEdit.tsx b/apps/deploy-web/src/components/new-deployment/ManifestEdit.tsx index 4e4331d97..c2d622f1a 100644 --- a/apps/deploy-web/src/components/new-deployment/ManifestEdit.tsx +++ b/apps/deploy-web/src/components/new-deployment/ManifestEdit.tsx @@ -13,6 +13,7 @@ import { event } from "nextjs-google-analytics"; import { useCertificate } from "@src/context/CertificateProvider"; import { useChainParam } from "@src/context/ChainParamProvider"; import { useWallet } from "@src/context/WalletProvider"; +import { useWhen } from "@src/hooks/useWhen"; import sdlStore from "@src/store/sdlStore"; import { TemplateCreation } from "@src/types"; import { AnalyticsEvents } from "@src/utils/analytics"; @@ -39,9 +40,11 @@ type Props = { selectedTemplate: TemplateCreation | null; editedManifest: string | null; setEditedManifest: Dispatch; + imageList?: string[]; + ssh?: boolean; }; -export const ManifestEdit: React.FunctionComponent = ({ editedManifest, setEditedManifest, onTemplateSelected, selectedTemplate }) => { +export const ManifestEdit: React.FunctionComponent = ({ editedManifest, setEditedManifest, onTemplateSelected, selectedTemplate, imageList, ssh }) => { const [parsingError, setParsingError] = useState(null); const [deploymentName, setDeploymentName] = useState(""); const [isCreatingDeployment, setIsCreatingDeployment] = useState(false); @@ -60,10 +63,14 @@ export const ManifestEdit: React.FunctionComponent = ({ editedManifest, s const { minDeposit } = useChainParam(); const searchParams = useSearchParams(); - const templateId = searchParams.get('templateId'); + const templateId = searchParams.get("templateId"); const fileUploadRef = useRef(null); + useWhen(ssh, () => { + setSelectedSdlEditMode("builder"); + }); + const propagateUploadedSdl = (event: React.ChangeEvent) => { const selectedFiles = event.target.files ?? []; const hasFileSelected = selectedFiles.length > 0; @@ -81,7 +88,7 @@ export const ManifestEdit: React.FunctionComponent = ({ editedManifest, s content: event.target?.result as string }); setEditedManifest(event.target?.result as string); - setSelectedSdlEditMode('yaml'); + setSelectedSdlEditMode("yaml"); }; reader.readAsText(fileUploaded); @@ -89,7 +96,7 @@ export const ManifestEdit: React.FunctionComponent = ({ editedManifest, s const triggerFileInput = () => { if (fileUploadRef.current) { - fileUploadRef.current.value = ''; + fileUploadRef.current.value = ""; fileUploadRef.current.click(); } }; @@ -106,8 +113,7 @@ export const ManifestEdit: React.FunctionComponent = ({ editedManifest, s const timer = Timer(500); timer.start().then(() => { - if (editedManifest) - createAndValidateDeploymentData(editedManifest, "TEST_DSEQ_VALIDATION"); + if (editedManifest) createAndValidateDeploymentData(editedManifest, "TEST_DSEQ_VALIDATION"); }); return () => { @@ -173,7 +179,7 @@ export const ManifestEdit: React.FunctionComponent = ({ editedManifest, s async function handleCreateClick(deposit: number, depositorAddress: string) { setIsCreatingDeployment(true); - const sdl = selectedSdlEditMode === "yaml" ? editedManifest : (sdlBuilderRef.current?.getSdl()); + const sdl = selectedSdlEditMode === "yaml" ? editedManifest : sdlBuilderRef.current?.getSdl(); if (!sdl) { setIsCreatingDeployment(false); @@ -241,7 +247,7 @@ export const ManifestEdit: React.FunctionComponent = ({ editedManifest, s } } - const onModeChange = (mode: "yaml" | "builder") => { + const changeMode = (mode: "yaml" | "builder") => { if (mode === selectedSdlEditMode) return; if (mode === "yaml") { @@ -250,6 +256,7 @@ export const ManifestEdit: React.FunctionComponent = ({ editedManifest, s } } else { const sdl = sdlBuilderRef.current?.getSdl(); + if (sdl) { setEditedManifest(sdl); } @@ -306,51 +313,55 @@ export const ManifestEdit: React.FunctionComponent = ({ editedManifest, s -
-
- - -
- {!templateId && ( - <> - + {!ssh && ( +
+
- - )} -
+ +
+ {!templateId && ( + <> + + + + )} +
+ )} {parsingError && {parsingError}} - {selectedSdlEditMode === "yaml" && ( + {!ssh && selectedSdlEditMode === "yaml" && ( - + )} - {selectedSdlEditMode === "builder" && } + {(ssh || selectedSdlEditMode === "builder") && ( + + )} {isDepositingDeployment && ( = ({ imageSource = "user-provided", ssh }) => { const { isLoading: isLoadingTemplates, templates } = useTemplates(); const [activeStep, setActiveStep] = useState(null); const [selectedTemplate, setSelectedTemplate] = useState(null); @@ -118,6 +124,8 @@ export function NewDeploymentContainer() { {activeStep === 0 && } {activeStep === 1 && ( } ); -} +}; diff --git a/apps/deploy-web/src/components/new-deployment/SdlBuilder.tsx b/apps/deploy-web/src/components/new-deployment/SdlBuilder.tsx index 91af8211f..8fc5703b5 100644 --- a/apps/deploy-web/src/components/new-deployment/SdlBuilder.tsx +++ b/apps/deploy-web/src/components/new-deployment/SdlBuilder.tsx @@ -2,18 +2,22 @@ import React, { Dispatch, useEffect, useRef, useState } from "react"; import { useFieldArray, useForm } from "react-hook-form"; import { Alert, Button, Spinner } from "@akashnetwork/ui/components"; +import cloneDeep from "lodash/cloneDeep"; import { nanoid } from "nanoid"; import { useGpuModels } from "@src/queries/useGpuQuery"; import { SdlBuilderFormValues, Service } from "@src/types"; -import { defaultService } from "@src/utils/sdl/data"; +import { defaultService, defaultSshVMService } from "@src/utils/sdl/data"; import { generateSdl } from "@src/utils/sdl/sdlGenerator"; import { importSimpleSdl } from "@src/utils/sdl/sdlImport"; +import { transformCustomSdlFields } from "@src/utils/sdl/transformCustomSdlFields"; import { SimpleServiceFormControl } from "../sdl/SimpleServiceFormControl"; interface Props { sdlString: string | null; setEditedManifest: Dispatch; + imageList?: string[]; + ssh?: boolean; } export type SdlBuilderRefType = { @@ -21,13 +25,13 @@ export type SdlBuilderRefType = { validate: () => Promise; }; -export const SdlBuilder = React.forwardRef(({ sdlString, setEditedManifest }, ref) => { +export const SdlBuilder = React.forwardRef(({ sdlString, setEditedManifest, imageList, ssh }, ref) => { const [error, setError] = useState(null); const formRef = useRef(null); const [isInit, setIsInit] = useState(false); const { control, trigger, watch, setValue } = useForm({ defaultValues: { - services: [{ ...defaultService }] + services: [cloneDeep(ssh ? defaultSshVMService : defaultService)] } }); const { @@ -73,7 +77,7 @@ export const SdlBuilder = React.forwardRef(({ sdlStrin }, [watch]); const getSdl = () => { - return generateSdl(_services as Service[]); + return generateSdl(transformCustomSdlFields(_services, { withSSH: ssh })); }; const createAndValidateSdl = (yamlStr: string) => { @@ -92,7 +96,6 @@ export const SdlBuilder = React.forwardRef(({ sdlStrin setError(err.message); } else { setError("Error while parsing SDL file"); - // setParsingError(err.message); console.error(err); } } @@ -128,6 +131,8 @@ export const SdlBuilder = React.forwardRef(({ sdlStrin serviceCollapsed={serviceCollapsed} setServiceCollapsed={setServiceCollapsed} hasSecretOption={false} + imageList={imageList} + ssh={ssh} /> ))} @@ -137,13 +142,15 @@ export const SdlBuilder = React.forwardRef(({ sdlStrin )} -
-
- + {!ssh && ( +
+
+ +
-
+ )} )}
diff --git a/apps/deploy-web/src/components/new-deployment/TemplateList.tsx b/apps/deploy-web/src/components/new-deployment/TemplateList.tsx index 972b9cb2c..e300a589c 100644 --- a/apps/deploy-web/src/components/new-deployment/TemplateList.tsx +++ b/apps/deploy-web/src/components/new-deployment/TemplateList.tsx @@ -10,11 +10,11 @@ import { useRouter } from "next/navigation"; import { useTemplates } from "@src/context/TemplatesProvider"; import { usePreviousRoute } from "@src/hooks/usePreviousRoute"; import sdlStore from "@src/store/sdlStore"; -import { ApiTemplate, TemplateCreation } from "@src/types"; +import { ApiTemplate } from "@src/types"; import { RouteStepKeys } from "@src/utils/constants"; import { cn } from "@src/utils/styleUtils"; import { helloWorldTemplate, ubuntuTemplate } from "@src/utils/templates"; -import { domainName, UrlService } from "@src/utils/urlUtils"; +import { domainName, NewDeploymentParams, UrlService } from "@src/utils/urlUtils"; import { CustomNextSeo } from "../shared/CustomNextSeo"; import { TemplateBox } from "../templates/TemplateBox"; import { DeployOptionBox } from "./DeployOptionBox"; @@ -46,9 +46,9 @@ export const TemplateList: React.FunctionComponent = () => { } }, [templates]); - function onSDLBuilderClick() { + function onSDLBuilderClick(page: NewDeploymentParams["page"] = "new-deployment") { setSdlEditMode("builder"); - router.push(UrlService.newDeployment({ step: RouteStepKeys.editDeployment })); + router.push(UrlService.newDeployment({ step: RouteStepKeys.editDeployment, page })); } function handleBackClick() { @@ -73,7 +73,7 @@ export const TemplateList: React.FunctionComponent = () => {
-
+
{ title="Build your template" description="With our new SDL Builder, you can create your own SDL from scratch in a few clicks!" icon={} - onClick={onSDLBuilderClick} + onClick={() => onSDLBuilderClick()} /> - {/* TODO: Coming soon - Plain Linux option will be available in future updates */} - {/* } - onClick={() => router.push(UrlService.plainLinux())} - /> */} + { + } + onClick={() => onSDLBuilderClick("plain-linux")} + /> + }
diff --git a/apps/deploy-web/src/components/sdl/EnvVarList.tsx b/apps/deploy-web/src/components/sdl/EnvVarList.tsx index d1412eda5..effba633c 100644 --- a/apps/deploy-web/src/components/sdl/EnvVarList.tsx +++ b/apps/deploy-web/src/components/sdl/EnvVarList.tsx @@ -11,9 +11,10 @@ type Props = { serviceIndex?: number; children?: ReactNode; setIsEditingEnv: Dispatch>; + ssh?: boolean; }; -export const EnvVarList: React.FunctionComponent = ({ currentService, setIsEditingEnv, serviceIndex }) => { +export const EnvVarList: React.FunctionComponent = ({ currentService, setIsEditingEnv, serviceIndex, ssh }) => { return (
@@ -23,6 +24,13 @@ export const EnvVarList: React.FunctionComponent = ({ currentService, set title={ <> A list of environment variables to expose to the running container. + {ssh && ( + <> +
+
+ Note: The SSH_PUBKEY environment variable is reserved and is going to be overridden by the value provided to the relevant field. + + )}

diff --git a/apps/deploy-web/src/components/sdl/ExposeList.tsx b/apps/deploy-web/src/components/sdl/ExposeList.tsx index 68830bcb1..bf319503d 100644 --- a/apps/deploy-web/src/components/sdl/ExposeList.tsx +++ b/apps/deploy-web/src/components/sdl/ExposeList.tsx @@ -12,9 +12,10 @@ type Props = { serviceIndex?: number; children?: ReactNode; setIsEditingExpose: Dispatch>; + ssh?: boolean; }; -export const ExposeList: React.FunctionComponent = ({ currentService, setIsEditingExpose, serviceIndex }) => { +export const ExposeList: React.FunctionComponent = ({ currentService, setIsEditingExpose, serviceIndex, ssh }) => { return (
@@ -24,6 +25,13 @@ export const ExposeList: React.FunctionComponent = ({ currentService, set title={ <> Expose is a list of port settings describing what can connect to the service. + {ssh && ( + <> +
+
+ Note: Port 22 is reserved for SSH and is going to be exposed by default. + + )}

diff --git a/apps/deploy-web/src/components/sdl/SimpleServiceFormControl.tsx b/apps/deploy-web/src/components/sdl/SimpleServiceFormControl.tsx index ff71512b5..a350f6383 100644 --- a/apps/deploy-web/src/components/sdl/SimpleServiceFormControl.tsx +++ b/apps/deploy-web/src/components/sdl/SimpleServiceFormControl.tsx @@ -10,11 +10,17 @@ import { CollapsibleContent, CollapsibleTrigger, CustomTooltip, - InputWithIcon + InputWithIcon, + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue } from "@akashnetwork/ui/components"; import { useTheme as useMuiTheme } from "@mui/material/styles"; import useMediaQuery from "@mui/material/useMediaQuery"; -import { BinMinusIn, InfoCircle, NavArrowDown, OpenInWindow } from "iconoir-react"; +import { BinMinusIn, InfoCircle, Key, NavArrowDown, OpenInWindow } from "iconoir-react"; import Image from "next/legacy/image"; import Link from "next/link"; @@ -52,6 +58,8 @@ type Props = { setValue: UseFormSetValue; gpuModels: GpuVendor[] | undefined; hasSecretOption?: boolean; + imageList?: string[]; + ssh?: boolean; }; export const SimpleServiceFormControl: React.FunctionComponent = ({ @@ -64,7 +72,9 @@ export const SimpleServiceFormControl: React.FunctionComponent = ({ setServiceCollapsed, setValue, gpuModels, - hasSecretOption + hasSecretOption, + imageList, + ssh }) => { const [isEditingCommands, setIsEditingCommands] = useState(null); const [isEditingEnv, setIsEditingEnv] = useState(null); @@ -219,59 +229,116 @@ export const SimpleServiceFormControl: React.FunctionComponent = ({ control={control} name={`services.${serviceIndex}.image`} rules={{ - required: "Docker image name is required.", - validate: value => { - const hasValidChars = /^[a-z0-9\-_/:.]+$/.test(value); - - if (!hasValidChars) { - return "Invalid docker image name."; - } - - return true; - } + required: "Docker image name is required." }} - render={({ field, fieldState }) => ( - - Docker Image / OS - - Docker image of the container. -
-
- Best practices: avoid using :latest image tags as Akash Providers heavily cache images. - - } + render={({ field, fieldState }) => + imageList?.length ? ( +
+ + {fieldState.error?.message &&

{fieldState.error.message}

} +
+ ) : ( + + Docker Image / OS + + Docker image of the container. +
+
+ Best practices: avoid using :latest image tags as Akash Providers heavily cache images. + + } + > + +
+
+ } + placeholder="Example: mydockerimage:1.01" + color="secondary" + // error={!!fieldState.error} + error={fieldState.error?.message} + className="flex-grow" + value={field.value} + onChange={event => field.onChange((event.target.value || "").toLowerCase())} + startIcon={Docker Logo} + endIcon={ + - - -
- } - placeholder="Example: mydockerimage:1.01" - color="secondary" - // error={!!fieldState.error} - error={fieldState.error?.message} - className="flex-grow" - value={field.value} - onChange={event => field.onChange((event.target.value || "").toLowerCase())} - startIcon={Docker Logo} - endIcon={ - - - - } - /> - )} + + + } + /> + ) + } />
+ {ssh && ( +
+ ( + + SSH Public Key + + SSH Public Key +
+
+ The key is added to environment variables of the container and then to ~/.ssh/authorized_keys on startup. + + } + > + +
+
+ } + placeholder="ssh-..." + color="secondary" + error={fieldState.error?.message} + className="flex-grow" + value={field.value} + onChange={event => field.onChange(event.target.value || "")} + startIcon={} + /> + )} + /> + + )} +
@@ -304,75 +371,79 @@ export const SimpleServiceFormControl: React.FunctionComponent = ({
- +
-
- -
+ {!ssh && ( +
+ +
+ )}
- +
-
+ {!ssh && ( +
+ { + if (!v) return "Service count is required."; + return true; } - value={field.value || ""} - // error={!!fieldState.error} - error={fieldState.error?.message} - onChange={event => { - const newValue = parseInt(event.target.value); - field.onChange(newValue); - - if (newValue) { - trigger(`services.${serviceIndex}.profile.cpu`); - trigger(`services.${serviceIndex}.profile.ram`); - trigger(`services.${serviceIndex}.profile.storage`); + }} + render={({ field, fieldState }) => ( + + Service Count + + The number of instances of the service to run. +
+
+ + View official documentation. + + + } + > + +
+
} - }} - min={1} - max={20} - step={1} - /> - )} - /> -
+ value={field.value || ""} + // error={!!fieldState.error} + error={fieldState.error?.message} + onChange={event => { + const newValue = parseInt(event.target.value); + field.onChange(newValue); + + if (newValue) { + trigger(`services.${serviceIndex}.profile.cpu`); + trigger(`services.${serviceIndex}.profile.ram`); + trigger(`services.${serviceIndex}.profile.storage`); + } + }} + min={1} + max={20} + step={1} + /> + )} + /> + + )}
diff --git a/apps/deploy-web/src/pages/new-deployment/index.tsx b/apps/deploy-web/src/pages/new-deployment/index.tsx index 55513bf99..1abbc241b 100644 --- a/apps/deploy-web/src/pages/new-deployment/index.tsx +++ b/apps/deploy-web/src/pages/new-deployment/index.tsx @@ -1,7 +1,3 @@ import { NewDeploymentContainer } from "@src/components/new-deployment/NewDeploymentContainer"; -function NewDeploymentPage() { - return ; -} - -export default NewDeploymentPage; +export default NewDeploymentContainer; diff --git a/apps/deploy-web/src/pages/plain-linux/index.tsx b/apps/deploy-web/src/pages/plain-linux/index.tsx index 7f89dd66c..b5a25705e 100644 --- a/apps/deploy-web/src/pages/plain-linux/index.tsx +++ b/apps/deploy-web/src/pages/plain-linux/index.tsx @@ -1,27 +1,5 @@ -import React from "react"; +import { NewDeploymentContainer } from "@src/components/new-deployment/NewDeploymentContainer"; -import Layout from "@src/components/layout/Layout"; -import { PlainVMForm } from "@src/components/sdl/PlainVMForm"; -import { CustomNextSeo } from "@src/components/shared/CustomNextSeo"; -import { Title } from "@src/components/shared/Title"; -import { domainName, UrlService } from "@src/utils/urlUtils"; - -function PlainLinuxPage() { - return ( - - - - Plain Linux - -

Choose from multiple linux distros. Deploy and SSH into it. Install and run what you want after that.

- - -
- ); +export default function NewDeploymentPage() { + return ; } - -export default PlainLinuxPage; diff --git a/apps/deploy-web/src/types/sdlBuilder.ts b/apps/deploy-web/src/types/sdlBuilder.ts index 1f51ea54d..6ca654ab4 100644 --- a/apps/deploy-web/src/types/sdlBuilder.ts +++ b/apps/deploy-web/src/types/sdlBuilder.ts @@ -10,6 +10,7 @@ export type Service = { env?: EnvironmentVariable[]; placement: Placement; count: number; + sshPubKey?: string; }; export type ImportService = { diff --git a/apps/deploy-web/src/utils/sdl/data.ts b/apps/deploy-web/src/utils/sdl/data.ts index ca4f6a674..5400747d7 100644 --- a/apps/deploy-web/src/utils/sdl/data.ts +++ b/apps/deploy-web/src/utils/sdl/data.ts @@ -21,6 +21,7 @@ export const defaultService: Service = { id: nanoid(), title: "service-1", image: "", + sshPubKey: "", profile: { cpu: 0.1, gpu: 1, @@ -76,6 +77,24 @@ export const defaultService: Service = { count: 1 }; +export const SSH_VM_IMAGES = { + "Ubuntu 24.04": "ghcr.io/akash-network/ubuntu-ssh-2404:3" +}; +export const sshVmDistros: string[] = Object.keys(SSH_VM_IMAGES); +export const sshVmImages: Set = new Set(Object.values(SSH_VM_IMAGES)); +export const SSH_EXPOSE = { + port: 22, + as: 22, + global: true, + to: [] +}; + +export const defaultSshVMService: Service = { + ...defaultService, + image: sshVmDistros[0], + expose: [] +}; + export const defaultRentGpuService: Service = { id: nanoid(), title: "service-1", diff --git a/apps/deploy-web/src/utils/sdl/transformCustomSdlFields.ts b/apps/deploy-web/src/utils/sdl/transformCustomSdlFields.ts new file mode 100644 index 000000000..fd0481ba6 --- /dev/null +++ b/apps/deploy-web/src/utils/sdl/transformCustomSdlFields.ts @@ -0,0 +1,94 @@ +import cloneDeep from "lodash/cloneDeep"; +import flowRight from "lodash/flowRight"; +import isMatch from "lodash/isMatch"; + +import { Service } from "@src/types"; +import { SSH_EXPOSE, SSH_VM_IMAGES } from "@src/utils/sdl/data"; + +interface TransformOptions { + withSSH?: boolean; +} + +export const transformCustomSdlFields = (services: Service[], options?: TransformOptions) => { + const pipeline = [addSshPubKey, ensureServiceCount]; + + if (options?.withSSH) { + pipeline.push(ensureSSHExpose); + pipeline.push(mapImage); + } + + const transform = flowRight(pipeline); + + return services.map(service => transform(service)); +}; + +function addSshPubKey({ sshPubKey, ...input }: Service) { + if (!sshPubKey) { + return input; + } + + const output = cloneDeep(input); + + output.env = output.env || []; + const sshPubKeyEnv = output.env.find(e => e.key === "SSH_PUB_KEY"); + + if (sshPubKeyEnv) { + sshPubKeyEnv.value = sshPubKey; + } else { + output.env.push({ + id: "SSH_PUBKEY", + key: "SSH_PUBKEY", + value: sshPubKey, + isSecret: false + }); + } + + return output; +} + +function ensureSSHExpose(service: Service) { + if (service.expose.some(exp => isMatch(exp, SSH_EXPOSE))) { + return service; + } + + if (service.expose.some(exp => exp.port === 22)) { + throw new Error("Expose outer port 22 is reserved"); + } + + if (service.expose.some(exp => exp.as === 22)) { + throw new Error("Expose inner port 22 is reserved"); + } + + const output = cloneDeep(service); + + output.expose.push({ + id: "ssh", + ...SSH_EXPOSE + }); + + return output; +} + +function ensureServiceCount(input: Service) { + if (input.count === 1) { + return input; + } + + const output = cloneDeep(input); + output.count = 1; + + return output; +} + +function mapImage(input: Service) { + const image = SSH_VM_IMAGES[input.image]; + + if (!image) { + throw new Error(`Unsupported SSH VM image: ${input.image}`); + } + + const output = cloneDeep(input); + output.image = image; + + return output; +} diff --git a/apps/deploy-web/src/utils/urlUtils.ts b/apps/deploy-web/src/utils/urlUtils.ts index 603ad8c95..00b0e23d2 100644 --- a/apps/deploy-web/src/utils/urlUtils.ts +++ b/apps/deploy-web/src/utils/urlUtils.ts @@ -1,11 +1,12 @@ import { FaqAnchorType } from "@src/pages/faq"; import { mainnetId, selectedNetworkId } from "./constants"; -type NewDeploymentParams = { +export type NewDeploymentParams = { step?: string; dseq?: string | number; redeploy?: string | number; templateId?: string; + page?: "new-deployment" | "plain-linux"; }; function getSelectedNetworkQueryParam() { @@ -75,7 +76,8 @@ export class UrlService { // New deployment static newDeployment = (params: NewDeploymentParams = {}) => { const { step, dseq, redeploy, templateId } = params; - return `/new-deployment${appendSearchParams({ dseq, step, templateId, redeploy })}`; + const page = params.page || "new-deployment"; + return `/${page}${appendSearchParams({ dseq, step, templateId, redeploy })}`; }; }