Skip to content

Commit a56f527

Browse files
authored
Merge pull request #6183: Add support for loading .pth files in st2 venv
2 parents 12639d6 + 1dab0d8 commit a56f527

File tree

7 files changed

+167
-56
lines changed

7 files changed

+167
-56
lines changed

.github/workflows/ci.yaml

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -253,13 +253,21 @@ jobs:
253253
run: |
254254
script -e -c "make .ci-prepare-integration" && exit 0
255255
- name: Extend the path for upcoming tasks
256-
run: |
257-
echo ${HOME}/work/st2/st2/virtualenv/bin
258-
echo ${HOME}/work/st2/st2/virtualenv/bin >> $GITHUB_PATH
256+
# pants uses PEP 660 editable wheels to add our code to the virtualenv.
257+
# But PEP 660 editable wheels do not include 'scripts'.
258+
# https://peps.python.org/pep-0660/#limitations
259+
# So, we need to include each bin dir in PATH instead of virtualenv/bin.
260+
run: |
261+
for component_bin in ${GITHUB_WORKSPACE}/st2*/bin; do
262+
echo ${component_bin} | tee -a $GITHUB_PATH
263+
done
264+
echo ${GITHUB_WORKSPACE}/virtualenv/bin | tee -a $GITHUB_PATH
259265
- name: Create symlinks to find the binaries when running st2 actions
266+
# st2 is actually a console_script entry point, not just a 'script'
267+
# so it IS included in the virtualenv. But, st2-run-pack-tests might not be included.
260268
run: |
261-
ln -s ${HOME}/work/st2/st2/virtualenv/bin/st2 /usr/local/bin/st2
262-
ln -s ${HOME}/work/st2/st2/virtualenv/bin/st2-run-pack-tests /usr/local/bin/st2-run-pack-tests
269+
ln -s ${GITHUB_WORKSPACE}/virtualenv/bin/st2 /usr/local/bin/st2
270+
ln -s ${GITHUB_WORKSPACE}/st2common/bin/st2-run-pack-tests /usr/local/bin/st2-run-pack-tests
263271
- name: Install st2client
264272
timeout-minutes: 5
265273
run: |
@@ -270,7 +278,7 @@ jobs:
270278
env:
271279
ST2_CONF: /home/runner/work/st2/st2/conf/st2.ci.conf
272280
run: |
273-
sudo -E ST2_AUTH_TOKEN=$(st2 auth testu -p 'testp' -t) PATH=${PATH} virtualenv/bin/st2-self-check
281+
sudo -E ST2_AUTH_TOKEN=$(st2 auth testu -p 'testp' -t) PATH=${PATH} st2common/bin/st2-self-check
274282
- name: Compress Service Logs Before upload
275283
if: ${{ failure() }}
276284
run: |

CHANGELOG.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ Added
2424
* Continue introducing `pants <https://www.pantsbuild.org/docs>`_ to improve DX (Developer Experience)
2525
working on StackStorm, improve our security posture, and improve CI reliability thanks in part
2626
to pants' use of PEX lockfiles. This is not a user-facing addition.
27-
#6118 #6141 #6133 #6120 #6181
27+
#6118 #6141 #6133 #6120 #6181 #6183
2828
Contributed by @cognifloyd
2929
* Build of ST2 EL9 packages #6153
3030
Contributed by @amanda11
31+
* Ensure `.pth` files in the st2 virtualenv get loaded by pack virtualenvs. #6183
32+
Contributed by @cognifloyd
3133

3234
3.8.1 - December 13, 2023
3335
-------------------------

st2common/bin/st2-run-pack-tests

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ ENABLE_TIMING=false
6363

6464
VIRTUALENV_ACTIVATED=false
6565

66-
STACKSTORM_VIRTUALENV_BIN="/opt/stackstorm/st2/bin"
66+
STACKSTORM_VIRTUALENV="/opt/stackstorm/st2"
67+
STACKSTORM_VIRTUALENV_BIN="${STACKSTORM_VIRTUALENV}/bin"
6768
STACKSTORM_VIRTUALENV_PYTHON_BINARY="${STACKSTORM_VIRTUALENV_BIN}/python"
6869

6970
####################
@@ -194,6 +195,13 @@ if [ "${CREATE_VIRTUALENV}" = true ]; then
194195
mkdir -p ${VIRTUALENVS_DIR}
195196
virtualenv --no-download --system-site-packages ${VIRTUALENV_DIR}
196197

