RFC-0030 phase 2: per-function env/envFrom on the poolmgr executor (+ fission/environments runtime support)
Scoping issue for RFC-0030 phase 2 — per-function Env/EnvFrom on the poolmgr executor. Parent epic: #3628. Design: docs/rfc/0030-function-env-vars-secret-configmap-mapping.md §3.
Phases 1, 3, 4 shipped (newdeploy/container native injection, MountPath files mode, run-local parity). Poolmgr is the one execution backend still gated off: today FunctionSpec.validateEnvForAdmission rejects it outright —
// pkg/apis/core/v1/validation.go:434-440
if et := spec.InvokeStrategy.ExecutionStrategy.ExecutorType; et == ExecutorTypePoolmgr || et == "" {
errs = errors.Join(errs, MakeValidationErr(ErrorInvalidObject, "FunctionSpec.Env", "",
"env/envFrom are not supported on the poolmgr executor yet (lands with RFC-0030 phase 2); use the newdeploy or container executor"))
}Phase 2 is a coordinated cross-repo change: the executor/fetcher wire work here in fission/fission, plus runtime support in fission/environments (python + node first). Neither half is useful alone. This issue captures the full technical approach and the two decisions that must be made before the runtime PRs start, because they shape the wire contract those PRs implement.
⛔ Decision 1 (blocking): how a runtime advertises env-injection support
The executor must refuse to build a specialize payload carrying Env/EnvFrom for a runtime image that does not apply it — otherwise the function deploys with a silently-empty DATABASE_URL (RFC-0030 §3, acceptance E5). Two options for the capability signal:
- (a) Bump
Environment.Spec.Versionto 4. Cheap, but a weak and wrong-shaped signal: it is user-declared, CEL-bounded1..3and immutable (self == oldSelf), so every unrelated runtime would have to "claim" a version it does not implement, and v1 environments receive an empty specialize body so the payload cannot ride v1 at all. - (b) An explicit capability the runtime advertises (queried once at specialize, cached). Cleaner, and it is the only option that also enables a fetcher-side defense-in-depth check (see §3 — the fetcher today receives only
EnvVersionand has no capability endpoint). Cost: a new contract withfission/environments(a/v2/getinfo-style endpoint or a field in the specialize response).
Recommendation: (b). The advertisement shape is an input to the python/node PRs, so this must land first. Proposed minimal shape: the runtime exposes its supported feature set (e.g. {"specializeEnv": true}) on an unauthenticated read-only /info endpoint OR echoes it in the /v2/specialize response; the executor caches per-image. Discuss the exact shape here before implementation.
⛔ Decision 2 (blocking): poolmgr Env scope under dynamic/cluster tenancy
The specialize channel must be authenticated before secret-bearing env rides it (below). The NetworkPolicy prerequisite renders into a single namespace (functionNamespace/defaultNamespace), but under dynamic/cluster tenancy function pods live in runtime-onboarded namespaces where the tenant controller provisions ServiceAccounts, RoleBindings and derived-key Secrets — but no NetworkPolicy (pkg/tenant/provision.go). So the prerequisite is unenforceable in exactly the multi-tenant mode whose threat model motivates it. Choose:
- (a) The tenant controller provisions the specialize-guard NetworkPolicy per onboarded namespace (preferred — same provisioning path it already runs), or
- (b) §3 declares poolmgr
Env/EnvFromunsupported under dynamic/cluster tenancy until (a) lands, and admission rejects it there.
Work item A — fission/fission executor + fetcher wire contract
The security-critical core (RFC-0030 §3). FunctionLoadRequest is built by the executor and, on newdeploy, marshalled into the fetcher container's command args — i.e. into the Deployment pod template, readable by anyone with get pods/deployments and stored in etcd. Therefore the value must never be executor-side:
- Executor-side payload carries references only — an
EnvSourcesreference list (non-secret) on the executor-built type. Never a resolved value. (pkg/fetcher/config,pkg/fetcher/types.go.) - Fetcher resolves values pod-locally on a distinct type that is not reachable from
FunctionSpecializeRequest(the pod-args marshal serializes the whole parent, so any field hung offLoadReqis exposed through it). Resolution is same-namespace by the fetcher's existing per-namespace RBAC. Values are populated immediately before the127.0.0.1/v2/specializePOST. - Executor-side refuse-to-build capability enforcement (Decision 1) — the authoritative E5 backstop, located executor-side because that is where the live
*fv1.Environmentis in hand (pkg/fetcher/configis imported by the executor, notcmd/fetcher). A fetcher-side check is not implementable on today's contract; it becomes possible as defense-in-depth only if Decision 1 picks (b). - Path-B
fetcherVariantpredicate extension — the OCI B-direct specialize path must carry the references too. - Reader-rebuild fix — the fetcher's specialize retry loop reuses a single
bytes.Reader; safe only because retries currently fire on dial errors before the body is consumed. Once the payload carries env references, rebuild the reader per attempt or a retry silently mis-specializes with an empty payload. - Flip the gate at
validation.go:438from always-reject to a capability check. - RFC-0025 classifier (
pkg/versioning/classifier.go): the auto-publish runtime-affectingdefaultreturnsfalse, so new top-levelEnv/EnvFromfields mint no version until explicitly added to the switch. Its coverage test forces the decision — name it as a choice.
Work item B — fission/environments runtime PRs (python + node first)
Each runtime applies the resolved env map to the process environment before importing user code (valid because specialization precedes user-code load) and advertises support per Decision 1.
- python —
python/server.py,_load_v2(self, specialize_info)(currently readsfilepath/functionNameandimportlib.import_modules the user module): read the env map from the specialize body and apply toos.environbefore theimport_modulecall. Bumpenvconfig.jsonversion + capability signal. - nodejs —
nodejs/server.js,specializeV2(req, res)(currently resolvesfilepath/functionNamethenrequires the user module): apply the env map toprocess.envbefore loading the user module. Sameenvconfig.json/capability bump. - Tests: extend each runtime's
tests/local_test.shwith an env-injection case; regression case for E7 (a second/v2/specializeis already rejected by published runtimes — "Not a generic container" — so this is a regression test, not new runtime work). - Remaining runtimes (go, jvm, php, ruby, dotnet, …) follow in phase 4; python + node prove the contract.
Security prerequisites (land WITH the runtime contract)
- Authenticate the specialize channel. The env runtime's
:8888is pod-network-reachable, and the executor's OCI B-direct call (pkg/executor/executortype/poolmgr/oci_specialize.go) POSTs/v2/specializeover plain HTTP with no HMAC signer (unlike the fetcher'sServiceFetcher-verified:8000/specialize). Sign the B-direct path and/or requirenetworkPolicy.enabled: trueas a documented prerequisite (default isfalse). - One-shot specialize is the backstop, and already holds in published runtimes — no cross-repo change needed, covered as an E7 regression test.
Acceptance criteria (from RFC-0030, gate phase 2)
- E3 — no resolved secret value ever appears in executor logs, pod annotations/args, or k8s events. Integration grep-gate over the five serialization/construction sites: pod-args marshal (
pkg/fetcher/config), the fetcher client signed POST (pkg/fetcher/client), the B-direct load-only marshal (poolmgr/oci_specialize.go), and the two run-local constructors (pkg/fission-cli/cmd/function/run.go,run_local.go). - E5 — a new-field Function against an old runtime is never deployed-and-empty: rejected at admission where the Environment is visible, and refused by the executor when it builds the request where it is not. Capability-gate unit matrix + a test mutating the Environment after function admission + an integration case pinning an old env image.
- E7 — replay: a second specialize on a specialized pod is rejected (regression test).
Open question (sequencing)
Does the one-shot-specialize / channel-auth hardening belong to this RFC or a standalone hardening PR? It fixes a pre-existing replayable /v2/specialize surface; coupling this feature to a cross-repo runtime change may be the wrong coupling — but shipping poolmgr Env without it turns that surface into a credential-theft primitive. Proposed: split the channel-auth hardening (B-direct signing + one-shot verification) into its own PR that merges first, so Env lands on an already-authenticated channel.
Not release-blocking
The poolmgr gate rejects cleanly today; newdeploy and container already carry the full feature. This is an additive extension with a cross-repo dependency and two design decisions — it should be planned as its own effort, not squeezed into a release cut.
Source: fission/fission