Introduce upper constraints

This is a global setting that will apply across every project, a
constraint on the versions of things that can be installed via pip.

This is a first pass, adding basic functionality, by allowing a list
of constraint URLs to be provided. Future iterations of this feature may be
tied to a "superrepo" that contains all the projects _and_ the
constraints for those projects all in one place.

Change-Id: Ib5c2dd441b9294b775e755e595486a671d8de57f
This commit is contained in:
Jesse Keating 2016-06-09 13:08:31 -07:00
parent 6b40cee8bf
commit 235f6bb6f4
5 changed files with 60 additions and 6 deletions

View File

@ -18,6 +18,8 @@ import logging
import os import os
import threading import threading
import requests
from giftwrap.gerrit import GerritReview from giftwrap.gerrit import GerritReview
from stevedore.driver import DriverManager from stevedore.driver import DriverManager
from stevedore.extension import ExtensionManager from stevedore.extension import ExtensionManager
@ -36,6 +38,7 @@ class Builder(object):
self._temp_src_dir = None self._temp_src_dir = None
self._spec = spec self._spec = spec
self._thread_exit = [] self._thread_exit = []
self._constraints = []
@staticmethod @staticmethod
def builder_names(ext_mgr=None): def builder_names(ext_mgr=None):
@ -55,6 +58,30 @@ class Builder(object):
"Error was: %s", e) "Error was: %s", e)
return [] return []
def _get_constraints(self):
cfiles = []
try:
for count, constraint_url in \
enumerate(self._spec.settings.constraints, 1):
response = requests.get(constraint_url)
# Raise an error if we got a bad URL
response.raise_for_status()
constraints = response.text.encode('utf-8')
cfilepath = os.path.join(self._temp_dir,
'constraints-%s.txt' % count)
with open(cfilepath, 'w') as cfile:
cfile.write(constraints)
cfiles.append(cfilepath)
return cfiles
except Exception as e:
raise Exception("Unable to construct constraints. Error: %s" % e)
def _build_project(self, project): def _build_project(self, project):
try: try:
self._prepare_project_build(project) self._prepare_project_build(project)
@ -105,6 +132,9 @@ class Builder(object):
self._temp_src_dir = os.path.join(self._temp_dir, 'src') self._temp_src_dir = os.path.join(self._temp_dir, 'src')
LOG.debug("Temporary working directory: %s", self._temp_dir) LOG.debug("Temporary working directory: %s", self._temp_dir)
# get constraints paths
self._constraints = self._get_constraints()
threads = [] threads = []
for project in spec.projects: for project in spec.projects:
if spec.settings.parallel_build: if spec.settings.parallel_build:

View File

@ -84,8 +84,11 @@ class DockerBuilder(Builder):
def _install_pip_dependencies(self, venv_path, dependencies): def _install_pip_dependencies(self, venv_path, dependencies):
pip_path = self._get_venv_pip_path(venv_path) pip_path = self._get_venv_pip_path(venv_path)
install = "install"
for constraint in self._constraints:
install = "%s -c %s" % (install, constraint)
for dependency in dependencies: for dependency in dependencies:
self._execute("%s install %s" % (pip_path, dependency)) self._execute("%s %s %s" % (pip_path, install, dependency))
def _copy_sample_config(self, src_clone_dir, project): def _copy_sample_config(self, src_clone_dir, project):
src_config = os.path.join(src_clone_dir, 'etc') src_config = os.path.join(src_clone_dir, 'etc')
@ -96,7 +99,10 @@ class DockerBuilder(Builder):
def _install_project(self, venv_path, src_clone_dir): def _install_project(self, venv_path, src_clone_dir):
pip_path = self._get_venv_pip_path(venv_path) pip_path = self._get_venv_pip_path(venv_path)
self._execute("%s install %s" % (pip_path, src_clone_dir)) install = "install"
for constraint in self._constraints:
install = "%s -c %s" % (install, constraint)
self._execute("%s %s %s" % (pip_path, install, src_clone_dir))
def _finalize_project_build(self, project): def _finalize_project_build(self, project):
self._commands.append("rm -rf %s" % self._temp_dir) self._commands.append("rm -rf %s" % self._temp_dir)