198+
if [ -f "${STACKSTORM_VIRTUALENV_PYTHON_BINARY}" ]; then
199+
# ensure any .pth files in st2 venv get loaded with the pack venv too.
200+
ST2_SITE_PACKAGES=$(${STACKSTORM_VIRTUALENV_PYTHON_BINARY} -c "import sysconfig;print(sysconfig.get_path('platlib'))")
201+
PACK_SITE_PACKAGES=$(${VIRTUALENV_DIR}/bin/python3 -c "import sysconfig;print(sysconfig.get_path('platlib'))")
202+
echo "import sys; addsitedir('${ST2_SITE_PACKAGES}', known_paths)" > "${PACK_SITE_PACKAGES}/zzzzzzzzzz__st2__.pth"
203+
fi
204+
197205
# Activate the virtualenv
198206
activate_virtualenv
199207

@@ -346,16 +354,26 @@ if [ "${ENABLE_TIMING}" = true ]; then
346354
NOSE_OPTS+=(--with-timer)
347355
fi
348356

357+
NOSE=(nosetests)
358+
if head -n 1 $(command -v nosetests) | grep -q ' -sE$'; then
359+
# workaround pants+pex default of hermetic scripts so we can run nosetests with PYTHONPATH
360+
if [ -f "${STACKSTORM_VIRTUALENV_PYTHON_BINARY}" ]; then
361+
NOSE=(${STACKSTORM_VIRTUALENV_PYTHON_BINARY} -m "nose")
362+
else
363+
NOSE=(python3 -m "nose")
364+
fi
365+
fi
366+
349367
# Change to the pack's directory (required for test coverage reporting)
350368
pushd ${PACK_PATH} > /dev/null
351369

352370
# Execute the tests
353371
if [ "${TEST_LOCATION}" ]; then
354372
# Run a specific test file, class or method
355-
nosetests ${NOSE_OPTS[@]} ${TEST_LOCATION}
373+
${NOSE[@]} ${NOSE_OPTS[@]} ${TEST_LOCATION}
356374
else
357375
# Run all tests inside the pack
358-
nosetests ${NOSE_OPTS[@]} ${PACK_TESTS_PATH}
376+
${NOSE[@]} ${NOSE_OPTS[@]} ${PACK_TESTS_PATH}
359377
fi
360378
TESTS_EXIT_CODE=$?
361379

st2common/st2common/util/sandboxing.py

Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import os
2525
import sys
2626
from sysconfig import get_path
27+
from typing import Optional
2728

2829
from oslo_config import cfg
2930

@@ -32,20 +33,51 @@
3233
from st2common.content.utils import get_pack_base_path
3334

3435

35-
def get_python_lib():
36-
"""Replacement for distutil.sysconfig.get_python_lib, returns a string with the python platform lib path (to site-packages)"""
37-
return get_path("platlib")
38-
39-
4036
__all__ = [
37+
"get_site_packages_dir",
38+
"get_virtualenv_lib_path",
4139
"get_sandbox_python_binary_path",
4240
"get_sandbox_python_path",
4341
"get_sandbox_python_path_for_python_action",
4442
"get_sandbox_path",
4543
"get_sandbox_virtualenv_path",
44+
"is_in_virtualenv",
4645
]
4746

4847

