Run node code on the venv Fluksio was installed into
The workflow this serves: make a venv, install what you work with, then `pip install fluksio` into the same one. Building a second environment beside it was exactly wrong — the packages the nodes need are already here, and the Modules screen was asking for them a second time. `NODE_VENV=auto` (the default) adopts that venv. It declines in the three cases where adopting would be wrong: `managed` says otherwise, a managed venv already exists and may hold packages somebody installed on purpose, or the engine is not running from a venv at all. The images set `managed`, since the venv in them holds the app and nothing of anybody else's. An adopted venv is never written to. `uv pip sync` makes a venv hold exactly the manifest, so pointed at somebody's own environment it uninstalls their work and the engine with it — `sync()` refuses outright and `reconcile()` returns before it can be called at startup, which is where that would have happened first. The Modules screen lists what is installed and drops its editor; `pip` is how that environment changes. `fluksio serve` now names the interpreter node code runs on, which is the thing a data scientist most needs to know at that moment. `fluksio-worker` already defaulted `--python` to its own interpreter, so a GPU box works the same way — that was only ever undocumented. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ue1tkFWB1bcGy3aWhCKpU
This commit is contained in:
@@ -1521,6 +1521,11 @@ export const ModulesInfoSchema = {
|
||||
type: 'boolean',
|
||||
title: 'Applied',
|
||||
default: false
|
||||
},
|
||||
adopted: {
|
||||
type: 'boolean',
|
||||
title: 'Adopted',
|
||||
default: false
|
||||
}
|
||||
},
|
||||
type: 'object',
|
||||
|
||||
@@ -583,6 +583,7 @@ export type ModulesInfo = {
|
||||
requirements?: string;
|
||||
packages?: Array<ModulePackage>;
|
||||
applied?: boolean;
|
||||
adopted?: boolean;
|
||||
};
|
||||
|
||||
export type NewPassword = {
|
||||
|
||||
@@ -62,64 +62,81 @@ function Modules() {
|
||||
const requirements = draft ?? stored
|
||||
const packages = data?.packages ?? []
|
||||
const dirty = requirements !== stored
|
||||
// Fluksio was installed into an environment somebody else made, and node
|
||||
// code runs on it. Installing from here would mean `uv pip sync`, which
|
||||
// holds a venv to exactly one list — and would uninstall their work.
|
||||
const adopted = data?.adopted ?? false
|
||||
|
||||
return (
|
||||
<div className="grid gap-6">
|
||||
<div className="grid gap-1">
|
||||
<h1 className="text-2xl">Modules</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
The Python packages your function nodes can import. They are installed
|
||||
into an environment of their own, separate from the engine's, so a
|
||||
version you pin here is the one your code gets. The list is kept with
|
||||
your flows, so a rebuilt deployment installs the same set again.
|
||||
{adopted
|
||||
? "The Python packages your function nodes can import. Fluksio was installed into an environment you already had, so that is the one your nodes run on — and it is yours to change, with pip or uv rather than from here."
|
||||
: "The Python packages your function nodes can import. They are installed into an environment of their own, separate from the engine's, so a version you pin here is the one your code gets. The list is kept with your flows, so a rebuilt deployment installs the same set again."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<section className="grid gap-3">
|
||||
<h2 className={SECTION}>Requirements</h2>
|
||||
<textarea
|
||||
className="min-h-[160px] w-full rounded-md border border-border bg-card p-3 font-mono text-sm shadow-e1 outline-none focus-visible:border-primary"
|
||||
spellCheck={false}
|
||||
aria-label="Requirements"
|
||||
data-testid="requirements"
|
||||
placeholder={"requests==2.32.3\npandas>=2.2"}
|
||||
value={requirements}
|
||||
onChange={(event) => setDraft(event.target.value)}
|
||||
/>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
disabled={apply.isPending}
|
||||
data-testid="apply-modules"
|
||||
onClick={() => apply.mutate(requirements)}
|
||||
>
|
||||
<Package />
|
||||
{apply.isPending ? "Installing" : "Apply"}
|
||||
</Button>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{dirty
|
||||
? "Not applied yet."
|
||||
: data?.applied
|
||||
? "Installed and in step."
|
||||
: "What is installed does not match this list."}
|
||||
</span>
|
||||
</div>
|
||||
{output ? (
|
||||
<pre
|
||||
className="max-h-64 overflow-auto rounded-md border border-border bg-card p-3 font-mono text-xs shadow-e1"
|
||||
data-testid="modules-output"
|
||||
>
|
||||
{output}
|
||||
</pre>
|
||||
) : null}
|
||||
</section>
|
||||
{adopted ? null : (
|
||||
<section className="grid gap-3">
|
||||
<h2 className={SECTION}>Requirements</h2>
|
||||
<textarea
|
||||
className="min-h-[160px] w-full rounded-md border border-border bg-card p-3 font-mono text-sm shadow-e1 outline-none focus-visible:border-primary"
|
||||
spellCheck={false}
|
||||
aria-label="Requirements"
|
||||
data-testid="requirements"
|
||||
placeholder={"requests==2.32.3\npandas>=2.2"}
|
||||
value={requirements}
|
||||
onChange={(event) => setDraft(event.target.value)}
|
||||
/>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
disabled={apply.isPending}
|
||||
data-testid="apply-modules"
|
||||
onClick={() => apply.mutate(requirements)}
|
||||
>
|
||||
<Package />
|
||||
{apply.isPending ? "Installing" : "Apply"}
|
||||
</Button>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{dirty
|
||||
? "Not applied yet."
|
||||
: data?.applied
|
||||
? "Installed and in step."
|
||||
: "What is installed does not match this list."}
|
||||
</span>
|
||||
</div>
|
||||
{output ? (
|
||||
<pre
|
||||
className="max-h-64 overflow-auto rounded-md border border-border bg-card p-3 font-mono text-xs shadow-e1"
|
||||
data-testid="modules-output"
|
||||
>
|
||||
{output}
|
||||
</pre>
|
||||
) : null}
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="grid gap-3">
|
||||
<h2 className={SECTION}>Installed</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-sm text-muted-foreground" data-testid="modules-venv">
|
||||
{data
|
||||
? `Python ${data.python_version || "unknown"} at ${data.venv_path}`
|
||||
: "Reading the environment…"}
|
||||
</p>
|
||||
{adopted ? (
|
||||
<p
|
||||
className="text-sm text-muted-foreground"
|
||||
data-testid="modules-adopted"
|
||||
>
|
||||
Adopted — this is the environment Fluksio itself is installed in.
|
||||
Add a package the way you added the rest:{" "}
|
||||
<span className="font-mono">pip install …</span>, then sync or
|
||||
restart so the workers pick it up. For a venv the engine owns
|
||||
instead, set <span className="font-mono">NODE_VENV=managed</span>.
|
||||
</p>
|
||||
) : null}
|
||||
{packages.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Nothing installed yet — node code has the standard library.
|
||||
|
||||
Reference in New Issue
Block a user