Merge pull request 'pkgs/clan/lib(install): implement separate nixos-anywhere install phases' (#4710) from ke-install-phases into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4710
This commit is contained in:
hsjobeki
2025-08-12 15:34:15 +00:00
8 changed files with 136 additions and 29 deletions

View File

@@ -4,7 +4,7 @@
&[data-expanded] { &[data-expanded] {
@apply outline-def-2 outline-1 outline; @apply outline-def-2 outline-1 outline;
z-index: var(--z-index + 5); z-index: calc(var(--z-index) + 5);
} }
&[data-highlighted] { &[data-highlighted] {

View File

@@ -146,6 +146,7 @@ export const Select = (props: SelectProps) => {
<KSelect.HiddenSelect {...selectProps} /> <KSelect.HiddenSelect {...selectProps} />
<KSelect.Trigger <KSelect.Trigger
class={cx(styles.trigger)} class={cx(styles.trigger)}
style={{ "--z-index": zIndex() }}
data-loading={loading() || undefined} data-loading={loading() || undefined}
> >
<KSelect.Value<Option>> <KSelect.Value<Option>>

View File

@@ -251,6 +251,10 @@ export const Onboarding: Component<RouteSectionProps> = (props) => {
}, },
}).result; }).result;
await client.fetch("create_secrets_user", {
flake_dir: path,
}).result;
if (resp.status === "error") { if (resp.status === "error") {
setWelcomeError(resp.errors[0].message); setWelcomeError(resp.errors[0].message);
setState("welcome"); setState("welcome");

View File

@@ -18,29 +18,64 @@ type ResultDataMap = {
[K in OperationNames]: SuccessQuery<K>["data"]; [K in OperationNames]: SuccessQuery<K>["data"];
}; };
export const mockFetcher: Fetcher = <K extends OperationNames>( const mockFetcher: Fetcher = <K extends OperationNames>(
name: K, name: K,
_args: unknown, _args: unknown,
): ApiCall<K> => { ): ApiCall<K> => {
// TODO: Make this configurable for every story // TODO: Make this configurable for every story
const resultData: Partial<ResultDataMap> = { const resultData: Partial<ResultDataMap> = {
get_machine_flash_options: {
keymaps: ["DE_de", "US_en"],
languages: ["en", "de"],
},
get_system_file: ["id_rsa.pub"], get_system_file: ["id_rsa.pub"],
list_system_storage_devices: { list_system_storage_devices: {
blockdevices: [ blockdevices: [
{ {
name: "sda_bla_bla", name: "sda_bla_bla",
path: "/dev/sda", path: "/dev/sda",
id_link: "sda_bla_bla",
}, },
{ {
name: "sdb_foo_foo", name: "sdb_foo_foo",
path: "/dev/sdb", path: "/dev/sdb",
id_link: "sdb_foo_foo",
}, },
] as SuccessQuery<"list_system_storage_devices">["data"]["blockdevices"], ] as SuccessQuery<"list_system_storage_devices">["data"]["blockdevices"],
}, },
get_machine_disk_schemas: {
"single-disk": {
readme: "This is a single disk installation schema",
frontmatter: {
description: "Single disk installation schema",
},
name: "single-disk",
placeholders: {
mainDisk: {
label: "Main Disk",
required: true,
options: ["disk1", "usb1"],
},
},
},
},
get_generators: [
{
name: "funny.gritty",
prompts: [
{
name: "gritty.name",
description: "Name of the gritty",
prompt_type: "line",
display: {
label: "Gritty Name",
group: "User",
required: true,
},
},
],
},
],
run_generators: true,
get_machine_hardware_summary: {
hardware_config: "nixos-facter",
},
}; };
return { return {

View File

@@ -7,7 +7,7 @@ import { StepLayout } from "../../Steps";
const ChoiceLocalOrRemote = () => { const ChoiceLocalOrRemote = () => {
const stepSignal = useStepper<InstallSteps>(); const stepSignal = useStepper<InstallSteps>();
return ( return (
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3 size-full">
<div class="flex flex-col gap-6 rounded-md px-4 py-6 text-fg-def-1 bg-def-2"> <div class="flex flex-col gap-6 rounded-md px-4 py-6 text-fg-def-1 bg-def-2">
<div class="flex justify-between gap-2"> <div class="flex justify-between gap-2">
<div class="flex flex-col justify-center gap-1 px-1"> <div class="flex flex-col justify-center gap-1 px-1">

View File

@@ -16,10 +16,7 @@ import { Alert } from "@/src/components/Alert/Alert";
import { LoadingBar } from "@/src/components/LoadingBar/LoadingBar"; import { LoadingBar } from "@/src/components/LoadingBar/LoadingBar";
import { Button } from "@/src/components/Button/Button"; import { Button } from "@/src/components/Button/Button";
import Icon from "@/src/components/Icon/Icon"; import Icon from "@/src/components/Icon/Icon";
import { import { useSystemStorageOptions } from "@/src/hooks/queries";
useMachineFlashOptions,
useSystemStorageOptions,
} from "@/src/hooks/queries";
import { useApiClient } from "@/src/hooks/ApiClient"; import { useApiClient } from "@/src/hooks/ApiClient";
import { onMount } from "solid-js"; import { onMount } from "solid-js";
@@ -144,8 +141,6 @@ const ConfigureImage = () => {
throw new Error("No data returned from api call"); throw new Error("No data returned from api call");
}; };
const optionsQuery = useMachineFlashOptions();
let content: Node; let content: Node;
return ( return (
@@ -309,15 +304,17 @@ const FlashProgress = () => {
}); });
const handleCancel = async () => { const handleCancel = async () => {
const progress = store.flash.progress; if (store.flash) {
if (progress) { const progress = store.flash.progress;
await progress.cancel(); if (progress) {
await progress.cancel();
}
} }
stepSignal.previous(); stepSignal.previous();
}; };
return ( return (
<div class="flex size-full h-60 flex-col items-center justify-end bg-inv-4"> <div class="flex size-full flex-col items-center justify-center bg-inv-4">
<div class="mb-6 flex w-full max-w-md flex-col items-center gap-3 fg-inv-1"> <div class="mb-6 flex w-full max-w-md flex-col items-center gap-3 fg-inv-1">
<Typography <Typography
hierarchy="title" hierarchy="title"
@@ -344,7 +341,7 @@ const FlashDone = () => {
const stepSignal = useStepper<InstallSteps>(); const stepSignal = useStepper<InstallSteps>();
return ( return (
<div class="flex size-full flex-col items-center justify-between bg-inv-4"> <div class="flex size-full flex-col items-center justify-between bg-inv-4">
<div class="flex w-full max-w-md flex-col items-center gap-3 py-6 fg-inv-1"> <div class="flex size-full max-w-md flex-col items-center justify-center gap-3 py-6 fg-inv-1">
<div class="rounded-full bg-semantic-success-4"> <div class="rounded-full bg-semantic-success-4">
<Icon icon="Checkmark" class="size-9" /> <Icon icon="Checkmark" class="size-9" />
</div> </div>

View File

@@ -70,7 +70,6 @@ const ConfigureAddress = () => {
// Here you would typically trigger the ISO creation process // Here you would typically trigger the ISO creation process
stepSignal.next(); stepSignal.next();
console.log("Shit doesnt work", values);
}; };
return ( return (
@@ -318,6 +317,25 @@ interface PromptForm extends FieldValues {
promptValues: PromptValues; promptValues: PromptValues;
} }
const sanitize = (name: string) => {
return name.replace(".", "__dot__");
};
const restore = (name: string) => {
return name.replace("__dot__", ".");
};
const transformPromptValues = (
values: PromptValues,
transform: (s: string) => string,
): PromptValues =>
Object.fromEntries(
Object.entries(values).map(([key, value]) => [
transform(key),
Object.fromEntries(
Object.entries(value).map(([k, v]) => [transform(k), v]),
),
]),
);
interface PromptsFieldsProps { interface PromptsFieldsProps {
generators: MachineGenerators; generators: MachineGenerators;
} }
@@ -338,8 +356,11 @@ const PromptsFields = (props: PromptsFieldsProps) => {
if (!acc[groupName]) acc[groupName] = { fields: [], name: groupName }; if (!acc[groupName]) acc[groupName] = { fields: [], name: groupName };
acc[groupName].fields.push({ acc[groupName].fields.push({
prompt, prompt: {
generator: generator.name, ...prompt,
name: sanitize(prompt.name),
},
generator: sanitize(generator.name),
value: prompt.previous_value, value: prompt.previous_value,
}); });
} }
@@ -351,16 +372,24 @@ const PromptsFields = (props: PromptsFieldsProps) => {
const [formStore, { Form, Field }] = createForm<PromptForm>({ const [formStore, { Form, Field }] = createForm<PromptForm>({
initialValues: { initialValues: {
promptValues: store.install?.promptValues || {}, promptValues: transformPromptValues(
store.install?.promptValues || {},
sanitize,
),
}, },
}); });
console.log(groups); console.log(groups);
const handleSubmit: SubmitHandler<PromptForm> = (values, event) => { const handleSubmit: SubmitHandler<PromptForm> = (values, event) => {
console.log("vars submitted", values); const restoredValues: PromptValues = transformPromptValues(
values.promptValues,
restore,
);
set("install", (s) => ({ ...s, promptValues: values.promptValues })); console.log("vars submitted", restoredValues);
set("install", (s) => ({ ...s, promptValues: restoredValues }));
console.log("vars preloaded"); console.log("vars preloaded");
// Here you would typically trigger the ISO creation process // Here you would typically trigger the ISO creation process
stepSignal.next(); stepSignal.next();
@@ -545,7 +574,14 @@ const InstallSummary = () => {
); );
}; };
type InstallTopic = "generators" | "upload-secrets" | "nixos-anywhere"; type InstallTopic = [
"generators",
"upload-secrets",
"nixos-anywhere",
"formatting",
"rebooting",
"installing",
][number];
const InstallProgress = () => { const InstallProgress = () => {
const stepSignal = useStepper<InstallSteps>(); const stepSignal = useStepper<InstallSteps>();
@@ -563,7 +599,7 @@ const InstallProgress = () => {
); );
return ( return (
<div class="flex size-full h-60 flex-col items-center justify-end bg-inv-4"> <div class="flex size-full flex-col items-center justify-center bg-inv-4">
<div class="mb-6 flex w-full max-w-md flex-col items-center gap-3 fg-inv-1"> <div class="mb-6 flex w-full max-w-md flex-col items-center gap-3 fg-inv-1">
<Typography <Typography
hierarchy="title" hierarchy="title"
@@ -599,6 +635,15 @@ const InstallProgress = () => {
<Match when={installState()?.topic === "nixos-anywhere"}> <Match when={installState()?.topic === "nixos-anywhere"}>
Running nixos-anywhere ... Running nixos-anywhere ...
</Match> </Match>
<Match when={installState()?.topic === "formatting"}>
Formatting ...
</Match>
<Match when={installState()?.topic === "installing"}>
Installing ...
</Match>
<Match when={installState()?.topic === "rebooting"}>
Rebooting ...
</Match>
</Switch> </Switch>
</Match> </Match>
</Switch> </Switch>
@@ -625,7 +670,7 @@ const InstallDone = (props: InstallDoneProps) => {
const [store, get] = getStepStore<InstallStoreType>(stepSignal); const [store, get] = getStepStore<InstallStoreType>(stepSignal);
return ( return (
<div class="flex w-full flex-col items-center bg-inv-4"> <div class="flex size-full flex-col items-center justify-center bg-inv-4">
<div class="flex w-full max-w-md flex-col items-center gap-3 py-6 fg-inv-1"> <div class="flex w-full max-w-md flex-col items-center gap-3 py-6 fg-inv-1">
<div class="rounded-full bg-semantic-success-4"> <div class="rounded-full bg-semantic-success-4">
<Icon icon="Checkmark" class="size-9" /> <Icon icon="Checkmark" class="size-9" />

View File

@@ -22,7 +22,14 @@ log = logging.getLogger(__name__)
BuildOn = Literal["auto", "local", "remote"] BuildOn = Literal["auto", "local", "remote"]
Step = Literal["generators", "upload-secrets", "nixos-anywhere"] Step = Literal[
"generators",
"upload-secrets",
"nixos-anywhere",
"formatting",
"rebooting",
"installing",
]
def notify_install_step(current: Step) -> None: def notify_install_step(current: Step) -> None:
@@ -195,4 +202,22 @@ def run_machine_install(opts: InstallOptions, target_host: Remote) -> None:
) )
notify_install_step("nixos-anywhere") notify_install_step("nixos-anywhere")
run(cmd, RunOpts(log=Log.BOTH, prefix=machine.name, needs_user_terminal=True)) run(
[*cmd, "--phases", "kexec"],
RunOpts(log=Log.BOTH, prefix=machine.name, needs_user_terminal=True),
)
notify_install_step("formatting")
run(
[*cmd, "--phases", "disko"],
RunOpts(log=Log.BOTH, prefix=machine.name, needs_user_terminal=True),
)
notify_install_step("installing")
run(
[*cmd, "--phases", "install"],
RunOpts(log=Log.BOTH, prefix=machine.name, needs_user_terminal=True),
)
notify_install_step("rebooting")
run(
[*cmd, "--phases", "reboot"],
RunOpts(log=Log.BOTH, prefix=machine.name, needs_user_terminal=True),
)