View File

@ -70,8 +70,11 @@ class PackageBuilder(Builder):
def _install_pip_dependencies(self, venv_path, dependencies): def _install_pip_dependencies(self, venv_path, dependencies):
pip_path = self._get_venv_pip_path(venv_path) pip_path = self._get_venv_pip_path(venv_path)
install = "install"
for constraint in self._constraints:
install = "%s -c %s" % (install, constraint)
for dependency in dependencies: for dependency in dependencies:
self._execute("%s install %s" % (pip_path, dependency)) self._execute("%s %s %s" % (pip_path, install, dependency))
def _copy_sample_config(self, src_clone_dir, project): def _copy_sample_config(self, src_clone_dir, project):
src_config = os.path.join(src_clone_dir, 'etc') src_config = os.path.join(src_clone_dir, 'etc')
@ -87,7 +90,10 @@ class PackageBuilder(Builder):
def _install_project(self, venv_path, src_clone_dir): def _install_project(self, venv_path, src_clone_dir):
pip_path = self._get_venv_pip_path(venv_path) pip_path = self._get_venv_pip_path(venv_path)
self._execute("%s install %s" % (pip_path, src_clone_dir)) install = "install"
for constraint in self._constraints:
install = "%s -c %s" % (install, constraint)
self._execute("%s %s %s" % (pip_path, install, src_clone_dir))
def _finalize_project_build(self, project): def _finalize_project_build(self, project):
# build the package # build the package

View File

@ -31,9 +31,11 @@ class Settings(object):
package_name_format=None, version=None, package_name_format=None, version=None,
base_path=None, install_path=None, gerrit_dependencies=True, base_path=None, install_path=None, gerrit_dependencies=True,
force_overwrite=False, output_dir=None, include_config=True, force_overwrite=False, output_dir=None, include_config=True,
parallel_build=True): parallel_build=True, constraints=[]):
if not version: if not version:
raise Exception("'version' is a required settings") raise Exception("'version' is a required settings")
if not isinstance(constraints, list):
raise Exception("'constraints' is required to be a list")
self.build_type = build_type self.build_type = build_type
self._package_name_format = package_name_format self._package_name_format = package_name_format
self.version = version self.version = version
@ -44,6 +46,7 @@ class Settings(object):
self._output_dir = output_dir self._output_dir = output_dir
self.include_config = include_config self.include_config = include_config
self.parallel_build = parallel_build self.parallel_build = parallel_build
self.constraints = constraints
@property @property
def package_name_format(self): def package_name_format(self):

View File

@ -22,7 +22,8 @@ from giftwrap import settings
SAMPLE_SETTINGS = { SAMPLE_SETTINGS = {
'package_name_format': 'my-package-name', 'package_name_format': 'my-package-name',
'version': '1.2', 'version': '1.2',
'base_path': '/basepath' 'base_path': '/basepath',
'constraints': ['http://example.txt'],
} }
@ -35,6 +36,7 @@ class TestSettings(unittest.TestCase):
self.assertEquals('my-package-name', s.package_name_format) self.assertEquals('my-package-name', s.package_name_format)
self.assertEquals('1.2', s.version) self.assertEquals('1.2', s.version)
self.assertEquals('/basepath', s.base_path) self.assertEquals('/basepath', s.base_path)
self.assertEquals('http://example.txt', s.constraints[0])
def test_factory_has_default_base_path(self): def test_factory_has_default_base_path(self):
settings_dict = {'version': 'version'} settings_dict = {'version': 'version'}
@ -48,3 +50,10 @@ class TestSettings(unittest.TestCase):
with self.assertRaises(Exception): with self.assertRaises(Exception):
settings.Settings.factory(settings_dict) settings.Settings.factory(settings_dict)
def test_factory_raises_when_constraints_invalid(self):
settings_dict = SAMPLE_SETTINGS
settings_dict['constraints'] = 'notalist'
with self.assertRaises(Exception):
settings.Settings.factory(settings_dict)