Skip to content

Commit d71d504

Browse files
authored
Merge pull request #16 from Ratio1/fix-deeploy-native
Fix deeploy native
2 parents f220ad6 + 76fbf1b commit d71d504

22 files changed

+332
-36
lines changed

src/components/create-job/secondary-plugins/NativeInputsSection.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export default function NativeInputsSection({ index }: { index: number }) {
2626
displayLabel="custom parameters"
2727
maxEntries={50}
2828
predefinedEntries={pluginSignaturesCustomParams[pluginSignature] ?? []}
29+
enableJsonValues={true}
2930
/>
3031
</div>
3132
);

src/components/create-job/secondary-plugins/SecondaryPluginsCard.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export default function SecondaryPluginsCard() {
9494
crVisibility: CR_VISIBILITY_OPTIONS[0],
9595
crUsername: '',
9696
crPassword: '',
97+
ports: {},
9798
},
9899
...TUNNELING_DEFAULTS,
99100
...GENERIC_PLUGIN_DEFAULTS,
@@ -114,6 +115,7 @@ export default function SecondaryPluginsCard() {
114115
{ command: 'npm run build' },
115116
{ command: 'npm run start' },
116117
],
118+
ports: {},
117119
},
118120
...TUNNELING_DEFAULTS,
119121
...GENERIC_PLUGIN_DEFAULTS,

src/components/create-job/sections/AppParametersSection.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { BOOLEAN_TYPES } from '@data/booleanTypes';
22
import InputWithLabel from '@shared/InputWithLabel';
33
import NumberInputWithLabel from '@shared/NumberInputWithLabel';
4+
import PortMappingSection from '@shared/PortMappingSection';
45
import SelectWithLabel from '@shared/SelectWithLabel';
56
import { useFormContext } from 'react-hook-form';
67

78
export default function AppParametersSection({ baseName = 'deployment' }: { baseName?: string }) {
89
const { watch, trigger } = useFormContext();
910

1011
const enableTunneling: (typeof BOOLEAN_TYPES)[number] = watch(`${baseName}.enableTunneling`);
12+
const deploymentType = watch(`${baseName}.deploymentType.type`);
1113

1214
return (
1315
<div className="col gap-4">
@@ -33,6 +35,11 @@ export default function AppParametersSection({ baseName = 'deployment' }: { base
3335
<InputWithLabel name={`${baseName}.tunnelingToken`} label="Tunnel Token" placeholder="Starts with 'ey'" />
3436
</div>
3537
)}
38+
39+
{/* TODO: Check if deploying generic plugin/job */}
40+
{(deploymentType === 'container' || deploymentType === 'worker') && (
41+
<PortMappingSection name={`${baseName}.deploymentType.ports`} label="Port Mapping" />
42+
)}
3643
</div>
3744
);
3845
}

