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
4 months ago
f9d367e6
1c90bb5b
2/2
fmt.yml
success
2s
test.yml
success
5s
+138
-133
4 changed files
expand all
collapse all
unified
split
src
subcommands
restart.ts
run.ts
start.ts
utils.ts
+28
-24
src/subcommands/restart.ts
reviewed
···
29
29
getInstanceState(name),
30
30
Effect.flatMap((vm) =>
31
31
vm ? Effect.succeed(vm) : Effect.fail(new VmNotFoundError({ name }))
32
32
-
)
32
32
+
),
33
33
);
34
34
35
35
const killQemu = (vm: VirtualMachine) =>
···
38
38
success
39
39
? Effect.succeed(vm)
40
40
: Effect.fail(new KillQemuError({ vmName: vm.name }))
41
41
-
)
41
41
+
),
42
42
);
43
43
44
44
const sleep = (ms: number) =>
···
56
56
const setupFirmware = () => setupFirmwareFilesIfNeeded();
57
57
58
58
const buildQemuArgs = (vm: VirtualMachine, firmwareArgs: string[]) => {
59
59
-
const qemu =
60
60
-
Deno.build.arch === "aarch64"
61
61
-
? "qemu-system-aarch64"
62
62
-
: "qemu-system-x86_64";
59
59
+
const qemu = Deno.build.arch === "aarch64"
60
60
+
? "qemu-system-aarch64"
61
61
+
: "qemu-system-x86_64";
63
62
64
63
let coreosArgs: string[] = Effect.runSync(setupCoreOSArgs(vm.drivePath));
65
64
let alpineArgs: string[] = Effect.runSync(setupAlpineArgs(vm.isoPath));
···
101
100
vm.drivePath && [
102
101
"-drive",
103
102
`file=${vm.drivePath},format=${vm.diskFormat},if=virtio`,
104
104
-
]
103
103
+
],
105
104
),
106
105
...coreosArgs,
107
106
...alpineArgs,
···
109
108
};
110
109
111
110
const startQemu = (vm: VirtualMachine, qemuArgs: string[]) => {
112
112
-
const qemu =
113
113
-
Deno.build.arch === "aarch64"
114
114
-
? "qemu-system-aarch64"
115
115
-
: "qemu-system-x86_64";
111
111
+
const qemu = Deno.build.arch === "aarch64"
112
112
+
? "qemu-system-aarch64"
113
113
+
: "qemu-system-x86_64";
116
114
117
115
const logPath = `${LOGS_DIR}/${vm.name}.log`;
118
116
119
117
const fullCommand = vm.bridge
120
120
-
? `sudo ${qemu} ${qemuArgs
118
118
+
? `sudo ${qemu} ${
119
119
+
qemuArgs
121
120
.slice(1)
122
122
-
.join(" ")} >> "${logPath}" 2>&1 & echo $!`
121
121
+
.join(" ")
122
122
+
} >> "${logPath}" 2>&1 & echo $!`
123
123
: `${qemu} ${qemuArgs.join(" ")} >> "${logPath}" 2>&1 & echo $!`;
124
124
125
125
return Effect.tryPromise({
···
141
141
const logSuccess = (vm: VirtualMachine, qemuPid: number, logPath: string) =>
142
142
Effect.sync(() => {
143
143
console.log(
144
144
-
`${chalk.greenBright(vm.name)} restarted with PID ${chalk.greenBright(
145
145
-
qemuPid
146
146
-
)}.`
144
144
+
`${chalk.greenBright(vm.name)} restarted with PID ${
145
145
+
chalk.greenBright(
146
146
+
qemuPid,
147
147
+
)
148
148
+
}.`,
147
149
);
148
150
console.log(`Logs are being written to ${chalk.blueBright(logPath)}`);
149
151
});
150
152
151
153
const handleError = (
152
152
-
error: VmNotFoundError | KillQemuError | CommandError | Error
154
154
+
error: VmNotFoundError | KillQemuError | CommandError | Error,
153
155
) =>
154
156
Effect.sync(() => {
155
157
if (error instanceof VmNotFoundError) {
156
158
console.error(
157
157
-
`Virtual machine with name or ID ${chalk.greenBright(
158
158
-
error.name
159
159
-
)} not found.`
159
159
+
`Virtual machine with name or ID ${
160
160
+
chalk.greenBright(
161
161
+
error.name,
162
162
+
)
163
163
+
} not found.`,
160
164
);
161
165
} else if (error instanceof KillQemuError) {
162
166
console.error(
163
163
-
`Failed to stop virtual machine ${chalk.greenBright(error.vmName)}.`
167
167
+
`Failed to stop virtual machine ${chalk.greenBright(error.vmName)}.`,
164
168
);
165
169
} else {
166
170
console.error(`An error occurred: ${error}`);
···
186
190
pipe(
187
191
updateInstanceState(vm.id, "RUNNING", qemuPid),
188
192
Effect.flatMap(() => logSuccess(vm, qemuPid, logPath)),
189
189
-
Effect.flatMap(() => sleep(2000))
193
193
+
Effect.flatMap(() => sleep(2000)),
190
194
)
191
191
-
)
195
195
+
),
192
196
)
193
197
),
194
194
-
Effect.catchAll(handleError)
198
198
+
Effect.catchAll(handleError),
195
199
);
196
200
197
201
export default async function (name: string) {
+14
-14
src/subcommands/run.ts
reviewed
···
22
22
pulledImg
23
23
? Effect.succeed(pulledImg)
24
24
: Effect.fail(new PullImageError({ cause: "Failed to pull image" }))
25
25
-
)
25
25
+
),
26
26
);
27
27
-
})
27
27
+
}),
28
28
);
29
29
30
30
const createVolumeIfNeeded = (
31
31
-
image: Image
31
31
+
image: Image,
32
32
): Effect.Effect<[Image, Volume?], Error, never> =>
33
33
parseFlags(Deno.args).flags.volume
34
34
? Effect.gen(function* () {
35
35
-
const size = parseFlags(Deno.args).flags.size as string | undefined;
36
36
-
const volumeName = parseFlags(Deno.args).flags.volume as string;
37
37
-
const volume = yield* getVolume(volumeName);
38
38
-
if (volume) {
39
39
-
return [image, volume];
40
40
-
}
41
41
-
const newVolume = yield* createVolume(volumeName, image, size);
42
42
-
return [image, newVolume];
43
43
-
})
35
35
+
const size = parseFlags(Deno.args).flags.size as string | undefined;
36
36
+
const volumeName = parseFlags(Deno.args).flags.volume as string;
37
37
+
const volume = yield* getVolume(volumeName);
38
38
+
if (volume) {
39
39
+
return [image, volume];
40
40
+
}
41
41
+
const newVolume = yield* createVolume(volumeName, image, size);
42
42
+
return [image, newVolume];
43
43
+
})
44
44
: Effect.succeed([image]);
45
45
46
46
const runImage = ([image, volume]: [Image, Volume?]) =>
···
74
74
console.error(`Failed to run image: ${error.cause} ${image}`);
75
75
Deno.exit(1);
76
76
})
77
77
-
)
78
78
-
)
77
77
+
),
78
78
+
),
79
79
);
80
80
}
81
81
+46
-44
src/subcommands/start.ts
reviewed
···
18
18
}> {}
19
19
20
20
export class VmAlreadyRunningError extends Data.TaggedError(
21
21
-
"VmAlreadyRunningError"
21
21
+
"VmAlreadyRunningError",
22
22
)<{
23
23
name: string;
24
24
}> {}
···
32
32
getInstanceState(name),
33
33
Effect.flatMap((vm) =>
34
34
vm ? Effect.succeed(vm) : Effect.fail(new VmNotFoundError({ name }))
35
35
-
)
35
35
+
),
36
36
);
37
37
38
38
const logStarting = (vm: VirtualMachine) =>
···
45
45
export const setupFirmware = () => setupFirmwareFilesIfNeeded();
46
46
47
47
export const buildQemuArgs = (vm: VirtualMachine, firmwareArgs: string[]) => {
48
48
-
const qemu =
49
49
-
Deno.build.arch === "aarch64"
50
50
-
? "qemu-system-aarch64"
51
51
-
: "qemu-system-x86_64";
48
48
+
const qemu = Deno.build.arch === "aarch64"
49
49
+
? "qemu-system-aarch64"
50
50
+
: "qemu-system-x86_64";
52
51
53
52
let coreosArgs: string[] = Effect.runSync(setupCoreOSArgs(vm.drivePath));
54
53
let alpineArgs: string[] = Effect.runSync(setupAlpineArgs(vm.isoPath));
···
90
89
vm.drivePath && [
91
90
"-drive",
92
91
`file=${vm.drivePath},format=${vm.diskFormat},if=virtio`,
93
93
-
]
92
92
+
],
94
93
),
95
94
...coreosArgs,
96
95
...alpineArgs,
···
107
106
export const startDetachedQemu = (
108
107
name: string,
109
108
vm: VirtualMachine,
110
110
-
qemuArgs: string[]
109
109
+
qemuArgs: string[],
111
110
) => {
112
112
-
const qemu =
113
113
-
Deno.build.arch === "aarch64"
114
114
-
? "qemu-system-aarch64"
115
115
-
: "qemu-system-x86_64";
111
111
+
const qemu = Deno.build.arch === "aarch64"
112
112
+
? "qemu-system-aarch64"
113
113
+
: "qemu-system-x86_64";
116
114
117
115
const logPath = `${LOGS_DIR}/${vm.name}.log`;
118
116
119
117
const fullCommand = vm.bridge
120
120
-
? `sudo ${qemu} ${qemuArgs
118
118
+
? `sudo ${qemu} ${
119
119
+
qemuArgs
121
120
.slice(1)
122
122
-
.join(" ")} >> "${logPath}" 2>&1 & echo $!`
121
121
+
.join(" ")
122
122
+
} >> "${logPath}" 2>&1 & echo $!`
123
123
: `${qemu} ${qemuArgs.join(" ")} >> "${logPath}" 2>&1 & echo $!`;
124
124
125
125
return Effect.tryPromise({
···
150
150
Effect.flatMap(({ qemuPid, logPath }) =>
151
151
pipe(
152
152
updateInstanceState(name, "RUNNING", qemuPid),
153
153
-
Effect.map(() => ({ vm, qemuPid, logPath }))
153
153
+
Effect.map(() => ({ vm, qemuPid, logPath })),
154
154
)
155
155
-
)
155
155
+
),
156
156
);
157
157
};
158
158
···
167
167
}) =>
168
168
Effect.sync(() => {
169
169
console.log(
170
170
-
`Virtual machine ${vm.name} started in background (PID: ${qemuPid})`
170
170
+
`Virtual machine ${vm.name} started in background (PID: ${qemuPid})`,
171
171
);
172
172
console.log(`Logs will be written to: ${logPath}`);
173
173
});
···
175
175
const startInteractiveQemu = (
176
176
name: string,
177
177
vm: VirtualMachine,
178
178
-
qemuArgs: string[]
178
178
+
qemuArgs: string[],
179
179
) => {
180
180
-
const qemu =
181
181
-
Deno.build.arch === "aarch64"
182
182
-
? "qemu-system-aarch64"
183
183
-
: "qemu-system-x86_64";
180
180
+
const qemu = Deno.build.arch === "aarch64"
181
181
+
? "qemu-system-aarch64"
182
182
+
: "qemu-system-x86_64";
184
183
185
184
return Effect.tryPromise({
186
185
try: async () => {
···
216
215
});
217
216
218
217
export const createVolumeIfNeeded = (
219
219
-
vm: VirtualMachine
218
218
+
vm: VirtualMachine,
220
219
): Effect.Effect<[VirtualMachine, Volume?], Error, never> =>
221
220
Effect.gen(function* () {
222
221
const { flags } = parseFlags(Deno.args);
···
231
230
232
231
if (!vm.drivePath) {
233
232
throw new Error(
234
234
-
`Cannot create volume: Virtual machine ${vm.name} has no drivePath defined.`
233
233
+
`Cannot create volume: Virtual machine ${vm.name} has no drivePath defined.`,
235
234
);
236
235
}
237
236
···
274
273
diskFormat: volume ? "qcow2" : vm.diskFormat,
275
274
volume: volume?.path,
276
275
},
277
277
-
firmwareArgs
276
276
+
firmwareArgs,
278
277
)
279
278
),
280
279
Effect.flatMap((qemuArgs) =>
···
284
283
startDetachedQemu(name, { ...vm, volume: volume?.path }, qemuArgs)
285
284
),
286
285
Effect.tap(logDetachedSuccess),
287
287
-
Effect.map(() => 0) // Exit code 0
286
286
+
Effect.map(() => 0), // Exit code 0
288
287
)
289
289
-
)
288
288
+
),
290
289
)
291
290
),
292
292
-
Effect.catchAll(handleError)
291
291
+
Effect.catchAll(handleError),
293
292
);
294
293
295
294
const startInteractiveEffect = (name: string) =>
···
310
309
diskFormat: volume ? "qcow2" : vm.diskFormat,
311
310
volume: volume?.path,
312
311
},
313
313
-
firmwareArgs
312
312
+
firmwareArgs,
314
313
)
315
314
),
316
315
Effect.flatMap((qemuArgs) =>
317
316
startInteractiveQemu(name, { ...vm, volume: volume?.path }, qemuArgs)
318
317
),
319
319
-
Effect.map((status) => (status.success ? 0 : status.code || 1))
318
318
+
Effect.map((status) => (status.success ? 0 : status.code || 1)),
320
319
)
321
320
),
322
322
-
Effect.catchAll(handleError)
321
321
+
Effect.catchAll(handleError),
323
322
);
324
323
325
324
export default async function (name: string, detach: boolean = false) {
326
325
const exitCode = await Effect.runPromise(
327
327
-
detach ? startDetachedEffect(name) : startInteractiveEffect(name)
326
326
+
detach ? startDetachedEffect(name) : startInteractiveEffect(name),
328
327
);
329
328
330
329
if (detach) {
···
338
337
const { flags } = parseFlags(Deno.args);
339
338
return {
340
339
...vm,
341
341
-
memory:
342
342
-
flags.memory || flags.m ? String(flags.memory || flags.m) : vm.memory,
340
340
+
memory: flags.memory || flags.m
341
341
+
? String(flags.memory || flags.m)
342
342
+
: vm.memory,
343
343
cpus: flags.cpus || flags.C ? Number(flags.cpus || flags.C) : vm.cpus,
344
344
cpu: flags.cpu || flags.c ? String(flags.cpu || flags.c) : vm.cpu,
345
345
diskFormat: flags.diskFormat ? String(flags.diskFormat) : vm.diskFormat,
346
346
-
portForward:
347
347
-
flags.portForward || flags.p
348
348
-
? String(flags.portForward || flags.p)
349
349
-
: vm.portForward,
350
350
-
drivePath:
351
351
-
flags.image || flags.i ? String(flags.image || flags.i) : vm.drivePath,
352
352
-
bridge:
353
353
-
flags.bridge || flags.b ? String(flags.bridge || flags.b) : vm.bridge,
354
354
-
diskSize:
355
355
-
flags.size || flags.s ? String(flags.size || flags.s) : vm.diskSize,
346
346
+
portForward: flags.portForward || flags.p
347
347
+
? String(flags.portForward || flags.p)
348
348
+
: vm.portForward,
349
349
+
drivePath: flags.image || flags.i
350
350
+
? String(flags.image || flags.i)
351
351
+
: vm.drivePath,
352
352
+
bridge: flags.bridge || flags.b
353
353
+
? String(flags.bridge || flags.b)
354
354
+
: vm.bridge,
355
355
+
diskSize: flags.size || flags.s
356
356
+
? String(flags.size || flags.s)
357
357
+
: vm.diskSize,
356
358
};
357
359
}
+50
-51
src/utils.ts
reviewed
···
70
70
export const isValidISOurl = (url?: string): boolean => {
71
71
return Boolean(
72
72
(url?.startsWith("http://") || url?.startsWith("https://")) &&
73
73
-
url?.endsWith(".iso")
73
73
+
url?.endsWith(".iso"),
74
74
);
75
75
};
76
76
···
96
96
});
97
97
98
98
export const validateImage = (
99
99
-
image: string
99
99
+
image: string,
100
100
): Effect.Effect<string, InvalidImageNameError, never> => {
101
101
const regex =
102
102
/^(?:[a-zA-Z0-9.-]+(?:\.[a-zA-Z0-9.-]+)*\/)?[a-z0-9]+(?:[._-][a-z0-9]+)*\/[a-z0-9]+(?:[._-][a-z0-9]+)*(?::[a-zA-Z0-9._-]+)?$/;
···
107
107
image,
108
108
cause:
109
109
"Image name does not conform to expected format. Should be in the format 'repository/name:tag'.",
110
110
-
})
110
110
+
}),
111
111
);
112
112
}
113
113
return Effect.succeed(image);
···
116
116
export const extractTag = (name: string) =>
117
117
pipe(
118
118
validateImage(name),
119
119
-
Effect.flatMap((image) => Effect.succeed(image.split(":")[1] || "latest"))
119
119
+
Effect.flatMap((image) => Effect.succeed(image.split(":")[1] || "latest")),
120
120
);
121
121
122
122
export const failOnMissingImage = (
123
123
-
image: Image | undefined
123
123
+
image: Image | undefined,
124
124
): Effect.Effect<Image, Error, never> =>
125
125
image
126
126
? Effect.succeed(image)
127
127
: Effect.fail(new NoSuchImageError({ cause: "No such image" }));
128
128
129
129
export const du = (
130
130
-
path: string
130
130
+
path: string,
131
131
): Effect.Effect<number, LogCommandError, never> =>
132
132
Effect.tryPromise({
133
133
try: async () => {
···
159
159
exists
160
160
? Effect.succeed(true)
161
161
: du(path).pipe(Effect.map((size) => size < EMPTY_DISK_THRESHOLD_KB))
162
162
-
)
162
162
+
),
163
163
);
164
164
165
165
export const downloadIso = (url: string, options: Options) =>
···
181
181
if (driveSize > EMPTY_DISK_THRESHOLD_KB) {
182
182
console.log(
183
183
chalk.yellowBright(
184
184
-
`Drive image ${options.image} is not empty (size: ${driveSize} KB), skipping ISO download to avoid overwriting existing data.`
185
185
-
)
184
184
+
`Drive image ${options.image} is not empty (size: ${driveSize} KB), skipping ISO download to avoid overwriting existing data.`,
185
185
+
),
186
186
);
187
187
return null;
188
188
}
···
200
200
if (outputExists) {
201
201
console.log(
202
202
chalk.yellowBright(
203
203
-
`File ${outputPath} already exists, skipping download.`
204
204
-
)
203
203
+
`File ${outputPath} already exists, skipping download.`,
204
204
+
),
205
205
);
206
206
return outputPath;
207
207
}
···
250
250
if (!success) {
251
251
console.error(
252
252
chalk.redBright(
253
253
-
"Failed to get QEMU prefix from Homebrew. Ensure QEMU is installed via Homebrew."
254
254
-
)
253
253
+
"Failed to get QEMU prefix from Homebrew. Ensure QEMU is installed via Homebrew.",
254
254
+
),
255
255
);
256
256
Deno.exit(1);
257
257
}
···
264
264
try: () =>
265
265
Deno.copyFile(
266
266
`${brewPrefix}/share/qemu/edk2-arm-vars.fd`,
267
267
-
edk2VarsAarch64
267
267
+
edk2VarsAarch64,
268
268
),
269
269
catch: (error) => new LogCommandError({ cause: error }),
270
270
});
···
309
309
const configOK = yield* pipe(
310
310
fileExists("config.ign"),
311
311
Effect.flatMap(() => Effect.succeed(true)),
312
312
-
Effect.catchAll(() => Effect.succeed(false))
312
312
+
Effect.catchAll(() => Effect.succeed(false)),
313
313
);
314
314
if (!configOK) {
315
315
console.error(
316
316
chalk.redBright(
317
317
-
"CoreOS image requires a config.ign file in the current directory."
318
318
-
)
317
317
+
"CoreOS image requires a config.ign file in the current directory.",
318
318
+
),
319
319
);
320
320
Deno.exit(1);
321
321
}
···
350
350
imagePath &&
351
351
imagePath.endsWith(".qcow2") &&
352
352
imagePath.startsWith(
353
353
-
`di-${Deno.build.arch === "aarch64" ? "arm64" : "amd64"}-console-`
353
353
+
`di-${Deno.build.arch === "aarch64" ? "arm64" : "amd64"}-console-`,
354
354
)
355
355
) {
356
356
return ["-drive", `file=${imagePath},format=qcow2,if=virtio`];
···
381
381
Effect.gen(function* () {
382
382
const macAddress = yield* generateRandomMacAddress();
383
383
384
384
-
const qemu =
385
385
-
Deno.build.arch === "aarch64"
386
386
-
? "qemu-system-aarch64"
387
387
-
: "qemu-system-x86_64";
384
384
+
const qemu = Deno.build.arch === "aarch64"
385
385
+
? "qemu-system-aarch64"
386
386
+
: "qemu-system-x86_64";
388
387
389
388
const firmwareFiles = yield* setupFirmwareFilesIfNeeded();
390
389
let coreosArgs: string[] = yield* setupCoreOSArgs(isoPath || options.image);
···
442
441
options.image && [
443
442
"-drive",
444
443
`file=${options.image},format=${options.diskFormat},if=virtio`,
445
445
-
]
444
444
+
],
446
445
),
447
446
];
448
447
···
457
456
const logPath = `${LOGS_DIR}/${name}.log`;
458
457
459
458
const fullCommand = options.bridge
460
460
-
? `sudo ${qemu} ${qemuArgs
459
459
+
? `sudo ${qemu} ${
460
460
+
qemuArgs
461
461
.slice(1)
462
462
-
.join(" ")} >> "${logPath}" 2>&1 & echo $!`
462
462
+
.join(" ")
463
463
+
} >> "${logPath}" 2>&1 & echo $!`
463
464
: `${qemu} ${qemuArgs.join(" ")} >> "${logPath}" 2>&1 & echo $!`;
464
465
465
466
const { stdout } = yield* Effect.tryPromise({
···
485
486
cpus: options.cpus,
486
487
cpu: options.cpu,
487
488
diskSize: options.size || "20G",
488
488
-
diskFormat:
489
489
-
(isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) ||
489
489
+
diskFormat: (isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) ||
490
490
options.diskFormat ||
491
491
"raw",
492
492
portForward: options.portForward,
···
505
505
});
506
506
507
507
console.log(
508
508
-
`Virtual machine ${name} started in background (PID: ${qemuPid})`
508
508
+
`Virtual machine ${name} started in background (PID: ${qemuPid})`,
509
509
);
510
510
console.log(`Logs will be written to: ${logPath}`);
511
511
···
528
528
cpus: options.cpus,
529
529
cpu: options.cpu,
530
530
diskSize: options.size || "20G",
531
531
-
diskFormat:
532
532
-
(isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) ||
531
531
+
diskFormat: (isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) ||
533
532
options.diskFormat ||
534
533
"raw",
535
534
portForward: options.portForward,
···
637
636
if (pathExists) {
638
637
console.log(
639
638
chalk.yellowBright(
640
640
-
`Drive image ${path} already exists, skipping creation.`
641
641
-
)
639
639
+
`Drive image ${path} already exists, skipping creation.`,
640
640
+
),
642
641
);
643
642
return;
644
643
}
···
665
664
});
666
665
667
666
export const fileExists = (
668
668
-
path: string
667
667
+
path: string,
669
668
): Effect.Effect<void, NoSuchFileError, never> =>
670
669
Effect.try({
671
670
try: () => Deno.statSync(path),
···
673
672
});
674
673
675
674
export const constructCoreOSImageURL = (
676
676
-
image: string
675
675
+
image: string,
677
676
): Effect.Effect<string, InvalidImageNameError, never> => {
678
677
// detect with regex if image matches coreos pattern: fedora-coreos or fedora-coreos-<version> or coreos or coreos-<version>
679
678
const coreosRegex = /^(fedora-coreos|coreos)(-(\d+\.\d+\.\d+\.\d+))?$/;
···
681
680
if (match) {
682
681
const version = match[3] || FEDORA_COREOS_DEFAULT_VERSION;
683
682
return Effect.succeed(
684
684
-
FEDORA_COREOS_IMG_URL.replaceAll(FEDORA_COREOS_DEFAULT_VERSION, version)
683
683
+
FEDORA_COREOS_IMG_URL.replaceAll(FEDORA_COREOS_DEFAULT_VERSION, version),
685
684
);
686
685
}
687
686
···
689
688
new InvalidImageNameError({
690
689
image,
691
690
cause: "Image name does not match CoreOS naming conventions.",
692
692
-
})
691
691
+
}),
693
692
);
694
693
};
695
694
···
718
717
});
719
718
720
719
export const constructNixOSImageURL = (
721
721
-
image: string
720
720
+
image: string,
722
721
): Effect.Effect<string, InvalidImageNameError, never> => {
723
722
// detect with regex if image matches NixOS pattern: nixos or nixos-<version>
724
723
const nixosRegex = /^(nixos)(-(\d+\.\d+))?$/;
···
726
725
if (match) {
727
726
const version = match[3] || NIXOS_DEFAULT_VERSION;
728
727
return Effect.succeed(
729
729
-
NIXOS_ISO_URL.replaceAll(NIXOS_DEFAULT_VERSION, version)
728
728
+
NIXOS_ISO_URL.replaceAll(NIXOS_DEFAULT_VERSION, version),
730
729
);
731
730
}
732
731
···
734
733
new InvalidImageNameError({
735
734
image,
736
735
cause: "Image name does not match NixOS naming conventions.",
737
737
-
})
736
736
+
}),
738
737
);
739
738
};
740
739
741
740
export const constructFedoraImageURL = (
742
742
-
image: string
741
741
+
image: string,
743
742
): Effect.Effect<string, InvalidImageNameError, never> => {
744
743
// detect with regex if image matches Fedora pattern: fedora
745
744
const fedoraRegex = /^(fedora)$/;
···
752
751
new InvalidImageNameError({
753
752
image,
754
753
cause: "Image name does not match Fedora naming conventions.",
755
755
-
})
754
754
+
}),
756
755
);
757
756
};
758
757
759
758
export const constructGentooImageURL = (
760
760
-
image: string
759
759
+
image: string,
761
760
): Effect.Effect<string, InvalidImageNameError, never> => {
762
761
// detect with regex if image matches genroo pattern: gentoo-20251116T161545Z or gentoo
763
762
const gentooRegex = /^(gentoo)(-(\d{8}T\d{6}Z))?$/;
···
766
765
return Effect.succeed(
767
766
GENTOO_IMG_URL.replaceAll("20251116T161545Z", match[3]).replaceAll(
768
767
"20251116T233105Z",
769
769
-
match[3]
770
770
-
)
768
768
+
match[3],
769
769
+
),
771
770
);
772
771
}
773
772
···
779
778
new InvalidImageNameError({
780
779
image,
781
780
cause: "Image name does not match Gentoo naming conventions.",
782
782
-
})
781
781
+
}),
783
782
);
784
783
};
785
784
786
785
export const constructDebianImageURL = (
787
787
-
image: string
786
786
+
image: string,
788
787
): Effect.Effect<string, InvalidImageNameError, never> => {
789
788
// detect with regex if image matches debian pattern: debian-<version> or debian
790
789
const debianRegex = /^(debian)(-(\d+\.\d+\.\d+))?$/;
791
790
const match = image.match(debianRegex);
792
791
if (match?.[3]) {
793
792
return Effect.succeed(
794
794
-
DEBIAN_ISO_URL.replaceAll(DEBIAN_DEFAULT_VERSION, match[3])
793
793
+
DEBIAN_ISO_URL.replaceAll(DEBIAN_DEFAULT_VERSION, match[3]),
795
794
);
796
795
}
797
796
···
803
802
new InvalidImageNameError({
804
803
image,
805
804
cause: "Image name does not match Debian naming conventions.",
806
806
-
})
805
805
+
}),
807
806
);
808
807
};
809
808
810
809
export const constructAlpineImageURL = (
811
811
-
image: string
810
810
+
image: string,
812
811
): Effect.Effect<string, InvalidImageNameError, never> => {
813
812
// detect with regex if image matches alpine pattern: alpine-<version> or alpine
814
813
const alpineRegex = /^(alpine)(-(\d+\.\d+(\.\d+)?))?$/;
815
814
const match = image.match(alpineRegex);
816
815
if (match?.[3]) {
817
816
return Effect.succeed(
818
818
-
ALPINE_ISO_URL.replaceAll(ALPINE_DEFAULT_VERSION, match[3])
817
817
+
ALPINE_ISO_URL.replaceAll(ALPINE_DEFAULT_VERSION, match[3]),
819
818
);
820
819
}
821
820
···
827
826
new InvalidImageNameError({
828
827
image,
829
828
cause: "Image name does not match Alpine naming conventions.",
830
830
-
})
829
829
+
}),
831
830
);
832
831
};