Skip to content

Commit 30bf0bf

Browse files
committed
Adds initial builder-focal
This will enable creating the builder image sd-builder-focal from the Dockerfile in this commit. We can then update the next image_hash in the next commit.
1 parent 172138e commit 30bf0bf

16 files changed

+1180
-0
lines changed

molecule/builder-focal/Dockerfile

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# ubuntu:focal-20200720
2+
FROM ubuntu@sha256:60f560e52264ed1cb7829a0d59b1ee7740d7580e0eb293aca2d722136edb1e24
3+
4+
5+
# additional meta-data makes it easier to clean up, find
6+
LABEL org="Freedom of the Press"
7+
LABEL image_name="focal-sd-builder-app"
8+
ARG DEBIAN_FRONTEND=noninteractive
9+
RUN apt-get -y update && apt-get upgrade -y && apt-get install -y \
10+
apache2-dev \
11+
aptitude \
12+
coreutils \
13+
debhelper \
14+
devscripts \
15+
dh-python \
16+
dh-systemd \
17+
gdb \
18+
git \
19+
gnupg2 \
20+
haveged \
21+
inotify-tools \
22+
libffi-dev \
23+
libssl-dev \
24+
make \
25+
ntp \
26+
paxctl \
27+
python3-all \
28+
python3-pip \
29+
python3-setuptools \
30+
rsync \
31+
ruby \
32+
sqlite \
33+
sudo \
34+
tzdata \
35+
libevent-dev \
36+
unzip
37+
38+
# TEMPORARY: install dh-virtualenv from debian unstable
39+
# No pubkey verification is performed on this package, using only for
40+
# research spike on focal package support.
41+
RUN curl -s https://deb.debian.org/debian/pool/main/d/dh-virtualenv/dh-virtualenv_1.2-1_all.deb -o /tmp/dh-virtualenv_1.2-1_all.deb
42+
RUN apt install -y /tmp/dh-virtualenv_1.2-1_all.deb
43+
44+
RUN paxctl -cm /usr/bin/python3.8 && mkdir -p /tmp/build
45+
RUN apt-get clean \
46+
&& rm -rf /var/lib/apt/lists/*

molecule/builder-focal/Makefile

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
DATE_STR := $(shell date +"%Y_%m_%d")
2+
BUILDER_IMAGE ?= "quay.io/freedomofpress/sd-docker-builder-focal:$(DATE_STR)"
3+
4+
.PHONY: build-container
5+
build-container: ## Build Docker image for Debian package creation
6+
@echo "███Building Docker image $(BUILDER_IMAGE) for Debian package creation..."
7+
@docker build --no-cache -t $(BUILDER_IMAGE) .
8+
9+
.PHONY: push-container
10+
push-container: ## Push the Docker image for Debian package creation to quay.io
11+
@echo "███Pushing Docker image for Debian package creation to quay.io..."
12+
@./push.sh
13+
14+
.PHONY: help
15+
help: ## Print this message and exit.
16+
@printf "Molecule scenario for building a Docker container for Debian package creation.\n"
17+
@printf "Subcommands:\n\n"
18+
@awk 'BEGIN {FS = ":.*?## "} /^[0-9a-zA-Z_-]+:.*?## / {printf "\033[36m%s\033[0m : %s\n", $$1, $$2}' $(MAKEFILE_LIST) \
19+
| sort \
20+
| column -s ':' -t
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
securedrop_build_xenial_support: True

molecule/builder-focal/create.yml

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
- name: Create
3+
hosts: localhost
4+
connection: local
5+
gather_facts: False
6+
vars:
7+
molecule_file: "{{ lookup('env', 'MOLECULE_FILE') }}"
8+
molecule_ephemeral_directory: "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}"
9+
molecule_scenario_directory: "{{ lookup('env', 'MOLECULE_SCENARIO_DIRECTORY') }}"
10+
molecule_yml: "{{ lookup('file', molecule_file) | from_yaml }}"
11+
image_hash: "{{ lookup('pipe', 'egrep -v ^# image_hash') }}"
12+
default_image: "quay.io/freedomofpress/sd-docker-builder-focal@sha256:{{image_hash}}"
13+
image: "{{ lookup('env', 'BUILDER_IMAGE') | default(default_image, true) }}"
14+
tasks:
15+
- debug:
16+
msg: "Building with Docker image {{ image }}"
17+
18+
- name: Create builders
19+
docker_container:
20+
name: "{{ item.name }}"
21+
hostname: "{{ item.name }}"
22+
image: "{{ image }}"
23+
state: started
24+
command: "tail -f /dev/null"
25+
privileged: "{{ item.privileged | default(omit) }}"
26+
volumes: "{{ item.volumes | default(omit) }}"
27+
capabilities: "{{ item.capabilities | default(omit) }}"
28+
with_items: "{{ molecule_yml.platforms }}"

molecule/builder-focal/deployment.py

+251
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright (c) 2013 - 2014 Spotify AB
3+
4+
# This file is part of dh-virtualenv.
5+
6+
# dh-virtualenv is free software: you can redistribute it and/or
7+
# modify it under the terms of the GNU General Public License as
8+
# published by the Free Software Foundation, either version 2 of the
9+
# License, or (at your option) any later version.
10+
11+
# dh-virtualenv is distributed in the hope that it will be useful, but
12+
# WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
# General Public License for more details.
15+
16+
# You should have received a copy of the GNU General Public License
17+
# along with dh-virtualenv. If not, see
18+
# <http://www.gnu.org/licenses/>.
19+
20+
import os
21+
import re
22+
import shutil
23+
import subprocess
24+
import tempfile
25+
26+
ROOT_ENV_KEY = 'DH_VIRTUALENV_INSTALL_ROOT'
27+
DEFAULT_INSTALL_DIR = '/usr/share/python/'
28+
29+
30+
class Deployment(object):
31+
def __init__(self, package, extra_urls=[], preinstall=[],
32+
pypi_url=None, setuptools=False, python=None,
33+
builtin_venv=False, sourcedirectory=None, verbose=False,
34+
extra_pip_arg=[], use_system_packages=False,
35+
skip_install=False,
36+
install_suffix=None,
37+
requirements_filename='requirements.txt'):
38+
39+
self.package = package
40+
install_root = os.environ.get(ROOT_ENV_KEY, DEFAULT_INSTALL_DIR)
41+
self.install_suffix = install_suffix
42+
43+
self.debian_root = os.path.join(
44+
'debian', package, install_root.lstrip('/'))
45+
46+
if install_suffix is None:
47+
self.virtualenv_install_dir = os.path.join(install_root, self.package)
48+
self.package_dir = os.path.join(self.debian_root, package)
49+
else:
50+
self.virtualenv_install_dir = os.path.join(install_root, install_suffix)
51+
self.package_dir = os.path.join(self.debian_root, install_suffix)
52+
53+
self.bin_dir = os.path.join(self.package_dir, 'bin')
54+
self.local_bin_dir = os.path.join(self.package_dir, 'local', 'bin')
55+
56+
self.extra_urls = extra_urls
57+
self.preinstall = preinstall
58+
self.extra_pip_arg = extra_pip_arg
59+
self.pypi_url = pypi_url
60+
self.log_file = tempfile.NamedTemporaryFile()
61+
self.verbose = verbose
62+
self.setuptools = setuptools
63+
self.python = python
64+
self.builtin_venv = builtin_venv
65+
self.sourcedirectory = '.' if sourcedirectory is None else sourcedirectory
66+
self.use_system_packages = use_system_packages
67+
self.skip_install = skip_install
68+
self.requirements_filename = requirements_filename
69+
70+
@classmethod
71+
def from_options(cls, package, options):
72+
verbose = options.verbose or os.environ.get('DH_VERBOSE') == '1'
73+
return cls(package,
74+
extra_urls=options.extra_index_url,
75+
preinstall=options.preinstall,
76+
pypi_url=options.pypi_url,
77+
setuptools=options.setuptools,
78+
python=options.python,
79+
builtin_venv=options.builtin_venv,
80+
sourcedirectory=options.sourcedirectory,
81+
verbose=verbose,
82+
extra_pip_arg=options.extra_pip_arg,
83+
use_system_packages=options.use_system_packages,
84+
skip_install=options.skip_install,
85+
install_suffix=options.install_suffix,
86+
requirements_filename=options.requirements_filename)
87+
88+
def clean(self):
89+
shutil.rmtree(self.debian_root)
90+
91+
def create_virtualenv(self):
92+
if self.builtin_venv:
93+
virtualenv = [self.python, '-m', 'venv']
94+
else:
95+
virtualenv = ['virtualenv']
96+
97+
if self.use_system_packages:
98+
virtualenv.append('--system-site-packages')
99+
else:
100+
virtualenv.append('--no-site-packages')
101+
102+
if self.setuptools:
103+
virtualenv.append('--setuptools')
104+
105+
if self.verbose:
106+
virtualenv.append('--verbose')
107+
108+
if self.python:
109+
virtualenv.extend(('--python', self.python))
110+
111+
virtualenv.append(self.package_dir)
112+
subprocess.check_call(virtualenv)
113+
114+
# We need to prefix the pip run with the location of python
115+
# executable. Otherwise it would just blow up due to too long
116+
# shebang-line.
117+
self.pip_prefix = [
118+
os.path.abspath(os.path.join(self.bin_dir, 'python')),
119+
os.path.abspath(os.path.join(self.bin_dir, 'pip')),
120+
]
121+
122+
123+
update_pip_command = [
124+
os.path.abspath(os.path.join(self.bin_dir, 'python')),
125+
"-m", "pip", "install", "--require-hashes", "--verbose", "--no-deps", "--no-compile", "--no-cache-dir", "--ignore-installed", "-r", "/root/update_virtualenv.txt"
126+
]
127+
subprocess.check_call(update_pip_command)
128+
if self.verbose:
129+
self.pip_prefix.append('-v')
130+
131+
self.pip_prefix.append('install')
132+
133+
if self.pypi_url:
134+
self.pip_prefix.append('--pypi-url={0}'.format(self.pypi_url))
135+
self.pip_prefix.extend([
136+
'--extra-index-url={0}'.format(url) for url in self.extra_urls
137+
])
138+
self.pip_prefix.append('--log={0}'.format(os.path.abspath(self.log_file.name)))
139+
140+
# Add in any user supplied pip args
141+
if self.extra_pip_arg:
142+
self.pip_prefix.extend(self.extra_pip_arg)
143+
144+
def pip(self, *args):
145+
return self.pip_prefix + list(args)
146+
147+
def install_dependencies(self):
148+
# Install preinstall stage packages. This is handy if you need
149+
# a custom package to install dependencies (think something
150+
# along lines of setuptools), but that does not get installed
151+
# by default virtualenv.
152+
if self.preinstall:
153+
subprocess.check_call(self.pip(*self.preinstall))
154+
155+
requirements_path = os.path.join(self.sourcedirectory, self.requirements_filename)
156+
if os.path.exists(requirements_path):
157+
subprocess.check_call(self.pip('-r', requirements_path))
158+
159+
def run_tests(self):
160+
python = os.path.abspath(os.path.join(self.bin_dir, 'python'))
161+
setup_py = os.path.join(self.sourcedirectory, 'setup.py')
162+
if os.path.exists(setup_py):
163+
subprocess.check_call([python, 'setup.py', 'test'], cwd=self.sourcedirectory)
164+
165+
def fix_shebangs(self):
166+
"""Translate /usr/bin/python and /usr/bin/env python sheband
167+
lines to point to our virtualenv python.
168+
"""
169+
grep_proc = subprocess.Popen(
170+
['grep', '-l', '-r', '-e', r'^#!.*bin/\(env \)\?python',
171+
self.bin_dir],
172+
stdout=subprocess.PIPE
173+
)
174+
files, stderr = grep_proc.communicate()
175+
files = files.strip()
176+
if not files:
177+
return
178+
179+
pythonpath = os.path.join(self.virtualenv_install_dir, 'bin/python')
180+
for f in files.split('\n'):
181+
subprocess.check_call(
182+
['sed', '-i', r's|^#!.*bin/\(env \)\?python|#!{0}|'.format(
183+
pythonpath),
184+
f])
185+
186+
def fix_activate_path(self):
187+
"""Replace the `VIRTUAL_ENV` path in bin/activate to reflect the
188+
post-install path of the virtualenv.
189+
"""
190+
activate_settings = [
191+
[
192+
'VIRTUAL_ENV="{0}"'.format(self.virtualenv_install_dir),
193+
r'^VIRTUAL_ENV=.*$',
194+
"activate"
195+
],
196+
[
197+
'setenv VIRTUAL_ENV "{0}"'.format(self.virtualenv_install_dir),
198+
r'^setenv VIRTUAL_ENV.*$',
199+
"activate.csh"
200+
],
201+
[
202+
'set -gx VIRTUAL_ENV "{0}"'.format(self.virtualenv_install_dir),
203+
r'^set -gx VIRTUAL_ENV.*$',
204+
"activate.fish"
205+
],
206+
]
207+
208+
for activate_args in activate_settings:
209+
virtualenv_path = activate_args[0]
210+
pattern = re.compile(activate_args[1], flags=re.M)
211+
activate_file = activate_args[2]
212+
213+
with open(os.path.join(self.bin_dir, activate_file), 'r+') as fh:
214+
content = pattern.sub(virtualenv_path, fh.read())
215+
fh.seek(0)
216+
fh.truncate()
217+
fh.write(content)
218+
219+
def install_package(self):
220+
if not self.skip_install:
221+
subprocess.check_call(self.pip('.'), cwd=os.path.abspath(self.sourcedirectory))
222+
223+
def fix_local_symlinks(self):
224+
# The virtualenv might end up with a local folder that points outside the package
225+
# Specifically it might point at the build environment that created it!
226+
# Make those links relative
227+
# See https://github.com/pypa/virtualenv/commit/5cb7cd652953441a6696c15bdac3c4f9746dfaa1
228+
local_dir = os.path.join(self.package_dir, "local")
229+
if not os.path.isdir(local_dir):
230+
return
231+
elif os.path.samefile(self.package_dir, local_dir):
232+
# "local" points directly to its containing directory
233+
os.unlink(local_dir)
234+
os.symlink(".", local_dir)
235+
return
236+
237+
for d in os.listdir(local_dir):
238+
path = os.path.join(local_dir, d)
239+
if not os.path.islink(path):
240+
continue
241+
242+
existing_target = os.readlink(path)
243+
if not os.path.isabs(existing_target):
244+
# If the symlink is already relative, we don't
245+
# want to touch it.
246+
continue
247+
248+
new_target = os.path.relpath(existing_target, local_dir)
249+
os.unlink(path)
250+
os.symlink(new_target, path)
251+

molecule/builder-focal/destroy.yml

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
- name: Destroy
3+
hosts: localhost
4+
connection: local
5+
gather_facts: False
6+
vars:
7+
molecule_file: "{{ lookup('env', 'MOLECULE_FILE') }}"
8+
molecule_yml: "{{ lookup('file', molecule_file) | from_yaml }}"
9+
tasks:
10+
- name: Destroy molecule instance(s)
11+
docker_container:
12+
name: "{{ item.name }}"
13+
state: absent
14+
force_kill: "{{ item.force_kill | default(True) }}"
15+
with_items: "{{ molecule_yml.platforms }}"

molecule/builder-focal/image_hash

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# sha256 digest quay.io/freedomofpress/sd-docker-builder-xenial:2020_06_17
2+
918f618085a22adb3259c090a019736b72912bb951fe41d03c0dc3ceeb6dbd26

0 commit comments

Comments
 (0)