@kumori/aurora-backend-handler 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1611 @@
1
+ import {
2
+ ComponentSpec,
3
+ } from "@kumori/kumori-module-generator";
4
+ import { Parameter } from "@kumori/kumori-module-generator/dist/types";
5
+ import { getReferenceDomain } from "../websocket-manager";
6
+
7
+ import { createVolume } from "./resources-api-service";
8
+ import {
9
+ buildServiceDeploymentModule,
10
+ ComponentSpec as ComponentSpecDSL,
11
+ ServiceSpec as ServiceSpecDSL,
12
+ } from "@kumori/kumori-dsl-generator";
13
+ import { MarketplaceService, Resource, Service } from "@hestekumori/aurora-interfaces";
14
+
15
+ export type ResourceBundle =
16
+ | { secret: { name: string; configResource: string } }
17
+ | { volume: { name: string; configResource: string } }
18
+ | { certificate: { name: string; configResource: string } }
19
+ | { domain: { name: string; configResource: string } }
20
+ | { port: { name: string; configResource: string } };
21
+
22
+ export interface Role {
23
+ name: string;
24
+ artifact: {
25
+ artifactKind: "component" | "service";
26
+ moduleDomain: string;
27
+ moduleName: string;
28
+ moduleVersion: [number, number, number];
29
+ artifactName: string;
30
+ config: {
31
+ parameters: Parameter[];
32
+ resources: ResourceBundle[];
33
+ scale?: { detail: { [key: string]: number } };
34
+ };
35
+ };
36
+ }
37
+
38
+ export interface Connector {
39
+ clientRole: string;
40
+ clientChannel: string;
41
+ serverRole: string;
42
+ serverChannel: string;
43
+ }
44
+
45
+ export interface DeploymentParameter {
46
+ name: string;
47
+ value: string;
48
+ type: "string" | "bool" | "number";
49
+ }
50
+
51
+ export type DeploymentResource =
52
+ | { secret: { name: string; resource: string } }
53
+ | { volume: { name: string; resource: string } }
54
+ | {
55
+ volume: {
56
+ name: string;
57
+ volume: { size: number; unit: "M"; type: "nonreplicated" };
58
+ };
59
+ }
60
+ | {
61
+ volume: {
62
+ name: string;
63
+ volume: { size: number; unit: "M"; type: "persistent" };
64
+ };
65
+ }
66
+ | {
67
+ volume: {
68
+ name: string;
69
+ volume: { size: number; unit: "M"; type: "volatile" };
70
+ };
71
+ }
72
+ | { certificate: { name: string; resource: string } }
73
+ | { domain: { name: string; resource: string } }
74
+ | { ca: { name: string; resource: string } }
75
+ | { port: { name: string; resource: string } };
76
+ export interface ServiceSpecForm {
77
+ tenantId: string;
78
+ accountId: string;
79
+ environmentId: string;
80
+ serviceId: string;
81
+ cpuRequirements: number;
82
+ memoryRequirements: number;
83
+ registryUrl: string;
84
+ imageTag: string;
85
+ clientChannels: Array<{ name: string }>;
86
+ channels: Array<{
87
+ channelName: string;
88
+ containerPort: number;
89
+ isPublic: boolean;
90
+ protocol: "HTTPS" | "TCP";
91
+ publicPort?: string;
92
+ domain?: string;
93
+ }>;
94
+ clientChannelsExtra?: Array<{ name: string }>;
95
+ defaultExecutable: { cmd?: string; entryPoint?: string };
96
+ config: {
97
+ parameters: Array<{
98
+ name: string;
99
+ value: string;
100
+ type: string;
101
+ content?: string;
102
+ size?: number;
103
+ }>;
104
+ resources: Array<{
105
+ name: string;
106
+ type: string;
107
+ size?: number;
108
+ value?: string;
109
+ content?: string;
110
+ kind?: string;
111
+ key?: string;
112
+ }>;
113
+ };
114
+ environment?: any;
115
+ fileSystem?: any;
116
+ scaling?: {
117
+ cpu: {
118
+ up: string;
119
+ down: string;
120
+ };
121
+ memory: {
122
+ up: string;
123
+ down: string;
124
+ };
125
+ instances: {
126
+ max: number;
127
+ min: number;
128
+ };
129
+ histeresys: string;
130
+ };
131
+ }
132
+
133
+ export interface ServiceWithLocalComponentSpec {
134
+ type: "service";
135
+ subtype: "service_with_local_component";
136
+ tenantId: string;
137
+ accountId: string;
138
+ environmentId: string;
139
+ deploymentId: string;
140
+ moduleDomain: string;
141
+ moduleId: string;
142
+ moduleVersion: [number, number, number];
143
+ labels: Record<string, string>;
144
+ bundleTargetDir: string;
145
+ bundleTargetKind: string;
146
+ channels: {
147
+ client: string[];
148
+ server: Array<{ name: string; port?: number }>;
149
+ };
150
+ config: {
151
+ parameters: any[];
152
+ resources: any[];
153
+ };
154
+ roles: Role[];
155
+ topology: Connector[];
156
+ local_components: {
157
+ [key: string]: ComponentSpec;
158
+ };
159
+ deploymentConfig: {
160
+ parameters: DeploymentParameter[];
161
+ resources: DeploymentResource[];
162
+ scale: { detail: { [key: string]: number } };
163
+ meta: any;
164
+ };
165
+ }
166
+ interface ArtifactConfigParameterDto {
167
+ name: string;
168
+ defaultValue?: string | boolean | number;
169
+ value?: string;
170
+ type: ParameterTypeDto;
171
+ }
172
+ type ParameterTypeDto = "string" | "number" | "bool";
173
+ type ComponentEnvironmentDto =
174
+ | ComponentEnvironmentParamDto
175
+ | ComponentEnvironmentSecretDto;
176
+ interface ComponentEnvironmentParamDto {
177
+ varName: string;
178
+ param: string;
179
+ }
180
+ interface ComponentEnvironmentSecretDto {
181
+ varName: string;
182
+ secret: string;
183
+ }
184
+ type ArtifactConfigResource =
185
+ | CA<ArtifactConfigResourceSpec>
186
+ | Secret<ArtifactConfigResourceSpec>
187
+ | Volume<ArtifactConfigResourceSpec>
188
+ | Domain<ArtifactConfigResourceSpec>
189
+ | Port<ArtifactConfigResourceSpec>
190
+ | Certificate<ArtifactConfigResourceSpec>;
191
+
192
+ interface ArtifactConfigResourceSpec {
193
+ name: string;
194
+ }
195
+ interface Secret<VARIANT> {
196
+ secret: VARIANT;
197
+ }
198
+ interface CA<VARIANT> {
199
+ ca: VARIANT;
200
+ }
201
+ interface Volume<VARIANT> {
202
+ volume: VARIANT;
203
+ }
204
+ interface Domain<VARIANT> {
205
+ domain: VARIANT;
206
+ }
207
+ interface Port<VARIANT> {
208
+ port: VARIANT;
209
+ }
210
+ interface Certificate<VARIANT> {
211
+ certificate: VARIANT;
212
+ }
213
+ type ComponentFilesystemDto = FilesystemVolumeDto | FilesystemFileDto;
214
+
215
+ interface FilesystemVolumeDto {
216
+ path: string;
217
+ resourceVolume: string;
218
+ }
219
+
220
+ interface FilesystemFileDto {
221
+ path: string;
222
+ param?: string;
223
+ value?: string;
224
+ }
225
+ interface ServiceSpecDSLWithLocalComponent extends ServiceSpecDSL {
226
+ local_components?: {
227
+ [key: string]: ComponentSpecDSL;
228
+ };
229
+ }
230
+
231
+ /**
232
+ * Function to return the value if it is not null or undefined, otherwise it returns the default value
233
+ * @param value Value to check
234
+ * @param defaultValue Default value to return
235
+ * @returns The value if it is not null or undefined, otherwise it returns the default value
236
+ */
237
+ export function withDefaultValue<T>(
238
+ value: T | null | undefined,
239
+ defaultValue: T
240
+ ): T {
241
+ return value != null ? value : defaultValue;
242
+ }
243
+ /**
244
+ * Function to format the parameters of a service deployment.
245
+ * @param formParams Parameters to format.
246
+ * @returns An object with the parameters, environment, fileSystem and resources formatted.
247
+ */
248
+ export function handleParametersToGenerateData(
249
+ formParams: Array<{
250
+ name: string;
251
+ value: string | boolean | number;
252
+ type: string;
253
+ content?: string;
254
+ size?: number;
255
+ }>,
256
+ formResources: Array<{
257
+ name: string;
258
+ type: string;
259
+ size?: number;
260
+ value?: string;
261
+ content?: string;
262
+ kind?: string;
263
+ key?: string;
264
+ }>
265
+ ): {
266
+ parameters: any[];
267
+ environment: any[];
268
+ fileSystem: any[];
269
+ resources: any[];
270
+ } {
271
+ const parametersResult: ArtifactConfigParameterDto[] = [];
272
+ const environmentResult: ComponentEnvironmentDto[] = [];
273
+ const resourcesResult: ArtifactConfigResource[] = [];
274
+ const fileSystemResult: ComponentFilesystemDto[] = [];
275
+
276
+ formParams.forEach((parameter, index) => {
277
+ switch (parameter.type) {
278
+ case "secret":
279
+ environmentResult.push({
280
+ varName: parameter.name,
281
+ secret: parameter.name as string,
282
+ });
283
+ resourcesResult.push({ secret: { name: parameter.value as string } });
284
+ break;
285
+
286
+ case "file":
287
+ parametersResult.push({
288
+ name: 'CONFIG_FILE_'+index,
289
+ type: "string",
290
+ defaultValue: (parameter.value as string) || "",
291
+ });
292
+ fileSystemResult.push({
293
+ path: `${parameter.name}`,
294
+ param: 'CONFIG_FILE_'+index,
295
+ });
296
+ break;
297
+
298
+ case "fileContent":
299
+ parametersResult.push({
300
+ name: 'CONFIG_FILE_'+index,
301
+ type: "string",
302
+ defaultValue: parameter.content || (parameter.value as string) || "",
303
+ });
304
+ fileSystemResult.push({
305
+ path: (parameter.value as string) || `${parameter.name}`,
306
+ param: 'CONFIG_FILE_'+index,
307
+ });
308
+ break;
309
+
310
+ case "volume":
311
+ if (parameter.content) {
312
+ resourcesResult.push({ volume: { name: parameter.name } });
313
+ fileSystemResult.push({
314
+ path: parameter.content,
315
+ resourceVolume: parameter.name,
316
+ });
317
+ } else {
318
+ resourcesResult.push({ volume: { name: parameter.name } });
319
+ fileSystemResult.push({
320
+ path: parameter.value as string,
321
+ resourceVolume: parameter.name,
322
+ });
323
+ }
324
+ break;
325
+
326
+ case "string":
327
+ parametersResult.push({
328
+ name: parameter.name,
329
+ type: "string",
330
+ defaultValue: parameter.value as string,
331
+ });
332
+ environmentResult.push({
333
+ varName: parameter.name,
334
+ param: parameter.name,
335
+ });
336
+ break;
337
+ case "number":
338
+ parametersResult.push({
339
+ name: parameter.name,
340
+ type: "number",
341
+ defaultValue: Number(parameter.value),
342
+ });
343
+ environmentResult.push({
344
+ varName: parameter.name,
345
+ param: parameter.name,
346
+ });
347
+ break;
348
+
349
+ case "boolean":
350
+ case "bool":
351
+ parametersResult.push({
352
+ name: parameter.name,
353
+ type: "bool",
354
+ defaultValue: parameter.value === "true" || parameter.value === true,
355
+ });
356
+ environmentResult.push({
357
+ varName: parameter.name,
358
+ param: parameter.name,
359
+ });
360
+ break;
361
+ case "serviceUrl":
362
+ parametersResult.push({
363
+ name: parameter.name,
364
+ type: "string",
365
+ defaultValue: parameter.value as string,
366
+ });
367
+ environmentResult.push({
368
+ varName: parameter.name,
369
+ param: parameter.name,
370
+ });
371
+ break;
372
+
373
+ default:
374
+ console.warn(`Unknown parameter type: ${parameter.type}`);
375
+ parametersResult.push({
376
+ name: parameter.name,
377
+ type: "string",
378
+ defaultValue: parameter.value?.toString() || "",
379
+ });
380
+ environmentResult.push({
381
+ varName: parameter.name,
382
+ param: parameter.name,
383
+ });
384
+ break;
385
+ }
386
+ });
387
+ formResources.forEach((resource) => {
388
+ switch (resource.type) {
389
+ case "secret":
390
+ resourcesResult.push({ secret: { name: resource.name } });
391
+ environmentResult.push({
392
+ varName: resource.name,
393
+ secret: resource.name as string,
394
+ });
395
+ break;
396
+ case "volume":
397
+ resourcesResult.push({ volume: { name: resource.name } });
398
+ fileSystemResult.push({
399
+ path: resource.key || '',
400
+ resourceVolume: resource.name,
401
+ });
402
+ break;
403
+ // case "certificate":
404
+ // resourcesResult.push({ certificate: { name: resource.name } });
405
+ // break;
406
+ // case "domain":
407
+ // resourcesResult.push({ domain: { name: resource.name } });
408
+ // break;
409
+ // case "port":
410
+ // resourcesResult.push({ port: { name: resource.name } });
411
+ // break;
412
+ default:
413
+ console.warn(`Unknown resource type: ${resource.type}`);
414
+ break;
415
+ }
416
+ });
417
+
418
+ return {
419
+ parameters: parametersResult,
420
+ environment: environmentResult,
421
+ fileSystem: fileSystemResult,
422
+ resources: resourcesResult,
423
+ };
424
+ }
425
+ /**
426
+ * Function to transform the Service object to a ServiceSpecForm object.
427
+ * @param service Service object with the data of the service.
428
+ * @returns A ServiceSpecForm object with the data of the service.
429
+ */
430
+ export function transformServiceToForm(service: Service): ServiceSpecForm {
431
+ return {
432
+ tenantId: service.tenant,
433
+ accountId: service.account,
434
+ environmentId: service.environment,
435
+ serviceId: service.name,
436
+ cpuRequirements: service.usage.limit.cpu.max,
437
+ memoryRequirements: service.usage.limit.memory.max,
438
+ registryUrl: service.registry,
439
+ imageTag: service.imageName,
440
+ clientChannels: service.clientChannels.map((ch) => ({ name: ch.name })),
441
+ channels: service.serverChannels.map((ch) => ({
442
+ channelName: ch.name,
443
+ containerPort: ch.port ?? 0,
444
+ isPublic: ch.isPublic ?? false,
445
+ protocol:
446
+ ch.protocol === "http"
447
+ ? "HTTPS"
448
+ : ch.protocol === "tcp"
449
+ ? "TCP"
450
+ : "TCP",
451
+ publicPort: ch.portNum?.toString() || "",
452
+ domain: ch.portNum?.toString() || "",
453
+ })),
454
+ clientChannelsExtra: service.duplexChannels.map((ch) => ({
455
+ name: ch.name,
456
+ })),
457
+ defaultExecutable: {
458
+ cmd: service.cmd,
459
+ entryPoint: service.entrypoint,
460
+ },
461
+ config: {
462
+ parameters: service.parameters.map((param) => ({
463
+ name: param.name,
464
+ value: param.value?.toString() || "",
465
+ type: param.type,
466
+ })),
467
+ resources: service.resources.map((r) => ({
468
+ name: r.name,
469
+ type: r.type,
470
+ size: r.maxItems,
471
+ value: r.value,
472
+ kind: r.kind,
473
+ key: r.key,
474
+ })),
475
+ },
476
+ environment: [service.environment],
477
+ fileSystem: service.resources
478
+ .filter((resource) => resource.type === "volume")
479
+ .map((resource) => ({
480
+ path: resource.key,
481
+ resourceVolume: resource.name,
482
+ })),
483
+ scaling: service.role[0].scalling,
484
+ };
485
+ }
486
+ /**
487
+ * Function to generate a ServiceWithLocalComponentSpec object from a ServiceSpecForm object.
488
+ * @param form ServiceSpecForm object with the data of the service.
489
+ * @returns ServiceWithLocalComponentSpec object with the data of the service.
490
+ */
491
+ export async function generateServiceSpec(
492
+ form: ServiceSpecForm,
493
+ marketplaceItem?: MarketplaceService
494
+ ): Promise<ServiceWithLocalComponentSpec> {
495
+ const formParams = form.config.parameters;
496
+ const formResources = form.config.resources;
497
+ const {
498
+ parameters,
499
+ environment,
500
+ fileSystem,
501
+ resources: componentResources,
502
+ } = handleParametersToGenerateData(formParams, formResources);
503
+ const serverConfigDomainResources: ArtifactConfigResource[] = form.channels
504
+ .filter((channel) => channel.protocol === "HTTPS" && channel.isPublic)
505
+ .map((channel) => ({ domain: { name: `${channel.channelName}_domain` } }));
506
+ if (serverConfigDomainResources.length) {
507
+ serverConfigDomainResources.push({
508
+ certificate: { name: "main_inbound_servercert" },
509
+ });
510
+ }
511
+
512
+ const serviceConfigPortResources: ArtifactConfigResource[] = form.channels
513
+ .filter((channel) => channel.protocol === "TCP" && channel.isPublic)
514
+ .map((channel) => ({ port: { name: `${channel.channelName}_port` } }));
515
+
516
+ const serviceConfigResources: ArtifactConfigResource[] = form.config.resources
517
+ .filter(
518
+ (resource) => resource.type === "volume" || resource.type === "secret"
519
+ )
520
+ .map((resource) => {
521
+ if (resource.type === "secret") {
522
+ return { secret: { name: resource.name } };
523
+ }
524
+ if (resource.type === "volume") {
525
+ return { volume: { name: resource.name } };
526
+ }
527
+ return { secret: { name: "" } };
528
+ });
529
+ const volatileVolumeResources: ArtifactConfigResource[] =
530
+ form.config.parameters
531
+ .filter((param) => param.type === "volume" && param.size)
532
+ .map((param) => ({ volume: { name: param.name } }));
533
+
534
+ const roleVolatileVolumeResources: ResourceBundle[] = form.config.parameters
535
+ .filter((param) => param.type === "volume" && param.size)
536
+ .map((param) => ({
537
+ volume: { name: param.name, configResource: param.name },
538
+ }));
539
+
540
+ const rolesParameters: Parameter[] = form.config.parameters
541
+ .filter(
542
+ (resource) =>
543
+ resource.type === "string" ||
544
+ resource.type === "boolean" ||
545
+ resource.type === "number" ||
546
+ resource.type === "fileContent"
547
+ )
548
+ .map((resource) => {
549
+ if (resource.type === "string" || resource.type === "fileContent") {
550
+ return {
551
+ name: resource.name,
552
+ type: "string",
553
+ configParam: resource.name,
554
+ };
555
+ }
556
+ if (resource.type === "boolean") {
557
+ return {
558
+ name: resource.name,
559
+ type: "bool",
560
+ configParam: resource.name,
561
+ };
562
+ }
563
+ if (resource.type === "number") {
564
+ return {
565
+ name: resource.name,
566
+ type: "number",
567
+ configParam: resource.name,
568
+ };
569
+ }
570
+ return {
571
+ name: resource.name,
572
+ type: "string",
573
+ configParam: resource.name,
574
+ };
575
+ });
576
+
577
+ const rolesResources: ResourceBundle[] = form.config.resources
578
+ .filter(
579
+ (resource) => resource.type === "volume" || resource.type === "secret"
580
+ )
581
+ .map((resource) => {
582
+ if (resource.type === "secret") {
583
+ return {
584
+ secret: { name: resource.name, configResource: resource.name },
585
+ };
586
+ }
587
+ if (resource.type === "volume") {
588
+ return {
589
+ volume: { name: resource.name, configResource: resource.name },
590
+ };
591
+ }
592
+ return { secret: { name: "", configResource: "" } };
593
+ });
594
+
595
+ const roles: Role[] = form.channels
596
+ .filter((channel) => channel.isPublic)
597
+ .map((channel) => {
598
+ if (channel.protocol === "HTTPS") {
599
+ return {
600
+ name: `${channel.channelName}_inbound`,
601
+ artifact: {
602
+ artifactKind: "service",
603
+ moduleDomain: "kumori.systems",
604
+ moduleName: "builtins/inbound",
605
+ moduleVersion: [1, 3, 0],
606
+ artifactName: "",
607
+ config: {
608
+ parameters: [
609
+ { name: "type", value: "https", type: "string" },
610
+ // { name: "websocket", value: "true", type: "bool", configParam: "websocket" },
611
+ ],
612
+ resources: [
613
+ {
614
+ certificate: {
615
+ name: "servercert",
616
+ configResource: "main_inbound_servercert",
617
+ },
618
+ },
619
+ {
620
+ domain: {
621
+ name: "serverdomain",
622
+ configResource: `${channel.channelName}_domain`,
623
+ },
624
+ },
625
+ ],
626
+ },
627
+ },
628
+ };
629
+ }
630
+ return {
631
+ name: `${channel.channelName}_inbound`,
632
+ artifact: {
633
+ artifactKind: "service",
634
+ moduleDomain: " ",
635
+ moduleName: "builtins/inbound",
636
+ moduleVersion: [1, 3, 0],
637
+ artifactName: "",
638
+ config: {
639
+ parameters: [{ name: "type", value: "tcp", type: "string" }],
640
+ resources: [
641
+ {
642
+ port: {
643
+ name: "port",
644
+ configResource: `${channel.channelName}_port`,
645
+ },
646
+ },
647
+ ],
648
+ },
649
+ },
650
+ };
651
+ });
652
+
653
+ const topologyServerChannels: Connector[] = form.channels.flatMap(
654
+ (channel) => {
655
+ const result: Connector[] = [
656
+ {
657
+ clientRole: "self",
658
+ clientChannel: channel.channelName,
659
+ serverRole: withDefaultValue(form.serviceId, ""),
660
+ serverChannel: channel.channelName,
661
+ },
662
+ ];
663
+ if (channel.isPublic) {
664
+ result.push({
665
+ clientRole: `${channel.channelName}_inbound`,
666
+ clientChannel: "inbound",
667
+ serverRole: withDefaultValue(form.serviceId, ""),
668
+ serverChannel: channel.channelName,
669
+ });
670
+ }
671
+ return result;
672
+ }
673
+ );
674
+ const topologyClientsChannels: Connector[] = (form.clientChannels || []).map(
675
+ (channel) => ({
676
+ clientRole: withDefaultValue(form.serviceId, ""),
677
+ clientChannel: channel.name,
678
+ serverRole: "self",
679
+ serverChannel: channel.name,
680
+ })
681
+ );
682
+ const topology: Connector[] = [
683
+ ...topologyServerChannels,
684
+ ...topologyClientsChannels,
685
+ ];
686
+
687
+ const deploymentConfigParameters: DeploymentParameter[] =
688
+ form.config.parameters
689
+ .filter(
690
+ (resource) =>
691
+ resource.type === "string" ||
692
+ resource.type === "number" ||
693
+ resource.type === "boolean" ||
694
+ resource.type === "fileContent"
695
+ )
696
+ .map((resource) => {
697
+ if (resource.type === "string") {
698
+ return { name: resource.name, value: resource.value, type: "string" };
699
+ }
700
+ if (resource.type === "fileContent") {
701
+ return {
702
+ name: resource.name,
703
+ value: resource.content || "",
704
+ type: "string",
705
+ };
706
+ }
707
+ if (resource.type === "boolean") {
708
+ return {
709
+ name: resource.name,
710
+ value: resource.value.toString(),
711
+ type: "bool",
712
+ };
713
+ }
714
+ if (resource.type === "number") {
715
+ return { name: resource.name, value: resource.value, type: "number" };
716
+ }
717
+ return { name: resource.name, value: resource.value, type: "string" };
718
+ });
719
+ const createdVolatileVolumes = new Map<string, string>();
720
+ if (marketplaceItem) {
721
+ //TODO: Quitar cuando se soporten los volumenes inline?
722
+ const volatileResources = form.config.resources.filter(
723
+ (resource) =>
724
+ resource.type === "volume" &&
725
+ resource.kind === "volatile" &&
726
+ resource.size
727
+ );
728
+
729
+ for (const resource of volatileResources) {
730
+ try {
731
+ const volumeName = `${form.tenantId}-${form.serviceId}-${resource.name}`;
732
+ const volumeResource: Resource = {
733
+ name: volumeName,
734
+ kind: "volatile",
735
+ value: String(resource.size || 1),
736
+ type: "volume",
737
+ maxItems: resource.size,
738
+ };
739
+ await createVolume(form.tenantId, volumeResource, "");
740
+ createdVolatileVolumes.set(resource.name, volumeName);
741
+ } catch (error) {
742
+ console.error(
743
+ `Error creating volatile volume for ${resource.name}:`,
744
+ error
745
+ );
746
+ throw error;
747
+ }
748
+ }
749
+ }
750
+
751
+ const deploymentConfigResources: DeploymentResource[] = form.config.resources
752
+ .filter(
753
+ (resource) => resource.type === "secret" || resource.type === "volume"
754
+ )
755
+ .map((resource) => {
756
+ if (resource.type === "secret") {
757
+ return {
758
+ secret: {
759
+ name: resource.name,
760
+ resource: withDefaultValue(resource.value, ""),
761
+ },
762
+ };
763
+ }
764
+ if (resource.size) {
765
+ const volType = (resource as any).kind || "nonreplicated";
766
+ switch (volType) {
767
+ case "persistent":
768
+ return {
769
+ volume: {
770
+ name: resource.name,
771
+ volume: {
772
+ size: (resource.size || 0) * 1000,
773
+ unit: "M" as const,
774
+ type: "persistent" as const,
775
+ },
776
+ },
777
+ } as {
778
+ volume: {
779
+ name: string;
780
+ volume: { size: number; unit: "M"; type: "persistent" };
781
+ };
782
+ };
783
+ case "volatile":
784
+ if (createdVolatileVolumes.has(resource.name)) {
785
+ return {
786
+ volume: {
787
+ name: resource.name,
788
+ resource: createdVolatileVolumes.get(resource.name)!,
789
+ },
790
+ };
791
+ }
792
+ return {
793
+ volume: {
794
+ name: resource.name,
795
+ volume: {
796
+ size: (resource.size || 0) * 1000,
797
+ unit: "M" as const,
798
+ type: "volatile" as const,
799
+ },
800
+ },
801
+ } as {
802
+ volume: {
803
+ name: string;
804
+ volume: { size: number; unit: "M"; type: "volatile" };
805
+ };
806
+ };
807
+ case "nonreplicated":
808
+ default:
809
+ return {
810
+ volume: {
811
+ name: resource.name,
812
+ volume: {
813
+ size: (resource.size || 0) * 1000,
814
+ unit: "M" as const,
815
+ type: "nonreplicated" as const,
816
+ },
817
+ },
818
+ } as {
819
+ volume: {
820
+ name: string;
821
+ volume: { size: number; unit: "M"; type: "nonreplicated" };
822
+ };
823
+ };
824
+ }
825
+ }
826
+ return {
827
+ volume: {
828
+ name: resource.name,
829
+ resource: withDefaultValue(resource.value, ""),
830
+ },
831
+ };
832
+ });
833
+ const deploymentConfigDomain: DeploymentResource[] = form.channels
834
+ .filter((channel) => channel.protocol === "HTTPS" && channel.isPublic)
835
+ .map((channel) => ({
836
+ domain: {
837
+ name: `${channel.channelName}_domain`,
838
+ resource: withDefaultValue(channel.domain, ""),
839
+ },
840
+ }));
841
+ if (deploymentConfigDomain.length) {
842
+ deploymentConfigDomain.push({
843
+ certificate: {
844
+ name: "main_inbound_servercert",
845
+ resource: `wildcard-${form.tenantId}-domain-${getReferenceDomain()}`,
846
+ },
847
+ });
848
+ }
849
+ const deploymentConfigPort: DeploymentResource[] = form.channels
850
+ .filter((channel) => channel.protocol === "TCP" && channel.isPublic)
851
+ .map((channel) => ({
852
+ port: {
853
+ name: `${channel.channelName}_port`,
854
+ resource: withDefaultValue(channel.publicPort, ""),
855
+ },
856
+ }));
857
+
858
+ const component: ComponentSpec = {
859
+ type: "component",
860
+ tenantId: withDefaultValue(form.tenantId, ""),
861
+ accountId: withDefaultValue(form.accountId, ""),
862
+ environmentId: withDefaultValue(form.environmentId, ""),
863
+ deploymentId: "",
864
+ moduleDomain: "kumori.examples",
865
+ moduleId: withDefaultValue(form.serviceId, ""),
866
+ moduleVersion: [0, 0, 1],
867
+ labels: {},
868
+ bundleTargetDir: "",
869
+ bundleTargetKind: "",
870
+ channels: {
871
+ client: form.clientChannels.map((ch) => ch.name) ?? [],
872
+ server:
873
+ form.channels
874
+ .filter((ch) => ch.channelName)
875
+ .map((ch) => {
876
+ if (ch.containerPort === 0) {
877
+ return { name: ch.channelName };
878
+ } else return { name: ch.channelName, port: ch.containerPort };
879
+ }) ?? [],
880
+ },
881
+ cpuRequirements: withDefaultValue(form.cpuRequirements, 0) * 1000,
882
+ memoryRequirements: withDefaultValue(form.memoryRequirements, 0) * 1000,
883
+ registryUrl: withDefaultValue(form.registryUrl, "docker.io").trim(),
884
+ imageTag: withDefaultValue(form.imageTag, "").trim(),
885
+ registryCredentialsSecret: form.config.resources.map((r) => {
886
+ if (r.type === "secret" && r.key === "registryCredentials") {
887
+ return r.name;
888
+ }
889
+ })[0],
890
+ config: {
891
+ parameters,
892
+ resources: componentResources,
893
+ },
894
+ environment: environment,
895
+ filesystem: fileSystem ?? [],
896
+ defaultExecutable: {
897
+ cmd: withDefaultValue(form.defaultExecutable.cmd, undefined),
898
+ entrypoint: withDefaultValue(
899
+ form.defaultExecutable.entryPoint,
900
+ undefined
901
+ ),
902
+ },
903
+ };
904
+ const scaling: {
905
+ simple: {
906
+ [key: string]: {
907
+ scale_up: { cpu: number; memory: number };
908
+ scale_down: { cpu: number; memory: number };
909
+ hysteresis: number;
910
+ min_replicas: number;
911
+ max_replicas: number;
912
+ };
913
+ };
914
+ } = {
915
+ simple: {},
916
+ };
917
+
918
+ scaling.simple[form.serviceId] = {
919
+ scale_up: {
920
+ cpu: parseInt(form.scaling?.cpu.up || "") || 0,
921
+ memory: parseInt(form.scaling?.memory.up || "") || 0,
922
+ },
923
+ scale_down: {
924
+ cpu: parseInt(form.scaling?.cpu.down || "") || 0,
925
+ memory: parseInt(form.scaling?.memory.down || "") || 0,
926
+ },
927
+ hysteresis: parseInt(form.scaling?.histeresys || "") || 0,
928
+ min_replicas: form.scaling?.instances.min || 0,
929
+ max_replicas: form.scaling?.instances.max || 0,
930
+ };
931
+ const autoscalingDefined = form.scaling;
932
+ const serviceSpec: ServiceWithLocalComponentSpec = {
933
+ type: "service",
934
+ subtype: "service_with_local_component",
935
+ tenantId: withDefaultValue(form.tenantId, ""),
936
+ accountId: withDefaultValue(form.accountId, ""),
937
+ environmentId: withDefaultValue(form.environmentId, ""),
938
+ deploymentId: "",
939
+ moduleDomain: "kumori.examples",
940
+ moduleId: withDefaultValue(form.serviceId, ""),
941
+ moduleVersion: [0, 0, 1],
942
+ labels: {},
943
+ bundleTargetDir: "",
944
+ bundleTargetKind: "",
945
+ channels: {
946
+ client: form.clientChannels.map((ch) => ch.name) ?? [],
947
+ server:
948
+ form.channels
949
+ .filter((ch) => ch.channelName)
950
+ .map((ch) => {
951
+ if (ch.containerPort === 0) {
952
+ return { name: ch.channelName };
953
+ } else return { name: ch.channelName, port: ch.containerPort };
954
+ }) ?? [],
955
+ },
956
+ config: {
957
+ parameters,
958
+ resources: [
959
+ ...serverConfigDomainResources,
960
+ ...serviceConfigPortResources,
961
+ ...serviceConfigResources,
962
+ ...volatileVolumeResources,
963
+ ],
964
+ },
965
+ roles: [
966
+ marketplaceItem !== null && marketplaceItem !== undefined
967
+ ? {
968
+ name: withDefaultValue(marketplaceItem.deploymentData.name, ""),
969
+ artifact: {
970
+ artifactKind:
971
+ marketplaceItem.type === "service" ? "service" : "component",
972
+ moduleDomain: withDefaultValue(marketplaceItem.domain, ""),
973
+ moduleName: withDefaultValue(marketplaceItem.module, ""),
974
+ artifactName: withDefaultValue(marketplaceItem.serviceName, ""),
975
+ moduleVersion: [
976
+ Number(marketplaceItem.version.split(".")[0]),
977
+ Number(marketplaceItem.version.split(".")[1]),
978
+ Number(marketplaceItem.version.split(".")[2]),
979
+ ] as [number, number, number],
980
+ config: {
981
+ parameters: [...rolesParameters],
982
+ resources: [...rolesResources, ...roleVolatileVolumeResources],
983
+ ...(marketplaceItem.type === "service" && {
984
+ scale: {
985
+ detail: (marketplaceItem.roles ?? []).reduce(
986
+ (acc, role) => ({
987
+ ...acc,
988
+ [withDefaultValue(role, "")]: 1,
989
+ }),
990
+ {}
991
+ ),
992
+ },
993
+ }),
994
+ },
995
+ },
996
+ }
997
+ : {
998
+ name: withDefaultValue(form.serviceId, ""),
999
+ artifact: {
1000
+ artifactKind: "component" as const,
1001
+ moduleDomain: "mod.local",
1002
+ moduleName: "",
1003
+ artifactName: withDefaultValue(form.serviceId, ""),
1004
+ moduleVersion: [0, 0, 0] as [number, number, number],
1005
+ config: {
1006
+ parameters: [...rolesParameters],
1007
+ resources: [...rolesResources, ...roleVolatileVolumeResources],
1008
+ },
1009
+ },
1010
+ },
1011
+ ...roles,
1012
+ ],
1013
+ topology,
1014
+ local_components:
1015
+ marketplaceItem !== null && marketplaceItem !== undefined
1016
+ ? {}
1017
+ : {
1018
+ [withDefaultValue(form.serviceId, "")]: component,
1019
+ },
1020
+ deploymentConfig: {
1021
+ parameters: [...deploymentConfigParameters],
1022
+ resources: [
1023
+ ...deploymentConfigDomain,
1024
+ ...deploymentConfigPort,
1025
+ ...deploymentConfigResources,
1026
+ ],
1027
+ scale: {
1028
+ detail: {
1029
+ [withDefaultValue(form.serviceId, "")]: 1,
1030
+ },
1031
+ },
1032
+ meta: autoscalingDefined
1033
+ ? {
1034
+ scaling: scaling,
1035
+ }
1036
+ : {},
1037
+ },
1038
+ };
1039
+
1040
+ return serviceSpec;
1041
+ }
1042
+ /**
1043
+ * Function to generate a ServiceSpecDSL for marketplace items with package.
1044
+ * @param form ServiceSpecForm object with the data of the service.
1045
+ * @param marketplaceItem MarketplaceService object with marketplace data.
1046
+ * @returns ServiceSpecDSL object for buildServiceDeploymentModule.
1047
+ */
1048
+ async function generateServiceSpecDSL(
1049
+ form: ServiceSpecForm,
1050
+ marketplaceItem?: MarketplaceService
1051
+ ): Promise<ServiceSpecDSLWithLocalComponent> {
1052
+ const formParams = form.config.parameters;
1053
+ const formResources = form.config.resources;
1054
+ const {
1055
+ parameters,
1056
+ environment,
1057
+ fileSystem,
1058
+ resources: componentResources,
1059
+ } = handleParametersToGenerateData(formParams, formResources);
1060
+
1061
+ const serverConfigDomainResources: ArtifactConfigResource[] = form.channels
1062
+ .filter((channel) => channel.protocol === "HTTPS" && channel.isPublic)
1063
+ .map((channel) => ({ domain: { name: `${channel.channelName}_domain` } }));
1064
+ if (serverConfigDomainResources.length) {
1065
+ serverConfigDomainResources.push({
1066
+ certificate: { name: "main_inbound_servercert" },
1067
+ });
1068
+ }
1069
+
1070
+ const serviceConfigPortResources: ArtifactConfigResource[] = form.channels
1071
+ .filter((channel) => channel.protocol === "TCP" && channel.isPublic)
1072
+ .map((channel) => ({ port: { name: `${channel.channelName}_port` } }));
1073
+
1074
+ const serviceConfigResources: ArtifactConfigResource[] = form.config.resources
1075
+ .filter(
1076
+ (resource) => resource.type === "volume" || resource.type === "secret"
1077
+ )
1078
+ .map((resource) => {
1079
+ if (resource.type === "secret") {
1080
+ return { secret: { name: resource.name } };
1081
+ }
1082
+ if (resource.type === "volume") {
1083
+ return { volume: { name: resource.name } };
1084
+ }
1085
+ return { secret: { name: "" } };
1086
+ });
1087
+
1088
+ const volatileVolumeResources: ArtifactConfigResource[] =
1089
+ form.config.parameters
1090
+ .filter((param) => param.type === "volume" && param.size)
1091
+ .map((param) => ({ volume: { name: param.name } }));
1092
+
1093
+
1094
+
1095
+ const rolesParameters: Parameter[] = form.config.parameters
1096
+ .filter(
1097
+ (resource) =>
1098
+ resource.type === "string" ||
1099
+ resource.type === "boolean" ||
1100
+ resource.type === "number" ||
1101
+ resource.type === "file"
1102
+ )
1103
+ .map((resource, index) => {
1104
+ if (resource.type === "string" ) {
1105
+ return {
1106
+ name: resource.name,
1107
+ type: "string",
1108
+ configParam: resource.name,
1109
+ };
1110
+ }
1111
+ if(resource.type === "file") {
1112
+ return {
1113
+ name: "CONFIG_FILE_"+index,
1114
+ type: "string",
1115
+ configParam: "CONFIG_FILE_"+index,
1116
+ };
1117
+ }
1118
+ if (resource.type === "boolean" || resource.type === "bool") {
1119
+ return {
1120
+ name: resource.name,
1121
+ type: "bool",
1122
+ configParam: resource.name,
1123
+ };
1124
+ }
1125
+ if (resource.type === "number") {
1126
+ return {
1127
+ name: resource.name,
1128
+ type: "number",
1129
+ configParam: resource.name,
1130
+ };
1131
+ }
1132
+ return {
1133
+ name: resource.name,
1134
+ type: "string",
1135
+ configParam: resource.name,
1136
+ };
1137
+ });
1138
+
1139
+ const rolesResources: ResourceBundle[] = form.config.resources
1140
+ .filter(
1141
+ (resource) => resource.type === "volume" || resource.type === "secret"
1142
+ )
1143
+ .map((resource) => {
1144
+ if (resource.type === "secret") {
1145
+ return {
1146
+ secret: { name: resource.name, configResource: resource.name || '' },
1147
+ };
1148
+ }
1149
+ return { volume: { name: resource.name, configResource: resource.name } };
1150
+ });
1151
+ const inboundRoles = form.channels
1152
+ .filter((channel) => channel.isPublic)
1153
+ .map((channel) => {
1154
+ if (channel.protocol === "HTTPS") {
1155
+ return {
1156
+ name: `${channel.channelName}_inbound`,
1157
+ artifact: {
1158
+ artifactKind: "service" as const,
1159
+ moduleDomain: "kumori",
1160
+ moduleName: "builtin",
1161
+ moduleVersion: [1, 3, 0] as [number, number, number],
1162
+ artifactName: "HTTPInbound",
1163
+ packageLocation: "",
1164
+ config: {
1165
+ parameters: [
1166
+ { name: "type", value: "https", type: "string" as const },
1167
+ ],
1168
+ resources: [
1169
+ {
1170
+ certificate: {
1171
+ name: "servercert",
1172
+ configResource: "main_inbound_servercert",
1173
+ },
1174
+ },
1175
+ {
1176
+ domain: {
1177
+ name: "serverdomain",
1178
+ configResource: `${channel.channelName}_domain`,
1179
+ },
1180
+ },
1181
+ ],
1182
+ },
1183
+ },
1184
+ };
1185
+ }
1186
+ return {
1187
+ name: `${channel.channelName}_inbound`,
1188
+ artifact: {
1189
+ artifactKind: "service" as const,
1190
+ moduleDomain: "kumori",
1191
+ moduleName: "builtin",
1192
+ moduleVersion: [1, 3, 0] as [number, number, number],
1193
+ artifactName: "TCPInbound",
1194
+ packageLocation: "",
1195
+ config: {
1196
+ parameters: [{ name: "type", value: "tcp", type: "string" as const }],
1197
+ resources: [
1198
+ {
1199
+ port: {
1200
+ name: "port",
1201
+ configResource: `${channel.channelName}_port`,
1202
+ },
1203
+ },
1204
+ ],
1205
+ },
1206
+ },
1207
+ };
1208
+ });
1209
+ const topologyServerChannels = form.channels.flatMap((channel) => {
1210
+ const result = [
1211
+ {
1212
+ clientRole: "self",
1213
+ clientChannel: channel.channelName,
1214
+ serverRole: withDefaultValue(form.serviceId, ""),
1215
+ serverChannel: channel.channelName,
1216
+ },
1217
+ ];
1218
+ if (channel.isPublic) {
1219
+ result.push({
1220
+ clientRole: `${channel.channelName}_inbound`,
1221
+ clientChannel: "inbound",
1222
+ serverRole: withDefaultValue(form.serviceId, ""),
1223
+ serverChannel: channel.channelName,
1224
+ });
1225
+ }
1226
+ return result;
1227
+ });
1228
+
1229
+ const topologyClientsChannels = (form.clientChannels || []).map(
1230
+ (channel) => ({
1231
+ clientRole: withDefaultValue(form.serviceId, ""),
1232
+ clientChannel: channel.name,
1233
+ serverRole: "self",
1234
+ serverChannel: channel.name,
1235
+ })
1236
+ );
1237
+
1238
+ const topology = [...topologyServerChannels, ...topologyClientsChannels];
1239
+
1240
+ const deploymentConfigParameters: DeploymentParameter[] =
1241
+ form.config.parameters
1242
+ .filter(
1243
+ (resource) =>
1244
+ resource.type === "string" ||
1245
+ resource.type === "number" ||
1246
+ resource.type === "boolean" ||
1247
+ resource.type === "bool" ||
1248
+ resource.type === "file"
1249
+ )
1250
+ .map((resource, index) => {
1251
+ if (resource.type === "string") {
1252
+ return { name: resource.name, value: resource.value, type: "string" };
1253
+ }
1254
+ if (resource.type === "file") {
1255
+ return {
1256
+ name: "CONFIG_FILE_" + index,
1257
+ value: resource.value || "",
1258
+ type: "string",
1259
+ };
1260
+ }
1261
+ if (resource.type === "boolean" || resource.type === "bool") {
1262
+ return {
1263
+ name: resource.name,
1264
+ value: resource.value.toString(),
1265
+ type: "bool",
1266
+ };
1267
+ }
1268
+ if (resource.type === "number") {
1269
+ return { name: resource.name, value: resource.value, type: "number" };
1270
+ }
1271
+ return { name: resource.name, value: resource.value, type: "string" };
1272
+ });
1273
+
1274
+
1275
+
1276
+
1277
+ const deploymentConfigResources: DeploymentResource[] = form.config.resources
1278
+ .filter(
1279
+ (resource) => resource.type === "secret" || resource.type === "volume"
1280
+ )
1281
+ .map((resource) => {
1282
+ if (resource.type === "secret") {
1283
+ return {
1284
+ secret: {
1285
+ name: resource.name,
1286
+ resource: withDefaultValue(resource.value, ""),
1287
+ },
1288
+ };
1289
+ }
1290
+ if (resource.size) {
1291
+ const volType = (resource as any).kind || "nonreplicated";
1292
+ switch (volType) {
1293
+ case "persistent":
1294
+ return {
1295
+ volume: {
1296
+ name: resource.name,
1297
+ volume: {
1298
+ size: (resource.size || 0) * 1000,
1299
+ unit: "M" as const,
1300
+ type: "persistent" as const,
1301
+ },
1302
+ },
1303
+ };
1304
+ case "volatile":
1305
+ return {
1306
+ volume: {
1307
+ name: resource.name,
1308
+ volume: {
1309
+ size: (resource.size || 0) * 1000,
1310
+ unit: "M" as const,
1311
+ type: "volatile" as const,
1312
+ },
1313
+ },
1314
+ };
1315
+ case "nonreplicated":
1316
+ default:
1317
+ return {
1318
+ volume: {
1319
+ name: resource.name,
1320
+ volume: {
1321
+ size: (resource.size || 0) * 1000,
1322
+ unit: "M" as const,
1323
+ type: "nonreplicated" as const,
1324
+ },
1325
+ },
1326
+ };
1327
+ }
1328
+ }
1329
+ return {
1330
+ volume: {
1331
+ name: resource.name,
1332
+ resource: withDefaultValue(resource.value, ""),
1333
+ },
1334
+ };
1335
+ });
1336
+
1337
+ const deploymentConfigDomain: DeploymentResource[] = form.channels
1338
+ .filter((channel) => channel.protocol === "HTTPS" && channel.isPublic)
1339
+ .map((channel) => ({
1340
+ domain: {
1341
+ name: `${channel.channelName}_domain`,
1342
+ resource: withDefaultValue(channel.domain, ""),
1343
+ },
1344
+ }));
1345
+ if (deploymentConfigDomain.length) {
1346
+ deploymentConfigDomain.push({
1347
+ certificate: {
1348
+ name: "main_inbound_servercert",
1349
+ resource: `wildcard-${form.tenantId}-domain-${getReferenceDomain()}`,
1350
+ },
1351
+ });
1352
+ }
1353
+
1354
+ const deploymentConfigPort: DeploymentResource[] = form.channels
1355
+ .filter((channel) => channel.protocol === "TCP" && channel.isPublic)
1356
+ .map((channel) => ({
1357
+ port: {
1358
+ name: `${channel.channelName}_port`,
1359
+ resource: channel.publicPort || "",
1360
+ },
1361
+ }));
1362
+
1363
+ const scaling: {
1364
+ simple: {
1365
+ [key: string]: {
1366
+ scale_up: { cpu: number; memory: number };
1367
+ scale_down: { cpu: number; memory: number };
1368
+ hysteresis: number;
1369
+ min_replicas: number;
1370
+ max_replicas: number;
1371
+ };
1372
+ };
1373
+ } = {
1374
+ simple: {},
1375
+ };
1376
+
1377
+ scaling.simple[form.serviceId] = {
1378
+ scale_up: {
1379
+ cpu: parseInt(form.scaling?.cpu.up || "") || 0,
1380
+ memory: parseInt(form.scaling?.memory.up || "") || 0,
1381
+ },
1382
+ scale_down: {
1383
+ cpu: parseInt(form.scaling?.cpu.down || "") || 0,
1384
+ memory: parseInt(form.scaling?.memory.down || "") || 0,
1385
+ },
1386
+ hysteresis: parseInt(form.scaling?.histeresys || "") || 0,
1387
+ min_replicas: form.scaling?.instances.min || 0,
1388
+ max_replicas: form.scaling?.instances.max || 0,
1389
+ };
1390
+
1391
+ const hasMarketplacePackage = marketplaceItem?.package;
1392
+
1393
+ const serviceSpec: ServiceSpecDSLWithLocalComponent = {
1394
+ type: "service" as const,
1395
+ subtype: marketplaceItem ? "" : "service_with_local_component",
1396
+ packageLocation: "deployment",
1397
+ artifactName: withDefaultValue(
1398
+ marketplaceItem?.artifact || form.serviceId + "_service",
1399
+ ""
1400
+ ),
1401
+ tenantId: withDefaultValue(form.tenantId, ""),
1402
+ accountId: withDefaultValue(form.accountId, ""),
1403
+ environmentId: withDefaultValue(form.environmentId, ""),
1404
+ deploymentId: "",
1405
+ moduleDomain: "kumori.examples",
1406
+ moduleId: withDefaultValue(marketplaceItem?.module || form.serviceId, ""),
1407
+ moduleVersion: [0, 0, 1] as [number, number, number],
1408
+ labels: {},
1409
+ bundleTargetDir: "",
1410
+ bundleTargetKind: "",
1411
+ channels: {
1412
+ client: (form.clientChannels || []).map((ch) => ch.name),
1413
+ server: form.channels
1414
+ .filter((ch) => ch.channelName)
1415
+ .map((ch) => {
1416
+ if (ch.containerPort === 0) {
1417
+ return { name: ch.channelName };
1418
+ } else {
1419
+ return { name: ch.channelName, port: ch.containerPort };
1420
+ }
1421
+ }),
1422
+ },
1423
+ config: {
1424
+ parameters,
1425
+ resources: [
1426
+ ...serverConfigDomainResources,
1427
+ ...serviceConfigPortResources,
1428
+ ...serviceConfigResources,
1429
+ ...volatileVolumeResources,
1430
+ ],
1431
+ },
1432
+ roles: [
1433
+ {
1434
+ name: withDefaultValue(
1435
+ marketplaceItem?.deploymentData?.name || form.serviceId,
1436
+ ""
1437
+ ),
1438
+ artifact: {
1439
+ artifactKind:
1440
+ marketplaceItem?.type === "service"
1441
+ ? ("service" as const)
1442
+ : ("component" as const),
1443
+ moduleDomain: hasMarketplacePackage
1444
+ ? marketplaceItem?.domain || "kumori.systems"
1445
+ : "mod.local",
1446
+ moduleName: withDefaultValue(
1447
+ marketplaceItem?.module || form.serviceId,
1448
+ ""
1449
+ ),
1450
+ moduleVersion: marketplaceItem?.version
1451
+ ? (marketplaceItem.version.split(".").map(Number) as [
1452
+ number,
1453
+ number,
1454
+ number
1455
+ ])
1456
+ : ([0, 0, 1] as [number, number, number]),
1457
+ artifactName: withDefaultValue(
1458
+ marketplaceItem?.artifact || form.serviceId + "_component",
1459
+ ""
1460
+ ),
1461
+ packageLocation: hasMarketplacePackage
1462
+ ? marketplaceItem?.package || ""
1463
+ : "deployment",
1464
+ config: {
1465
+ parameters: [...rolesParameters],
1466
+ resources: [...rolesResources],
1467
+ },
1468
+ },
1469
+ },
1470
+ ...inboundRoles,
1471
+ ],
1472
+ topology,
1473
+ deploymentConfig: {
1474
+ parameters: [...deploymentConfigParameters],
1475
+ resources: [
1476
+ ...deploymentConfigDomain,
1477
+ ...deploymentConfigPort,
1478
+ ...deploymentConfigResources,
1479
+ ],
1480
+ scale: {
1481
+ detail: {
1482
+ [withDefaultValue(form.serviceId, "")]: 1,
1483
+ },
1484
+ },
1485
+ meta: {
1486
+ deploymentMeta: {
1487
+ environment: form.environment || [],
1488
+ team: "development",
1489
+ },
1490
+ },
1491
+ },
1492
+ };
1493
+
1494
+ if (!marketplaceItem) {
1495
+ const localComponent: ComponentSpecDSL = {
1496
+ type: "component",
1497
+ artifactName: withDefaultValue(form.serviceId + "_component", ""),
1498
+ packageLocation: "deployment",
1499
+ tenantId: withDefaultValue(form.tenantId, ""),
1500
+ accountId: withDefaultValue(form.accountId, ""),
1501
+ environmentId: withDefaultValue(form.environmentId, ""),
1502
+ deploymentId: "",
1503
+ moduleDomain: "kumori.examples",
1504
+ moduleId: withDefaultValue(form.serviceId, ""),
1505
+ moduleVersion: [0, 0, 1],
1506
+ labels: {},
1507
+ bundleTargetDir: "",
1508
+ bundleTargetKind: "",
1509
+ channels: {
1510
+ client: (form.clientChannels || []).map((ch) => ch.name),
1511
+ server: form.channels
1512
+ .filter((ch) => ch.channelName)
1513
+ .map((ch) => {
1514
+ if (ch.containerPort === 0) {
1515
+ return { name: ch.channelName };
1516
+ } else {
1517
+ return { name: ch.channelName, port: ch.containerPort };
1518
+ }
1519
+ }),
1520
+ },
1521
+ cpuRequirements: withDefaultValue(form.cpuRequirements, 0) * 1000,
1522
+ memoryRequirements: withDefaultValue(form.memoryRequirements, 0) * 1000,
1523
+ registryUrl: withDefaultValue(form.registryUrl, "docker.io").trim(),
1524
+ imageTag: withDefaultValue(form.imageTag, "").trim(),
1525
+ registryCredentialsSecret: form.config.resources
1526
+ .map((r) => {
1527
+ if (r.type === "secret" && r.key === "registryCredentials") {
1528
+ return r.name;
1529
+ }
1530
+ })
1531
+ .find((name) => name !== undefined),
1532
+ config: {
1533
+ parameters,
1534
+ resources: componentResources,
1535
+ },
1536
+ environment: environment,
1537
+ filesystem: fileSystem ?? [],
1538
+ defaultExecutable: {
1539
+ cmd: withDefaultValue(form.defaultExecutable.cmd, undefined),
1540
+ entrypoint: withDefaultValue(
1541
+ form.defaultExecutable.entryPoint,
1542
+ undefined
1543
+ ),
1544
+ },
1545
+ };
1546
+
1547
+ serviceSpec.local_components = {
1548
+ [withDefaultValue(form.serviceId + "_component", "")]: localComponent,
1549
+ };
1550
+ }
1551
+
1552
+ return serviceSpec;
1553
+ }
1554
+ /**
1555
+ * Function to deploy a service.
1556
+ * @param service Service object with the data of the service.
1557
+ * @returns The body of the request to deploy the service.
1558
+ */
1559
+ export async function deployServiceHelper(
1560
+ service: Service,
1561
+ marketplaceItem?: MarketplaceService
1562
+ ): Promise<FormData> {
1563
+ const serviceForm: ServiceSpecForm = transformServiceToForm(service);
1564
+
1565
+ let CUEBundle;
1566
+ const serviceSpecDSL = await generateServiceSpecDSL(
1567
+ serviceForm,
1568
+ marketplaceItem
1569
+ );
1570
+ CUEBundle = buildServiceDeploymentModule(serviceSpecDSL);
1571
+
1572
+ // if (marketplaceItem?.package) {
1573
+ // } else {
1574
+ // const serviceSpec = await generateServiceSpec(serviceForm, marketplaceItem);
1575
+ // CUEBundle = await buildServiceDeploymentModuleWithLocalComponent(
1576
+ // serviceSpec
1577
+ // );
1578
+ // }
1579
+ const resolvedCUEBundle = await CUEBundle;
1580
+ //downloadZipDev(resolvedCUEBundle, true);
1581
+ if (service.download) {
1582
+ downloadZipDev(resolvedCUEBundle, true);
1583
+ return new FormData();
1584
+ } else {
1585
+ const formData = new FormData();
1586
+ formData.append("bundle", resolvedCUEBundle);
1587
+ formData.append(
1588
+ "meta",
1589
+ JSON.stringify({
1590
+ targetAccount: withDefaultValue(serviceForm.accountId, ""),
1591
+ targetEnvironment: withDefaultValue(serviceForm.environmentId, ""),
1592
+ })
1593
+ );
1594
+ formData.append(
1595
+ "labels",
1596
+ JSON.stringify({ project: withDefaultValue(service.project, "") })
1597
+ );
1598
+ formData.append("comment", " ");
1599
+ return formData;
1600
+ }
1601
+ }
1602
+ function downloadZipDev(bundle: Blob, download: boolean) {
1603
+ if (!download) {
1604
+ return;
1605
+ }
1606
+ const url = URL.createObjectURL(bundle);
1607
+ const downloadLink: HTMLAnchorElement = document.createElement("a");
1608
+ downloadLink.target = "_blank";
1609
+ downloadLink.href = url;
1610
+ downloadLink.click();
1611
+ }