src/components/create-job/steps/deployment/NativeDeployment.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ function NativeDeployment({ isEditingJob }: { isEditingJob?: boolean }) {
3636
displayLabel="custom parameters"
3737
maxEntries={50}
3838
predefinedEntries={pluginSignaturesCustomParams[pluginSignature] ?? []}
39+
enableJsonValues={true}
3940
/>
4041
</SlateCard>
4142

src/lib/deeploy-utils.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import _ from 'lodash';
2929
import { FieldValues, UseFieldArrayAppend, UseFieldArrayRemove } from 'react-hook-form';
3030
import { formatUnits } from 'viem';
3131
import { environment } from './config';
32-
import { deepSort } from './utils';
32+
import { deepSort, parseIfJson } from './utils';
3333

3434
export const GITHUB_REPO_REGEX = new RegExp('^https?://github\\.com/([^\\s/]+)/([^\\s/]+?)(?:\\.git)?(?:/.*)?$', 'i');
3535

@@ -123,7 +123,7 @@ export const downloadDataAsJson = (data: any, filename: string) => {
123123
export const generateDeeployNonce = (): string => {
124124
const now = new Date();
125125
const unixTimestamp = now.getTime();
126-
console.log({ now, unixTimestamp });
126+
// console.log({ now, unixTimestamp });
127127
return `0x${unixTimestamp.toString(16)}`;
128128
};
129129

@@ -170,10 +170,11 @@ export const formatFileVolumes = (fileVolumes: { name: string; mountingPoint: st
170170
return formatted;
171171
};
172172

173-
export const formatContainerResources = (containerOrWorkerType: ContainerOrWorkerType) => {
173+
export const formatContainerResources = (containerOrWorkerType: ContainerOrWorkerType, ports?: Record<string, string>) => {
174174
return {
175175
cpu: containerOrWorkerType.cores,
176176
memory: `${containerOrWorkerType.ram}g`,
177+
...(ports && Object.keys(ports).length > 0 && { ports }),
177178
};
178179
};
179180

@@ -208,7 +209,7 @@ export const formatServiceDraftJobPayload = (job: ServiceDraftJob) => {
208209
return formatServiceJobPayload(containerType, job.specifications, job.deployment);
209210
};
210211

211-
export const formatGenericJobVariables = (plugin: GenericSecondaryPlugin) => {
212+
const formatGenericJobVariables = (plugin: GenericSecondaryPlugin) => {
212213
return {
213214
envVars: formatEnvVars(plugin.envVars),
214215
dynamicEnvVars: formatDynamicEnvVars(plugin.dynamicEnvVars),
@@ -217,16 +218,27 @@ export const formatGenericJobVariables = (plugin: GenericSecondaryPlugin) => {
217218
};
218219
};
219220

220-
export const formatNativeJobPluginSignature = (plugin: NativeSecondaryPlugin) => {
221+
const formatNativeJobPluginSignature = (plugin: NativeSecondaryPlugin) => {
221222
return plugin.pluginSignature === PLUGIN_SIGNATURE_TYPES[PLUGIN_SIGNATURE_TYPES.length - 1]
222223
? plugin.customPluginSignature
223224
: plugin.pluginSignature;
224225
};
225226

227+
const formatNativeJobCustomParams = (pluginConfig: any, plugin: NativeSecondaryPlugin) => {
228+
if (!_.isEmpty(plugin.customParams)) {
229+
plugin.customParams.forEach((param) => {
230+
if (param.key) {
231+
pluginConfig[param.key] = parseIfJson(param.value);
232+
}
233+
});
234+
}
235+
};
236+
226237
export const formatGenericPluginConfigAndSignature = (
227238
resources: {
228239
cpu: number;
229240
memory: string;
241+
ports?: Record<string, string>;
230242
},
231243
plugin: GenericSecondaryPlugin,
232244
) => {
@@ -291,7 +303,7 @@ export const formatGenericJobPayload = (
291303
const spareNodes = formatNodes(deployment.spareNodes);
292304

293305
const { pluginConfig, pluginSignature } = formatGenericPluginConfigAndSignature(
294-
formatContainerResources(containerType),
306+
formatContainerResources(containerType, deployment.deploymentType.ports),
295307
deployment,
296308
);
297309

@@ -331,7 +343,7 @@ export const formatNativeJobPayload = (
331343
}
332344
});
333345

334-
const nodeResources = formatContainerResources(workerType);
346+
const nodeResources = formatContainerResources(workerType, undefined);
335347
const targetNodes = formatNodes(deployment.targetNodes);
336348
const targetNodesCount = formatTargetNodesCount(targetNodes, specifications.targetNodesCount);
337349

@@ -348,13 +360,7 @@ export const formatNativeJobPayload = (
348360
NGROK_USE_API: true,
349361
};
350362

351-
if (!_.isEmpty(deployment.customParams)) {
352-
deployment.customParams.forEach((param) => {
353-
if (param.key) {
354-
primaryPluginConfig[param.key] = param.value;
355-
}
356-
});
357-
}
363+
formatNativeJobCustomParams(primaryPluginConfig, deployment);
358364

359365
// Build plugins array starting with the primary plugin
360366
const plugins = [primaryPluginConfig];
@@ -424,7 +430,7 @@ export const formatServiceJobPayload = (
424430
deployment: ServiceJobDeployment,
425431
) => {
426432
const jobTags = formatJobTags(specifications);
427-
const containerResources = formatContainerResources(containerType);
433+
const containerResources = formatContainerResources(containerType, undefined);
428434
const targetNodes = formatNodes(deployment.targetNodes);
429435
const spareNodes = formatNodes(deployment.spareNodes);
430436

src/lib/utils.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export const extractRepositoryPath = (url: string): string => {
106106
* @param length - The length of the password (default: 24)
107107
* @returns A secure password string
108108
*/
109-
function generateSecurePassword(length: number = 24): string {
109+
export function generateSecurePassword(length: number = 24): string {
110110
const lowercase = 'abcdefghijklmnopqrstuvwxyz';
111111
const uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
112112
const numbers = '0123456789';
@@ -132,3 +132,12 @@ function generateSecurePassword(length: number = 24): string {
132132
.sort(() => Math.random() - 0.5)
133133
.join('');
134134
}
135+
136+
export function parseIfJson<T = any>(input: string): T | string {
137+
try {
138+
const parsed = JSON.parse(input);
139+
return parsed;
140+
} catch {
141+
return input;
142+
}
143+
}

src/pages/deeploys/job/EditJob.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ export default function EditJob() {
280280
target_nodes: targetNodes,
281281
target_nodes_count: 0,
282282
app_params: {
283-
CONTAINER_RESOURCES: formatContainerResources(containerType),
283+
CONTAINER_RESOURCES: formatContainerResources(containerType, undefined),
284284
},
285285
project_id: job.projectHash,
286286
chainstore_response: true,

src/schemas/common.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const keyValueEntrySchema = z
88
.object({
99
key: z.string().optional(),
1010
value: z.string().optional(),
11+
valueType: z.enum(['text', 'json']).optional().default('text'),
1112
})
1213
.refine(
1314
(data) => {
@@ -38,6 +39,24 @@ export const keyValueEntrySchema = z
3839
message: 'Value is required',
3940
path: ['value'],
4041
},
42+
)
43+
.refine(
44+
(data) => {
45+
// If valueType is 'json', validate JSON syntax
46+
if (data.valueType === 'json' && data.value && data.value.trim() !== '') {
47+
try {
48+
JSON.parse(data.value);
49+
return true;
50+
} catch {
51+
return false;
52+
}
53+
}
54+
return true;
55+
},
56+
{
57+
message: 'Invalid JSON format',
58+
path: ['value'],
59+
},
4160
);
4261

4362
// Schema for array of key-value entries with duplicate key validation

src/schemas/steps/deployment.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ const validations = {
3939
.max(256, 'Value cannot exceed 256 characters')
4040
.regex(/^https?:\/\/.+/, 'Must be a valid URI'),
4141

42+
optionalUri: z
43+
.string()
44+
.refine((val) => val === '' || val.length >= 2, 'Value must be at least 2 characters')
45+
.refine((val) => val === '' || val.length <= 256, 'Value cannot exceed 256 characters')
46+
.refine((val) => val === '' || /^https?:\/\/.+/.test(val), 'Must be a valid URI'),
47+
4248
port: z.union([
4349
z.literal(''),
4450
z
@@ -215,6 +221,7 @@ const containerDeploymentTypeSchema = z.object({
215221
crVisibility: z.enum(CR_VISIBILITY_OPTIONS, { required_error: 'Value is required' }),
216222
crUsername: z.union([getStringSchema(3, 128), z.literal('')]).optional(),
217223
crPassword: z.union([getStringSchema(3, 256), z.literal('')]).optional(),
224+
ports: z.record(z.string(), z.string()).optional(),
218225
});
219226

220227
const workerDeploymentTypeSchema = z.object({
@@ -247,6 +254,7 @@ const workerDeploymentTypeSchema = z.object({
247254
message: 'Duplicate commands are not allowed',
248255
},
249256
),
257+
ports: z.record(z.string(), z.string()).optional(),
250258
});
251259

252260
export const deploymentTypeSchema = z.discriminatedUnion('type', [containerDeploymentTypeSchema, workerDeploymentTypeSchema]);
@@ -325,7 +333,7 @@ const nativeAppDeploymentSchemaWihtoutRefinements = baseDeploymentSchema.extend(
325333
customParams: validations.customParams,
326334
pipelineParams: validations.pipelineParams,
327335
pipelineInputType: z.enum(PIPELINE_INPUT_TYPES, { required_error: 'Value is required' }),
328-
pipelineInputUri: validations.uri.optional(),
336+
pipelineInputUri: validations.optionalUri.optional(),
329337
chainstoreResponse: validations.chainstoreResponse,
330338
secondaryPlugins: z.array(secondaryPluginSchema).max(5, 'Only 5 secondary plugins allowed').optional(),
331339
});

src/shared/PortMappingSection.tsx

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import ConfigSectionTitle from '@components/job/config/ConfigSectionTitle';
2+
import StyledInput from '@shared/StyledInput';
3+
import VariableSectionRemove from '@shared/jobs/VariableSectionRemove';
4+
import { useState } from 'react';
5+
import { useFormContext } from 'react-hook-form';
6+
import { RiAddLine } from 'react-icons/ri';
7+
import DeeployWarning from './jobs/DeeployWarning';
8+
9+
interface PortMappingSectionProps {
10+
name: string;
11+
label?: string;
12+
}
13+
14+
export default function PortMappingSection({ name, label = 'Port Mapping' }: PortMappingSectionProps) {
15+
const { setValue, watch, formState, trigger } = useFormContext();
16+
const ports = watch(name) || {};
17+
const [portEntries, setPortEntries] = useState<Array<{ id: string; hostPort: string; containerPort: string }>>(
18+
Object.entries(ports).map(([hostPort, containerPort], index) => ({
19+
id: `port-${index}`,
20+
hostPort,
21+
containerPort: String(containerPort),
22+
})),
23+
);
24+
25+
const updatePorts = (entries: Array<{ id: string; hostPort: string; containerPort: string }>) => {
26+
const newPorts: Record<string, string> = {};
27+
entries.forEach((entry) => {
28+
if (entry.hostPort && entry.containerPort) {
29+
newPorts[entry.hostPort] = entry.containerPort;
30+
}
31+
});
32+
setValue(name, newPorts);
33+
trigger(name);
34+
};
35+
36+
const addPortMapping = () => {
37+
const newEntry = {
38+
id: `port-${Date.now()}`,
39+
hostPort: '',
40+
containerPort: '',
41+
};
42+
const newEntries = [...portEntries, newEntry];
43+
setPortEntries(newEntries);
44+
};
45+
46+
const removePortMapping = (id: string) => {
47+
const newEntries = portEntries.filter((entry) => entry.id !== id);
48+
setPortEntries(newEntries);
49+
updatePorts(newEntries);
50+
};
51+
52+
const updateEntry = (id: string, field: 'hostPort' | 'containerPort', value: string) => {
53+
const newEntries = portEntries.map((entry) => (entry.id === id ? { ...entry, [field]: value } : entry));
54+
setPortEntries(newEntries);
55+
updatePorts(newEntries);
56+
};
57+
58+
// Check for duplicate host ports
59+
const hostPorts = portEntries.map((entry) => entry.hostPort).filter((port) => port);
60+
const duplicateHostPorts = hostPorts.filter((port, index) => hostPorts.indexOf(port) !== index);
61+
const hasDuplicateHostPorts = duplicateHostPorts.length > 0;
62+
63+
return (
64+
<div className="col gap-3">
65+
<ConfigSectionTitle title={label} />
66+
67+
<DeeployWarning
68+
title={<div>Port Availability</div>}
69+
description={
70+
<div>
71+
The plugin may fail to start if the specified host ports are not available on your target nodes. Ensure
72+
the ports you map are free and accessible.
73+
</div>
74+
}
75+
/>
76+
77+
{portEntries.length === 0 && <div className="text-sm text-gray-500 italic">No port mappings added yet.</div>}
78+
79+
{portEntries.map((entry, index) => (
80+
<div key={entry.id} className="row items-end gap-3">
81+
<div className="flex-1">
82+
<StyledInput
83+
placeholder="e.g., 8080"
84+
value={entry.hostPort}
85+
onChange={(e) => updateEntry(entry.id, 'hostPort', e.target.value)}
86+
isInvalid={hasDuplicateHostPorts && duplicateHostPorts.includes(entry.hostPort)}
87+
errorMessage={
88+
hasDuplicateHostPorts && duplicateHostPorts.includes(entry.hostPort)
89+
? 'Duplicate host port'
90+
: undefined
91+
}
92+
/>
93+
</div>
94+
<div className="flex-1">
95+
<StyledInput
96+
placeholder="e.g., 8081"
97+
value={entry.containerPort}
98+
onChange={(e) => updateEntry(entry.id, 'containerPort', e.target.value)}
99+
/>
100+
</div>
101+
<VariableSectionRemove onClick={() => removePortMapping(entry.id)} />
102+
</div>
103+
))}
104+
105+
{portEntries.length > 0 && (
106+
<div className="text-sm text-slate-500">
107+
Format: <span className="font-medium uppercase">host_port:container_port</span>
108+
</div>
109+
)}
110+
111+
{hasDuplicateHostPorts && (
112+
<div className="text-danger-600 text-sm">⚠️ Duplicate host ports detected. Each host port must be unique.</div>
113+
)}
114+
115+
{/* TODO: Use append */}
116+
<div className="row compact text-primary cursor-pointer gap-0.5 hover:opacity-50" onClick={addPortMapping}>
117+
<RiAddLine className="text-lg" /> Add
118+
</div>
119+
</div>
120+
);
121+
}

0 commit comments

Comments
 (0)