diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 64cab1f2..7703f80e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,13 +10,13 @@ on: jobs: - test-ubi8: - name: Test with a ubi8 rootfs + test-ubi9: + name: Test with a ubi9 rootfs runs-on: ubuntu-latest steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # Run the integration test script - name: Run integration tests diff --git a/Dockerfile b/Dockerfile index 9fbcf9d6..4d457cb5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,12 @@ COPY $BUILD_PATH /opt/mendix/build # Use nginx supplied by the base OS ENV NGINX_CUSTOM_BIN_PATH=/usr/sbin/nginx +# Set the user ID +ARG USER_UID=1001 + +# Copy start scripts +COPY scripts/startup.py scripts/vcap_application.json /opt/mendix/build/ + # Each comment corresponds to the script line: # 1. Create cache directory and directory for dependencies which can be shared # 2. Set permissions for compilation scripts @@ -35,44 +41,35 @@ ENV NGINX_CUSTOM_BIN_PATH=/usr/sbin/nginx # 6. Create symlink for java prefs used by CF buildpack # 7. Update ownership of /opt/mendix so that the app can run as a non-root user # 8. Update permissions of /opt/mendix so that the app can run as a non-root user -RUN mkdir -p /tmp/buildcache /tmp/cf-deps /var/mendix/build /var/mendix/build/.local &&\ - chmod +rx /opt/mendix/buildpack/compilation.py /opt/mendix/buildpack/git /opt/mendix/buildpack/buildpack/stage.py &&\ +RUN mkdir -p /tmp/buildcache/bust /tmp/cf-deps /var/mendix/build /var/mendix/build/.local &&\ + chmod +rx /opt/mendix/buildpack/compilation.py /opt/mendix/buildpack/buildpack/stage.py /opt/mendix/build/startup.py &&\ cd /opt/mendix/buildpack &&\ ./compilation.py /opt/mendix/build /tmp/buildcache /tmp/cf-deps 0 &&\ - rm -fr /tmp/buildcache /tmp/javasdk /tmp/opt /tmp/downloads /opt/mendix/buildpack/compilation.py /opt/mendix/buildpack/git &&\ + rm -fr /tmp/buildcache /tmp/javasdk /tmp/opt /tmp/downloads /opt/mendix/buildpack/compilation.py /var/mendix &&\ ln -s /opt/mendix/.java /opt/mendix/build &&\ - chown -R ${USER_UID}:0 /opt/mendix /var/mendix &&\ - chmod -R g=u /opt/mendix /var/mendix + chown -R ${USER_UID}:0 /opt/mendix &&\ + chmod -R g=u /opt/mendix FROM ${ROOTFS_IMAGE} LABEL Author="Mendix Digital Ecosystems" LABEL maintainer="digitalecosystems@mendix.com" -# Set the user ID -ARG USER_UID=1001 +# Install Ruby if Datadog is detected +ARG DD_API_KEY +RUN if [ ! -z "$DD_API_KEY" ] ; then\ + microdnf update -y && \ + microdnf install -y ruby && \ + microdnf clean all && rm -rf /var/cache/yum \ + ; fi + # Set the home path ENV HOME=/opt/mendix/build # Add the buildpack modules ENV PYTHONPATH "/opt/mendix/buildpack/lib/:/opt/mendix/buildpack/:/opt/mendix/buildpack/lib/python3.11/site-packages/" -# Copy start scripts -COPY scripts/startup.py scripts/vcap_application.json /opt/mendix/build/ - -# Create vcap home directory for Datadog configuration -RUN mkdir -p /home/vcap /opt/datadog-agent/run &&\ - chown -R ${USER_UID}:0 /home/vcap /opt/datadog-agent/run &&\ - chmod -R g=u /home/vcap /opt/datadog-agent/run - -# Each comment corresponds to the script line: -# 1. Make the startup script executable -# 2. Update ownership of /opt/mendix so that the app can run as a non-root user -# 3. Update permissions of /opt/mendix so that the app can run as a non-root user -# 4. Ensure that running Java 8 as root will still be able to load offline licenses -RUN chmod +rx /opt/mendix/build/startup.py &&\ - chown -R ${USER_UID}:0 /opt/mendix &&\ - chmod -R g=u /opt/mendix &&\ - ln -s /opt/mendix/.java /root +# Set the user ID +ARG USER_UID=1001 USER ${USER_UID} diff --git a/README.md b/README.md index 11a11d67..ef6900bc 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ This project is a goto reference for the following scenarios : * Docker 20.10 (Installation [here](https://docs.docker.com/engine/installation/)) * Earlier Docker versions are no longer compatible because they don't support multistage builds. To use Docker versions below 20.10, download an earlier Mendix Docker Buildpack release, such as [v2.3.2](https://github.com/mendix/docker-mendix-buildpack/releases/tag/v2.3.2) +* Python 3 * For preparing, a local installation of `curl` * For local testing, make sure you can run the [docker-compose command](https://docs.docker.com/compose/install/) * A Mendix app based on Mendix 8 or a later version @@ -86,7 +87,35 @@ When building the the `rootfs-builder.dockerfile` file, you can provide the foll - **CF_BUILDPACK_URL** specifies the URL where the CF buildpack should be downloaded from (for example, a local mirror). Defaults to `https://github.com/mendix/cf-mendix-buildpack/releases/download/${CF_BUILDPACK}/cf-mendix-buildpack.zip`. Specifying **CF_BUILDPACK_URL** will override the version from **CF_BUILDPACK**. - **BUILDPACK_XTRACE** can be used to enable CF Buildpack [debug logging](https://github.com/mendix/cf-mendix-buildpack#logging-and-debugging). Set this variable to `true` to enable debug logging. -### Compile an app +### Compile an MDA + +If your app is a source MPK file, an MPR project directory or a compressed MDA file, it needs to be converted or compiled into a format supported by CF Buildpack - an extracted MDA file. + +This feature is available in Docker Buildpack version v5.1.0 and later, and is intended to allow building Mendix 10 apps in custom CI/CD pipelines. + +To do this, run: + +```shell +./build.py --source --destination build-mda-dir +``` + +where: + +- **--source** is the path to the project source, such as a project directory (with a source MPR project) or an MPK file. +- **--destination** is a path to an empty directory where the script should output the build result. This directory will contain + * a compiled, extracted MDA file - in a subdirectory called `project`. + * a copy of the `Dockerfile` and the `scripts` directory. +- **--artifacts-repository** - an optional repository to cache MxBuild and Mono build images, for example `quay.io/example/mxbuild-artifacts`. By enabling this option, the `build.py` script will try to use a prebuilt image from this repository if available. + +After the `build.py` script completes, you can proceed with building the app image by running the following command (see next section for more details): + +```shell +docker build --tag mendix/mendix-buildpack:v1.2 +``` + +where `` is the same as used when calling `build.py`. + +### Build an image from an MDA Before running the container, it is necessary to build the image with your application. This buildpack contains Dockerfile with a script that will compile your application using [cf-mendix-buildpack](https://github.com/mendix/cf-mendix-buildpack/). diff --git a/build.py b/build.py new file mode 100755 index 00000000..a44dd690 --- /dev/null +++ b/build.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 + +import argparse +import pathlib +import os +import tempfile +import json +import sqlite3 +import zipfile +import atexit +import shutil +import subprocess +import sys +import selectors +import logging +import platform + +logging.basicConfig( + level=logging.INFO, + stream=sys.stdout +) + +def find_default_file(source, ext): + if os.path.isfile(source): + return source if source.name.endswith(ext) else None + files = [x for x in os.listdir(source) if x.endswith(ext)] + if len(files) == 1: + return os.path.join(source, files[0]) + if len(files) > 1: + raise Exception(f"More than one {ext} file found, can not continue") + return None + +def get_metadata_value(source_dir): + file_name = os.path.join(source_dir, 'model', 'metadata.json') + try: + with open(file_name) as file_handle: + return json.loads(file_handle.read()) + except IOError: + return None + +def extract_zip(mda_file): + temp_dir = tempfile.TemporaryDirectory(prefix='mendix-docker-buildpack') + with zipfile.ZipFile(mda_file) as zip_file: + zip_file.extractall(temp_dir.name) + return temp_dir + +BUILDER_PROCESS = None +def stop_processes(): + if BUILDER_PROCESS is not None: + proc = BUILDER_PROCESS + proc.terminate() + proc.communicate() + proc.wait() + +def container_call(args): + build_executables = ['podman', 'docker'] + build_executable = None + logger_stdout = None + logger_stderr = None + for builder in build_executables: + build_executable = shutil.which(builder) + if build_executable is not None: + logger_stderr = logging.getLogger(builder + '-stderr') + logger_stdout = logging.getLogger(builder + '-stdout') + break + if build_executable is None: + raise Exception('Cannot find Podman or Docker executable') + proc = subprocess.Popen([build_executable] + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) + BUILDER_PROCESS = proc + + sel = selectors.DefaultSelector() + sel.register(proc.stdout, selectors.EVENT_READ) + sel.register(proc.stderr, selectors.EVENT_READ) + + last_line_stdout = None + last_line_stderr = None + stdout_open, stderr_open = True, True + while stdout_open or stderr_open: + for key, _ in sel.select(): + data = key.fileobj.readline() + if data == '': + if key.fileobj is proc.stdout: + stdout_open = False + elif key.fileobj is proc.stderr: + stderr_open = False + continue + data = data.rstrip() + if key.fileobj is proc.stdout: + last_line_stdout = data + logger_stdout.info(data) + elif key.fileobj is proc.stderr: + last_line_stderr = data + # stderr is mostly used for progress notifications, not errors + logger_stderr.info(data) + + sel.close() + BUILDER_PROCESS = None + if proc.wait() != 0: + raise Exception(f"Builder returned with error: {last_line_stderr}") + return last_line_stdout + +def pull_image(image_url): + try: + container_call(['image', 'pull', image_url]) + return image_url + except: + return None + +def delete_container(container_id): + try: + container_call(['container', 'rm', '--force', container_id]) + except Exception as e: + logging.warning('Failed to delete container {}: {}'.format(container_id, e)) + +def build_mpr_builder(mx_version, dotnet, artifacts_repository=None): + builder_image_tag = f"mxbuild-{mx_version}-{dotnet}-{platform.machine()}" + builder_image_url = None + if artifacts_repository is not None: + builder_image_url = f"{artifacts_repository}:{builder_image_tag}" + image_url = pull_image(builder_image_url) + if image_url is not None: + return image_url + else: + builder_image_url = f"mendix-buildpack:{builder_image_tag}" + + prefix = '' + if platform.machine() == 'arm64' and dotnet == 'dotnet': + prefix = 'arm64-' + + mxbuild_filename = f"{prefix}mxbuild-{mx_version}.tar.gz" + mxbuild_url = f"https://download.mendix.com/runtimes/{mxbuild_filename}" + + build_args = ['--build-arg', f"MXBUILD_DOWNLOAD_URL={mxbuild_url}", + '--file', os.path.join('mxbuild', f"{dotnet}.dockerfile"), + '--tag', builder_image_url] + + container_call(['image', 'build'] + build_args + ['mxbuild']) + if artifacts_repository is not None: + try: + container_call(['image', 'push', builder_image_url]) + except Exception as e: + logging.warning('Failed to push mxbuild into artifacts repository: {}; continuing with the build'.format(e)) + return builder_image_url + +def get_git_commit(source_dir): + git_head = os.path.join(source_dir, '.git', 'HEAD') + if not os.path.isfile(git_head): + raise Exception('Project source doesn\'t contain git metadata') + with open(git_head) as git_head: + git_head_line = git_head.readline().split() + if len(git_head_line) == 1: + # Detached commit + return git_head_line[0] + if len(git_head_line) > 2: + raise Exception(f"Unsupported Git HEAD format {git_head_line}") + git_branch = git_head_line[1].split('/') + git_branch_file = os.path.join(*([source_dir, '.git'] + git_branch)) + if not os.path.isfile(git_branch_file): + raise Exception('Git branch file doesn\'t exist') + with open(git_branch_file) as git_branch_file: + return git_branch_file.readline() + + +def build_mpr(source_dir, mpr_file, destination, artifacts_repository=None): + cursor = sqlite3.connect(mpr_file).cursor() + cursor.execute("SELECT _ProductVersion FROM _MetaData LIMIT 1") + mx_version = cursor.fetchone()[0] + mx_version_value = parse_version(mx_version) + logging.debug('Detected Mendix version {}'.format('.'.join(map(str,mx_version_value)))) + dotnet = 'dotnet' if mx_version_value >= (10, 0, 0, 0) else 'mono' + builder_image = build_mpr_builder(mx_version, dotnet, artifacts_repository) + model_version = None + try: + model_version = get_git_commit(source_dir) + except Exception as e: + model_version = 'unversioned' + logging.warning('Cannot determine git commit ({}), will set model version to unversioned'.format(e)) + container_id = container_call(['container', 'create', builder_image, os.path.basename(mpr_file), model_version]) + atexit.register(delete_container, container_id) + container_call(['container', 'cp', os.path.abspath(source_dir)+'/.', f"{container_id}:/workdir/project"]) + build_result = container_call(['start', '--attach', '--interactive', container_id]) + + temp_dir = tempfile.TemporaryDirectory(prefix='mendix-docker-buildpack') + container_call(['container', 'cp', f"{container_id}:/workdir/output.mda", temp_dir.name]) + with zipfile.ZipFile(os.path.join(temp_dir.name, 'output.mda')) as zip_file: + zip_file.extractall(destination) + +def parse_version(version): + return tuple([ int(n) for n in version.split('.') ]) + +def prepare_destination(destination_path): + with os.scandir(destination_path) as entries: + for entry in entries: + if entry.is_dir() and not entry.is_symlink(): + shutil.rmtree(entry.path) + else: + os.remove(entry.path) + project_path = os.path.join(destination_path, 'project') + os.mkdir(project_path, 0o755) + shutil.copytree('scripts', os.path.join(destination_path, 'scripts')) + shutil.copyfile('Dockerfile', os.path.join(destination_path, 'Dockerfile')) + return project_path + +def prepare_mda(source_path, destination_path, artifacts_repository=None): + destination_path = prepare_destination(destination_path) + mpk_file = find_default_file(source_path, '.mpk') + extracted_dir = None + if mpk_file is not None: + extracted_dir = extract_zip(mpk_file) + source_path = extracted_dir.name + mpr_file = find_default_file(source_path, '.mpr') + if mpr_file is not None: + source_path = os.path.abspath(os.path.join(mpr_file, os.pardir)) + return build_mpr(source_path, mpr_file, destination_path, artifacts_repository) + mda_file = find_default_file(source_path, '.mda') + if mda_file is not None: + with zipfile.ZipFile(mda_file) as zip_file: + zip_file.extractall(destination_path) + elif os.path.isdir(source_path): + shutil.copytree(source_path, destination_path, dirs_exist_ok=True) + extracted_mda_file = get_metadata_value(destination_path) + if extracted_mda_file is not None: + return destination_path + else: + raise Exception('No supported files found in source path') + +def build_image(mda_dir): + # TODO: build the full image, or just copy MDA into destination? + mda_path = mda_dir.name if isinstance(mda_dir, tempfile.TemporaryDirectory) else mda_dir + mda_metadata = get_metadata_value(mda_path) + mx_version = mda_metadata['RuntimeVersion'] + java_version = mda_metadata.get('JavaVersion', 11) + logging.debug("Detected Mendix {} Java {}".format(mx_version, java_version)) + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Build a Mendix app') + 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)') + parser.add_argument('--destination', metavar='destination', required=True, nargs='?', type=pathlib.Path, help='Destination for MDA') + parser.add_argument('--artifacts-repository', required=False, nargs='?', metavar='artifacts_repository', type=str, help='Repository to use for caching build images') + parser.add_argument('action', metavar='action', choices=['build-mda-dir'], help='Action to perform') + + args = parser.parse_args() + + atexit.register(stop_processes) + try: + prepare_mda(args.source, args.destination, args.artifacts_repository) + except KeyboardInterrupt: + stop_processes() + raise + # build_image(args.destination) diff --git a/mxbuild/build b/mxbuild/build new file mode 100755 index 00000000..efa403b8 --- /dev/null +++ b/mxbuild/build @@ -0,0 +1,36 @@ +#!/bin/sh +set -e +MPR_FILENAME=$1 +MODEL_VERSION=$2 + +# Required to allow using deprecated checksums +export OPENSSL_ENABLE_SHA1_SIGNATURES=1 +export MONO_STRICT_MS_COMPLIANT=yes + +# Choose if mxbuild should run directly or from mono +MXBUILD_COMMAND=/opt/mendix/modeler/mxbuild +if [ ! -f $MXBUILD_COMMAND ]; then + MXBUILD_COMMAND="mono /opt/mendix/modeler/mxbuild.exe" +fi + +cd /workdir + +if [ -f /workdir/project ]; then + JAVA_VERSION=$(cat java-version) +elif [ -f /opt/mendix/modeler/mx ]; then + JAVA_VERSION=$(/opt/mendix/modeler/mx dump-mpr --unit-type 'Settings$ProjectSettings' /workdir/project/${MPR_FILENAME} | \ + jq -r '.units[] | select(.["$Type"]=="Settings$ProjectSettings") | .["settingsParts"][] | select(.["$Type"]=="Settings$RuntimeSettings").javaVersion | if (. == null or . == "null") then "Java11" else . end') +else + JAVA_VERSION=11 +fi + +JAVA_VERSION=$(echo -n $JAVA_VERSION| sed s/\^Java// | head) + +echo "Detected Java $JAVA_VERSION" +export JDK_HOME=/etc/alternatives/java_sdk_${JAVA_VERSION} + +$MXBUILD_COMMAND \ + --target=package \ + --java-home=${JDK_HOME} --java-exe-path=${JDK_HOME}/bin/java \ + --model-version=${MODEL_VERSION} \ + --output=/workdir/output.mda /workdir/project/${MPR_FILENAME} diff --git a/mxbuild/dotnet.dockerfile b/mxbuild/dotnet.dockerfile new file mode 100644 index 00000000..f6953214 --- /dev/null +++ b/mxbuild/dotnet.dockerfile @@ -0,0 +1,33 @@ +# Dockerfile that can convert an MPR project into an MDA file. +FROM registry.access.redhat.com/ubi9/ubi-minimal:latest + +# Set the locale +ENV LANG C.UTF-8 +ENV LC_ALL C.UTF-8 + +# Set the user ID +ARG USER_UID=1001 +ENV USER_UID=${USER_UID} + +# Install common prerequisites +RUN rpm -ivh https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm &&\ + microdnf update -y && \ + microdnf install -y glibc-langpack-en openssl fontconfig tzdata-java libgdiplus libicu tar jq \ + java-11-openjdk-devel java-17-openjdk-devel java-21-openjdk-devel && \ + microdnf clean all && rm -rf /var/cache/yum + +# Create user (for non-OpenShift clusters) +RUN echo "mendix:x:${USER_UID}:${USER_UID}:mendix user:/workdir:/sbin/nologin" >> /etc/passwd + +# Download and extract MxBuild +ARG MXBUILD_DOWNLOAD_URL +RUN mkdir -p /opt/mendix && curl -sL $MXBUILD_DOWNLOAD_URL | tar -C /opt/mendix -xzf - --owner=root:0 --group=root:0 --mode='uga=rX' +COPY --chown=0:0 --chmod=0755 build /opt/mendix/build + +# Prepare build context +ENV HOME /workdir +RUN mkdir -p /workdir/project /workdir/output /workdir/.local/share/Mendix &&\ + chown -R ${USER_UID}:${USER_UID} /workdir &&\ + chmod -R 755 /workdir + +ENTRYPOINT ["/opt/mendix/build"] diff --git a/mxbuild/mono.dockerfile b/mxbuild/mono.dockerfile new file mode 100644 index 00000000..49554ccd --- /dev/null +++ b/mxbuild/mono.dockerfile @@ -0,0 +1,34 @@ +# Dockerfile that can convert an MPR project into an MDA file. +FROM --platform=linux/amd64 registry.access.redhat.com/ubi8/ubi-minimal:latest + +# Set the locale +ENV LANG C.UTF-8 +ENV LC_ALL C.UTF-8 + +# Set the user ID +ARG USER_UID=1001 +ENV USER_UID=${USER_UID} + +# Add mono repo +COPY --chown=0:0 mono/xamarin.gpg /etc/pki/rpm-gpg/RPM-GPG-KEY-mono-centos8-stable +COPY --chown=0:0 mono/mono-centos8-stable.repo /etc/yum.repos.d/mono-centos8-stable.repo + +# Install mono and common prerequisites +RUN rpm -ivh https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm &&\ + microdnf update -y && \ + microdnf install -y glibc-langpack-en openssl fontconfig tzdata-java mono-core-5.20.1.34 libgdiplus0 libicu tar jq \ + java-11-openjdk-devel java-17-openjdk-devel java-21-openjdk-devel && \ + microdnf clean all && rm -rf /var/cache/yum + +# Download and extract MxBuild +ARG MXBUILD_DOWNLOAD_URL +RUN mkdir -p /opt/mendix && curl -sL $MXBUILD_DOWNLOAD_URL | tar -C /opt/mendix -xzf - --owner=root:0 --group=root:0 --mode='uga=rX' +COPY --chown=0:0 --chmod=0755 build /opt/mendix/build + +# Prepare build context +ENV HOME /workdir +RUN mkdir -p /workdir/project /workdir/output /workdir/.local/share/Mendix &&\ + chown -R ${USER_UID}:${USER_UID} /workdir &&\ + chmod -R 755 /workdir + +ENTRYPOINT ["/opt/mendix/build"] diff --git a/scripts/mono/mono-centos8-stable.repo b/mxbuild/mono/mono-centos8-stable.repo similarity index 100% rename from scripts/mono/mono-centos8-stable.repo rename to mxbuild/mono/mono-centos8-stable.repo diff --git a/scripts/mono/xamarin.gpg b/mxbuild/mono/xamarin.gpg similarity index 100% rename from scripts/mono/xamarin.gpg rename to mxbuild/mono/xamarin.gpg diff --git a/rootfs-app.dockerfile b/rootfs-app.dockerfile index bd9d2a4b..24215472 100644 --- a/rootfs-app.dockerfile +++ b/rootfs-app.dockerfile @@ -1,6 +1,6 @@ # Dockerfile to create a Mendix Docker image based on either the source code or # Mendix Deployment Archive (aka mda file) -FROM --platform=linux/amd64 registry.access.redhat.com/ubi8/ubi-minimal:latest +FROM registry.access.redhat.com/ubi9/ubi-minimal:latest #This version does a full build originating from the Ubuntu Docker images LABEL Author="Mendix Digital Ecosystems" LABEL maintainer="digitalecosystems@mendix.com" @@ -11,20 +11,38 @@ ENV LC_ALL C.UTF-8 # install dependencies & remove package lists RUN microdnf update -y && \ - microdnf module enable nginx:1.20 -y && \ + microdnf module enable nginx:1.24 -y && \ 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 && \ microdnf clean all && rm -rf /var/cache/yum +# Set the user ID +ARG USER_UID=1001 + # Set nginx permissions RUN touch /run/nginx.pid && \ - chown -R 1001:0 /var/log/nginx /var/lib/nginx /run &&\ + chown -R ${USER_UID}:0 /var/log/nginx /var/lib/nginx /run &&\ chmod -R g=u /var/log/nginx /var/lib/nginx /run -# Set python alias to python3 (required for Datadog) -RUN alternatives --set python /usr/bin/python3 +# Set python alias to Python 3.11 to avoid using Java version +RUN if [ -f /usr/bin/python ] ; then rm /usr/bin/python; fi &&\ + if [ -f /usr/bin/python3 ] ; then rm /usr/bin/python3 ; fi &&\ + ln -s /usr/bin/python3.11 /usr/bin/python3 &&\ + ln -s /usr/bin/python3.11 /usr/bin/python -# Set the user ID -ARG USER_UID=1001 +# Create vcap home directory for Datadog configuration +RUN mkdir -p /home /app/log /opt/mendix/build /opt/datadog-agent/run &&\ + ln -s /opt/mendix/build /home/vcap &&\ + chown -R ${USER_UID}:0 /home/vcap /opt/datadog-agent/run /app/log &&\ + chmod -R g=u /home/vcap /opt/datadog-agent/run /app/log + +# Copy Cloud Foundry emulation scripts +COPY --chmod=0755 --chown=0:0 scripts/host /usr/local/bin/ + +# Prepare home directory and set permissions +RUN mkdir -p /opt/mendix &&\ + chown -R ${USER_UID}:0 /opt/mendix &&\ + chmod -R g=u /opt/mendix &&\ + ln -s /opt/mendix/.java /root # Create user (for non-OpenShift clusters) RUN echo "mendix:x:${USER_UID}:${USER_UID}:mendix user:/opt/mendix/build:/sbin/nologin" >> /etc/passwd diff --git a/rootfs-builder.dockerfile b/rootfs-builder.dockerfile index 65ecc8d3..215583b8 100644 --- a/rootfs-builder.dockerfile +++ b/rootfs-builder.dockerfile @@ -1,6 +1,6 @@ # Dockerfile to create a Mendix Docker image based on either the source code or # Mendix Deployment Archive (aka mda file) -FROM --platform=linux/amd64 registry.access.redhat.com/ubi8/ubi-minimal:latest +FROM registry.access.redhat.com/ubi9/ubi-minimal:latest #This version does a full build originating from the Ubuntu Docker images LABEL Author="Mendix Digital Ecosystems" LABEL maintainer="digitalecosystems@mendix.com" @@ -10,42 +10,34 @@ ENV LANG C.UTF-8 ENV LC_ALL C.UTF-8 # CF buildpack version -ARG CF_BUILDPACK=v5.0.16 +ARG CF_BUILDPACK=v5.0.20 # CF buildpack download URL ARG CF_BUILDPACK_URL=https://github.com/mendix/cf-mendix-buildpack/releases/download/${CF_BUILDPACK}/cf-mendix-buildpack.zip -# Set the user ID -ARG USER_UID=1001 -ENV USER_UID=${USER_UID} - # Allow specification of debugging options ARG BUILDPACK_XTRACE -# Add mono repo -COPY --chown=0:0 scripts/mono/xamarin.gpg /etc/pki/rpm-gpg/RPM-GPG-KEY-mono-centos8-stable -COPY --chown=0:0 scripts/mono/mono-centos8-stable.repo /etc/yum.repos.d/mono-centos8-stable.repo - # install dependencies & remove package lists -RUN rpm -ivh https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm &&\ - microdnf update -y && \ - microdnf module enable nginx:1.20 -y && \ - microdnf install -y wget curl glibc-langpack-en python311 openssl tar gzip unzip libpq nginx nginx-mod-stream binutils fontconfig findutils binutils && \ +RUN microdnf update -y && \ + microdnf module enable nginx:1.24 -y && \ + microdnf install -y wget glibc-langpack-en python311 openssl tar gzip unzip libpq nginx nginx-mod-stream binutils fontconfig findutils java-11-openjdk-headless java-17-openjdk-headless java-21-openjdk-headless && \ + microdnf remove -y /usr/bin/python && \ microdnf clean all && rm -rf /var/cache/yum -# Install RHEL alternatives to CF Buildpack dependencies -RUN microdnf install -y java-11-openjdk-headless java-11-openjdk-devel java-17-openjdk-headless java-17-openjdk-devel java-21-openjdk-headless java-21-openjdk-devel tzdata-java mono-core-5.20.1.34 libgdiplus0 libicu && \ - microdnf clean all && rm -rf /var/cache/yum +# Set the user ID +ARG USER_UID=1001 +ENV USER_UID=${USER_UID} # Set nginx permissions RUN touch /run/nginx.pid && \ - chown -R 1001:0 /var/log/nginx /var/lib/nginx /run/nginx.pid &&\ - chmod -R g=u /var/log/nginx /var/lib/nginx /run/nginx.pid - -# Pretend to be Ubuntu to bypass CF Buildpack's check -RUN rm /etc/*-release && printf 'NAME="Ubuntu"\nID=ubuntu\nVersion="22.04 LTS (Jammy Jellyfish)"\nVERSION_CODENAME=jammy\n' > /etc/os-release + chown -R ${USER_UID}:0 /var/log/nginx /var/lib/nginx /run &&\ + chmod -R g=u /var/log/nginx /var/lib/nginx /run -# Set python alias to python3 (required for Datadog) -RUN alternatives --set python /usr/bin/python3 +# Set python alias to Python 3.11 to avoid using Java version +RUN if [ -f /usr/bin/python ] ; then rm /usr/bin/python; fi &&\ + if [ -f /usr/bin/python3 ] ; then rm /usr/bin/python3 ; fi &&\ + ln -s /usr/bin/python3.11 /usr/bin/python3 &&\ + ln -s /usr/bin/python3.11 /usr/bin/python # Download and prepare CF Buildpack @@ -69,14 +61,16 @@ RUN mkdir -p /opt/mendix/buildpack /opt/mendix/build &&\ chmod -R g=u /opt/mendix # Copy python scripts which execute the buildpack (exporting the VCAP variables) -COPY scripts/compilation.py scripts/git scripts/mono-adapter /opt/mendix/buildpack/ +COPY scripts/compilation.py /opt/mendix/buildpack/ # Install the buildpack Python dependencies RUN PYTHON_BUILD_RPMS="python3.11-pip python3.11-devel libffi-devel gcc" && \ microdnf install -y $PYTHON_BUILD_RPMS && \ + mkdir -p /home/vcap/.local/bin/ && \ + if [ ! -f /home/vcap/.local/bin/pip ] ; then ln -s /usr/bin/pip3.11 /home/vcap/.local/bin/pip ; fi && \ rm /opt/mendix/buildpack/vendor/wheels/* && \ chmod +rx /opt/mendix/buildpack/bin/bootstrap-python && /opt/mendix/buildpack/bin/bootstrap-python /opt/mendix/buildpack /tmp/buildcache && \ microdnf remove -y $PYTHON_BUILD_RPMS && microdnf clean all && rm -rf /var/cache/yum # Add the buildpack modules -ENV PYTHONPATH "$PYTHONPATH:/opt/mendix/buildpack/lib/:/opt/mendix/buildpack/:/opt/mendix/buildpack/lib/python3.11/site-packages/" +ENV PYTHONPATH "$PYTHONPATH:/opt/mendix/buildpack/lib/:/opt/mendix/buildpack/:/opt/mendix/buildpack/lib/python3.11/site-packages" diff --git a/scripts/compilation.py b/scripts/compilation.py index 29d29129..03f16771 100755 --- a/scripts/compilation.py +++ b/scripts/compilation.py @@ -37,46 +37,10 @@ def replace_cf_dependencies(): mx_version = runtime.get_runtime_version("/opt/mendix/build") logging.debug("Detected Mendix version {0}".format(mx_version)) - # Only mono 5 is supported by Docker Buildpack - mono_dependency = get_dependency("mono.5-jammy", "/opt/mendix/buildpack") - logging.debug("Creating symlink for mono {0}".format(mono_dependency['artifact'])) - - util.mkdir_p("/tmp/buildcache/bust") - mono_cache_artifact = f"/tmp/buildcache/bust/mono-{mono_dependency['version']}-mx-ubuntu-jammy.tar.gz" - with tarfile.open(mono_cache_artifact, "w:gz") as tar: - if mx_version.major >= 10: - # Mono is not needed for Mendix 10, use the bash adapter script - symlinks = {'mono/bin/mono':'/opt/mendix/buildpack/mono-adapter', 'mono/lib': '/usr/lib64'} - else: - # Symlinks to use mono from host OS - symlinks = {'mono/bin':'/usr/bin', 'mono/lib': '/usr/lib64', 'mono/etc': '/etc'} - for source, destination in symlinks.items(): - symlink = tarfile.TarInfo(source) - symlink.type = tarfile.SYMTYPE - symlink.linkname = destination - tar.addfile(symlink) - get_jdk_dependency("java.11-jdk","java_sdk_11") - get_jdk_dependency("java.17-jdk","java_sdk_17") - get_jdk_dependency("java.21-jdk","java_sdk_21") get_jre_dependency("java.11-jre","jre_11") get_jre_dependency("java.17-jre","jre_17") get_jre_dependency("java.21-jre","jre_21") -# JDK 11, 17, 21 support by Docker Buildpack -def get_jdk_dependency(jdk_version, jdk_destination_version): - jdk_dependency = get_dependency(jdk_version, "/opt/mendix/buildpack") - logging.debug("Creating symlink for jdk {0}".format(jdk_dependency['artifact'])) - jdk_cache_artifact = f"/tmp/buildcache/bust/{jdk_dependency['artifact']}" - jdk_destination = '/etc/alternatives/'+jdk_destination_version - with tarfile.open(jdk_cache_artifact, "w:gz") as tar: - # Symlinks to use jdk from host OS - for jdk_dir in os.listdir(jdk_destination): - symlink = tarfile.TarInfo(f"jdk/{jdk_dir}") - symlink.type = tarfile.SYMTYPE - symlink.linkname = f"{jdk_destination}/{jdk_dir}" - tar.addfile(symlink) - - # JRE 11, 17, 21 support by Docker Buildpack def get_jre_dependency(jre_version, jre_destination_version): jre_dependency = get_dependency(jre_version, "/opt/mendix/buildpack") diff --git a/scripts/git b/scripts/git deleted file mode 100644 index cae87e94..00000000 --- a/scripts/git +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -echo Git_Commit diff --git a/scripts/host b/scripts/host new file mode 100755 index 00000000..a5a0d956 --- /dev/null +++ b/scripts/host @@ -0,0 +1,2 @@ +#!/bin/sh +cat /etc/hostname \ No newline at end of file diff --git a/scripts/mono-adapter b/scripts/mono-adapter deleted file mode 100755 index cc0caf13..00000000 --- a/scripts/mono-adapter +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/sh - -# This script works as a drop-in replacement for mono and allows using non-mono mxbuild in CF Buildpack. -# It's a temporary workaround until https://github.com/mendix/cf-mendix-buildpack/pull/661 is available in CF Buildpack v5. - -MONO_ARGS=() -# Rewrite the command to exclude mono and its args, only keep mxbuild -while [[ $# -gt 0 ]]; do - case $1 in - *mxbuild.exe) - # Remove .exe from mxbuild executable name - MONO_ARGS=(${1%.*}) - shift - ;; - *) - # Keep all args after mxbuild - if [ -n "$MONO_ARGS" ]; then - MONO_ARGS+=("$1") - fi - shift - ;; - esac -done - -echo "Launching MxBuild through mono adapter..." - -exec "${MONO_ARGS[@]}" diff --git a/scripts/vcap_application.json b/scripts/vcap_application.json index 954caf38..3a132828 100644 --- a/scripts/vcap_application.json +++ b/scripts/vcap_application.json @@ -1,6 +1,6 @@ { - "application_name": "docker_example", - "application_uris": [ "docker_example.com" ], + "application_name": "dockerexample", + "application_uris": [ "docker-buildpack.example.com" ], "limits": { "disk": 1024, "fds": 16384, diff --git a/tests/integrationtest.sh b/tests/integrationtest.sh index 1062f360..344941bb 100755 --- a/tests/integrationtest.sh +++ b/tests/integrationtest.sh @@ -1,12 +1,15 @@ #!/bin/sh set -eux docker version -docker-compose version +docker compose version echo "Downloading test project" -mkdir -p downloads build +mkdir -p downloads curl -L https://s3-eu-west-1.amazonaws.com/mx-buildpack-ci/BuildpackTestApp-mx-7-16.mda -o downloads/application.mpk -unzip downloads/application.mpk -d build/ + +echo "Building MDA file" +mkdir -p /tmp/mda-dir +./build.py --source downloads/application.mpk --destination /tmp/mda-dir build-mda-dir echo "Building app rootfs" docker build -t mendix-rootfs:app -f rootfs-app.dockerfile . @@ -17,10 +20,9 @@ docker build -t mendix-rootfs:builder -f rootfs-builder.dockerfile . echo "Building test app" export BUILDPACK_VERSION=`git rev-parse HEAD` docker build \ - --build-arg BUILD_PATH=build \ --build-arg BUILDER_ROOTFS_IMAGE=mendix-rootfs:builder \ --build-arg ROOTFS_IMAGE=mendix-rootfs:app \ - -t mendix-testapp:$BUILDPACK_VERSION . + -t mendix-testapp:$BUILDPACK_VERSION /tmp/mda-dir tests/test-generic.sh tests/docker-compose-postgres.yml diff --git a/tests/test-generic.sh b/tests/test-generic.sh index e7f85ace..63d3ef63 100755 --- a/tests/test-generic.sh +++ b/tests/test-generic.sh @@ -6,7 +6,7 @@ COMPOSEFILE=$1 TIMEOUT=180s echo "test.sh [TEST STARTED] starting docker-compose $COMPOSEFILE" -docker-compose -f $COMPOSEFILE up & +docker compose -f $COMPOSEFILE up & timeout $TIMEOUT bash -c 'until curl -s http://localhost:8080 | grep "Mendix"; do sleep 5; done' @@ -17,5 +17,5 @@ if [ $RETURN_CODE -eq "0" ]; then else echo "test.sh [TEST FAILED] App is not reachable in timeout delay $TIMEOUT for $COMPOSEFILE" fi -docker-compose -f $COMPOSEFILE kill +docker compose -f $COMPOSEFILE kill exit $RETURN_CODE