Skip to content

Install function dependencies independently of HOME#77

Open
alexellis wants to merge 1 commit into
masterfrom
uid-independent-deps
Open

Install function dependencies independently of HOME#77
alexellis wants to merge 1 commit into
masterfrom
uid-independent-deps

Conversation

@alexellis

Copy link
Copy Markdown
Member

The bug

A function with any dependency of its own crash-loops on a cluster installed with functions.setNonRootUser=true:

File "/home/app/function/handler.py", line 1, in <module>
    import requests
ModuleNotFoundError: No module named 'requests'

pip install --user resolves to $HOME/.local/..., which only works while the container runs as the user the image declares. setNonRootUser pins runAsUser: 12000, overriding USER app. A uid with no /etc/passwd entry gets HOME=/, so Python looks in /.local/... while pip installed into /home/app/.local/...:

as app:   HOME=/home/app  site=/home/app/.local/lib/python3.13/site-packages   <- installed here
as 12000: HOME=/          site=/.local/lib/python3.13/site-packages           <- looked here

It fails in the cluster only. local-run and docker run honour the image's own USER, so the image passes every local test and CI. The first sign of trouble is a CrashLoopBackOff. A Profile setting podSecurityContext.runAsUser, and OpenShift's per-namespace uid ranges, break it identically.

Reproduce without a cluster:

docker run --rm --user 12000 --entrypoint python <image> -c "import <pkg>"

The change

Install to a fixed path that no uid can move, and add it to the path — which is what the classic python3 template in templates-classic has always done, comment and all ("Allow any user-id for OpenShift users"):

- ENV PATH=$PATH:/home/app/.local/bin
+ ENV PATH=$PATH:/home/app/python/bin
+ ENV PYTHONPATH=/home/app/python
- RUN pip install --no-cache-dir --user -r requirements.txt
+ RUN pip install --no-cache-dir --target=/home/app/python -r requirements.txt

index.py also registers the directory with site.addsitedir(). That matters: .pth files are only processed for site directories, never for bare PYTHONPATH entries, so without it a dependency shipping a .pth would silently stop working for every build — a wider blast radius than the bug being fixed. Verified with setuptools, which ships distutils-precedence.pth.

Only the function's own requirements.txt changes. The template's own dependencies still go in as root, system-wide, untouched.

Who this affects

Fixed: clusters with setNonRootUser=true, any Profile pinning runAsUser, and OpenShift — where a function has at least one dependency. An empty requirements.txt masks the bug entirely, because the template's own deps are installed as root and survive.

No change: default installs (setNonRootUser defaults to false), and functions with no dependencies. Nobody needs to touch stack.yaml or handler code; the fix applies on the next faas-cli build. Deployed images are unaffected until rebuilt.

Testing

Adversarially reviewed, then verified per template. Built old vs new images and ran them:

old (--user) new (--target)
image's own USER ok ok
--user 12000 ModuleNotFoundError ok
--user 12100 ModuleNotFoundError ok
--user 65534 ModuleNotFoundError ok
--read-only --tmpfs /tmp fails at 12000 ok
.pth processed ok ok (via addsitedir)

Also checked: console scripts still land in bin/ and are on PATH; dependency shadowing is unchanged, with a function-pinned flask==2.2.5 still winning over the template's own copy at uid 12000; cryptography C extensions import correctly on alpine and debian and cross-build for arm64; python27-flask builds on pip 20 / Python 2.7 and the fix works there too; TEST_ENABLED=true tox/pytest/flake8 passes; image size unchanged. The function was run end to end under uid 12100 and served a request. The UndefinedVar buildkit warning is gone, since PYTHONPATH was previously prefixed with a $PYTHONPATH that is never set — which was putting an empty entry on sys.path.

Secondary improvement: importlib.metadata lookups now work at any uid; with --user they failed entirely under uid 12000 with No package metadata was found.