48+
def get_site_packages_dir() -> str:
49+
"""Returns a string with the python platform lib path (to site-packages)."""
50+
# This assumes we are running in the primary st2 virtualenv (typically /opt/stackstorm/st2)
51+
site_packages_dir = get_path("platlib")
52+
53+
sys_prefix = os.path.abspath(sys.prefix)
54+
if sys_prefix not in site_packages_dir:
55+
raise ValueError(
56+
f'The file with "{sys_prefix}" is not found in "{site_packages_dir}".'
57+
)
58+
59+
return site_packages_dir
60+
61+
62+
def get_virtualenv_lib_path(virtualenv_path: str) -> str:
63+
"""Returns the path to a virtualenv's lib/python3.* directory."""
64+
if not (virtualenv_path and os.path.isdir(virtualenv_path)):
65+
raise OSError(
66+
f"virtualenv_path must be an existing directory. virtualenv_path={virtualenv_path}"
67+
)
68+
69+
pack_virtualenv_lib_path = os.path.join(virtualenv_path, "lib")
70+
71+
virtualenv_directories = os.listdir(pack_virtualenv_lib_path)
72+
virtualenv_directories = [
73+
dir_name
74+
for dir_name in virtualenv_directories
75+
if fnmatch.fnmatch(dir_name, "python*")
76+
]
77+
78+
return os.path.join(pack_virtualenv_lib_path, virtualenv_directories[0])
79+
80+
4981
def get_sandbox_python_binary_path(pack=None):
5082
"""
5183
Return path to the Python binary for the provided pack.
@@ -114,14 +146,7 @@ def get_sandbox_python_path(inherit_from_parent=True, inherit_parent_virtualenv=
114146

115147
if inherit_parent_virtualenv and is_in_virtualenv():
116148
# We are running inside virtualenv
117-
site_packages_dir = get_python_lib()
118-
119-
sys_prefix = os.path.abspath(sys.prefix)
120-
if sys_prefix not in site_packages_dir:
121-
raise ValueError(
122-
f'The file with "{sys_prefix}" is not found in "{site_packages_dir}".'
123-
)
124-
149+
site_packages_dir = get_site_packages_dir()
125150
sandbox_python_path.append(site_packages_dir)
126151

127152
sandbox_python_path = ":".join(sandbox_python_path)
@@ -146,30 +171,20 @@ def get_sandbox_python_path_for_python_action(
146171
virtualenv_path = get_sandbox_virtualenv_path(pack=pack)
147172

148173
if virtualenv_path and os.path.isdir(virtualenv_path):
149-
pack_virtualenv_lib_path = os.path.join(virtualenv_path, "lib")
150-
151-
virtualenv_directories = os.listdir(pack_virtualenv_lib_path)
152-
virtualenv_directories = [
153-
dir_name
154-
for dir_name in virtualenv_directories
155-
if fnmatch.fnmatch(dir_name, "python*")
156-
]
157-
158174
# Add the pack's lib directory (lib/python3.x) in front of the PYTHONPATH.
159-
pack_actions_lib_paths = os.path.join(
160-
pack_base_path, "actions", ACTION_LIBS_DIR
161-
)
162-
pack_virtualenv_lib_path = os.path.join(virtualenv_path, "lib")
163-
pack_venv_lib_directory = os.path.join(
164-
pack_virtualenv_lib_path, virtualenv_directories[0]
165-
)
175+
pack_venv_lib_directory = get_virtualenv_lib_path(virtualenv_path)
166176

167177
# Add the pack's site-packages directory (lib/python3.x/site-packages)
168178
# in front of the Python system site-packages This is important because
169179
# we want Python 3 compatible libraries to be used from the pack virtual
170180
# environment and not system ones.
171181
pack_venv_site_packages_directory = os.path.join(
172-
pack_virtualenv_lib_path, virtualenv_directories[0], "site-packages"
182+
pack_venv_lib_directory, "site-packages"
183+
)
184+
185+
# Then add the actions/lib directory in the pack.
186+
pack_actions_lib_paths = os.path.join(
187+
pack_base_path, "actions", ACTION_LIBS_DIR
173188
)
174189

175190
full_sandbox_python_path = [
@@ -185,7 +200,7 @@ def get_sandbox_python_path_for_python_action(
185200
return sandbox_python_path
186201

187202

188-
def get_sandbox_virtualenv_path(pack):
203+
def get_sandbox_virtualenv_path(pack: str) -> Optional[str]:
189204
"""
190205
Return a path to the virtual environment for the provided pack.
191206
"""
@@ -194,7 +209,7 @@ def get_sandbox_virtualenv_path(pack):
194209
virtualenv_path = None
195210
else:
196211
system_base_path = cfg.CONF.system.base_path
197-
virtualenv_path = os.path.join(system_base_path, "virtualenvs", pack)
212+
virtualenv_path = str(os.path.join(system_base_path, "virtualenvs", pack))
198213

199214
return virtualenv_path
200215

st2common/st2common/util/virtualenvs.py

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
import os
2323
import re
2424
import shutil
25+
from logging import Logger
26+
from pathlib import Path
27+
from textwrap import dedent
2528

2629
import six
2730
from oslo_config import cfg
@@ -30,14 +33,19 @@
3033
from st2common.constants.pack import PACK_REF_WHITELIST_REGEX
3134
from st2common.constants.pack import BASE_PACK_REQUIREMENTS
3235
from st2common.constants.pack import RESERVED_PACK_LIST
36+
from st2common.util.sandboxing import (
37+
get_site_packages_dir,
38+
get_virtualenv_lib_path,
39+
is_in_virtualenv,
40+
)
3341
from st2common.util.shell import run_command
3442
from st2common.util.shell import quote_unix
3543
from st2common.util.compat import to_ascii
3644
from st2common.util.pack_management import apply_pack_owner_group
3745
from st2common.content.utils import get_packs_base_paths
3846
from st2common.content.utils import get_pack_directory
3947

40-
__all__ = ["setup_pack_virtualenv"]
48+
__all__ = ["setup_pack_virtualenv", "inject_st2_pth_into_virtualenv"]
4149

4250
LOG = logging.getLogger(__name__)
4351

@@ -52,6 +60,7 @@ def setup_pack_virtualenv(
5260
proxy_config=None,
5361
no_download=True,
5462
force_owner_group=True,
63+
inject_parent_virtualenv_sites=True,
5564
):
5665

5766
"""
@@ -112,7 +121,15 @@ def setup_pack_virtualenv(
112121
no_download=no_download,
113122
)
114123

