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
5db81a69
da3b4a5c
+70
-71
1 changed file
expand all
collapse all
unified
split
src
utils.ts
+70
-71
src/utils.ts
···
78
export const isValidISOurl = (url?: string): boolean => {
79
return Boolean(
80
(url?.startsWith("http://") || url?.startsWith("https://")) &&
81
-
url?.endsWith(".iso")
82
);
83
};
84
···
104
});
105
106
export const validateImage = (
107
-
image: string
108
): Effect.Effect<string, InvalidImageNameError, never> => {
109
const regex =
110
/^(?:[a-zA-Z0-9.-]+(?:\.[a-zA-Z0-9.-]+)*\/)?[a-z0-9]+(?:[._-][a-z0-9]+)*\/[a-z0-9]+(?:[._-][a-z0-9]+)*(?::[a-zA-Z0-9._-]+)?$/;
···
115
image,
116
cause:
117
"Image name does not conform to expected format. Should be in the format 'repository/name:tag'.",
118
-
})
119
);
120
}
121
return Effect.succeed(image);
···
124
export const extractTag = (name: string) =>
125
pipe(
126
validateImage(name),
127
-
Effect.flatMap((image) => Effect.succeed(image.split(":")[1] || "latest"))
128
);
129
130
export const failOnMissingImage = (
131
-
image: Image | undefined
132
): Effect.Effect<Image, Error, never> =>
133
image
134
? Effect.succeed(image)
135
: Effect.fail(new NoSuchImageError({ cause: "No such image" }));
136
137
export const du = (
138
-
path: string
139
): Effect.Effect<number, LogCommandError, never> =>
140
Effect.tryPromise({
141
try: async () => {
···
167
exists
168
? Effect.succeed(true)
169
: du(path).pipe(Effect.map((size) => size < EMPTY_DISK_THRESHOLD_KB))
170
-
)
171
);
172
173
export const downloadIso = (url: string, options: Options) =>
···
189
if (driveSize > EMPTY_DISK_THRESHOLD_KB) {
190
console.log(
191
chalk.yellowBright(
192
-
`Drive image ${options.image} is not empty (size: ${driveSize} KB), skipping ISO download to avoid overwriting existing data.`
193
-
)
194
);
195
return null;
196
}
···
208
if (outputExists) {
209
console.log(
210
chalk.yellowBright(
211
-
`File ${outputPath} already exists, skipping download.`
212
-
)
213
);
214
return outputPath;
215
}
···
220
chalk.blueBright(
221
`Downloading ${
222
url.endsWith(".iso") ? "ISO" : "image"
223
-
} from ${url}...`
224
-
)
225
);
226
const cmd = new Deno.Command("curl", {
227
args: ["-L", "-o", outputPath, url],
···
264
if (!success) {
265
console.error(
266
chalk.redBright(
267
-
"Failed to get QEMU prefix from Homebrew. Ensure QEMU is installed via Homebrew."
268
-
)
269
);
270
Deno.exit(1);
271
}
···
278
try: () =>
279
Deno.copyFile(
280
`${brewPrefix}/share/qemu/edk2-arm-vars.fd`,
281
-
edk2VarsAarch64
282
),
283
catch: (error) => new LogCommandError({ cause: error }),
284
});
···
323
const configOK = yield* pipe(
324
fileExists("config.ign"),
325
Effect.flatMap(() => Effect.succeed(true)),
326
-
Effect.catchAll(() => Effect.succeed(false))
327
);
328
if (!configOK) {
329
console.error(
330
chalk.redBright(
331
-
"CoreOS image requires a config.ign file in the current directory."
332
-
)
333
);
334
Deno.exit(1);
335
}
···
369
imagePath &&
370
imagePath.endsWith(".qcow2") &&
371
imagePath.startsWith(
372
-
`di-${Deno.build.arch === "aarch64" ? "arm64" : "amd64"}-console-`
373
)
374
) {
375
return [
···
384
385
export const setupAlpineArgs = (
386
imagePath?: string | null,
387
-
seed: string = "seed.iso"
388
) =>
389
Effect.sync(() => {
390
if (
···
405
406
export const setupDebianArgs = (
407
imagePath?: string | null,
408
-
seed: string = "seed.iso"
409
) =>
410
Effect.sync(() => {
411
if (
···
426
427
export const setupUbuntuArgs = (
428
imagePath?: string | null,
429
-
seed: string = "seed.iso"
430
) =>
431
Effect.sync(() => {
432
if (
···
447
448
export const setupAlmaLinuxArgs = (
449
imagePath?: string | null,
450
-
seed: string = "seed.iso"
451
) =>
452
Effect.sync(() => {
453
if (
···
468
469
export const setupRockyLinuxArgs = (
470
imagePath?: string | null,
471
-
seed: string = "seed.iso"
472
) =>
473
Effect.sync(() => {
474
if (
···
491
Effect.gen(function* () {
492
const macAddress = yield* generateRandomMacAddress();
493
494
-
const qemu =
495
-
Deno.build.arch === "aarch64"
496
-
? "qemu-system-aarch64"
497
-
: "qemu-system-x86_64";
498
499
const firmwareFiles = yield* setupFirmwareFilesIfNeeded();
500
let coreosArgs: string[] = yield* setupCoreOSArgs(isoPath || options.image);
501
let fedoraArgs: string[] = yield* setupFedoraArgs(
502
isoPath || options.image,
503
-
options.seed
504
);
505
let gentooArgs: string[] = yield* setupGentooArgs(
506
isoPath || options.image,
507
-
options.seed
508
);
509
let alpineArgs: string[] = yield* setupAlpineArgs(
510
isoPath || options.image,
511
-
options.seed
512
);
513
let debianArgs: string[] = yield* setupDebianArgs(
514
isoPath || options.image,
515
-
options.seed
516
);
517
let ubuntuArgs: string[] = yield* setupUbuntuArgs(
518
isoPath || options.image,
519
-
options.seed
520
);
521
let almalinuxArgs: string[] = yield* setupAlmaLinuxArgs(
522
isoPath || options.image,
523
-
options.seed
524
);
525
let rockylinuxArgs: string[] = yield* setupRockyLinuxArgs(
526
isoPath || options.image,
527
-
options.seed
528
);
529
530
if (coreosArgs.length > 0 && !isoPath) {
···
597
options.image && [
598
"-drive",
599
`file=${options.image},format=${options.diskFormat},if=virtio`,
600
-
]
601
),
602
];
603
···
612
const logPath = `${LOGS_DIR}/${name}.log`;
613
614
const fullCommand = options.bridge
615
-
? `sudo ${qemu} ${qemuArgs
0
616
.slice(1)
617
-
.join(" ")} >> "${logPath}" 2>&1 & echo $!`
0
618
: `${qemu} ${qemuArgs.join(" ")} >> "${logPath}" 2>&1 & echo $!`;
619
620
const { stdout } = yield* Effect.tryPromise({
···
640
cpus: options.cpus,
641
cpu: options.cpu,
642
diskSize: options.size || "20G",
643
-
diskFormat:
644
-
(isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) ||
645
options.diskFormat ||
646
"raw",
647
portForward: options.portForward,
···
661
});
662
663
console.log(
664
-
`Virtual machine ${name} started in background (PID: ${qemuPid})`
665
);
666
console.log(`Logs will be written to: ${logPath}`);
667
···
684
cpus: options.cpus,
685
cpu: options.cpu,
686
diskSize: options.size || "20G",
687
-
diskFormat:
688
-
(isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) ||
689
options.diskFormat ||
690
"raw",
691
portForward: options.portForward,
···
794
if (pathExists) {
795
console.log(
796
chalk.yellowBright(
797
-
`Drive image ${path} already exists, skipping creation.`
798
-
)
799
);
800
return;
801
}
···
822
});
823
824
export const fileExists = (
825
-
path: string
826
): Effect.Effect<void, NoSuchFileError, never> =>
827
Effect.try({
828
try: () => Deno.statSync(path),
···
830
});
831
832
export const constructCoreOSImageURL = (
833
-
image: string
834
): Effect.Effect<string, InvalidImageNameError, never> => {
835
// detect with regex if image matches coreos pattern: fedora-coreos or fedora-coreos-<version> or coreos or coreos-<version>
836
const coreosRegex = /^(fedora-coreos|coreos)(-(\d+\.\d+\.\d+\.\d+))?$/;
···
838
if (match) {
839
const version = match[3] || FEDORA_COREOS_DEFAULT_VERSION;
840
return Effect.succeed(
841
-
FEDORA_COREOS_IMG_URL.replaceAll(FEDORA_COREOS_DEFAULT_VERSION, version)
842
);
843
}
844
···
846
new InvalidImageNameError({
847
image,
848
cause: "Image name does not match CoreOS naming conventions.",
849
-
})
850
);
851
};
852
···
875
});
876
877
export const constructNixOSImageURL = (
878
-
image: string
879
): Effect.Effect<string, InvalidImageNameError, never> => {
880
// detect with regex if image matches NixOS pattern: nixos or nixos-<version>
881
const nixosRegex = /^(nixos)(-(\d+\.\d+))?$/;
···
883
if (match) {
884
const version = match[3] || NIXOS_DEFAULT_VERSION;
885
return Effect.succeed(
886
-
NIXOS_ISO_URL.replaceAll(NIXOS_DEFAULT_VERSION, version)
887
);
888
}
889
···
891
new InvalidImageNameError({
892
image,
893
cause: "Image name does not match NixOS naming conventions.",
894
-
})
895
);
896
};
897
898
export const constructFedoraImageURL = (
899
image: string,
900
-
cloud: boolean = false
901
): Effect.Effect<string, InvalidImageNameError, never> => {
902
// detect with regex if image matches Fedora pattern: fedora
903
const fedoraRegex = /^(fedora)$/;
···
910
new InvalidImageNameError({
911
image,
912
cause: "Image name does not match Fedora naming conventions.",
913
-
})
914
);
915
};
916
917
export const constructGentooImageURL = (
918
-
image: string
919
): Effect.Effect<string, InvalidImageNameError, never> => {
920
// detect with regex if image matches genroo pattern: gentoo-20251116T161545Z or gentoo
921
const gentooRegex = /^(gentoo)(-(\d{8}T\d{6}Z))?$/;
···
924
return Effect.succeed(
925
GENTOO_IMG_URL.replaceAll("20251116T161545Z", match[3]).replaceAll(
926
"20251116T233105Z",
927
-
match[3]
928
-
)
929
);
930
}
931
···
937
new InvalidImageNameError({
938
image,
939
cause: "Image name does not match Gentoo naming conventions.",
940
-
})
941
);
942
};
943
944
export const constructDebianImageURL = (
945
image: string,
946
-
cloud: boolean = false
947
): Effect.Effect<string, InvalidImageNameError, never> => {
948
if (cloud && image === "debian") {
949
return Effect.succeed(DEBIAN_CLOUD_IMG_URL);
···
954
const match = image.match(debianRegex);
955
if (match?.[3]) {
956
return Effect.succeed(
957
-
DEBIAN_ISO_URL.replaceAll(DEBIAN_DEFAULT_VERSION, match[3])
958
);
959
}
960
···
966
new InvalidImageNameError({
967
image,
968
cause: "Image name does not match Debian naming conventions.",
969
-
})
970
);
971
};
972
973
export const constructAlpineImageURL = (
974
-
image: string
975
): Effect.Effect<string, InvalidImageNameError, never> => {
976
// detect with regex if image matches alpine pattern: alpine-<version> or alpine
977
const alpineRegex = /^(alpine)(-(\d+\.\d+(\.\d+)?))?$/;
978
const match = image.match(alpineRegex);
979
if (match?.[3]) {
980
return Effect.succeed(
981
-
ALPINE_ISO_URL.replaceAll(ALPINE_DEFAULT_VERSION, match[3])
982
);
983
}
984
···
990
new InvalidImageNameError({
991
image,
992
cause: "Image name does not match Alpine naming conventions.",
993
-
})
994
);
995
};
996
997
export const constructUbuntuImageURL = (
998
image: string,
999
-
cloud: boolean = false
1000
): Effect.Effect<string, InvalidImageNameError, never> => {
1001
// detect with regex if image matches ubuntu pattern: ubuntu
1002
const ubuntuRegex = /^(ubuntu)$/;
···
1012
new InvalidImageNameError({
1013
image,
1014
cause: "Image name does not match Ubuntu naming conventions.",
1015
-
})
1016
);
1017
};
1018
1019
export const constructAlmaLinuxImageURL = (
1020
image: string,
1021
-
cloud: boolean = false
1022
): Effect.Effect<string, InvalidImageNameError, never> => {
1023
// detect with regex if image matches almalinux pattern: ubuntu
1024
const almaLinuxRegex = /^(almalinux)$/;
···
1034
new InvalidImageNameError({
1035
image,
1036
cause: "Image name does not match AlmaLinux naming conventions.",
1037
-
})
1038
);
1039
};
1040
1041
export const constructRockyLinuxImageURL = (
1042
image: string,
1043
-
cloud: boolean = false
1044
): Effect.Effect<string, InvalidImageNameError, never> => {
1045
// detect with regex if image matches rockylinux pattern: ubuntu
1046
const rockyLinuxRegex = /^(rockylinux)$/;
···
1056
new InvalidImageNameError({
1057
image,
1058
cause: "Image name does not match RockyLinux naming conventions.",
1059
-
})
1060
);
1061
};
···
78
export const isValidISOurl = (url?: string): boolean => {
79
return Boolean(
80
(url?.startsWith("http://") || url?.startsWith("https://")) &&
81
+
url?.endsWith(".iso"),
82
);
83
};
84
···
104
});
105
106
export const validateImage = (
107
+
image: string,
108
): Effect.Effect<string, InvalidImageNameError, never> => {
109
const regex =
110
/^(?:[a-zA-Z0-9.-]+(?:\.[a-zA-Z0-9.-]+)*\/)?[a-z0-9]+(?:[._-][a-z0-9]+)*\/[a-z0-9]+(?:[._-][a-z0-9]+)*(?::[a-zA-Z0-9._-]+)?$/;
···
115
image,
116
cause:
117
"Image name does not conform to expected format. Should be in the format 'repository/name:tag'.",
118
+
}),
119
);
120
}
121
return Effect.succeed(image);
···
124
export const extractTag = (name: string) =>
125
pipe(
126
validateImage(name),
127
+
Effect.flatMap((image) => Effect.succeed(image.split(":")[1] || "latest")),
128
);
129
130
export const failOnMissingImage = (
131
+
image: Image | undefined,
132
): Effect.Effect<Image, Error, never> =>
133
image
134
? Effect.succeed(image)
135
: Effect.fail(new NoSuchImageError({ cause: "No such image" }));
136
137
export const du = (
138
+
path: string,
139
): Effect.Effect<number, LogCommandError, never> =>
140
Effect.tryPromise({
141
try: async () => {
···
167
exists
168
? Effect.succeed(true)
169
: du(path).pipe(Effect.map((size) => size < EMPTY_DISK_THRESHOLD_KB))
170
+
),
171
);
172
173
export const downloadIso = (url: string, options: Options) =>
···
189
if (driveSize > EMPTY_DISK_THRESHOLD_KB) {
190
console.log(
191
chalk.yellowBright(
192
+
`Drive image ${options.image} is not empty (size: ${driveSize} KB), skipping ISO download to avoid overwriting existing data.`,
193
+
),
194
);
195
return null;
196
}
···
208
if (outputExists) {
209
console.log(
210
chalk.yellowBright(
211
+
`File ${outputPath} already exists, skipping download.`,
212
+
),
213
);
214
return outputPath;
215
}
···
220
chalk.blueBright(
221
`Downloading ${
222
url.endsWith(".iso") ? "ISO" : "image"
223
+
} from ${url}...`,
224
+
),
225
);
226
const cmd = new Deno.Command("curl", {
227
args: ["-L", "-o", outputPath, url],
···
264
if (!success) {
265
console.error(
266
chalk.redBright(
267
+
"Failed to get QEMU prefix from Homebrew. Ensure QEMU is installed via Homebrew.",
268
+
),
269
);
270
Deno.exit(1);
271
}
···
278
try: () =>
279
Deno.copyFile(
280
`${brewPrefix}/share/qemu/edk2-arm-vars.fd`,
281
+
edk2VarsAarch64,
282
),
283
catch: (error) => new LogCommandError({ cause: error }),
284
});
···
323
const configOK = yield* pipe(
324
fileExists("config.ign"),
325
Effect.flatMap(() => Effect.succeed(true)),
326
+
Effect.catchAll(() => Effect.succeed(false)),
327
);
328
if (!configOK) {
329
console.error(
330
chalk.redBright(
331
+
"CoreOS image requires a config.ign file in the current directory.",
332
+
),
333
);
334
Deno.exit(1);
335
}
···
369
imagePath &&
370
imagePath.endsWith(".qcow2") &&
371
imagePath.startsWith(
372
+
`di-${Deno.build.arch === "aarch64" ? "arm64" : "amd64"}-console-`,
373
)
374
) {
375
return [
···
384
385
export const setupAlpineArgs = (
386
imagePath?: string | null,
387
+
seed: string = "seed.iso",
388
) =>
389
Effect.sync(() => {
390
if (
···
405
406
export const setupDebianArgs = (
407
imagePath?: string | null,
408
+
seed: string = "seed.iso",
409
) =>
410
Effect.sync(() => {
411
if (
···
426
427
export const setupUbuntuArgs = (
428
imagePath?: string | null,
429
+
seed: string = "seed.iso",
430
) =>
431
Effect.sync(() => {
432
if (
···
447
448
export const setupAlmaLinuxArgs = (
449
imagePath?: string | null,
450
+
seed: string = "seed.iso",
451
) =>
452
Effect.sync(() => {
453
if (
···
468
469
export const setupRockyLinuxArgs = (
470
imagePath?: string | null,
471
+
seed: string = "seed.iso",
472
) =>
473
Effect.sync(() => {
474
if (
···
491
Effect.gen(function* () {
492
const macAddress = yield* generateRandomMacAddress();
493
494
+
const qemu = Deno.build.arch === "aarch64"
495
+
? "qemu-system-aarch64"
496
+
: "qemu-system-x86_64";
0
497
498
const firmwareFiles = yield* setupFirmwareFilesIfNeeded();
499
let coreosArgs: string[] = yield* setupCoreOSArgs(isoPath || options.image);
500
let fedoraArgs: string[] = yield* setupFedoraArgs(
501
isoPath || options.image,
502
+
options.seed,
503
);
504
let gentooArgs: string[] = yield* setupGentooArgs(
505
isoPath || options.image,
506
+
options.seed,
507
);
508
let alpineArgs: string[] = yield* setupAlpineArgs(
509
isoPath || options.image,
510
+
options.seed,
511
);
512
let debianArgs: string[] = yield* setupDebianArgs(
513
isoPath || options.image,
514
+
options.seed,
515
);
516
let ubuntuArgs: string[] = yield* setupUbuntuArgs(
517
isoPath || options.image,
518
+
options.seed,
519
);
520
let almalinuxArgs: string[] = yield* setupAlmaLinuxArgs(
521
isoPath || options.image,
522
+
options.seed,
523
);
524
let rockylinuxArgs: string[] = yield* setupRockyLinuxArgs(
525
isoPath || options.image,
526
+
options.seed,
527
);
528
529
if (coreosArgs.length > 0 && !isoPath) {
···
596
options.image && [
597
"-drive",
598
`file=${options.image},format=${options.diskFormat},if=virtio`,
599
+
],
600
),
601
];
602
···
611
const logPath = `${LOGS_DIR}/${name}.log`;
612
613
const fullCommand = options.bridge
614
+
? `sudo ${qemu} ${
615
+
qemuArgs
616
.slice(1)
617
+
.join(" ")
618
+
} >> "${logPath}" 2>&1 & echo $!`
619
: `${qemu} ${qemuArgs.join(" ")} >> "${logPath}" 2>&1 & echo $!`;
620
621
const { stdout } = yield* Effect.tryPromise({
···
641
cpus: options.cpus,
642
cpu: options.cpu,
643
diskSize: options.size || "20G",
644
+
diskFormat: (isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) ||
0
645
options.diskFormat ||
646
"raw",
647
portForward: options.portForward,
···
661
});
662
663
console.log(
664
+
`Virtual machine ${name} started in background (PID: ${qemuPid})`,
665
);
666
console.log(`Logs will be written to: ${logPath}`);
667
···
684
cpus: options.cpus,
685
cpu: options.cpu,
686
diskSize: options.size || "20G",
687
+
diskFormat: (isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) ||
0
688
options.diskFormat ||
689
"raw",
690
portForward: options.portForward,
···
793
if (pathExists) {
794
console.log(
795
chalk.yellowBright(
796
+
`Drive image ${path} already exists, skipping creation.`,
797
+
),
798
);
799
return;
800
}
···
821
});
822
823
export const fileExists = (
824
+
path: string,
825
): Effect.Effect<void, NoSuchFileError, never> =>
826
Effect.try({
827
try: () => Deno.statSync(path),
···
829
});
830
831
export const constructCoreOSImageURL = (
832
+
image: string,
833
): Effect.Effect<string, InvalidImageNameError, never> => {
834
// detect with regex if image matches coreos pattern: fedora-coreos or fedora-coreos-<version> or coreos or coreos-<version>
835
const coreosRegex = /^(fedora-coreos|coreos)(-(\d+\.\d+\.\d+\.\d+))?$/;
···
837
if (match) {
838
const version = match[3] || FEDORA_COREOS_DEFAULT_VERSION;
839
return Effect.succeed(
840
+
FEDORA_COREOS_IMG_URL.replaceAll(FEDORA_COREOS_DEFAULT_VERSION, version),
841
);
842
}
843
···
845
new InvalidImageNameError({
846
image,
847
cause: "Image name does not match CoreOS naming conventions.",
848
+
}),
849
);
850
};
851
···
874
});
875
876
export const constructNixOSImageURL = (
877
+
image: string,
878
): Effect.Effect<string, InvalidImageNameError, never> => {
879
// detect with regex if image matches NixOS pattern: nixos or nixos-<version>
880
const nixosRegex = /^(nixos)(-(\d+\.\d+))?$/;
···
882
if (match) {
883
const version = match[3] || NIXOS_DEFAULT_VERSION;
884
return Effect.succeed(
885
+
NIXOS_ISO_URL.replaceAll(NIXOS_DEFAULT_VERSION, version),
886
);
887
}
888
···
890
new InvalidImageNameError({
891
image,
892
cause: "Image name does not match NixOS naming conventions.",
893
+
}),
894
);
895
};
896
897
export const constructFedoraImageURL = (
898
image: string,
899
+
cloud: boolean = false,
900
): Effect.Effect<string, InvalidImageNameError, never> => {
901
// detect with regex if image matches Fedora pattern: fedora
902
const fedoraRegex = /^(fedora)$/;
···
909
new InvalidImageNameError({
910
image,
911
cause: "Image name does not match Fedora naming conventions.",
912
+
}),
913
);
914
};
915
916
export const constructGentooImageURL = (
917
+
image: string,
918
): Effect.Effect<string, InvalidImageNameError, never> => {
919
// detect with regex if image matches genroo pattern: gentoo-20251116T161545Z or gentoo
920
const gentooRegex = /^(gentoo)(-(\d{8}T\d{6}Z))?$/;
···
923
return Effect.succeed(
924
GENTOO_IMG_URL.replaceAll("20251116T161545Z", match[3]).replaceAll(
925
"20251116T233105Z",
926
+
match[3],
927
+
),
928
);
929
}
930
···
936
new InvalidImageNameError({
937
image,
938
cause: "Image name does not match Gentoo naming conventions.",
939
+
}),
940
);
941
};
942
943
export const constructDebianImageURL = (
944
image: string,
945
+
cloud: boolean = false,
946
): Effect.Effect<string, InvalidImageNameError, never> => {
947
if (cloud && image === "debian") {
948
return Effect.succeed(DEBIAN_CLOUD_IMG_URL);
···
953
const match = image.match(debianRegex);
954
if (match?.[3]) {
955
return Effect.succeed(
956
+
DEBIAN_ISO_URL.replaceAll(DEBIAN_DEFAULT_VERSION, match[3]),
957
);
958
}
959
···
965
new InvalidImageNameError({
966
image,
967
cause: "Image name does not match Debian naming conventions.",
968
+
}),
969
);
970
};
971
972
export const constructAlpineImageURL = (
973
+
image: string,
974
): Effect.Effect<string, InvalidImageNameError, never> => {
975
// detect with regex if image matches alpine pattern: alpine-<version> or alpine
976
const alpineRegex = /^(alpine)(-(\d+\.\d+(\.\d+)?))?$/;
977
const match = image.match(alpineRegex);
978
if (match?.[3]) {
979
return Effect.succeed(
980
+
ALPINE_ISO_URL.replaceAll(ALPINE_DEFAULT_VERSION, match[3]),
981
);
982
}
983
···
989
new InvalidImageNameError({
990
image,
991
cause: "Image name does not match Alpine naming conventions.",
992
+
}),
993
);
994
};
995
996
export const constructUbuntuImageURL = (
997
image: string,
998
+
cloud: boolean = false,
999
): Effect.Effect<string, InvalidImageNameError, never> => {
1000
// detect with regex if image matches ubuntu pattern: ubuntu
1001
const ubuntuRegex = /^(ubuntu)$/;
···
1011
new InvalidImageNameError({
1012
image,
1013
cause: "Image name does not match Ubuntu naming conventions.",
1014
+
}),
1015
);
1016
};
1017
1018
export const constructAlmaLinuxImageURL = (
1019
image: string,
1020
+
cloud: boolean = false,
1021
): Effect.Effect<string, InvalidImageNameError, never> => {
1022
// detect with regex if image matches almalinux pattern: ubuntu
1023
const almaLinuxRegex = /^(almalinux)$/;
···
1033
new InvalidImageNameError({
1034
image,
1035
cause: "Image name does not match AlmaLinux naming conventions.",
1036
+
}),
1037
);
1038
};
1039
1040
export const constructRockyLinuxImageURL = (
1041
image: string,
1042
+
cloud: boolean = false,
1043
): Effect.Effect<string, InvalidImageNameError, never> => {
1044
// detect with regex if image matches rockylinux pattern: ubuntu
1045
const rockyLinuxRegex = /^(rockylinux)$/;
···
1055
new InvalidImageNameError({
1056
image,
1057
cause: "Image name does not match RockyLinux naming conventions.",
1058
+
}),
1059
);
1060
};