tangled
alpha
login
or
join now
tsiry-sandratraina.com
/
vmx
1
fork
atom
A Docker-like CLI and HTTP API for managing headless VMs
1
fork
atom
overview
issues
pulls
pipelines
run format
tsiry-sandratraina.com
3 months ago
98dd6862
c94b8cdc
2/2
fmt.yml
success
2s
test.yml
success
8s
+149
-133
4 changed files
expand all
collapse all
unified
split
deno.json
src
api
machines.ts
migrations.ts
utils.ts
+1
-1
deno.json
···
29
29
"kysely": "npm:kysely@0.27.6",
30
30
"moniker": "npm:moniker@^0.1.2"
31
31
}
32
32
-
}
32
32
+
}
+26
-33
src/api/machines.ts
···
43
43
Effect.flatMap((params) =>
44
44
listInstances(params.all === "true" || params.all === "1")
45
45
),
46
46
-
presentation(c)
47
47
-
)
48
48
-
)
49
49
-
);
46
46
+
presentation(c),
47
47
+
),
48
48
+
));
50
49
51
50
app.post("/", (c) =>
52
51
Effect.runPromise(
···
57
56
const image = yield* getImage(params.image);
58
57
if (!image) {
59
58
return yield* Effect.fail(
60
60
-
new ImageNotFoundError({ id: params.image })
59
59
+
new ImageNotFoundError({ id: params.image }),
61
60
);
62
61
}
63
62
···
91
90
sshPwauth: false,
92
91
},
93
92
},
94
94
-
tempDir
93
93
+
tempDir,
95
94
);
96
95
}
97
96
···
116
115
seed: _.get(
117
116
params,
118
117
"seed",
119
119
-
params.users ? `${SEED_DIR}/seed-${name}.iso` : undefined
118
118
+
params.users ? `${SEED_DIR}/seed-${name}.iso` : undefined,
120
119
),
121
120
pid: 0,
122
121
});
···
126
125
})
127
126
),
128
127
presentation(c),
129
129
-
Effect.catchAll((error) => handleError(error, c))
130
130
-
)
131
131
-
)
132
132
-
);
128
128
+
Effect.catchAll((error) => handleError(error, c)),
129
129
+
),
130
130
+
));
133
131
134
132
app.get("/:id", (c) =>
135
133
Effect.runPromise(
136
134
pipe(
137
135
parseParams(c),
138
136
Effect.flatMap(({ id }) => getInstanceState(id)),
139
139
-
presentation(c)
140
140
-
)
141
141
-
)
142
142
-
);
137
137
+
presentation(c),
138
138
+
),
139
139
+
));
143
140
144
141
app.delete("/:id", (c) =>
145
142
Effect.runPromise(
···
158
155
})
159
156
),
160
157
presentation(c),
161
161
-
Effect.catchAll((error) => handleError(error, c))
162
162
-
)
163
163
-
)
164
164
-
);
158
158
+
Effect.catchAll((error) => handleError(error, c)),
159
159
+
),
160
160
+
));
165
161
166
162
app.post("/:id/start", (c) =>
167
163
Effect.runPromise(
···
186
182
? startRequest.portForward.join(",")
187
183
: vm.portForward,
188
184
},
189
189
-
firmwareArgs
185
185
+
firmwareArgs,
190
186
);
191
187
yield* createLogsDir();
192
188
yield* startDetachedQemu(vm.id, vm, qemuArgs);
···
194
190
})
195
191
),
196
192
presentation(c),
197
197
-
Effect.catchAll((error) => handleError(error, c))
198
198
-
)
199
199
-
)
200
200
-
);
193
193
+
Effect.catchAll((error) => handleError(error, c)),
194
194
+
),
195
195
+
));
201
196
202
197
app.post("/:id/stop", (c) =>
203
198
Effect.runPromise(
···
207
202
Effect.flatMap(killProcess),
208
203
Effect.flatMap(updateToStopped),
209
204
presentation(c),
210
210
-
Effect.catchAll((error) => handleError(error, c))
211
211
-
)
212
212
-
)
213
213
-
);
205
205
+
Effect.catchAll((error) => handleError(error, c)),
206
206
+
),
207
207
+
));
214
208
215
209
app.post("/:id/restart", (c) =>
216
210
Effect.runPromise(
···
237
231
? startRequest.portForward.join(",")
238
232
: vm.portForward,
239
233
},
240
240
-
firmwareArgs
234
234
+
firmwareArgs,
241
235
);
242
236
yield* createLogsDir();
243
237
yield* startDetachedQemu(vm.id, vm, qemuArgs);
···
245
239
})
246
240
),
247
241
presentation(c),
248
248
-
Effect.catchAll((error) => handleError(error, c))
249
249
-
)
250
250
-
)
251
251
-
);
242
242
+
Effect.catchAll((error) => handleError(error, c)),
243
243
+
),
244
244
+
));
252
245
253
246
export default app;
+52
-28
src/migrations.ts
···
34
34
.addColumn("isoPath", "varchar")
35
35
.addColumn("status", "varchar", (col) => col.notNull())
36
36
.addColumn("pid", "integer")
37
37
-
.addColumn("createdAt", "varchar", (col) =>
38
38
-
col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)
37
37
+
.addColumn(
38
38
+
"createdAt",
39
39
+
"varchar",
40
40
+
(col) => col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`),
39
41
)
40
40
-
.addColumn("updatedAt", "varchar", (col) =>
41
41
-
col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)
42
42
+
.addColumn(
43
43
+
"updatedAt",
44
44
+
"varchar",
45
45
+
(col) => col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`),
42
46
)
43
47
.execute();
44
48
},
···
153
157
.addColumn("size", "integer", (col) => col.notNull())
154
158
.addColumn("path", "varchar", (col) => col.notNull())
155
159
.addColumn("format", "varchar", (col) => col.notNull().defaultTo("qcow2"))
156
156
-
.addColumn("createdAt", "varchar", (col) =>
157
157
-
col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)
160
160
+
.addColumn(
161
161
+
"createdAt",
162
162
+
"varchar",
163
163
+
(col) => col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`),
158
164
)
159
165
.addUniqueConstraint("images_repository_tag_unique", [
160
166
"repository",
···
212
218
.createTable("volumes")
213
219
.addColumn("id", "varchar", (col) => col.primaryKey())
214
220
.addColumn("name", "varchar", (col) => col.notNull().unique())
215
215
-
.addColumn("baseImageId", "varchar", (col) =>
216
216
-
col.notNull().references("images.id").onDelete("cascade")
221
221
+
.addColumn(
222
222
+
"baseImageId",
223
223
+
"varchar",
224
224
+
(col) => col.notNull().references("images.id").onDelete("cascade"),
217
225
)
218
226
.addColumn("path", "varchar", (col) => col.notNull())
219
219
-
.addColumn("createdAt", "varchar", (col) =>
220
220
-
col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)
227
227
+
.addColumn(
228
228
+
"createdAt",
229
229
+
"varchar",
230
230
+
(col) => col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`),
221
231
)
222
232
.execute();
223
233
},
···
233
243
.createTable("volumes_new")
234
244
.addColumn("id", "varchar", (col) => col.primaryKey())
235
245
.addColumn("name", "varchar", (col) => col.notNull().unique())
236
236
-
.addColumn("baseImageId", "varchar", (col) =>
237
237
-
col.notNull().references("images.id").onDelete("cascade")
246
246
+
.addColumn(
247
247
+
"baseImageId",
248
248
+
"varchar",
249
249
+
(col) => col.notNull().references("images.id").onDelete("cascade"),
238
250
)
239
251
.addColumn("path", "varchar", (col) => col.notNull())
240
240
-
.addColumn("createdAt", "varchar", (col) =>
241
241
-
col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)
252
252
+
.addColumn(
253
253
+
"createdAt",
254
254
+
"varchar",
255
255
+
(col) => col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`),
242
256
)
243
257
.execute();
244
258
···
307
321
.addColumn("pid", "integer")
308
322
.addColumn("volume", "varchar")
309
323
.addColumn("seed", "varchar")
310
310
-
.addColumn("createdAt", "varchar", (col) =>
311
311
-
col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)
324
324
+
.addColumn(
325
325
+
"createdAt",
326
326
+
"varchar",
327
327
+
(col) => col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`),
312
328
)
313
313
-
.addColumn("updatedAt", "varchar", (col) =>
314
314
-
col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)
329
329
+
.addColumn(
330
330
+
"updatedAt",
331
331
+
"varchar",
332
332
+
(col) => col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`),
315
333
)
316
334
.execute();
317
335
···
321
339
`.execute(db);
322
340
323
341
await db.schema.dropTable("virtual_machines").execute();
324
324
-
await sql`ALTER TABLE virtual_machines_new RENAME TO virtual_machines`.execute(
325
325
-
db
326
326
-
);
342
342
+
await sql`ALTER TABLE virtual_machines_new RENAME TO virtual_machines`
343
343
+
.execute(
344
344
+
db,
345
345
+
);
327
346
},
328
347
329
348
async down(db: Kysely<unknown>): Promise<void> {
···
347
366
.addColumn("pid", "integer")
348
367
.addColumn("volume", "varchar")
349
368
.addColumn("seed", "varchar")
350
350
-
.addColumn("createdAt", "varchar", (col) =>
351
351
-
col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)
369
369
+
.addColumn(
370
370
+
"createdAt",
371
371
+
"varchar",
372
372
+
(col) => col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`),
352
373
)
353
353
-
.addColumn("updatedAt", "varchar", (col) =>
354
354
-
col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)
374
374
+
.addColumn(
375
375
+
"updatedAt",
376
376
+
"varchar",
377
377
+
(col) => col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`),
355
378
)
356
379
.execute();
357
380
···
361
384
`.execute(db);
362
385
363
386
await db.schema.dropTable("virtual_machines").execute();
364
364
-
await sql`ALTER TABLE virtual_machines_old RENAME TO virtual_machines`.execute(
365
365
-
db
366
366
-
);
387
387
+
await sql`ALTER TABLE virtual_machines_old RENAME TO virtual_machines`
388
388
+
.execute(
389
389
+
db,
390
390
+
);
367
391
},
368
392
};
369
393
+70
-71
src/utils.ts
···
65
65
export const isValidISOurl = (url?: string): boolean => {
66
66
return Boolean(
67
67
(url?.startsWith("http://") || url?.startsWith("https://")) &&
68
68
-
url?.endsWith(".iso")
68
68
+
url?.endsWith(".iso"),
69
69
);
70
70
};
71
71
···
91
91
});
92
92
93
93
export const validateImage = (
94
94
-
image: string
94
94
+
image: string,
95
95
): Effect.Effect<string, InvalidImageNameError, never> => {
96
96
const regex =
97
97
/^(?:[a-zA-Z0-9.-]+(?:\.[a-zA-Z0-9.-]+)*\/)?[a-z0-9]+(?:[._-][a-z0-9]+)*\/[a-z0-9]+(?:[._-][a-z0-9]+)*(?::[a-zA-Z0-9._-]+)?$/;
···
102
102
image,
103
103
cause:
104
104
"Image name does not conform to expected format. Should be in the format 'repository/name:tag'.",
105
105
-
})
105
105
+
}),
106
106
);
107
107
}
108
108
return Effect.succeed(image);
···
111
111
export const extractTag = (name: string) =>
112
112
pipe(
113
113
validateImage(name),
114
114
-
Effect.flatMap((image) => Effect.succeed(image.split(":")[1] || "latest"))
114
114
+
Effect.flatMap((image) => Effect.succeed(image.split(":")[1] || "latest")),
115
115
);
116
116
117
117
export const failOnMissingImage = (
118
118
-
image: Image | undefined
118
118
+
image: Image | undefined,
119
119
): Effect.Effect<Image, Error, never> =>
120
120
image
121
121
? Effect.succeed(image)
122
122
: Effect.fail(new NoSuchImageError({ cause: "No such image" }));
123
123
124
124
export const du = (
125
125
-
path: string
125
125
+
path: string,
126
126
): Effect.Effect<number, LogCommandError, never> =>
127
127
Effect.tryPromise({
128
128
try: async () => {
···
154
154
exists
155
155
? Effect.succeed(true)
156
156
: du(path).pipe(Effect.map((size) => size < EMPTY_DISK_THRESHOLD_KB))
157
157
-
)
157
157
+
),
158
158
);
159
159
160
160
export const downloadIso = (url: string, options: Options) =>
···
176
176
if (driveSize > EMPTY_DISK_THRESHOLD_KB) {
177
177
console.log(
178
178
chalk.yellowBright(
179
179
-
`Drive image ${options.image} is not empty (size: ${driveSize} KB), skipping ISO download to avoid overwriting existing data.`
180
180
-
)
179
179
+
`Drive image ${options.image} is not empty (size: ${driveSize} KB), skipping ISO download to avoid overwriting existing data.`,
180
180
+
),
181
181
);
182
182
return null;
183
183
}
···
195
195
if (outputExists) {
196
196
console.log(
197
197
chalk.yellowBright(
198
198
-
`File ${outputPath} already exists, skipping download.`
199
199
-
)
198
198
+
`File ${outputPath} already exists, skipping download.`,
199
199
+
),
200
200
);
201
201
return outputPath;
202
202
}
···
207
207
chalk.blueBright(
208
208
`Downloading ${
209
209
url.endsWith(".iso") ? "ISO" : "image"
210
210
-
} from ${url}...`
211
211
-
)
210
210
+
} from ${url}...`,
211
211
+
),
212
212
);
213
213
const cmd = new Deno.Command("curl", {
214
214
args: ["-L", "-o", outputPath, url],
···
251
251
if (!success) {
252
252
console.error(
253
253
chalk.redBright(
254
254
-
"Failed to get QEMU prefix from Homebrew. Ensure QEMU is installed via Homebrew."
255
255
-
)
254
254
+
"Failed to get QEMU prefix from Homebrew. Ensure QEMU is installed via Homebrew.",
255
255
+
),
256
256
);
257
257
Deno.exit(1);
258
258
}
···
265
265
try: () =>
266
266
Deno.copyFile(
267
267
`${brewPrefix}/share/qemu/edk2-arm-vars.fd`,
268
268
-
edk2VarsAarch64
268
268
+
edk2VarsAarch64,
269
269
),
270
270
catch: (error) => new LogCommandError({ cause: error }),
271
271
});
···
310
310
const configOK = yield* pipe(
311
311
fileExists("config.ign"),
312
312
Effect.flatMap(() => Effect.succeed(true)),
313
313
-
Effect.catchAll(() => Effect.succeed(false))
313
313
+
Effect.catchAll(() => Effect.succeed(false)),
314
314
);
315
315
if (!configOK) {
316
316
console.error(
317
317
chalk.redBright(
318
318
-
"CoreOS image requires a config.ign file in the current directory."
319
319
-
)
318
318
+
"CoreOS image requires a config.ign file in the current directory.",
319
319
+
),
320
320
);
321
321
Deno.exit(1);
322
322
}
···
356
356
imagePath &&
357
357
imagePath.endsWith(".qcow2") &&
358
358
imagePath.startsWith(
359
359
-
`di-${Deno.build.arch === "aarch64" ? "arm64" : "amd64"}-console-`
359
359
+
`di-${Deno.build.arch === "aarch64" ? "arm64" : "amd64"}-console-`,
360
360
)
361
361
) {
362
362
return [
···
371
371
372
372
export const setupAlpineArgs = (
373
373
imagePath?: string | null,
374
374
-
seed: string = "seed.iso"
374
374
+
seed: string = "seed.iso",
375
375
) =>
376
376
Effect.sync(() => {
377
377
if (
···
392
392
393
393
export const setupDebianArgs = (
394
394
imagePath?: string | null,
395
395
-
seed: string = "seed.iso"
395
395
+
seed: string = "seed.iso",
396
396
) =>
397
397
Effect.sync(() => {
398
398
if (
···
413
413
414
414
export const setupUbuntuArgs = (
415
415
imagePath?: string | null,
416
416
-
seed: string = "seed.iso"
416
416
+
seed: string = "seed.iso",
417
417
) =>
418
418
Effect.sync(() => {
419
419
if (
···
434
434
435
435
export const setupAlmaLinuxArgs = (
436
436
imagePath?: string | null,
437
437
-
seed: string = "seed.iso"
437
437
+
seed: string = "seed.iso",
438
438
) =>
439
439
Effect.sync(() => {
440
440
if (
···
455
455
456
456
export const setupRockyLinuxArgs = (
457
457
imagePath?: string | null,
458
458
-
seed: string = "seed.iso"
458
458
+
seed: string = "seed.iso",
459
459
) =>
460
460
Effect.sync(() => {
461
461
if (
···
478
478
Effect.gen(function* () {
479
479
const macAddress = yield* generateRandomMacAddress();
480
480
481
481
-
const qemu =
482
482
-
Deno.build.arch === "aarch64"
483
483
-
? "qemu-system-aarch64"
484
484
-
: "qemu-system-x86_64";
481
481
+
const qemu = Deno.build.arch === "aarch64"
482
482
+
? "qemu-system-aarch64"
483
483
+
: "qemu-system-x86_64";
485
484
486
485
const firmwareFiles = yield* setupFirmwareFilesIfNeeded();
487
486
let coreosArgs: string[] = yield* setupCoreOSArgs(isoPath || options.image);
488
487
let fedoraArgs: string[] = yield* setupFedoraArgs(
489
488
isoPath || options.image,
490
490
-
options.seed
489
489
+
options.seed,
491
490
);
492
491
let gentooArgs: string[] = yield* setupGentooArgs(
493
492
isoPath || options.image,
494
494
-
options.seed
493
493
+
options.seed,
495
494
);
496
495
let alpineArgs: string[] = yield* setupAlpineArgs(
497
496
isoPath || options.image,
498
498
-
options.seed
497
497
+
options.seed,
499
498
);
500
499
let debianArgs: string[] = yield* setupDebianArgs(
501
500
isoPath || options.image,
502
502
-
options.seed
501
501
+
options.seed,
503
502
);
504
503
let ubuntuArgs: string[] = yield* setupUbuntuArgs(
505
504
isoPath || options.image,
506
506
-
options.seed
505
505
+
options.seed,
507
506
);
508
507
let almalinuxArgs: string[] = yield* setupAlmaLinuxArgs(
509
508
isoPath || options.image,
510
510
-
options.seed
509
509
+
options.seed,
511
510
);
512
511
let rockylinuxArgs: string[] = yield* setupRockyLinuxArgs(
513
512
isoPath || options.image,
514
514
-
options.seed
513
513
+
options.seed,
515
514
);
516
515
517
516
if (coreosArgs.length > 0 && !isoPath) {
···
584
583
options.image && [
585
584
"-drive",
586
585
`file=${options.image},format=${options.diskFormat},if=virtio`,
587
587
-
]
586
586
+
],
588
587
),
589
588
];
590
589
···
599
598
const logPath = `${LOGS_DIR}/${name}.log`;
600
599
601
600
const fullCommand = options.bridge
602
602
-
? `sudo ${qemu} ${qemuArgs
601
601
+
? `sudo ${qemu} ${
602
602
+
qemuArgs
603
603
.slice(1)
604
604
-
.join(" ")} >> "${logPath}" 2>&1 & echo $!`
604
604
+
.join(" ")
605
605
+
} >> "${logPath}" 2>&1 & echo $!`
605
606
: `${qemu} ${qemuArgs.join(" ")} >> "${logPath}" 2>&1 & echo $!`;
606
607
607
608
const { stdout } = yield* Effect.tryPromise({
···
627
628
cpus: options.cpus,
628
629
cpu: options.cpu,
629
630
diskSize: options.size || "20G",
630
630
-
diskFormat:
631
631
-
(isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) ||
631
631
+
diskFormat: (isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) ||
632
632
options.diskFormat ||
633
633
"raw",
634
634
portForward: options.portForward,
···
647
647
});
648
648
649
649
console.log(
650
650
-
`Virtual machine ${name} started in background (PID: ${qemuPid})`
650
650
+
`Virtual machine ${name} started in background (PID: ${qemuPid})`,
651
651
);
652
652
console.log(`Logs will be written to: ${logPath}`);
653
653
···
670
670
cpus: options.cpus,
671
671
cpu: options.cpu,
672
672
diskSize: options.size || "20G",
673
673
-
diskFormat:
674
674
-
(isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) ||
673
673
+
diskFormat: (isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) ||
675
674
options.diskFormat ||
676
675
"raw",
677
676
portForward: options.portForward,
···
779
778
if (pathExists) {
780
779
console.log(
781
780
chalk.yellowBright(
782
782
-
`Drive image ${path} already exists, skipping creation.`
783
783
-
)
781
781
+
`Drive image ${path} already exists, skipping creation.`,
782
782
+
),
784
783
);
785
784
return;
786
785
}
···
807
806
});
808
807
809
808
export const fileExists = (
810
810
-
path: string
809
809
+
path: string,
811
810
): Effect.Effect<void, NoSuchFileError, never> =>
812
811
Effect.try({
813
812
try: () => Deno.statSync(path),
···
815
814
});
816
815
817
816
export const constructCoreOSImageURL = (
818
818
-
image: string
817
817
+
image: string,
819
818
): Effect.Effect<string, InvalidImageNameError, never> => {
820
819
// detect with regex if image matches coreos pattern: fedora-coreos or fedora-coreos-<version> or coreos or coreos-<version>
821
820
const coreosRegex = /^(fedora-coreos|coreos)(-(\d+\.\d+\.\d+\.\d+))?$/;
···
823
822
if (match) {
824
823
const version = match[3] || FEDORA_COREOS_DEFAULT_VERSION;
825
824
return Effect.succeed(
826
826
-
FEDORA_COREOS_IMG_URL.replaceAll(FEDORA_COREOS_DEFAULT_VERSION, version)
825
825
+
FEDORA_COREOS_IMG_URL.replaceAll(FEDORA_COREOS_DEFAULT_VERSION, version),
827
826
);
828
827
}
829
828
···
831
830
new InvalidImageNameError({
832
831
image,
833
832
cause: "Image name does not match CoreOS naming conventions.",
834
834
-
})
833
833
+
}),
835
834
);
836
835
};
837
836
···
860
859
});
861
860
862
861
export const constructNixOSImageURL = (
863
863
-
image: string
862
862
+
image: string,
864
863
): Effect.Effect<string, InvalidImageNameError, never> => {
865
864
// detect with regex if image matches NixOS pattern: nixos or nixos-<version>
866
865
const nixosRegex = /^(nixos)(-(\d+\.\d+))?$/;
···
868
867
if (match) {
869
868
const version = match[3] || NIXOS_DEFAULT_VERSION;
870
869
return Effect.succeed(
871
871
-
NIXOS_ISO_URL.replaceAll(NIXOS_DEFAULT_VERSION, version)
870
870
+
NIXOS_ISO_URL.replaceAll(NIXOS_DEFAULT_VERSION, version),
872
871
);
873
872
}
874
873
···
876
875
new InvalidImageNameError({
877
876
image,
878
877
cause: "Image name does not match NixOS naming conventions.",
879
879
-
})
878
878
+
}),
880
879
);
881
880
};
882
881
883
882
export const constructFedoraImageURL = (
884
883
image: string,
885
885
-
cloud: boolean = false
884
884
+
cloud: boolean = false,
886
885
): Effect.Effect<string, InvalidImageNameError, never> => {
887
886
// detect with regex if image matches Fedora pattern: fedora
888
887
const fedoraRegex = /^(fedora)$/;
···
895
894
new InvalidImageNameError({
896
895
image,
897
896
cause: "Image name does not match Fedora naming conventions.",
898
898
-
})
897
897
+
}),
899
898
);
900
899
};
901
900
902
901
export const constructGentooImageURL = (
903
903
-
image: string
902
902
+
image: string,
904
903
): Effect.Effect<string, InvalidImageNameError, never> => {
905
904
// detect with regex if image matches genroo pattern: gentoo-20251116T161545Z or gentoo
906
905
const gentooRegex = /^(gentoo)(-(\d{8}T\d{6}Z))?$/;
···
909
908
return Effect.succeed(
910
909
GENTOO_IMG_URL.replaceAll("20251116T161545Z", match[3]).replaceAll(
911
910
"20251116T233105Z",
912
912
-
match[3]
913
913
-
)
911
911
+
match[3],
912
912
+
),
914
913
);
915
914
}
916
915
···
922
921
new InvalidImageNameError({
923
922
image,
924
923
cause: "Image name does not match Gentoo naming conventions.",
925
925
-
})
924
924
+
}),
926
925
);
927
926
};
928
927
929
928
export const constructDebianImageURL = (
930
929
image: string,
931
931
-
cloud: boolean = false
930
930
+
cloud: boolean = false,
932
931
): Effect.Effect<string, InvalidImageNameError, never> => {
933
932
if (cloud && image === "debian") {
934
933
return Effect.succeed(DEBIAN_CLOUD_IMG_URL);
···
939
938
const match = image.match(debianRegex);
940
939
if (match?.[3]) {
941
940
return Effect.succeed(
942
942
-
DEBIAN_ISO_URL.replaceAll(DEBIAN_DEFAULT_VERSION, match[3])
941
941
+
DEBIAN_ISO_URL.replaceAll(DEBIAN_DEFAULT_VERSION, match[3]),
943
942
);
944
943
}
945
944
···
951
950
new InvalidImageNameError({
952
951
image,
953
952
cause: "Image name does not match Debian naming conventions.",
954
954
-
})
953
953
+
}),
955
954
);
956
955
};
957
956
958
957
export const constructAlpineImageURL = (
959
959
-
image: string
958
958
+
image: string,
960
959
): Effect.Effect<string, InvalidImageNameError, never> => {
961
960
// detect with regex if image matches alpine pattern: alpine-<version> or alpine
962
961
const alpineRegex = /^(alpine)(-(\d+\.\d+(\.\d+)?))?$/;
963
962
const match = image.match(alpineRegex);
964
963
if (match?.[3]) {
965
964
return Effect.succeed(
966
966
-
ALPINE_ISO_URL.replaceAll(ALPINE_DEFAULT_VERSION, match[3])
965
965
+
ALPINE_ISO_URL.replaceAll(ALPINE_DEFAULT_VERSION, match[3]),
967
966
);
968
967
}
969
968
···
975
974
new InvalidImageNameError({
976
975
image,
977
976
cause: "Image name does not match Alpine naming conventions.",
978
978
-
})
977
977
+
}),
979
978
);
980
979
};
981
980
982
981
export const constructUbuntuImageURL = (
983
982
image: string,
984
984
-
cloud: boolean = false
983
983
+
cloud: boolean = false,
985
984
): Effect.Effect<string, InvalidImageNameError, never> => {
986
985
// detect with regex if image matches ubuntu pattern: ubuntu
987
986
const ubuntuRegex = /^(ubuntu)$/;
···
997
996
new InvalidImageNameError({
998
997
image,
999
998
cause: "Image name does not match Ubuntu naming conventions.",
1000
1000
-
})
999
999
+
}),
1001
1000
);
1002
1001
};
1003
1002
1004
1003
export const constructAlmaLinuxImageURL = (
1005
1004
image: string,
1006
1006
-
cloud: boolean = false
1005
1005
+
cloud: boolean = false,
1007
1006
): Effect.Effect<string, InvalidImageNameError, never> => {
1008
1007
// detect with regex if image matches almalinux pattern: almalinux, almalinux
1009
1008
const almaLinuxRegex = /^(almalinux|alma)$/;
···
1019
1018
new InvalidImageNameError({
1020
1019
image,
1021
1020
cause: "Image name does not match AlmaLinux naming conventions.",
1022
1022
-
})
1021
1021
+
}),
1023
1022
);
1024
1023
};
1025
1024
1026
1025
export const constructRockyLinuxImageURL = (
1027
1026
image: string,
1028
1028
-
cloud: boolean = false
1027
1027
+
cloud: boolean = false,
1029
1028
): Effect.Effect<string, InvalidImageNameError, never> => {
1030
1029
// detect with regex if image matches rockylinux pattern: rocky. rockylinux
1031
1030
const rockyLinuxRegex = /^(rockylinux|rocky)$/;
···
1041
1040
new InvalidImageNameError({
1042
1041
image,
1043
1042
cause: "Image name does not match RockyLinux naming conventions.",
1044
1044
-
})
1043
1043
+
}),
1045
1044
);
1046
1045
};