115-
# 2. Install base requirements which are common to all the packs
124+
# 2. Inject the st2 site-packages dir to enable .pth file loading
125+
if inject_parent_virtualenv_sites:
126+
logger.debug("Injecting st2 venv site-packages via .pth file")
127+
inject_st2_pth_into_virtualenv(
128+
virtualenv_path=virtualenv_path,
129+
logger=logger,
130+
)
131+
132+
# 3. Install base requirements which are common to all the packs
116133
logger.debug("Installing base requirements")
117134
for requirement in BASE_PACK_REQUIREMENTS:
118135
install_requirement(
@@ -122,7 +139,7 @@ def setup_pack_virtualenv(
122139
logger=logger,
123140
)
124141

125-
# 3. Install pack-specific requirements
142+
# 4. Install pack-specific requirements
126143
requirements_file_path = os.path.join(pack_path, "requirements.txt")
127144
has_requirements = os.path.isfile(requirements_file_path)
128145

@@ -139,7 +156,7 @@ def setup_pack_virtualenv(
139156
else:
140157
logger.debug("No pack specific requirements found")
141158

142-
# 4. Set the owner group
159+
# 5. Set the owner group
143160
if force_owner_group:
144161
apply_pack_owner_group(pack_path=virtualenv_path)
145162

@@ -252,6 +269,55 @@ def remove_virtualenv(virtualenv_path, logger=None):
252269
return True
253270

254271

272+
def inject_st2_pth_into_virtualenv(virtualenv_path: str, logger: Logger = None) -> None:
273+
"""
274+
Add a .pth file to the pack virtualenv that loads any .pth files from the st2 virtualenv.
275+
276+
If the primary st2 venv (typically /opt/stackstorm/st2) contains any .pth files,
277+
the pack's virtualenv would not load them because that directory is on the PYTHONPATH,
278+
but it is not a "sites" directory that the "site" module will try to load from.
279+
To work around this, we add an .pth file that registers the st2 venv's
280+
site-packages directory as a "sites" directory.
281+
282+
Sites dirs get loaded from (in order): venv, [user site-packages,] system site-packages.
283+
After the sites dirs are loaded (including loading their .pth files), sitecustomize gets
284+
imported. We do not use sitecustomize because we need the st2 venv's .pth files to load
285+
after the pack venv and before system site-packages. So, we use a .pth file that loads
286+
as late as possible.
287+
"""
288+
logger = logger or LOG
289+
if not is_in_virtualenv():
290+
# If this is installed in the system-packages directory, then
291+
# its site-packages directory should already be included.
292+
logger.debug("Not in a virtualenv; Skipping st2 .pth injection.")
293+
return
294+
295+
parent_site_packages_dir = get_site_packages_dir()
296+
297+
contents = dedent(
298+
# The .pth format requires that any code be on one line.
299+
# https://docs.python.org/3/library/site.html
300+
# The line gets passed through exec() which uses the scope of site.addpackage()
301+
f"""\
302+
# This file is auto-generated by StackStorm.
303+
import sys; addsitedir('{parent_site_packages_dir}', known_paths)
304+
"""
305+
# TODO: Maybe use this to adjust PATH and PYTHONPATH as well.
306+
# This could replace manipulations in python_runner and sensor process_container:
307+
# env["PATH"] = st2common.util.sandboxing.get_sandbox_path(...)
308+
# env["PYTHONPATH"] = st2common.util.sandboxing.get_sandbox_python_path*(...)
309+
)
310+
311+
# .pth filenames are sorted() before loading, so use many "z"s to ensure
312+
# any st2 virtualenv .pth files get loaded after .pth files in the pack's virtualenv.
313+
pth_file = (
314+
Path(get_virtualenv_lib_path(virtualenv_path))
315+
/ "site-packages"
316+
/ "zzzzzzzzzz__st2__.pth"
317+
)
318+
pth_file.write_text(contents)
319+
320+
255321
def install_requirements(
256322
virtualenv_path, requirements_file_path, proxy_config=None, logger=None
257323
):

0 commit comments

Comments
 (0)