Mark Beierl (Canonical)
Feature
common, devops, IM, LCM, MON, N2VC, NBI, osmclient, PLA, POL, RO
The history of OSM saw us installing the software as Debian (.deb) packages onto an Ubuntu based operating system. Over time, this had changed to being docker image based, with python packages still being provided as .deb installable components.
This presents us with a new problem: the mixing and matching of Python dependencies across pip, the python package installer, and apt, the Ubuntu OS level package installer. Python modules can be shipped using either of the two package installers, but they are at odds with each other as pip will not alter or update anything that was installed by apt, as it is considered to be a system level package manager and should be the authoritative manager of system packages. Having said that, not all versions of Python modules that are in pip are available in .deb format. Also, the apt repositories choose their own cadence for when versions are released, and might not carry versions that we require.
This change proposes moving away from using .deb+apt to manage dependency versions, and to rely solely on Python being able to express its own dependencies based on the Python Package Index (PyPI). The scope of this change involves all modules that produce Python packages, and can be broken down into the following areas.
This example uses the POL module as it is a fairly simple chain to follow:
POL requires osm_commonosm_common does not require anything from OSM, but has some upstream dependenciesStarting with common:
All projects will follow the same pattern:
flake8, cover, safety, distbuild will produce the .deb package, which must include the project's requirements.txt fileNo requirements may be expressed in setup.py. Anything that is in that file will be used when creating the .deb package and force the installation of the .deb version of the dependency. To avoid this, we must use requirements files external to setup.py until we move away from .deb packaging entirely.
requirements.in: this file is used to express the upstream dependencies from PyPi for the module. It is permissible to express the modules in the following ways:
aiokafka This means we are looking to use the latest version at this point in timeaiokafka<=0.6.0 This one means we are looking for an older version, and it cannot be more recent than 0.6.0Version ranges in requirements.in is how we control the dependency matrix when a version in upstream is known to cause problems. This should be avoided by spending the time to adjust our code to work with the particular version, but is acceptable as a measure to proceed until the time is available to resolve the issue.
requirements-test.in: this file is used to express additional modules that are used when unit testing the software only. For example, mocking libraries are often needed for a unit test, but are not part of the production code.
requirements-dist.in: this is used to install packages that are required while producing the final distributable package. For example, to create a .deb, we need the stdeb module. For uploading to PyPI, we would need twine.
All .in files must be compiled into their corresponding .txt equivalents, using pip-compile. This takes the expressed modules, with any possible version constraints and produces a final list of all the required modules at a specific version. For example, requirements.in containing aiokafka would produce a requirements.txt as follows:
aiokafka==0.5.2 kafka-python==1.4.6 # via aiokafka
Note that while only aiokafka was mentioned, the pip-compile tool looked further upstream and found that kafka-python was also required, found the best version, and noted why it was included in the requirements.txt
Moving on to POL:
Now that the .deb package has been created without any dependencies, we face the next problem: how does the downstream software (in our example POL) know what osm_common requires in order to function? To support this, we will include common's requirements.txt file in its .deb package.
The Dockerfile (in devops, stage 3) for POL would then look something like this:
FROM ubuntu 18.04
RUN apt --yes update
RUN apt --yes install python3 python3-pip
...
RUN apt --yes install python3-osm-common${COMMON_VERSION} \
python3-osm-policy-module${POL_VERSION}
RUN pip3 install -r /usr/lib/python3/dist-packages/osm_common/requirements.txt \
-r /usr/lib/python3/dist-packages/osm_pol/requirements.txt
The packages that used to be automatically installed by apt now must be explicitly installed via the pip3 install command. This is the desired outcome: we now have once place to express requirements, which is in the source code of the module itself, and complete control over the final list of dependencies that gets installed.