pip install --user resolves to $HOME/.local, which only works while the
container runs as the user the image declares. OpenFaaS can be installed
with functions.setNonRootUser=true, which pins runAsUser to 12000 and
overrides that USER. A uid with no passwd entry gets HOME=/, so the
packages are no longer on the import path and the function crash-loops:

  ModuleNotFoundError: No module named 'requests'

It fails in the cluster only. local-run and docker run both honour the
image's own USER, so the image passes every local test and CI, and the
first sign of trouble is a CrashLoopBackOff. A Profile setting
runAsUser, and OpenShift's per-namespace uid ranges, break it the same
way.

Install to /home/app/python instead, which no uid can move, and add it
to the path. This is what the classic python3 template has always done.
Dependency shadowing is unchanged: the directory still precedes
site-packages, so a function may pin its own version of something the
template also installs.

index.py registers the directory with site.addsitedir rather than
relying on PYTHONPATH alone, because .pth files are only processed for
site directories. Without it, a dependency shipping a .pth would stop
working for every build, not just the ones this change fixes.

Signed-off-by: Alex Ellis (OpenFaaS Ltd) <alexellis2@gmail.com>
@reviewfn

reviewfn Bot commented Jul 24, 2026

Copy link
Copy Markdown

AI Pull Request Overview

Disclaimer: This review was generated by automated AI and may contain errors. Do not trust its outputs without human verification.

Summary

  • The PR changes function dependency installation from $HOME-derived user site packages to a fixed /home/app/python target.
  • The fixed target is added to both PATH and PYTHONPATH across all scoped Python Flask and HTTP templates.
  • Each affected index.py registers /home/app/python as a site directory before importing template or function code, preserving .pth processing.
  • The change is consistent across Alpine, Debian, Python 3, and Python 2.7 templates in the scoped diff.
  • I found no concrete blocking correctness, security, migration, or compatibility issue in the final scoped diff.

Approval rating (1-10)

9/10. The fix is focused, consistent, and addresses the arbitrary-uid dependency lookup failure without introducing an evident regression.

Summary per file

Summary per file
File path Summary
template/python27-flask/Dockerfile Installs function dependencies into /home/app/python and exposes fixed binary/module paths.
template/python27-flask/index.py Registers the fixed dependency directory as a site directory before imports.
template/python3-flask-debian/Dockerfile Moves function dependency installs to fixed target path for Debian Flask template.
template/python3-flask-debian/index.py Registers fixed dependency path for .pth handling before template imports.
template/python3-flask/Dockerfile Moves Alpine Flask function dependencies from user site to fixed target path.
template/python3-flask/index.py Registers fixed dependency directory before Flask and handler imports.
template/python3-http-debian/Dockerfile Moves Debian HTTP template function dependencies to fixed target path.
template/python3-http-debian/index.py Registers fixed dependency directory after shebang and before imports.
template/python3-http/Dockerfile Moves Alpine HTTP template function dependencies to fixed target path.
template/python3-http/index.py Registers fixed dependency directory after shebang and before imports.

Overall Assessment

The change directly addresses the reported arbitrary-uid runtime failure by removing dependency discovery from $HOME and placing function-installed packages under a stable path. Adding /home/app/python/bin preserves console-script discovery, while PYTHONPATH plus site.addsitedir() covers normal imports and .pth processing. The implementation is applied consistently across the scoped templates and keeps template-owned dependency installation separate from function-owned dependency installation.

Detailed Review

Detailed Review

No findings.

The reviewed Dockerfile changes are narrowly scoped to the function dependency install step and matching runtime path environment. The index.py changes execute before flask, waitress, and user handler imports, which is the right point to make the fixed target available. The HTTP templates preserve the shebang as the first line, and the Python 2.7 template uses APIs compatible with that runtime.

AI agent details.

Agent processing time: 1m17.779s
Environment preparation time: 3.063s
Total time from webhook: 1m23.047s

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant