Skip to content

Commit 5da05de

Browse files
committed
Copy data into container instead of mounting volumes.
This helps if Docker/Podman runs as a different user. Other changes: * Detect Git commit * Fixed log levels (stdout/stderr can be used for different purposes) * Stop container on termination * Allow to use a prebuilt repository cache * Migrated to ubi9 where possible
1 parent 98a6a72 commit 5da05de

8 files changed

Lines changed: 192 additions & 111 deletions

File tree

Dockerfile

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ COPY $BUILD_PATH /opt/mendix/build
2626
# Use nginx supplied by the base OS
2727
ENV NGINX_CUSTOM_BIN_PATH=/usr/sbin/nginx
2828

29+
# Set the user ID
30+
ARG USER_UID=1001
31+
32+
# Copy start scripts
33+
COPY scripts/startup.py scripts/vcap_application.json /opt/mendix/build/
34+
2935
# Each comment corresponds to the script line:
3036
# 1. Create cache directory and directory for dependencies which can be shared
3137
# 2. Set permissions for compilation scripts
@@ -35,45 +41,25 @@ ENV NGINX_CUSTOM_BIN_PATH=/usr/sbin/nginx
3541
# 6. Create symlink for java prefs used by CF buildpack
3642
# 7. Update ownership of /opt/mendix so that the app can run as a non-root user
3743
# 8. Update permissions of /opt/mendix so that the app can run as a non-root user
38-
RUN mkdir -p /tmp/buildcache /tmp/cf-deps /var/mendix/build /var/mendix/build/.local &&\
39-
chmod +rx /opt/mendix/buildpack/compilation.py /opt/mendix/buildpack/git /opt/mendix/buildpack/buildpack/stage.py &&\
44+
RUN mkdir -p /tmp/buildcache/bust /tmp/cf-deps /var/mendix/build /var/mendix/build/.local &&\
45+
chmod +rx /opt/mendix/buildpack/compilation.py /opt/mendix/buildpack/buildpack/stage.py /opt/mendix/build/startup.py &&\
4046
cd /opt/mendix/buildpack &&\
4147
./compilation.py /opt/mendix/build /tmp/buildcache /tmp/cf-deps 0 &&\
42-
rm -fr /tmp/buildcache /tmp/javasdk /tmp/opt /tmp/downloads /opt/mendix/buildpack/compilation.py /opt/mendix/buildpack/git &&\
48+
rm -fr /tmp/buildcache /tmp/javasdk /tmp/opt /tmp/downloads /opt/mendix/buildpack/compilation.py /var/mendix &&\
4349
ln -s /opt/mendix/.java /opt/mendix/build &&\
44-
chown -R ${USER_UID}:0 /opt/mendix /var/mendix &&\
45-
chmod -R g=u /opt/mendix /var/mendix
50+
chown -R ${USER_UID}:0 /opt/mendix &&\
51+
chmod -R g=u /opt/mendix
4652

4753
FROM ${ROOTFS_IMAGE}
4854
LABEL Author="Mendix Digital Ecosystems"
4955
LABEL maintainer="digitalecosystems@mendix.com"
5056

51-
# Set the user ID
52-
ARG USER_UID=1001
5357
# Set the home path
5458
ENV HOME=/opt/mendix/build
5559

5660
# Add the buildpack modules
5761
ENV PYTHONPATH "/opt/mendix/buildpack/lib/:/opt/mendix/buildpack/:/opt/mendix/buildpack/lib/python3.11/site-packages/"
5862

59-
# Copy start scripts
60-
COPY scripts/startup.py scripts/vcap_application.json /opt/mendix/build/
61-
62-
# Create vcap home directory for Datadog configuration
63-
RUN mkdir -p /home/vcap /opt/datadog-agent/run &&\
64-
chown -R ${USER_UID}:0 /home/vcap /opt/datadog-agent/run &&\
65-
chmod -R g=u /home/vcap /opt/datadog-agent/run
66-
67-
# Each comment corresponds to the script line:
68-
# 1. Make the startup script executable
69-
# 2. Update ownership of /opt/mendix so that the app can run as a non-root user
70-
# 3. Update permissions of /opt/mendix so that the app can run as a non-root user
71-
# 4. Ensure that running Java 8 as root will still be able to load offline licenses
72-
RUN chmod +rx /opt/mendix/build/startup.py &&\
73-
chown -R ${USER_UID}:0 /opt/mendix &&\
74-
chmod -R g=u /opt/mendix &&\
75-
ln -s /opt/mendix/.java /root
76-
7763
USER ${USER_UID}
7864

7965
# Copy build artifacts from build container

build.py

Lines changed: 134 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -45,24 +45,36 @@ def extract_zip(mda_file):
4545
zip_file.extractall(temp_dir.name)
4646
return temp_dir
4747

48+
BUILDER_PROCESS = None
49+
def stop_processes():
50+
if BUILDER_PROCESS is not None:
51+
proc = BUILDER_PROCESS
52+
proc.terminate()
53+
proc.communicate()
54+
proc.wait()
55+
4856
def container_call(args):
4957
build_executables = ['podman', 'docker']
5058
build_executable = None
59+
logger_stdout = None
60+
logger_stderr = None
5161
for builder in build_executables:
52-
builder = shutil.which(builder)
53-
if builder is not None:
54-
build_executable = builder
62+
build_executable = shutil.which(builder)
63+
if build_executable is not None:
64+
logger_stderr = logging.getLogger(builder + '-stderr')
65+
logger_stdout = logging.getLogger(builder + '-stdout')
5566
break
5667
if build_executable is None:
5768
raise Exception('Cannot find Podman or Docker executable')
5869
proc = subprocess.Popen([build_executable] + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
70+
BUILDER_PROCESS = proc
5971

6072
sel = selectors.DefaultSelector()
6173
sel.register(proc.stdout, selectors.EVENT_READ)
6274
sel.register(proc.stderr, selectors.EVENT_READ)
63-
logger = logging.getLogger(build_executable)
6475

65-
last_line = None
76+
last_line_stdout = None
77+
last_line_stderr = None
6678
stderr_open, stderr_open = True, True
6779
while stderr_open or stderr_open:
6880
for key, _ in sel.select():
@@ -75,67 +87,137 @@ def container_call(args):
7587
continue
7688
data = data.rstrip()
7789
if key.fileobj is proc.stdout:
78-
last_line = data
79-
logger.info(data)
90+
last_line_stdout = data
91+
logger_stdout.info(data)
8092
elif key.fileobj is proc.stderr:
81-
logger.error(data)
93+
last_line_stderr = data
94+
# stderr is mostly used for progress notifications, not errors
95+
logger_stderr.info(data)
8296

8397
sel.close()
98+
BUILDER_PROCESS = None
8499
if proc.wait() != 0:
85-
raise Exception('Builder returned with error')
86-
return last_line
100+
raise Exception(f"Builder returned with error: {last_line_stderr}")
101+
return last_line_stdout
102+
103+
def pull_image(image_url):
104+
try:
105+
container_call(['image', 'pull', image_url])
106+
return image_url
107+
except:
108+
return None
109+
110+
def delete_container(container_id):
111+
try:
112+
container_call(['container', 'rm', '--force', container_id])
113+
except Exception as e:
114+
logging.warning('Failed to delete container {}: {}'.format(container_id, e))
115+
116+
def build_mpr_builder(mx_version, dotnet, artifacts_repository=None):
117+
builder_image_tag = f"mxbuild-{mx_version}-{dotnet}-{platform.machine()}"
118+
builder_image_url = None
119+
if artifacts_repository is not None:
120+
builder_image_url = f"{artifacts_repository}:{builder_image_tag}"
121+
image_hash = pull_image(builder_image_url)
122+
if image_hash is not None:
123+
return builder_image_url
87124

88-
def build_mpr_builder(mx_version, dotnet):
89125
prefix = ''
90126
if platform.machine() == 'arm64' and dotnet == 'dotnet':
91127
prefix = 'arm64-'
92128

93129
mxbuild_filename = f"{prefix}mxbuild-{mx_version}.tar.gz"
94130
mxbuild_url = f"https://download.mendix.com/runtimes/{mxbuild_filename}"
95131

96-
# TODO: build image only if it doesn't exist yet
97-
return container_call(['build',
98-
'--build-arg', f"MXBUILD_DOWNLOAD_URL={mxbuild_url}",\
99-
'--file', f"mxbuild/rootfs-mxbuild-{dotnet}.dockerfile",
100-
'mxbuild'])
101-
102-
def build_mpr(source_dir, mpr_file):
103-
print(f"MPR file {mpr_file}")
132+
build_args = ['--build-arg', f"MXBUILD_DOWNLOAD_URL={mxbuild_url}",
133+
'--file', f"mxbuild/{dotnet}.dockerfile"]
134+
if artifacts_repository is not None:
135+
build_args += ['--tag', builder_image_url]
136+
137+
image_id = container_call(['image', 'build'] + build_args + ['mxbuild'])
138+
if artifacts_repository is not None:
139+
try:
140+
container_call(['image', 'push', builder_image_url])
141+
except Exception as e:
142+
logging.warning('Failed to push mxbuild into artifacts repository: {}; continuing with the build'.format(e))
143+
return image_id
144+
145+
def get_git_commit(source_dir):
146+
git_head = os.path.join(source_dir, '.git', 'HEAD')
147+
if not os.path.isfile(git_head):
148+
return None
149+
with open(git_head) as git_head:
150+
git_head_line = git_head.readline().split()
151+
if len(git_head_line) == 1:
152+
# Detached commit
153+
return git_head_line[0]
154+
if len(git_head_line) > 2:
155+
return Exception(f"Unsupported Git HEAD format {git_head_line}")
156+
git_branch = git_head_line[1].split('/')
157+
git_branch_file = os.path.join(*([source_dir, '.git'] + git_branch))
158+
if not os.path.isfile(git_branch_file):
159+
return Exception('Git branch file doesn\'t exist')
160+
with open(git_branch_file) as git_branch_file:
161+
return git_branch_file.readline()
162+
163+
164+
def build_mpr(source_dir, mpr_file, destination, artifacts_repository=None):
104165
cursor = sqlite3.connect(mpr_file).cursor()
105166
cursor.execute("SELECT _ProductVersion FROM _MetaData LIMIT 1")
106167
mx_version = cursor.fetchone()[0]
107168
mx_version_value = parse_version(mx_version)
108-
logging.debug("Detected Mendix version {}".format(mx_version_value))
109-
if mx_version_value >= (10, 0, 0, 0):
110-
builder_image = build_mpr_builder(mx_version, 'dotnet')
111-
build_result = container_call(['run', '--volume', os.path.abspath(source_dir)+':/workdir/project:rw', builder_image])
112-
else:
113-
builder_image = build_mpr_builder(mx_version, 'mono')
114-
build_result = container_call(['run', '--volume', os.path.abspath(source_dir)+':/workdir/project:rw', builder_image])
115-
raise Exception('TODO')
169+
logging.debug('Detected Mendix version {}'.format('.'.join(map(str,mx_version_value))))
170+
dotnet = 'dotnet' if mx_version_value >= (10, 0, 0, 0) else 'mono'
171+
builder_image = build_mpr_builder(mx_version, dotnet, artifacts_repository)
172+
model_version = get_git_commit(source_dir)
173+
model_version = 'unversioned' if model_version is None else model_version
174+
175+
container_id = container_call(['container', 'create', builder_image, os.path.basename(mpr_file), model_version])
176+
atexit.register(delete_container, container_id)
177+
container_call(['container', 'cp', os.path.abspath(source_dir)+'/.', f"{container_id}:/workdir/project"])
178+
build_result = container_call(['start', '--attach', '--interactive', container_id])
179+
180+
temp_dir = tempfile.TemporaryDirectory(prefix='mendix-docker-buildpack')
181+
container_call(['container', 'cp', f"{container_id}:/workdir/output.mda", temp_dir.name])
182+
with zipfile.ZipFile(os.path.join(temp_dir.name, 'output.mda')) as zip_file:
183+
zip_file.extractall(destination)
116184

117185
def parse_version(version):
118186
return tuple([ int(n) for n in version.split('.') ])
119187

120-
def prepare_mda(source_dir):
121-
mpk_file = find_default_file(source_dir, '.mpk')
188+
def prepare_destination(destination_path):
189+
with os.scandir(destination_path) as entries:
190+
for entry in entries:
191+
if entry.is_dir() and not entry.is_symlink():
192+
shutil.rmtree(entry.path)
193+
else:
194+
os.remove(entry.path)
195+
project_path = os.path.join(destination_path, 'project')
196+
os.mkdir(project_path, 0o755)
197+
shutil.copytree('scripts', os.path.join(destination_path, 'scripts'))
198+
shutil.copyfile('Dockerfile', os.path.join(destination_path, 'Dockerfile'))
199+
return project_path
200+
201+
def prepare_mda(source_path, destination_path, artifacts_repository=None):
202+
destination_path = prepare_destination(destination_path)
203+
mpk_file = find_default_file(source_path, '.mpk')
122204
extracted_dir = None
123205
if mpk_file is not None:
124206
extracted_dir = extract_zip(mpk_file)
125-
source_dir = extracted_dir.name
126-
mpr_file = find_default_file(source_dir, '.mpr')
207+
source_path = extracted_dir.name
208+
mpr_file = find_default_file(source_path, '.mpr')
127209
if mpr_file is not None:
128-
return build_mpr(source_dir, mpr_file)
129-
mda_file = find_default_file(source_dir, '.mda')
210+
source_path = os.path.abspath(os.path.join(mpr_file, os.pardir))
211+
return build_mpr(source_path, mpr_file, destination_path, artifacts_repository)
212+
mda_file = find_default_file(source_path, '.mda')
130213
if mda_file is not None:
131-
extracted_dir = extract_zip(mda_file)
132-
source_dir = extracted_dir.name
133-
extracted_mda_file = get_metadata_value(source_dir)
134-
# TODO: pre-download MxRuntime & place into CF Buildpack's cache dir
214+
with zipfile.ZipFile(mda_file) as zip_file:
215+
zip_file.extractall(project_path)
216+
extracted_mda_file = get_metadata_value(destination_path)
135217
if extracted_mda_file is not None:
136-
return source_dir
218+
return destination_path
137219
else:
138-
raise Exception('No supported files found in source dir')
220+
raise Exception('No supported files found in source path')
139221

140222
def build_image(mda_dir):
141223
# TODO: build the full image, or just copy MDA into destination?
@@ -144,15 +226,22 @@ def build_image(mda_dir):
144226
mx_version = mda_metadata['RuntimeVersion']
145227
java_version = mda_metadata.get('JavaVersion', 11)
146228
print(mda_metadata['RuntimeVersion'])
229+
print(mda_metadata['JavaVersion'])
147230

148231
if __name__ == '__main__':
149-
parser = argparse.ArgumentParser(description='Build a Docker image of a Mendix app')
150-
parser.add_argument('source_dir', metavar='source_dir', type=pathlib.Path, help='Path to source Mendix app (MDA file, MPK file, MPR directory or extracted MDA directory)')
151-
152-
# TODO: allow to specify Podman args and replace URLs
232+
parser = argparse.ArgumentParser(description='Build a Mendix app')
233+
parser.add_argument('--source', metavar='source', required=True, nargs='?', type=pathlib.Path, help='Path to source Mendix app (MDA file, MPK file, MPR directory or extracted MDA directory)')
234+
parser.add_argument('--destination', metavar='destination', required=True, nargs='?', type=pathlib.Path, help='Destination for MDA')
235+
parser.add_argument('--artifacts-repository', required=False, nargs='?', metavar='artifacts_repository', type=str, help='Repository to use for caching build images')
236+
parser.add_argument('action', metavar='action', choices=['build-mda'], help='Action to perform')
153237

154238
args = parser.parse_args()
155239

156-
mda_dir = prepare_mda(args.source_dir)
157-
build_image(mda_dir)
240+
atexit.register(stop_processes)
241+
try:
242+
prepare_mda(args.source, args.destination, args.artifacts_repository)
243+
except KeyboardInterrupt:
244+
stop_processes()
245+
raise
246+
# build_image(args.destination)
158247

mxbuild/build

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#!/bin/sh
22
set -e
3+
MPR_FILENAME=$1
4+
MODEL_VERSION=$2
35

46
# Required to allow using deprecated checksums
57
export OPENSSL_ENABLE_SHA1_SIGNATURES=1
@@ -16,7 +18,7 @@ cd /workdir
1618
if [ -f /workdir/project ]; then
1719
JAVA_VERSION=$(cat java-version)
1820
elif [ -f /opt/mendix/modeler/mx ]; then
19-
JAVA_VERSION=$(/opt/mendix/modeler/mx dump-mpr --unit-type 'Settings$ProjectSettings' /workdir/project/*.mpr | \
21+
JAVA_VERSION=$(/opt/mendix/modeler/mx dump-mpr --unit-type 'Settings$ProjectSettings' /workdir/project/${MPR_FILENAME} | \
2022
jq -r '.units[] | select(.["$Type"]=="Settings$ProjectSettings") | .["settingsParts"][] | select(.["$Type"]=="Settings$RuntimeSettings").javaVersion | if (. == null or . == "null") then "Java11" else . end')
2123
else
2224
JAVA_VERSION=11
@@ -31,4 +33,4 @@ $MXBUILD_COMMAND \
3133
--target=package \
3234
--java-home=${JDK_HOME} --java-exe-path=${JDK_HOME}/bin/java \
3335
--model-version=${MODEL_VERSION} \
34-
--output=/workdir/output.mda /workdir/project/*.mpr
36+
--output=/workdir/output.mda /workdir/project/${MPR_FILENAME}
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ COPY --chown=0:0 --chmod=0755 build /opt/mendix/build
2626

2727
# Prepare build context
2828
ENV HOME /workdir
29-
RUN mkdir -p /workdir/project &&\
30-
mkdir -p /workdir/.local/share/Mendix &&\
29+
RUN mkdir -p /workdir/project /workdir/output /workdir/.local/share/Mendix &&\
3130
chown -R ${USER_UID}:${USER_UID} /workdir &&\
3231
chmod -R 755 /workdir
3332

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ COPY --chown=0:0 --chmod=0755 build /opt/mendix/build
2727

2828
# Prepare build context
2929
ENV HOME /workdir
30-
RUN mkdir -p /workdir/project &&\
31-
mkdir -p /workdir/.local/share/Mendix &&\
30+
RUN mkdir -p /workdir/project /workdir/output /workdir/.local/share/Mendix &&\
3231
chown -R ${USER_UID}:${USER_UID} /workdir &&\
3332
chmod -R 755 /workdir
3433

rootfs-app.dockerfile

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Dockerfile to create a Mendix Docker image based on either the source code or
22
# Mendix Deployment Archive (aka mda file)
3-
FROM --platform=linux/amd64 registry.access.redhat.com/ubi8/ubi-minimal:latest
3+
FROM registry.access.redhat.com/ubi9/ubi-minimal:latest
44
#This version does a full build originating from the Ubuntu Docker images
55
LABEL Author="Mendix Digital Ecosystems"
66
LABEL maintainer="digitalecosystems@mendix.com"
@@ -11,20 +11,34 @@ ENV LC_ALL C.UTF-8
1111

1212
# install dependencies & remove package lists
1313
RUN microdnf update -y && \
14-
microdnf module enable nginx:1.20 -y && \
14+
microdnf module enable nginx:1.24 -y && \
1515
microdnf install -y glibc-langpack-en python311 openssl nginx nginx-mod-stream java-11-openjdk-headless java-17-openjdk-headless java-21-openjdk-headless tzdata-java fontconfig binutils && \
1616
microdnf clean all && rm -rf /var/cache/yum
1717

18+
# Set the user ID
19+
ARG USER_UID=1001
20+
1821
# Set nginx permissions
1922
RUN touch /run/nginx.pid && \
20-
chown -R 1001:0 /var/log/nginx /var/lib/nginx /run &&\
23+
chown -R ${USER_UID}:0 /var/log/nginx /var/lib/nginx /run &&\
2124
chmod -R g=u /var/log/nginx /var/lib/nginx /run
2225

23-
# Set python alias to python3 (required for Datadog)
24-
RUN alternatives --set python /usr/bin/python3
26+
# Set python alias to Python 3.11 to avoid using Java version
27+
RUN if [ -f /usr/bin/python ] ; then rm /usr/bin/python; fi &&\
28+
if [ -f /usr/bin/python3 ] ; then rm /usr/bin/python3 ; fi &&\
29+
ln -s /usr/bin/python3.11 /usr/bin/python3 &&\
30+
ln -s /usr/bin/python3.11 /usr/bin/python
2531

26-
# Set the user ID
27-
ARG USER_UID=1001
32+
# Create vcap home directory for Datadog configuration
33+
RUN mkdir -p /home/vcap /opt/datadog-agent/run &&\
34+
chown -R ${USER_UID}:0 /home/vcap /opt/datadog-agent/run &&\
35+
chmod -R g=u /home/vcap /opt/datadog-agent/run
36+
37+
# Prepare home directory and set permissions
38+
RUN mkdir -p /opt/mendix &&\
39+
chown -R ${USER_UID}:0 /opt/mendix &&\
40+
chmod -R g=u /opt/mendix &&\
41+
ln -s /opt/mendix/.java /root
2842

2943
# Create user (for non-OpenShift clusters)
3044
RUN echo "mendix:x:${USER_UID}:${USER_UID}:mendix user:/opt/mendix/build:/sbin/nologin" >> /etc/passwd

0 commit comments

Comments
 (0)