From 865c130fa68c9fe5ce60d35351d3fafad6da6b4b Mon Sep 17 00:00:00 2001 From: Neo Technology Build Agent Date: Tue, 23 Jul 2024 13:55:16 +0000 Subject: [PATCH] Publishing 4.4.36 --- 4.4.36/bullseye/community/Dockerfile | 56 ++ .../local-package/docker-entrypoint.sh | 647 ++++++++++++++++++ .../local-package/neo4j-admin-report.sh | 57 ++ .../local-package/neo4j-plugins.json | 60 ++ .../community/local-package/semver.jq | 22 + .../community/local-package/utilities.sh | 54 ++ 4.4.36/bullseye/enterprise/Dockerfile | 56 ++ .../local-package/docker-entrypoint.sh | 647 ++++++++++++++++++ .../local-package/neo4j-admin-report.sh | 57 ++ .../local-package/neo4j-plugins.json | 60 ++ .../enterprise/local-package/semver.jq | 22 + .../enterprise/local-package/utilities.sh | 54 ++ 4.4.36/ubi9/community/Dockerfile | 96 +++ .../local-package/docker-entrypoint.sh | 647 ++++++++++++++++++ .../local-package/neo4j-admin-report.sh | 57 ++ .../local-package/neo4j-plugins.json | 60 ++ 4.4.36/ubi9/community/local-package/semver.jq | 22 + .../ubi9/community/local-package/utilities.sh | 54 ++ 4.4.36/ubi9/enterprise/Dockerfile | 96 +++ .../local-package/docker-entrypoint.sh | 647 ++++++++++++++++++ .../local-package/neo4j-admin-report.sh | 57 ++ .../local-package/neo4j-plugins.json | 60 ++ .../ubi9/enterprise/local-package/semver.jq | 22 + .../enterprise/local-package/utilities.sh | 54 ++ 24 files changed, 3664 insertions(+) create mode 100644 4.4.36/bullseye/community/Dockerfile create mode 100755 4.4.36/bullseye/community/local-package/docker-entrypoint.sh create mode 100755 4.4.36/bullseye/community/local-package/neo4j-admin-report.sh create mode 100644 4.4.36/bullseye/community/local-package/neo4j-plugins.json create mode 100755 4.4.36/bullseye/community/local-package/semver.jq create mode 100644 4.4.36/bullseye/community/local-package/utilities.sh create mode 100644 4.4.36/bullseye/enterprise/Dockerfile create mode 100755 4.4.36/bullseye/enterprise/local-package/docker-entrypoint.sh create mode 100755 4.4.36/bullseye/enterprise/local-package/neo4j-admin-report.sh create mode 100644 4.4.36/bullseye/enterprise/local-package/neo4j-plugins.json create mode 100755 4.4.36/bullseye/enterprise/local-package/semver.jq create mode 100644 4.4.36/bullseye/enterprise/local-package/utilities.sh create mode 100644 4.4.36/ubi9/community/Dockerfile create mode 100755 4.4.36/ubi9/community/local-package/docker-entrypoint.sh create mode 100755 4.4.36/ubi9/community/local-package/neo4j-admin-report.sh create mode 100644 4.4.36/ubi9/community/local-package/neo4j-plugins.json create mode 100755 4.4.36/ubi9/community/local-package/semver.jq create mode 100644 4.4.36/ubi9/community/local-package/utilities.sh create mode 100644 4.4.36/ubi9/enterprise/Dockerfile create mode 100755 4.4.36/ubi9/enterprise/local-package/docker-entrypoint.sh create mode 100755 4.4.36/ubi9/enterprise/local-package/neo4j-admin-report.sh create mode 100644 4.4.36/ubi9/enterprise/local-package/neo4j-plugins.json create mode 100755 4.4.36/ubi9/enterprise/local-package/semver.jq create mode 100644 4.4.36/ubi9/enterprise/local-package/utilities.sh diff --git a/4.4.36/bullseye/community/Dockerfile b/4.4.36/bullseye/community/Dockerfile new file mode 100644 index 0000000..f932a1b --- /dev/null +++ b/4.4.36/bullseye/community/Dockerfile @@ -0,0 +1,56 @@ +FROM debian:bullseye-slim +ENV JAVA_HOME=/opt/java/openjdk +COPY --from=eclipse-temurin:11 $JAVA_HOME $JAVA_HOME +ENV PATH="${JAVA_HOME}/bin:${PATH}" \ + NEO4J_SHA256=e870cd27e1c829998921addfc289dc0f44f838a4241e3cadf3acd94953a66a38 \ + NEO4J_TARBALL=neo4j-community-4.4.36-unix.tar.gz \ + NEO4J_EDITION=community \ + NEO4J_HOME="/var/lib/neo4j" \ + LANG=C.UTF-8 +ARG NEO4J_URI=https://dist.neo4j.org/neo4j-community-4.4.36-unix.tar.gz + +RUN addgroup --gid 7474 --system neo4j && adduser --uid 7474 --system --no-create-home --home "${NEO4J_HOME}" --ingroup neo4j neo4j + +COPY ./local-package/* /startup/ + +RUN apt update \ + && apt-get install -y curl gcc git jq make procps tini wget \ + && curl --fail --silent --show-error --location --remote-name ${NEO4J_URI} \ + && echo "${NEO4J_SHA256} ${NEO4J_TARBALL}" | sha256sum -c --strict --quiet \ + && tar --extract --file ${NEO4J_TARBALL} --directory /var/lib \ + && mv /var/lib/neo4j-* "${NEO4J_HOME}" \ + && rm ${NEO4J_TARBALL} \ + && mv /startup/neo4j-admin-report.sh "${NEO4J_HOME}"/bin/neo4j-admin-report \ + && mv "${NEO4J_HOME}"/data /data \ + && mv "${NEO4J_HOME}"/logs /logs \ + && chown -R neo4j:neo4j /data \ + && chmod -R 777 /data \ + && chown -R neo4j:neo4j /logs \ + && chmod -R 777 /logs \ + && chown -R neo4j:neo4j "${NEO4J_HOME}" \ + && chmod -R 777 "${NEO4J_HOME}" \ + && chmod -R 755 "${NEO4J_HOME}/bin" \ + && ln -s /data "${NEO4J_HOME}"/data \ + && ln -s /logs "${NEO4J_HOME}"/logs \ + && ln -s /startup/docker-entrypoint.sh /docker-entrypoint.sh \ + && git clone https://github.com/ncopa/su-exec.git \ + && cd su-exec \ + && git checkout 4c3bb42b093f14da70d8ab924b487ccfbb1397af \ + && echo d6c40440609a23483f12eb6295b5191e94baf08298a856bab6e15b10c3b82891 su-exec.c | sha256sum -c \ + && echo 2a87af245eb125aca9305a0b1025525ac80825590800f047419dc57bba36b334 Makefile | sha256sum -c \ + && make \ + && mv /su-exec/su-exec /usr/bin/su-exec \ + && apt-get -y purge --auto-remove curl gcc git make \ + && rm -rf /var/lib/apt/lists/* /su-exec + + +ENV PATH "${NEO4J_HOME}"/bin:$PATH + +WORKDIR "${NEO4J_HOME}" + +VOLUME /data /logs + +EXPOSE 7474 7473 7687 + +ENTRYPOINT ["tini", "-g", "--", "/startup/docker-entrypoint.sh"] +CMD ["neo4j"] diff --git a/4.4.36/bullseye/community/local-package/docker-entrypoint.sh b/4.4.36/bullseye/community/local-package/docker-entrypoint.sh new file mode 100755 index 0000000..2b105f4 --- /dev/null +++ b/4.4.36/bullseye/community/local-package/docker-entrypoint.sh @@ -0,0 +1,647 @@ +#!/bin/bash -eu + +cmd="$1" + +# load useful utility functions +. /startup/utilities.sh + +function is_readable +{ + # this code is fairly ugly but works no matter who this script is running as. + # It would be nice if the writability tests could use this logic somehow. + local _file=${1} + perm=$(stat -c %a "${_file}") + + # everyone permission + if [[ ${perm:2:1} -ge 4 ]]; then + return 0 + fi + # owner permissions + if [[ ${perm:0:1} -ge 4 ]]; then + if [[ "$(stat -c %U ${_file})" = "${userid}" ]] || [[ "$(stat -c %u ${_file})" = "${userid}" ]]; then + return 0 + fi + fi + # group permissions + if [[ ${perm:1:1} -ge 4 ]]; then + if containsElement "$(stat -c %g ${_file})" "${groups[@]}" || containsElement "$(stat -c %G ${_file})" "${groups[@]}" ; then + return 0 + fi + fi + return 1 +} + +function is_writable +{ + # It would be nice if this and the is_readable function could combine somehow + local _file=${1} + perm=$(stat -c %a "${_file}") + + # everyone permission + if containsElement ${perm:2:1} 2 3 6 7; then + return 0 + fi + # owner permissions + if containsElement ${perm:0:1} 2 3 6 7; then + if [[ "$(stat -c %U ${_file})" = "${userid}" ]] || [[ "$(stat -c %u ${_file})" = "${userid}" ]]; then + return 0 + fi + fi + # group permissions + if containsElement ${perm:1:1} 2 3 6 7; then + if containsElement "$(stat -c %g ${_file})" "${groups[@]}" || containsElement "$(stat -c %G ${_file})" "${groups[@]}" ; then + return 0 + fi + fi + return 1 +} + +function check_mounted_folder_readable +{ + local _directory=${1} + debug_msg "checking ${_directory} is readable" + if ! is_readable "${_directory}"; then + print_permissions_advice_and_fail "${_directory}" "${userid}" "${groupid}" + fi +} + +function check_mounted_folder_writable_with_chown +{ +# The /data and /log directory are a bit different because they are very likely to be mounted by the user but not +# necessarily writable. +# This depends on whether a user ID is passed to the container and which folders are mounted. +# +# No user ID passed to container: +# 1) No folders are mounted. +# The /data and /log folder are owned by neo4j by default, so should be writable already. +# 2) Both /log and /data are mounted. +# This means on start up, /data and /logs are owned by an unknown user and we should chown them to neo4j for +# backwards compatibility. +# +# User ID passed to container: +# 1) Both /data and /logs are mounted +# The /data and /logs folders are owned by an unknown user but we *should* have rw permission to them. +# That should be verified and error (helpfully) if not. +# 2) User mounts /data or /logs *but not both* +# The unmounted folder is still owned by neo4j, which should already be writable. The mounted folder should +# have rw permissions through user id. This should be verified. +# 3) No folders are mounted. +# The /data and /log folder are owned by neo4j by default, and these are already writable by the user. +# (This is a very unlikely use case). + + local mountFolder=${1} + debug_msg "checking ${mountFolder} is writable" + if running_as_root && ! secure_mode_enabled; then + # check folder permissions + if ! is_writable "${mountFolder}" ; then + # warn that we're about to chown the folder and then chown it + echo "Warning: Folder mounted to \"${mountFolder}\" is not writable from inside container. Changing folder owner to ${userid}." + chown -R "${userid}":"${groupid}" "${mountFolder}" + # check permissions on files in the folder + elif [ $(su-exec "${userid}":"${groupid}" find "${mountFolder}" -not -writable | wc -l) -gt 0 ]; then + echo "Warning: Some files inside \"${mountFolder}\" are not writable from inside container. Changing folder owner to ${userid}." + chown -R "${userid}":"${groupid}" "${mountFolder}" + fi + else + if [[ ! -w "${mountFolder}" ]] && [[ "$(stat -c %U ${mountFolder})" != "neo4j" ]]; then + print_permissions_advice_and_fail "${mountFolder}" "${userid}" "${groupid}" + fi + fi +} + +function load_plugin_from_location +{ + # Install a plugin from location at runtime. + local _plugin_name="${1}" + local _location="${2}" + + local _plugins_dir="${NEO4J_HOME}/plugins" + if [ -d /plugins ]; then + local _plugins_dir="/plugins" + fi + + local _destination="${_plugins_dir}/${_plugin_name}.jar" + + # Now we install the plugin that is shipped with Neo4j + for filename in ${_location}; do + echo "Installing Plugin '${_plugin_name}' from ${_location} to ${_destination}" + cp --preserve "${filename}" "${_destination}" + chmod +rw ${_destination} + done + + if ! is_readable "${_destination}"; then + echo >&2 "Plugin at '${_destination}' is not readable" + exit 1 + fi +} + +function load_plugin_from_url +{ + # Load a plugin at runtime. The provided github repository must have a versions.json on the master branch with the + # correct format. + local _plugin_name="${1}" #e.g. apoc, graph-algorithms, graph-ql + + local _plugins_dir="${NEO4J_HOME}/plugins" + if [ -d /plugins ]; then + local _plugins_dir="/plugins" + fi + local _versions_json_url="$(jq --raw-output "with_entries( select(.key==\"${_plugin_name}\") ) | to_entries[] | .value.versions" /startup/neo4j-plugins.json )" + debug_msg "Will read ${_plugin_name} versions.json from ${_versions_json_url}" + # Using the same name for the plugin irrespective of version ensures we don't end up with different versions of the same plugin + local _destination="${_plugins_dir}/${_plugin_name}.jar" + local _neo4j_version="$(neo4j --version | cut -d' ' -f2)" + + # Now we call out to github to get the versions.json for this plugin and we parse that to find the url for the correct plugin jar for our neo4j version + echo "Fetching versions.json for Plugin '${_plugin_name}' from ${_versions_json_url}" + local _versions_json + if ! _versions_json="$(wget -q --timeout 300 --tries 30 -O - "${_versions_json_url}")"; then + debug_msg "ERROR: could not fetch '${_versions_json}'" + echo >&2 "ERROR: could not query ${_versions_json_url} for plugin compatibility information. + This could indicate a problem with your network or this container's network settings. + Neo4j will continue to start, but \"${_plugin_name}\" will not be loaded." + return + fi + local _plugin_jar_url="$(echo "${_versions_json}" | jq -L/startup --raw-output "import \"semver\" as lib; [ .[] | select(.neo4j|lib::semver(\"${_neo4j_version}\")) ] | min_by(.neo4j) | .jar")" + if [[ -z "${_plugin_jar_url}" ]] || [[ "${_plugin_jar_url}" == "null" ]]; then + debug_msg "ERROR: '${_versions_json_url}' does not contain an entry for ${_neo4j_version}" + echo >&2 "ERROR: No compatible \"${_plugin_name}\" plugin found for Neo4j ${_neo4j_version} ${NEO4J_EDITION}. + This can happen with the newest Neo4j versions when a compatible plugin has not yet been released. + You can either use an older version of Neo4j, or continue without ${_plugin_name}. + Neo4j will continue to start, but \"${_plugin_name}\" will not be loaded." + else + echo "Installing Plugin '${_plugin_name}' from ${_plugin_jar_url} to ${_destination} " + wget -q --timeout 300 --tries 30 --output-document="${_destination}" "${_plugin_jar_url}" + + if ! is_readable "${_destination}"; then + echo >&2 "Plugin at '${_destination}' is not readable" + exit 1 + fi + fi +} + +function apply_plugin_default_configuration +{ + # Set the correct Load a plugin at runtime. The provided github repository must have a versions.json on the master branch with the + # correct format. + local _plugin_name="${1}" #e.g. apoc, graph-algorithms, graphql + local _reference_conf="${2}" # used to determine if we can override properties + local _neo4j_conf="${NEO4J_HOME}/conf/neo4j.conf" + + local _property _value + echo "Applying default values for plugin ${_plugin_name} to neo4j.conf" + for _entry in $(jq --compact-output --raw-output "with_entries( select(.key==\"${_plugin_name}\") ) | to_entries[] | .value.properties | to_entries[]" /startup/neo4j-plugins.json); do + _property="$(jq --raw-output '.key' <<< "${_entry}")" + _value="$(jq --raw-output '.value' <<< "${_entry}")" + debug_msg "${_plugin_name} requires setting ${_property}=${_value}" + + # the first grep strips out comments + if grep -o "^[^#]*" "${_reference_conf}" | grep -q --fixed-strings "${_property}=" ; then + # property is already set in the user provided config. In this case we don't override what has been set explicitly by the user. + echo "Skipping ${_property} for plugin ${_plugin_name} because it is already set." + echo "You may need to add ${_value} to the ${_property} setting in your configuration file." + else + if grep -o "^[^#]*" "${_neo4j_conf}" | grep -q --fixed-strings "${_property}=" ; then + sed --in-place "s/${_property}=/&${_value},/" "${_neo4j_conf}" + debug_msg "${_property} was already in the configuration file, so ${_value} was added to it." + else + echo -e "\n${_property}=${_value}" >> "${_neo4j_conf}" + debug_msg "${_property}=${_value} has been added to the configuration file." + fi + fi + done +} + +function install_neo4j_labs_plugins +{ + # first verify that the requested plugins are valid. + debug_msg "One or more NEO4J_PLUGINS have been requested." + local _known_plugins=($(jq --raw-output "keys[]" /startup/neo4j-plugins.json)) + debug_msg "Checking requested plugins are known and can be installed." + for plugin_name in $(echo "${NEO4J_PLUGINS}" | jq --raw-output '.[]'); do + if ! containsElement "${plugin_name}" "${_known_plugins[@]}"; then + printf >&2 "\"%s\" is not a known Neo4j plugin. Options are:\n%s" "${plugin_name}" "$(jq --raw-output "keys[1:][]" /startup/neo4j-plugins.json)" + exit 1 + fi + done + + # We store a copy of the config before we modify it for the plugins to allow us to see if there are user-set values in the input config that we shouldn't override + local _old_config="$(mktemp)" + if [ -e "${NEO4J_HOME}"/conf/neo4j.conf ]; then + cp "${NEO4J_HOME}"/conf/neo4j.conf "${_old_config}" + else + touch "${NEO4J_HOME}"/conf/neo4j.conf + touch "${_old_config}" + fi + for plugin_name in $(echo "${NEO4J_PLUGINS}" | jq --raw-output '.[]'); do + debug_msg "Plugin ${plugin_name} has been requested" + local _location="$(jq --raw-output "with_entries( select(.key==\"${plugin_name}\") ) | to_entries[] | .value.location" /startup/neo4j-plugins.json )" + if [ "${_location}" != "null" -a -n "$(shopt -s nullglob; echo ${_location})" ]; then + debug_msg "$plugin_name is already in the container at ${_location}" + load_plugin_from_location "${plugin_name}" "${_location}" + else + debug_msg "$plugin_name must be downloaded." + load_plugin_from_url "${plugin_name}" + fi + debug_msg "Applying plugin specific configurations." + apply_plugin_default_configuration "${plugin_name}" "${_old_config}" + done + rm "${_old_config}" +} + +function add_docker_default_to_conf +{ + # docker defaults should NOT overwrite values already in the conf file + local _setting="${1}" + local _value="${2}" + + if ! grep -q "^${_setting}=" "${NEO4J_HOME}"/conf/neo4j.conf + then + debug_msg "Appended ${_setting}=${_value} to ${NEO4J_HOME}/conf/neo4j.conf" + echo -e "\n"${_setting}=${_value} >> "${NEO4J_HOME}"/conf/neo4j.conf + fi +} + +function add_env_setting_to_conf +{ + # settings from environment variables should overwrite values already in the conf + local _setting=${1} + local _value=${2} + local _append_not_replace_configs=("dbms.jvm.additional") + + if grep -q -F "${_setting}=" "${NEO4J_HOME}"/conf/neo4j.conf; then + if containsElement "${_setting}" "${_append_not_replace_configs[@]}"; then + debug_msg "${_setting} will be appended to neo4j.conf without replacing existing settings." + else + # Remove any lines containing the setting already + debug_msg "Removing existing setting for ${_setting}" + sed --in-place "/^${_setting}=.*/d" "${NEO4J_HOME}"/conf/neo4j.conf + fi + fi + # Then always append setting to file + debug_msg "Appended ${_setting}=${_value} to ${NEO4J_HOME}/conf/neo4j.conf" + echo "${_setting}=${_value}" >> "${NEO4J_HOME}"/conf/neo4j.conf +} + +function set_initial_password +{ + local _neo4j_auth="${1}" + + # set the neo4j initial password only if you run the database server + if [ "${cmd}" == "neo4j" ]; then + if [ "${_neo4j_auth:-}" == "none" ]; then + debug_msg "Authentication is requested to be unset" + add_env_setting_to_conf "dbms.security.auth_enabled" "false" + elif [[ "${_neo4j_auth:-}" =~ ^([^/]+)\/([^/]+)/?([tT][rR][uU][eE])?$ ]]; then + admin_user="${BASH_REMATCH[1]}" + password="${BASH_REMATCH[2]}" + do_reset="${BASH_REMATCH[3]}" + + if [ "${password}" == "neo4j" ]; then + echo >&2 "Invalid value for password. It cannot be 'neo4j', which is the default." + exit 1 + fi + if [ "${admin_user}" != "neo4j" ]; then + echo >&2 "Invalid admin username, it must be neo4j." + exit 1 + fi + + if running_as_root; then + # running set-initial-password as root will create subfolders to /data as root, causing startup fail when neo4j can't read or write the /data/dbms folder + # creating the folder first will avoid that + mkdir -p /data/dbms + debug_msg "Making sure /data/dbms is owned by ${userid}:${groupid}" + chown "${userid}":"${groupid}" /data/dbms + fi + + local extra_args=() + if [ "${do_reset}" == "true" ]; then + extra_args+=("--require-password-change") + fi + if [ "${EXTENDED_CONF+"yes"}" == "yes" ]; then + extra_args+=("--expand-commands") + fi + if debugging_enabled; then + extra_args+=("--verbose") + fi + debug_msg "Setting initial password" + debug_msg "${neo4j_admin_cmd} set-initial-password ***** ${extra_args[*]}" + if debugging_enabled; then + # don't suppress any output or errors in debugging mode + ${neo4j_admin_cmd} set-initial-password "${password}" "${extra_args[@]}" + else + # Will exit with error if users already exist (and print a message explaining that) + # we probably don't want the message though, since it throws an error message on restarting the container. + ${neo4j_admin_cmd} set-initial-password "${password}" "${extra_args[@]}" 2>/dev/null || true + fi + + elif [ -n "${_neo4j_auth:-}" ]; then + echo "$_neo4j_auth is invalid" + echo >&2 "Invalid value for NEO4J_AUTH: '${_neo4j_auth}'" + exit 1 + fi + fi +} + +# ==== CODE STARTS ==== +debug_msg "DEBUGGING ENABLED" + +# If we're running as root, then run as the neo4j user. Otherwise +# docker is running with --user and we simply use that user. Note +# that su-exec, despite its name, does not replicate the functionality +# of exec, so we need to use both +if running_as_root; then + userid="neo4j" + groupid="neo4j" + groups=($(id -G neo4j)) + exec_cmd="exec su-exec neo4j:neo4j" + neo4j_admin_cmd="su-exec neo4j:neo4j neo4j-admin" + debug_msg "Running as root user inside neo4j image" +else + userid="$(id -u)" + groupid="$(id -g)" + groups=($(id -G)) + exec_cmd="exec" + neo4j_admin_cmd="neo4j-admin" + debug_msg "Running as user ${userid}:${groupid} inside neo4j image" +fi +readonly userid +readonly groupid +readonly groups +readonly exec_cmd +readonly neo4j_admin_cmd + +# Need to chown the home directory +if running_as_root; then + debug_msg "chowning ${NEO4J_HOME} recursively to ${userid}":"${groupid}" + chown -R "${userid}":"${groupid}" "${NEO4J_HOME}" + chmod 700 "${NEO4J_HOME}" + find "${NEO4J_HOME}" -mindepth 1 -maxdepth 1 -type d -exec chmod -R 700 {} \; + debug_msg "Setting all files in ${NEO4J_HOME}/conf to permissions 600" + find "${NEO4J_HOME}"/conf -type f -exec chmod -R 600 {} \; +fi + +# ==== CHECK LICENSE AGREEMENT ==== + +# Only prompt for license agreement if command contains "neo4j" in it +if [[ "${cmd}" == *"neo4j"* ]]; then + if [ "${NEO4J_EDITION}" == "enterprise" ]; then + if [ "${NEO4J_ACCEPT_LICENSE_AGREEMENT:=no}" != "yes" ]; then + echo >&2 " +In order to use Neo4j Enterprise Edition you must accept the license agreement. + +The license agreement is available at https://neo4j.com/terms/licensing/ +If you have a support contract the following terms apply https://neo4j.com/terms/support-terms/ + +(c) Neo4j Sweden AB. All Rights Reserved. +Use of this Software without a proper commercial license +with Neo4j, Inc. or its affiliates is prohibited. +Neo4j has the right to terminate your usage if you are not compliant. + +More information is also available at: https://neo4j.com/licensing/ +If you have further inquiries about licensing, please contact us via https://neo4j.com/contact-us/ + +To accept the commercial license agreement set the environment variable +NEO4J_ACCEPT_LICENSE_AGREEMENT=yes + +To do this you can use the following docker argument: + + --env=NEO4J_ACCEPT_LICENSE_AGREEMENT=yes +" + exit 1 + fi + fi +fi + +# NEO4JLABS_PLUGINS is renamed to NEO4J_PLUGINS in 5.x, but we want the new name to work against 4.4 images too +if [ -n "${NEO4JLABS_PLUGINS:-}" ]; +then + : ${NEO4J_PLUGINS:=${NEO4JLABS_PLUGINS:-}} +fi + +# ==== RENAME LEGACY ENVIRONMENT CONF VARIABLES ==== + +# Env variable naming convention: +# - prefix NEO4J_ +# - double underscore char '__' instead of single underscore '_' char in the setting name +# - underscore char '_' instead of dot '.' char in the setting name +# Example: +# NEO4J_dbms_tx__log_rotation_retention__policy env variable to set +# dbms.tx_log.rotation.retention_policy setting + +# Backward compatibility - map old hardcoded env variables into new naming convention (if they aren't set already) +# Set some to default values if unset +: ${NEO4J_dbms_tx__log_rotation_retention__policy:=${NEO4J_dbms_txLog_rotation_retentionPolicy:-}} +: ${NEO4J_dbms_unmanaged__extension__classes:=${NEO4J_dbms_unmanagedExtensionClasses:-}} +: ${NEO4J_dbms_allow__format__migration:=${NEO4J_dbms_allowFormatMigration:-}} +: ${NEO4J_dbms_connectors_default__advertised__address:=${NEO4J_dbms_connectors_defaultAdvertisedAddress:-}} + +if [ "${NEO4J_EDITION}" == "enterprise" ]; + then + : ${NEO4J_causal__clustering_expected__core__cluster__size:=${NEO4J_causalClustering_expectedCoreClusterSize:-}} + : ${NEO4J_causal__clustering_initial__discovery__members:=${NEO4J_causalClustering_initialDiscoveryMembers:-}} + debug_msg "Copying contents of /conf to ${NEO4J_HOME}/conf/*" + : ${NEO4J_causal__clustering_discovery__advertised__address:=${NEO4J_causalClustering_discoveryAdvertisedAddress:-}} + : ${NEO4J_causal__clustering_transaction__advertised__address:=${NEO4J_causalClustering_transactionAdvertisedAddress:-}} + : ${NEO4J_causal__clustering_raft__advertised__address:=${NEO4J_causalClustering_raftAdvertisedAddress:-}} +fi + +# unset old hardcoded unsupported env variables +unset NEO4J_dbms_txLog_rotation_retentionPolicy NEO4J_UDC_SOURCE \ + NEO4J_dbms_unmanagedExtensionClasses NEO4J_dbms_allowFormatMigration \ + NEO4J_dbms_connectors_defaultAdvertisedAddress NEO4J_ha_serverId \ + NEO4J_ha_initialHosts NEO4J_causalClustering_expectedCoreClusterSize \ + NEO4J_causalClustering_initialDiscoveryMembers \ + NEO4J_causalClustering_discoveryListenAddress \ + NEO4J_causalClustering_discoveryAdvertisedAddress \ + NEO4J_causalClustering_transactionListenAddress \ + NEO4J_causalClustering_transactionAdvertisedAddress \ + NEO4J_causalClustering_raftListenAddress \ + NEO4J_causalClustering_raftAdvertisedAddress + +# ==== CHECK FILE PERMISSIONS ON MOUNTED FOLDERS ==== + + +if [ -d /conf ]; then + check_mounted_folder_readable "/conf" + rm -rf "${NEO4J_HOME}"/conf/* + debug_msg "Copying contents of /conf to ${NEO4J_HOME}/conf/*" + find /conf -type f -exec cp --preserve=ownership,mode {} "${NEO4J_HOME}"/conf \; +fi + +if [ -d /ssl ]; then + check_mounted_folder_readable "/ssl" + rm -rf "${NEO4J_HOME}"/certificates + ln -s /ssl "${NEO4J_HOME}"/certificates +fi + +if [ -d /plugins ]; then + if [[ -n "${NEO4J_PLUGINS:-}" ]]; then + # We need write permissions to write the required plugins to /plugins + debug_msg "Extra plugins were requested. Ensuring the mounted /plugins folder has the required write permissions." + check_mounted_folder_writable_with_chown "/plugins" + fi + check_mounted_folder_readable "/plugins" + : ${NEO4J_dbms_directories_plugins:="/plugins"} +fi + +if [ -d /import ]; then + check_mounted_folder_readable "/import" + : ${NEO4J_dbms_directories_import:="/import"} +fi + +if [ -d /metrics ]; then + # metrics is enterprise only + if [ "${NEO4J_EDITION}" == "enterprise" ]; + then + check_mounted_folder_writable_with_chown "/metrics" + : ${NEO4J_dbms_directories_metrics:="/metrics"} + fi +fi + +if [ -d /logs ]; then + check_mounted_folder_writable_with_chown "/logs" + : ${NEO4J_dbms_directories_logs:="/logs"} +fi + +if [ -d /data ]; then + check_mounted_folder_writable_with_chown "/data" + if [ -d /data/databases ]; then + check_mounted_folder_writable_with_chown "/data/databases" + fi + if [ -d /data/dbms ]; then + check_mounted_folder_writable_with_chown "/data/dbms" + fi + if [ -d /data/transactions ]; then + check_mounted_folder_writable_with_chown "/data/transactions" + fi +fi + +if [ -d /licenses ]; then + check_mounted_folder_readable "/licenses" + : ${NEO4J_dbms_directories_licenses:="/licenses"} +fi + +# ==== SET CONFIGURATIONS ==== + +## == DOCKER SPECIFIC DEFAULT CONFIGURATIONS === +## these should not override *any* configurations set by the user + +debug_msg "Setting docker specific configuration overrides" +add_docker_default_to_conf "dbms.memory.pagecache.size" "512M" +add_docker_default_to_conf "dbms.default_listen_address" "0.0.0.0" + +# set enterprise only docker defaults +if [ "${NEO4J_EDITION}" == "enterprise" ]; +then + debug_msg "Setting docker specific Enterprise Edition overrides" + add_docker_default_to_conf "causal_clustering.discovery_advertised_address" "$(hostname):5000" + add_docker_default_to_conf "causal_clustering.transaction_advertised_address" "$(hostname):6000" + add_docker_default_to_conf "causal_clustering.raft_advertised_address" "$(hostname):7000" + add_docker_default_to_conf "dbms.routing.advertised_address" "$(hostname):7688" +fi + +## == ENVIRONMENT VARIABLE CONFIGURATIONS === +## these override BOTH defaults and any existing values in the neo4j.conf file + +# these are docker control envs that have the NEO4J_ prefix but we don't want to add to the config. +not_configs=("NEO4J_ACCEPT_LICENSE_AGREEMENT" "NEO4J_AUTH" "NEO4J_AUTH_PATH" "NEO4J_DEBUG" "NEO4J_EDITION" \ + "NEO4J_HOME" "NEO4J_PLUGINS" "NEO4J_SHA256" "NEO4J_TARBALL") + +debug_msg "Applying configuration settings that have been set using environment variables." +# list env variables with prefix NEO4J_ and create settings from them +for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do + if containsElement "$i" "${not_configs[@]}"; then + continue + fi + setting=$(echo "${i}" | sed 's|^NEO4J_||' | sed 's|_|.|g' | sed 's|\.\.|_|g') + value=$(echo "${!i}") + # Don't allow settings with no value or settings that start with a number (neo4j converts settings to env variables and you cannot have an env variable that starts with a number) + if [[ -n ${value} ]]; then + if [[ ! "${setting}" =~ ^[0-9]+.*$ ]]; then + add_env_setting_to_conf "${setting}" "${value}" + else + echo >&2 "WARNING: ${setting} not written to conf file. Settings that start with a number are not permitted." + fi + fi +done + +# ==== SET PASSWORD AND PLUGINS ==== + +if [[ -n "${NEO4J_AUTH_PATH:-}" ]]; then + # Validate the existence of the password file + if [ ! -f "${NEO4J_AUTH_PATH}" ]; then + echo >&2 "The password file '${NEO4J_AUTH_PATH}' does not exist" + exit 1 + fi + # validate the password file is readable + check_mounted_folder_readable "${NEO4J_AUTH_PATH}" + + debug_msg "Setting initial password from file ${NEO4J_AUTH_PATH}" + set_initial_password "$(cat ${NEO4J_AUTH_PATH})" +else + debug_msg "Setting initial password from environment" + set_initial_password "${NEO4J_AUTH:-}" +fi + + +if [[ ! -z "${NEO4J_PLUGINS:-}" ]]; then + # NEO4J_PLUGINS should be a json array of plugins like '["graph-algorithms", "apoc", "streams", "graphql"]' + install_neo4j_labs_plugins +fi + +# ==== CLEANUP RUN FILE ==== + +if [ -f "${NEO4J_HOME}"/run/neo4j.pid ]; +then + rm "${NEO4J_HOME}"/run/neo4j.pid +fi + +# ==== INVOKE NEO4J STARTUP ==== + +[ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT} + +if [ "${cmd}" == "dump-config" ]; then + if [ ! -d "/conf" ]; then + echo >&2 "You must mount a folder to /conf so that the configuration file(s) can be dumped to there." + exit 1 + fi + check_mounted_folder_writable_with_chown "/conf" + cp --recursive "${NEO4J_HOME}"/conf/* /conf + echo "Config Dumped" + exit 0 +fi + +# this prints out a command for us to run. +# the command is something like: `java ...[lots of java options]... neo4j.mainClass ...[some neo4j options]...` +# putting debug messages here causes the function to break +function get_neo4j_run_cmd { + + local extra_args=() + + if [ "${EXTENDED_CONF+"yes"}" == "yes" ]; then + extra_args+=("--expand-commands") + fi + if debugging_enabled ; then + extra_args+=("--verbose") + fi + + if running_as_root; then + su-exec neo4j:neo4j neo4j console --dry-run "${extra_args[@]}" + else + neo4j console --dry-run "${extra_args[@]}" + fi +} + +# Use su-exec to drop privileges to neo4j user +# Note that su-exec, despite its name, does not replicate the +# functionality of exec, so we need to use both +if [ "${cmd}" == "neo4j" ]; then + # separate declaration and use of get_neo4j_run_cmd so that error codes are correctly surfaced + debug_msg "getting full neo4j run command" + neo4j_console_cmd="$(get_neo4j_run_cmd)" + debug_msg "${exec_cmd} ${neo4j_console_cmd}" + eval ${exec_cmd} ${neo4j_console_cmd?:No Neo4j command was generated} +else + debug_msg "${exec_cmd}" "$@" + ${exec_cmd} "$@" +fi diff --git a/4.4.36/bullseye/community/local-package/neo4j-admin-report.sh b/4.4.36/bullseye/community/local-package/neo4j-admin-report.sh new file mode 100755 index 0000000..c4c3915 --- /dev/null +++ b/4.4.36/bullseye/community/local-package/neo4j-admin-report.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# load useful utility functions +. /startup/utilities.sh + +function find_report_destination +{ + local to_flag="--to" + + while [[ $# -gt 0 ]]; do + case $1 in + # for arg in "$@"; do + # case $arg in + "${to_flag}"=*) + echo ${1#*=} + return + ;; + "${to_flag}") + echo ${2} + return + ;; + *) + shift + ;; + esac + done + mkdir -p /tmp/reports + echo "/tmp/reports" +} + +report_cmd=("neo4j-admin" "report") + +# note, these debug messages are unlikely to work in a docker exec, since environment isn't preserved. +debug_msg "Called ${0}" +debug_msg "neo4j-admin report command is:" "${report_cmd[@]}" "$@" + +# find report destination. This could be specified by argument to neo4j-admin or it could be the default location. +report_destination=$(find_report_destination "$@") +debug_msg "report_destination will be ${report_destination}" + +debug_msg "Determining which user to run neo4j-admin as." +if running_as_root; then + debug_msg "running neo4j-admin report as root" + if [[ ! $(su-exec neo4j:neo4j test -w "${report_destination}") ]]; then + debug_msg "reowning ${report_destination} to neo4j:neo4j" + chown neo4j:neo4j "${report_destination}" + fi + debug_msg su-exec neo4j:neo4j "${report_cmd[@]}" "$@" + su-exec neo4j:neo4j "${report_cmd[@]}" "$@" +else + debug_msg "running neo4j-admin report as user defined by --user flag" + if [[ ! -w "${report_destination}" ]]; then + print_permissions_advice_and_fail "${report_destination}" "$(id -u)" "$(id -g)" + fi + debug_msg "${report_cmd[@]}" "$@" + "${report_cmd[@]}" "$@" +fi \ No newline at end of file diff --git a/4.4.36/bullseye/community/local-package/neo4j-plugins.json b/4.4.36/bullseye/community/local-package/neo4j-plugins.json new file mode 100644 index 0000000..d738bd1 --- /dev/null +++ b/4.4.36/bullseye/community/local-package/neo4j-plugins.json @@ -0,0 +1,60 @@ +{ + "apoc": { + "versions": "https://neo4j-contrib.github.io/neo4j-apoc-procedures/versions.json", + "properties": { + "dbms.security.procedures.unrestricted": "apoc.*" + } + }, + "apoc-core": { + "location": "/var/lib/neo4j/labs/apoc-*-core.jar", + "versions": "https://neo4j-contrib.github.io/neo4j-apoc-procedures/versions.json", + "properties": { + "dbms.security.procedures.unrestricted": "apoc.*" + } + }, + "bloom": { + "location": "/var/lib/neo4j/products/bloom-plugin-*.jar", + "versions": "https://bloom-plugins.s3.eu-west-2.amazonaws.com/versions.json", + "properties": { + "dbms.unmanaged_extension_classes": "com.neo4j.bloom.server=/browser/bloom", + "dbms.security.procedures.unrestricted": "bloom.*", + "neo4j.bloom.license_file": "/licenses/bloom.license" + } + }, + "streams": { + "versions": "https://neo4j-contrib.github.io/neo4j-streams/versions.json", + "properties": {} + }, + "graphql": { + "versions": "https://neo4j-graphql.github.io/neo4j-graphql/versions.json", + "properties": { + "dbms.unmanaged_extension_classes": "org.neo4j.graphql=/graphql", + "dbms.security.procedures.unrestricted": "graphql.*" + } + }, + "graph-algorithms": { + "versions": "https://neo4j-contrib.github.io/neo4j-graph-algorithms/versions.json", + "properties": { + "dbms.security.procedures.unrestricted": "algo.*" + } + }, + "graph-data-science": { + "versions": "https://graphdatascience.ninja/versions.json", + "location": "/var/lib/neo4j/products/neo4j-graph-data-science-*.jar", + "properties": { + "dbms.security.procedures.unrestricted": "gds.*" + } + }, + "n10s": { + "versions": "https://neo4j-labs.github.io/neosemantics/versions.json", + "properties": { + "dbms.security.procedures.unrestricted":"semantics.*" + } + }, + "_testing": { + "versions": "http://host.testcontainers.internal:3000/versions.json", + "properties": { + "dbms.security.procedures.unrestricted": "com.neo4j.docker.neo4jserver.plugins.*" + } + } +} diff --git a/4.4.36/bullseye/community/local-package/semver.jq b/4.4.36/bullseye/community/local-package/semver.jq new file mode 100755 index 0000000..2d8c144 --- /dev/null +++ b/4.4.36/bullseye/community/local-package/semver.jq @@ -0,0 +1,22 @@ +def _semver_obj2obj($req): + if . == $req then true + elif .major != $req.major then false + elif .minor != $req.minor and .minor != "x" and .minor != "*" then false + elif .patch != $req.patch and .patch != "x" and .patch != "*" then false + elif $req.minor == null and ( .minor == "x" or .minor == "*" ) then false + elif $req.patch == null and ( .patch == "x" or .patch == "*" ) then false + elif $req.major == null and $req.minor == null and $req.patch == null then false + else true end; + +def _ver2obj: + if type == "object" then . + elif type == "string" and test("(?[0-9]+)(\\.(?[0-9x*]+))?(\\.?(?[0-9x*]+))?") then capture("(?[0-9]+)(\\.(?[0-9x*]+))?(\\.?(?[0-9x*]+))?") + elif type == "string" and . == "" then {major: null, minor:null, patch:null} + elif type == "number" then {minor:floor,patch:(.-floor)} + else {major: .} end; + +# Returns true if input version spec semantically matches the requested version +def semver($req): + if $req == null or $req == "" then false + elif . == $req then true + else _ver2obj|_semver_obj2obj($req|_ver2obj) end; diff --git a/4.4.36/bullseye/community/local-package/utilities.sh b/4.4.36/bullseye/community/local-package/utilities.sh new file mode 100644 index 0000000..4943afe --- /dev/null +++ b/4.4.36/bullseye/community/local-package/utilities.sh @@ -0,0 +1,54 @@ + +function running_as_root +{ + test "$(id -u)" = "0" +} + +function secure_mode_enabled +{ + test "${SECURE_FILE_PERMISSIONS:=no}" = "yes" +} + +function debugging_enabled +{ + test "${NEO4J_DEBUG+yes}" = "yes" +} + +function debug_msg +{ + if debugging_enabled; then + echo "$@" + fi +} + +function containsElement +{ + local e match="$1" + shift + for e; do [[ "$e" == "$match" ]] && return 0; done + return 1 +} + +function is_writable +{ + ${exec_cmd} test -w "${1}" +} + +function print_permissions_advice_and_fail +{ + local _directory=${1} + local _userid=${2} + local _groupid=${3} + echo >&2 " +Folder ${_directory} is not accessible for user: ${_userid} or group ${_groupid}. This is commonly a file permissions issue on the mounted folder. + +Hints to solve the issue: +1) Make sure the folder exists before mounting it. Docker will create the folder using root permissions before starting the Neo4j container. The root permissions disallow Neo4j from writing to the mounted folder. +2) Pass the folder owner's user ID and group ID to docker run, so that docker runs as that user. +If the folder is owned by the current user, this can be done by adding this flag to your docker run command: + --user=\$(id -u):\$(id -g) + " + exit 1 +} + + diff --git a/4.4.36/bullseye/enterprise/Dockerfile b/4.4.36/bullseye/enterprise/Dockerfile new file mode 100644 index 0000000..962cc26 --- /dev/null +++ b/4.4.36/bullseye/enterprise/Dockerfile @@ -0,0 +1,56 @@ +FROM debian:bullseye-slim +ENV JAVA_HOME=/opt/java/openjdk +COPY --from=eclipse-temurin:11 $JAVA_HOME $JAVA_HOME +ENV PATH="${JAVA_HOME}/bin:${PATH}" \ + NEO4J_SHA256=e6ec47c9a33aee60b3269d8b4e01f0b9ddd98a0520e4d15489a57b344bc43521 \ + NEO4J_TARBALL=neo4j-enterprise-4.4.36-unix.tar.gz \ + NEO4J_EDITION=enterprise \ + NEO4J_HOME="/var/lib/neo4j" \ + LANG=C.UTF-8 +ARG NEO4J_URI=https://dist.neo4j.org/neo4j-enterprise-4.4.36-unix.tar.gz + +RUN addgroup --gid 7474 --system neo4j && adduser --uid 7474 --system --no-create-home --home "${NEO4J_HOME}" --ingroup neo4j neo4j + +COPY ./local-package/* /startup/ + +RUN apt update \ + && apt-get install -y curl gcc git jq make procps tini wget \ + && curl --fail --silent --show-error --location --remote-name ${NEO4J_URI} \ + && echo "${NEO4J_SHA256} ${NEO4J_TARBALL}" | sha256sum -c --strict --quiet \ + && tar --extract --file ${NEO4J_TARBALL} --directory /var/lib \ + && mv /var/lib/neo4j-* "${NEO4J_HOME}" \ + && rm ${NEO4J_TARBALL} \ + && mv /startup/neo4j-admin-report.sh "${NEO4J_HOME}"/bin/neo4j-admin-report \ + && mv "${NEO4J_HOME}"/data /data \ + && mv "${NEO4J_HOME}"/logs /logs \ + && chown -R neo4j:neo4j /data \ + && chmod -R 777 /data \ + && chown -R neo4j:neo4j /logs \ + && chmod -R 777 /logs \ + && chown -R neo4j:neo4j "${NEO4J_HOME}" \ + && chmod -R 777 "${NEO4J_HOME}" \ + && chmod -R 755 "${NEO4J_HOME}/bin" \ + && ln -s /data "${NEO4J_HOME}"/data \ + && ln -s /logs "${NEO4J_HOME}"/logs \ + && ln -s /startup/docker-entrypoint.sh /docker-entrypoint.sh \ + && git clone https://github.com/ncopa/su-exec.git \ + && cd su-exec \ + && git checkout 4c3bb42b093f14da70d8ab924b487ccfbb1397af \ + && echo d6c40440609a23483f12eb6295b5191e94baf08298a856bab6e15b10c3b82891 su-exec.c | sha256sum -c \ + && echo 2a87af245eb125aca9305a0b1025525ac80825590800f047419dc57bba36b334 Makefile | sha256sum -c \ + && make \ + && mv /su-exec/su-exec /usr/bin/su-exec \ + && apt-get -y purge --auto-remove curl gcc git make \ + && rm -rf /var/lib/apt/lists/* /su-exec + + +ENV PATH "${NEO4J_HOME}"/bin:$PATH + +WORKDIR "${NEO4J_HOME}" + +VOLUME /data /logs + +EXPOSE 7474 7473 7687 + +ENTRYPOINT ["tini", "-g", "--", "/startup/docker-entrypoint.sh"] +CMD ["neo4j"] diff --git a/4.4.36/bullseye/enterprise/local-package/docker-entrypoint.sh b/4.4.36/bullseye/enterprise/local-package/docker-entrypoint.sh new file mode 100755 index 0000000..2b105f4 --- /dev/null +++ b/4.4.36/bullseye/enterprise/local-package/docker-entrypoint.sh @@ -0,0 +1,647 @@ +#!/bin/bash -eu + +cmd="$1" + +# load useful utility functions +. /startup/utilities.sh + +function is_readable +{ + # this code is fairly ugly but works no matter who this script is running as. + # It would be nice if the writability tests could use this logic somehow. + local _file=${1} + perm=$(stat -c %a "${_file}") + + # everyone permission + if [[ ${perm:2:1} -ge 4 ]]; then + return 0 + fi + # owner permissions + if [[ ${perm:0:1} -ge 4 ]]; then + if [[ "$(stat -c %U ${_file})" = "${userid}" ]] || [[ "$(stat -c %u ${_file})" = "${userid}" ]]; then + return 0 + fi + fi + # group permissions + if [[ ${perm:1:1} -ge 4 ]]; then + if containsElement "$(stat -c %g ${_file})" "${groups[@]}" || containsElement "$(stat -c %G ${_file})" "${groups[@]}" ; then + return 0 + fi + fi + return 1 +} + +function is_writable +{ + # It would be nice if this and the is_readable function could combine somehow + local _file=${1} + perm=$(stat -c %a "${_file}") + + # everyone permission + if containsElement ${perm:2:1} 2 3 6 7; then + return 0 + fi + # owner permissions + if containsElement ${perm:0:1} 2 3 6 7; then + if [[ "$(stat -c %U ${_file})" = "${userid}" ]] || [[ "$(stat -c %u ${_file})" = "${userid}" ]]; then + return 0 + fi + fi + # group permissions + if containsElement ${perm:1:1} 2 3 6 7; then + if containsElement "$(stat -c %g ${_file})" "${groups[@]}" || containsElement "$(stat -c %G ${_file})" "${groups[@]}" ; then + return 0 + fi + fi + return 1 +} + +function check_mounted_folder_readable +{ + local _directory=${1} + debug_msg "checking ${_directory} is readable" + if ! is_readable "${_directory}"; then + print_permissions_advice_and_fail "${_directory}" "${userid}" "${groupid}" + fi +} + +function check_mounted_folder_writable_with_chown +{ +# The /data and /log directory are a bit different because they are very likely to be mounted by the user but not +# necessarily writable. +# This depends on whether a user ID is passed to the container and which folders are mounted. +# +# No user ID passed to container: +# 1) No folders are mounted. +# The /data and /log folder are owned by neo4j by default, so should be writable already. +# 2) Both /log and /data are mounted. +# This means on start up, /data and /logs are owned by an unknown user and we should chown them to neo4j for +# backwards compatibility. +# +# User ID passed to container: +# 1) Both /data and /logs are mounted +# The /data and /logs folders are owned by an unknown user but we *should* have rw permission to them. +# That should be verified and error (helpfully) if not. +# 2) User mounts /data or /logs *but not both* +# The unmounted folder is still owned by neo4j, which should already be writable. The mounted folder should +# have rw permissions through user id. This should be verified. +# 3) No folders are mounted. +# The /data and /log folder are owned by neo4j by default, and these are already writable by the user. +# (This is a very unlikely use case). + + local mountFolder=${1} + debug_msg "checking ${mountFolder} is writable" + if running_as_root && ! secure_mode_enabled; then + # check folder permissions + if ! is_writable "${mountFolder}" ; then + # warn that we're about to chown the folder and then chown it + echo "Warning: Folder mounted to \"${mountFolder}\" is not writable from inside container. Changing folder owner to ${userid}." + chown -R "${userid}":"${groupid}" "${mountFolder}" + # check permissions on files in the folder + elif [ $(su-exec "${userid}":"${groupid}" find "${mountFolder}" -not -writable | wc -l) -gt 0 ]; then + echo "Warning: Some files inside \"${mountFolder}\" are not writable from inside container. Changing folder owner to ${userid}." + chown -R "${userid}":"${groupid}" "${mountFolder}" + fi + else + if [[ ! -w "${mountFolder}" ]] && [[ "$(stat -c %U ${mountFolder})" != "neo4j" ]]; then + print_permissions_advice_and_fail "${mountFolder}" "${userid}" "${groupid}" + fi + fi +} + +function load_plugin_from_location +{ + # Install a plugin from location at runtime. + local _plugin_name="${1}" + local _location="${2}" + + local _plugins_dir="${NEO4J_HOME}/plugins" + if [ -d /plugins ]; then + local _plugins_dir="/plugins" + fi + + local _destination="${_plugins_dir}/${_plugin_name}.jar" + + # Now we install the plugin that is shipped with Neo4j + for filename in ${_location}; do + echo "Installing Plugin '${_plugin_name}' from ${_location} to ${_destination}" + cp --preserve "${filename}" "${_destination}" + chmod +rw ${_destination} + done + + if ! is_readable "${_destination}"; then + echo >&2 "Plugin at '${_destination}' is not readable" + exit 1 + fi +} + +function load_plugin_from_url +{ + # Load a plugin at runtime. The provided github repository must have a versions.json on the master branch with the + # correct format. + local _plugin_name="${1}" #e.g. apoc, graph-algorithms, graph-ql + + local _plugins_dir="${NEO4J_HOME}/plugins" + if [ -d /plugins ]; then + local _plugins_dir="/plugins" + fi + local _versions_json_url="$(jq --raw-output "with_entries( select(.key==\"${_plugin_name}\") ) | to_entries[] | .value.versions" /startup/neo4j-plugins.json )" + debug_msg "Will read ${_plugin_name} versions.json from ${_versions_json_url}" + # Using the same name for the plugin irrespective of version ensures we don't end up with different versions of the same plugin + local _destination="${_plugins_dir}/${_plugin_name}.jar" + local _neo4j_version="$(neo4j --version | cut -d' ' -f2)" + + # Now we call out to github to get the versions.json for this plugin and we parse that to find the url for the correct plugin jar for our neo4j version + echo "Fetching versions.json for Plugin '${_plugin_name}' from ${_versions_json_url}" + local _versions_json + if ! _versions_json="$(wget -q --timeout 300 --tries 30 -O - "${_versions_json_url}")"; then + debug_msg "ERROR: could not fetch '${_versions_json}'" + echo >&2 "ERROR: could not query ${_versions_json_url} for plugin compatibility information. + This could indicate a problem with your network or this container's network settings. + Neo4j will continue to start, but \"${_plugin_name}\" will not be loaded." + return + fi + local _plugin_jar_url="$(echo "${_versions_json}" | jq -L/startup --raw-output "import \"semver\" as lib; [ .[] | select(.neo4j|lib::semver(\"${_neo4j_version}\")) ] | min_by(.neo4j) | .jar")" + if [[ -z "${_plugin_jar_url}" ]] || [[ "${_plugin_jar_url}" == "null" ]]; then + debug_msg "ERROR: '${_versions_json_url}' does not contain an entry for ${_neo4j_version}" + echo >&2 "ERROR: No compatible \"${_plugin_name}\" plugin found for Neo4j ${_neo4j_version} ${NEO4J_EDITION}. + This can happen with the newest Neo4j versions when a compatible plugin has not yet been released. + You can either use an older version of Neo4j, or continue without ${_plugin_name}. + Neo4j will continue to start, but \"${_plugin_name}\" will not be loaded." + else + echo "Installing Plugin '${_plugin_name}' from ${_plugin_jar_url} to ${_destination} " + wget -q --timeout 300 --tries 30 --output-document="${_destination}" "${_plugin_jar_url}" + + if ! is_readable "${_destination}"; then + echo >&2 "Plugin at '${_destination}' is not readable" + exit 1 + fi + fi +} + +function apply_plugin_default_configuration +{ + # Set the correct Load a plugin at runtime. The provided github repository must have a versions.json on the master branch with the + # correct format. + local _plugin_name="${1}" #e.g. apoc, graph-algorithms, graphql + local _reference_conf="${2}" # used to determine if we can override properties + local _neo4j_conf="${NEO4J_HOME}/conf/neo4j.conf" + + local _property _value + echo "Applying default values for plugin ${_plugin_name} to neo4j.conf" + for _entry in $(jq --compact-output --raw-output "with_entries( select(.key==\"${_plugin_name}\") ) | to_entries[] | .value.properties | to_entries[]" /startup/neo4j-plugins.json); do + _property="$(jq --raw-output '.key' <<< "${_entry}")" + _value="$(jq --raw-output '.value' <<< "${_entry}")" + debug_msg "${_plugin_name} requires setting ${_property}=${_value}" + + # the first grep strips out comments + if grep -o "^[^#]*" "${_reference_conf}" | grep -q --fixed-strings "${_property}=" ; then + # property is already set in the user provided config. In this case we don't override what has been set explicitly by the user. + echo "Skipping ${_property} for plugin ${_plugin_name} because it is already set." + echo "You may need to add ${_value} to the ${_property} setting in your configuration file." + else + if grep -o "^[^#]*" "${_neo4j_conf}" | grep -q --fixed-strings "${_property}=" ; then + sed --in-place "s/${_property}=/&${_value},/" "${_neo4j_conf}" + debug_msg "${_property} was already in the configuration file, so ${_value} was added to it." + else + echo -e "\n${_property}=${_value}" >> "${_neo4j_conf}" + debug_msg "${_property}=${_value} has been added to the configuration file." + fi + fi + done +} + +function install_neo4j_labs_plugins +{ + # first verify that the requested plugins are valid. + debug_msg "One or more NEO4J_PLUGINS have been requested." + local _known_plugins=($(jq --raw-output "keys[]" /startup/neo4j-plugins.json)) + debug_msg "Checking requested plugins are known and can be installed." + for plugin_name in $(echo "${NEO4J_PLUGINS}" | jq --raw-output '.[]'); do + if ! containsElement "${plugin_name}" "${_known_plugins[@]}"; then + printf >&2 "\"%s\" is not a known Neo4j plugin. Options are:\n%s" "${plugin_name}" "$(jq --raw-output "keys[1:][]" /startup/neo4j-plugins.json)" + exit 1 + fi + done + + # We store a copy of the config before we modify it for the plugins to allow us to see if there are user-set values in the input config that we shouldn't override + local _old_config="$(mktemp)" + if [ -e "${NEO4J_HOME}"/conf/neo4j.conf ]; then + cp "${NEO4J_HOME}"/conf/neo4j.conf "${_old_config}" + else + touch "${NEO4J_HOME}"/conf/neo4j.conf + touch "${_old_config}" + fi + for plugin_name in $(echo "${NEO4J_PLUGINS}" | jq --raw-output '.[]'); do + debug_msg "Plugin ${plugin_name} has been requested" + local _location="$(jq --raw-output "with_entries( select(.key==\"${plugin_name}\") ) | to_entries[] | .value.location" /startup/neo4j-plugins.json )" + if [ "${_location}" != "null" -a -n "$(shopt -s nullglob; echo ${_location})" ]; then + debug_msg "$plugin_name is already in the container at ${_location}" + load_plugin_from_location "${plugin_name}" "${_location}" + else + debug_msg "$plugin_name must be downloaded." + load_plugin_from_url "${plugin_name}" + fi + debug_msg "Applying plugin specific configurations." + apply_plugin_default_configuration "${plugin_name}" "${_old_config}" + done + rm "${_old_config}" +} + +function add_docker_default_to_conf +{ + # docker defaults should NOT overwrite values already in the conf file + local _setting="${1}" + local _value="${2}" + + if ! grep -q "^${_setting}=" "${NEO4J_HOME}"/conf/neo4j.conf + then + debug_msg "Appended ${_setting}=${_value} to ${NEO4J_HOME}/conf/neo4j.conf" + echo -e "\n"${_setting}=${_value} >> "${NEO4J_HOME}"/conf/neo4j.conf + fi +} + +function add_env_setting_to_conf +{ + # settings from environment variables should overwrite values already in the conf + local _setting=${1} + local _value=${2} + local _append_not_replace_configs=("dbms.jvm.additional") + + if grep -q -F "${_setting}=" "${NEO4J_HOME}"/conf/neo4j.conf; then + if containsElement "${_setting}" "${_append_not_replace_configs[@]}"; then + debug_msg "${_setting} will be appended to neo4j.conf without replacing existing settings." + else + # Remove any lines containing the setting already + debug_msg "Removing existing setting for ${_setting}" + sed --in-place "/^${_setting}=.*/d" "${NEO4J_HOME}"/conf/neo4j.conf + fi + fi + # Then always append setting to file + debug_msg "Appended ${_setting}=${_value} to ${NEO4J_HOME}/conf/neo4j.conf" + echo "${_setting}=${_value}" >> "${NEO4J_HOME}"/conf/neo4j.conf +} + +function set_initial_password +{ + local _neo4j_auth="${1}" + + # set the neo4j initial password only if you run the database server + if [ "${cmd}" == "neo4j" ]; then + if [ "${_neo4j_auth:-}" == "none" ]; then + debug_msg "Authentication is requested to be unset" + add_env_setting_to_conf "dbms.security.auth_enabled" "false" + elif [[ "${_neo4j_auth:-}" =~ ^([^/]+)\/([^/]+)/?([tT][rR][uU][eE])?$ ]]; then + admin_user="${BASH_REMATCH[1]}" + password="${BASH_REMATCH[2]}" + do_reset="${BASH_REMATCH[3]}" + + if [ "${password}" == "neo4j" ]; then + echo >&2 "Invalid value for password. It cannot be 'neo4j', which is the default." + exit 1 + fi + if [ "${admin_user}" != "neo4j" ]; then + echo >&2 "Invalid admin username, it must be neo4j." + exit 1 + fi + + if running_as_root; then + # running set-initial-password as root will create subfolders to /data as root, causing startup fail when neo4j can't read or write the /data/dbms folder + # creating the folder first will avoid that + mkdir -p /data/dbms + debug_msg "Making sure /data/dbms is owned by ${userid}:${groupid}" + chown "${userid}":"${groupid}" /data/dbms + fi + + local extra_args=() + if [ "${do_reset}" == "true" ]; then + extra_args+=("--require-password-change") + fi + if [ "${EXTENDED_CONF+"yes"}" == "yes" ]; then + extra_args+=("--expand-commands") + fi + if debugging_enabled; then + extra_args+=("--verbose") + fi + debug_msg "Setting initial password" + debug_msg "${neo4j_admin_cmd} set-initial-password ***** ${extra_args[*]}" + if debugging_enabled; then + # don't suppress any output or errors in debugging mode + ${neo4j_admin_cmd} set-initial-password "${password}" "${extra_args[@]}" + else + # Will exit with error if users already exist (and print a message explaining that) + # we probably don't want the message though, since it throws an error message on restarting the container. + ${neo4j_admin_cmd} set-initial-password "${password}" "${extra_args[@]}" 2>/dev/null || true + fi + + elif [ -n "${_neo4j_auth:-}" ]; then + echo "$_neo4j_auth is invalid" + echo >&2 "Invalid value for NEO4J_AUTH: '${_neo4j_auth}'" + exit 1 + fi + fi +} + +# ==== CODE STARTS ==== +debug_msg "DEBUGGING ENABLED" + +# If we're running as root, then run as the neo4j user. Otherwise +# docker is running with --user and we simply use that user. Note +# that su-exec, despite its name, does not replicate the functionality +# of exec, so we need to use both +if running_as_root; then + userid="neo4j" + groupid="neo4j" + groups=($(id -G neo4j)) + exec_cmd="exec su-exec neo4j:neo4j" + neo4j_admin_cmd="su-exec neo4j:neo4j neo4j-admin" + debug_msg "Running as root user inside neo4j image" +else + userid="$(id -u)" + groupid="$(id -g)" + groups=($(id -G)) + exec_cmd="exec" + neo4j_admin_cmd="neo4j-admin" + debug_msg "Running as user ${userid}:${groupid} inside neo4j image" +fi +readonly userid +readonly groupid +readonly groups +readonly exec_cmd +readonly neo4j_admin_cmd + +# Need to chown the home directory +if running_as_root; then + debug_msg "chowning ${NEO4J_HOME} recursively to ${userid}":"${groupid}" + chown -R "${userid}":"${groupid}" "${NEO4J_HOME}" + chmod 700 "${NEO4J_HOME}" + find "${NEO4J_HOME}" -mindepth 1 -maxdepth 1 -type d -exec chmod -R 700 {} \; + debug_msg "Setting all files in ${NEO4J_HOME}/conf to permissions 600" + find "${NEO4J_HOME}"/conf -type f -exec chmod -R 600 {} \; +fi + +# ==== CHECK LICENSE AGREEMENT ==== + +# Only prompt for license agreement if command contains "neo4j" in it +if [[ "${cmd}" == *"neo4j"* ]]; then + if [ "${NEO4J_EDITION}" == "enterprise" ]; then + if [ "${NEO4J_ACCEPT_LICENSE_AGREEMENT:=no}" != "yes" ]; then + echo >&2 " +In order to use Neo4j Enterprise Edition you must accept the license agreement. + +The license agreement is available at https://neo4j.com/terms/licensing/ +If you have a support contract the following terms apply https://neo4j.com/terms/support-terms/ + +(c) Neo4j Sweden AB. All Rights Reserved. +Use of this Software without a proper commercial license +with Neo4j, Inc. or its affiliates is prohibited. +Neo4j has the right to terminate your usage if you are not compliant. + +More information is also available at: https://neo4j.com/licensing/ +If you have further inquiries about licensing, please contact us via https://neo4j.com/contact-us/ + +To accept the commercial license agreement set the environment variable +NEO4J_ACCEPT_LICENSE_AGREEMENT=yes + +To do this you can use the following docker argument: + + --env=NEO4J_ACCEPT_LICENSE_AGREEMENT=yes +" + exit 1 + fi + fi +fi + +# NEO4JLABS_PLUGINS is renamed to NEO4J_PLUGINS in 5.x, but we want the new name to work against 4.4 images too +if [ -n "${NEO4JLABS_PLUGINS:-}" ]; +then + : ${NEO4J_PLUGINS:=${NEO4JLABS_PLUGINS:-}} +fi + +# ==== RENAME LEGACY ENVIRONMENT CONF VARIABLES ==== + +# Env variable naming convention: +# - prefix NEO4J_ +# - double underscore char '__' instead of single underscore '_' char in the setting name +# - underscore char '_' instead of dot '.' char in the setting name +# Example: +# NEO4J_dbms_tx__log_rotation_retention__policy env variable to set +# dbms.tx_log.rotation.retention_policy setting + +# Backward compatibility - map old hardcoded env variables into new naming convention (if they aren't set already) +# Set some to default values if unset +: ${NEO4J_dbms_tx__log_rotation_retention__policy:=${NEO4J_dbms_txLog_rotation_retentionPolicy:-}} +: ${NEO4J_dbms_unmanaged__extension__classes:=${NEO4J_dbms_unmanagedExtensionClasses:-}} +: ${NEO4J_dbms_allow__format__migration:=${NEO4J_dbms_allowFormatMigration:-}} +: ${NEO4J_dbms_connectors_default__advertised__address:=${NEO4J_dbms_connectors_defaultAdvertisedAddress:-}} + +if [ "${NEO4J_EDITION}" == "enterprise" ]; + then + : ${NEO4J_causal__clustering_expected__core__cluster__size:=${NEO4J_causalClustering_expectedCoreClusterSize:-}} + : ${NEO4J_causal__clustering_initial__discovery__members:=${NEO4J_causalClustering_initialDiscoveryMembers:-}} + debug_msg "Copying contents of /conf to ${NEO4J_HOME}/conf/*" + : ${NEO4J_causal__clustering_discovery__advertised__address:=${NEO4J_causalClustering_discoveryAdvertisedAddress:-}} + : ${NEO4J_causal__clustering_transaction__advertised__address:=${NEO4J_causalClustering_transactionAdvertisedAddress:-}} + : ${NEO4J_causal__clustering_raft__advertised__address:=${NEO4J_causalClustering_raftAdvertisedAddress:-}} +fi + +# unset old hardcoded unsupported env variables +unset NEO4J_dbms_txLog_rotation_retentionPolicy NEO4J_UDC_SOURCE \ + NEO4J_dbms_unmanagedExtensionClasses NEO4J_dbms_allowFormatMigration \ + NEO4J_dbms_connectors_defaultAdvertisedAddress NEO4J_ha_serverId \ + NEO4J_ha_initialHosts NEO4J_causalClustering_expectedCoreClusterSize \ + NEO4J_causalClustering_initialDiscoveryMembers \ + NEO4J_causalClustering_discoveryListenAddress \ + NEO4J_causalClustering_discoveryAdvertisedAddress \ + NEO4J_causalClustering_transactionListenAddress \ + NEO4J_causalClustering_transactionAdvertisedAddress \ + NEO4J_causalClustering_raftListenAddress \ + NEO4J_causalClustering_raftAdvertisedAddress + +# ==== CHECK FILE PERMISSIONS ON MOUNTED FOLDERS ==== + + +if [ -d /conf ]; then + check_mounted_folder_readable "/conf" + rm -rf "${NEO4J_HOME}"/conf/* + debug_msg "Copying contents of /conf to ${NEO4J_HOME}/conf/*" + find /conf -type f -exec cp --preserve=ownership,mode {} "${NEO4J_HOME}"/conf \; +fi + +if [ -d /ssl ]; then + check_mounted_folder_readable "/ssl" + rm -rf "${NEO4J_HOME}"/certificates + ln -s /ssl "${NEO4J_HOME}"/certificates +fi + +if [ -d /plugins ]; then + if [[ -n "${NEO4J_PLUGINS:-}" ]]; then + # We need write permissions to write the required plugins to /plugins + debug_msg "Extra plugins were requested. Ensuring the mounted /plugins folder has the required write permissions." + check_mounted_folder_writable_with_chown "/plugins" + fi + check_mounted_folder_readable "/plugins" + : ${NEO4J_dbms_directories_plugins:="/plugins"} +fi + +if [ -d /import ]; then + check_mounted_folder_readable "/import" + : ${NEO4J_dbms_directories_import:="/import"} +fi + +if [ -d /metrics ]; then + # metrics is enterprise only + if [ "${NEO4J_EDITION}" == "enterprise" ]; + then + check_mounted_folder_writable_with_chown "/metrics" + : ${NEO4J_dbms_directories_metrics:="/metrics"} + fi +fi + +if [ -d /logs ]; then + check_mounted_folder_writable_with_chown "/logs" + : ${NEO4J_dbms_directories_logs:="/logs"} +fi + +if [ -d /data ]; then + check_mounted_folder_writable_with_chown "/data" + if [ -d /data/databases ]; then + check_mounted_folder_writable_with_chown "/data/databases" + fi + if [ -d /data/dbms ]; then + check_mounted_folder_writable_with_chown "/data/dbms" + fi + if [ -d /data/transactions ]; then + check_mounted_folder_writable_with_chown "/data/transactions" + fi +fi + +if [ -d /licenses ]; then + check_mounted_folder_readable "/licenses" + : ${NEO4J_dbms_directories_licenses:="/licenses"} +fi + +# ==== SET CONFIGURATIONS ==== + +## == DOCKER SPECIFIC DEFAULT CONFIGURATIONS === +## these should not override *any* configurations set by the user + +debug_msg "Setting docker specific configuration overrides" +add_docker_default_to_conf "dbms.memory.pagecache.size" "512M" +add_docker_default_to_conf "dbms.default_listen_address" "0.0.0.0" + +# set enterprise only docker defaults +if [ "${NEO4J_EDITION}" == "enterprise" ]; +then + debug_msg "Setting docker specific Enterprise Edition overrides" + add_docker_default_to_conf "causal_clustering.discovery_advertised_address" "$(hostname):5000" + add_docker_default_to_conf "causal_clustering.transaction_advertised_address" "$(hostname):6000" + add_docker_default_to_conf "causal_clustering.raft_advertised_address" "$(hostname):7000" + add_docker_default_to_conf "dbms.routing.advertised_address" "$(hostname):7688" +fi + +## == ENVIRONMENT VARIABLE CONFIGURATIONS === +## these override BOTH defaults and any existing values in the neo4j.conf file + +# these are docker control envs that have the NEO4J_ prefix but we don't want to add to the config. +not_configs=("NEO4J_ACCEPT_LICENSE_AGREEMENT" "NEO4J_AUTH" "NEO4J_AUTH_PATH" "NEO4J_DEBUG" "NEO4J_EDITION" \ + "NEO4J_HOME" "NEO4J_PLUGINS" "NEO4J_SHA256" "NEO4J_TARBALL") + +debug_msg "Applying configuration settings that have been set using environment variables." +# list env variables with prefix NEO4J_ and create settings from them +for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do + if containsElement "$i" "${not_configs[@]}"; then + continue + fi + setting=$(echo "${i}" | sed 's|^NEO4J_||' | sed 's|_|.|g' | sed 's|\.\.|_|g') + value=$(echo "${!i}") + # Don't allow settings with no value or settings that start with a number (neo4j converts settings to env variables and you cannot have an env variable that starts with a number) + if [[ -n ${value} ]]; then + if [[ ! "${setting}" =~ ^[0-9]+.*$ ]]; then + add_env_setting_to_conf "${setting}" "${value}" + else + echo >&2 "WARNING: ${setting} not written to conf file. Settings that start with a number are not permitted." + fi + fi +done + +# ==== SET PASSWORD AND PLUGINS ==== + +if [[ -n "${NEO4J_AUTH_PATH:-}" ]]; then + # Validate the existence of the password file + if [ ! -f "${NEO4J_AUTH_PATH}" ]; then + echo >&2 "The password file '${NEO4J_AUTH_PATH}' does not exist" + exit 1 + fi + # validate the password file is readable + check_mounted_folder_readable "${NEO4J_AUTH_PATH}" + + debug_msg "Setting initial password from file ${NEO4J_AUTH_PATH}" + set_initial_password "$(cat ${NEO4J_AUTH_PATH})" +else + debug_msg "Setting initial password from environment" + set_initial_password "${NEO4J_AUTH:-}" +fi + + +if [[ ! -z "${NEO4J_PLUGINS:-}" ]]; then + # NEO4J_PLUGINS should be a json array of plugins like '["graph-algorithms", "apoc", "streams", "graphql"]' + install_neo4j_labs_plugins +fi + +# ==== CLEANUP RUN FILE ==== + +if [ -f "${NEO4J_HOME}"/run/neo4j.pid ]; +then + rm "${NEO4J_HOME}"/run/neo4j.pid +fi + +# ==== INVOKE NEO4J STARTUP ==== + +[ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT} + +if [ "${cmd}" == "dump-config" ]; then + if [ ! -d "/conf" ]; then + echo >&2 "You must mount a folder to /conf so that the configuration file(s) can be dumped to there." + exit 1 + fi + check_mounted_folder_writable_with_chown "/conf" + cp --recursive "${NEO4J_HOME}"/conf/* /conf + echo "Config Dumped" + exit 0 +fi + +# this prints out a command for us to run. +# the command is something like: `java ...[lots of java options]... neo4j.mainClass ...[some neo4j options]...` +# putting debug messages here causes the function to break +function get_neo4j_run_cmd { + + local extra_args=() + + if [ "${EXTENDED_CONF+"yes"}" == "yes" ]; then + extra_args+=("--expand-commands") + fi + if debugging_enabled ; then + extra_args+=("--verbose") + fi + + if running_as_root; then + su-exec neo4j:neo4j neo4j console --dry-run "${extra_args[@]}" + else + neo4j console --dry-run "${extra_args[@]}" + fi +} + +# Use su-exec to drop privileges to neo4j user +# Note that su-exec, despite its name, does not replicate the +# functionality of exec, so we need to use both +if [ "${cmd}" == "neo4j" ]; then + # separate declaration and use of get_neo4j_run_cmd so that error codes are correctly surfaced + debug_msg "getting full neo4j run command" + neo4j_console_cmd="$(get_neo4j_run_cmd)" + debug_msg "${exec_cmd} ${neo4j_console_cmd}" + eval ${exec_cmd} ${neo4j_console_cmd?:No Neo4j command was generated} +else + debug_msg "${exec_cmd}" "$@" + ${exec_cmd} "$@" +fi diff --git a/4.4.36/bullseye/enterprise/local-package/neo4j-admin-report.sh b/4.4.36/bullseye/enterprise/local-package/neo4j-admin-report.sh new file mode 100755 index 0000000..c4c3915 --- /dev/null +++ b/4.4.36/bullseye/enterprise/local-package/neo4j-admin-report.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# load useful utility functions +. /startup/utilities.sh + +function find_report_destination +{ + local to_flag="--to" + + while [[ $# -gt 0 ]]; do + case $1 in + # for arg in "$@"; do + # case $arg in + "${to_flag}"=*) + echo ${1#*=} + return + ;; + "${to_flag}") + echo ${2} + return + ;; + *) + shift + ;; + esac + done + mkdir -p /tmp/reports + echo "/tmp/reports" +} + +report_cmd=("neo4j-admin" "report") + +# note, these debug messages are unlikely to work in a docker exec, since environment isn't preserved. +debug_msg "Called ${0}" +debug_msg "neo4j-admin report command is:" "${report_cmd[@]}" "$@" + +# find report destination. This could be specified by argument to neo4j-admin or it could be the default location. +report_destination=$(find_report_destination "$@") +debug_msg "report_destination will be ${report_destination}" + +debug_msg "Determining which user to run neo4j-admin as." +if running_as_root; then + debug_msg "running neo4j-admin report as root" + if [[ ! $(su-exec neo4j:neo4j test -w "${report_destination}") ]]; then + debug_msg "reowning ${report_destination} to neo4j:neo4j" + chown neo4j:neo4j "${report_destination}" + fi + debug_msg su-exec neo4j:neo4j "${report_cmd[@]}" "$@" + su-exec neo4j:neo4j "${report_cmd[@]}" "$@" +else + debug_msg "running neo4j-admin report as user defined by --user flag" + if [[ ! -w "${report_destination}" ]]; then + print_permissions_advice_and_fail "${report_destination}" "$(id -u)" "$(id -g)" + fi + debug_msg "${report_cmd[@]}" "$@" + "${report_cmd[@]}" "$@" +fi \ No newline at end of file diff --git a/4.4.36/bullseye/enterprise/local-package/neo4j-plugins.json b/4.4.36/bullseye/enterprise/local-package/neo4j-plugins.json new file mode 100644 index 0000000..d738bd1 --- /dev/null +++ b/4.4.36/bullseye/enterprise/local-package/neo4j-plugins.json @@ -0,0 +1,60 @@ +{ + "apoc": { + "versions": "https://neo4j-contrib.github.io/neo4j-apoc-procedures/versions.json", + "properties": { + "dbms.security.procedures.unrestricted": "apoc.*" + } + }, + "apoc-core": { + "location": "/var/lib/neo4j/labs/apoc-*-core.jar", + "versions": "https://neo4j-contrib.github.io/neo4j-apoc-procedures/versions.json", + "properties": { + "dbms.security.procedures.unrestricted": "apoc.*" + } + }, + "bloom": { + "location": "/var/lib/neo4j/products/bloom-plugin-*.jar", + "versions": "https://bloom-plugins.s3.eu-west-2.amazonaws.com/versions.json", + "properties": { + "dbms.unmanaged_extension_classes": "com.neo4j.bloom.server=/browser/bloom", + "dbms.security.procedures.unrestricted": "bloom.*", + "neo4j.bloom.license_file": "/licenses/bloom.license" + } + }, + "streams": { + "versions": "https://neo4j-contrib.github.io/neo4j-streams/versions.json", + "properties": {} + }, + "graphql": { + "versions": "https://neo4j-graphql.github.io/neo4j-graphql/versions.json", + "properties": { + "dbms.unmanaged_extension_classes": "org.neo4j.graphql=/graphql", + "dbms.security.procedures.unrestricted": "graphql.*" + } + }, + "graph-algorithms": { + "versions": "https://neo4j-contrib.github.io/neo4j-graph-algorithms/versions.json", + "properties": { + "dbms.security.procedures.unrestricted": "algo.*" + } + }, + "graph-data-science": { + "versions": "https://graphdatascience.ninja/versions.json", + "location": "/var/lib/neo4j/products/neo4j-graph-data-science-*.jar", + "properties": { + "dbms.security.procedures.unrestricted": "gds.*" + } + }, + "n10s": { + "versions": "https://neo4j-labs.github.io/neosemantics/versions.json", + "properties": { + "dbms.security.procedures.unrestricted":"semantics.*" + } + }, + "_testing": { + "versions": "http://host.testcontainers.internal:3000/versions.json", + "properties": { + "dbms.security.procedures.unrestricted": "com.neo4j.docker.neo4jserver.plugins.*" + } + } +} diff --git a/4.4.36/bullseye/enterprise/local-package/semver.jq b/4.4.36/bullseye/enterprise/local-package/semver.jq new file mode 100755 index 0000000..2d8c144 --- /dev/null +++ b/4.4.36/bullseye/enterprise/local-package/semver.jq @@ -0,0 +1,22 @@ +def _semver_obj2obj($req): + if . == $req then true + elif .major != $req.major then false + elif .minor != $req.minor and .minor != "x" and .minor != "*" then false + elif .patch != $req.patch and .patch != "x" and .patch != "*" then false + elif $req.minor == null and ( .minor == "x" or .minor == "*" ) then false + elif $req.patch == null and ( .patch == "x" or .patch == "*" ) then false + elif $req.major == null and $req.minor == null and $req.patch == null then false + else true end; + +def _ver2obj: + if type == "object" then . + elif type == "string" and test("(?[0-9]+)(\\.(?[0-9x*]+))?(\\.?(?[0-9x*]+))?") then capture("(?[0-9]+)(\\.(?[0-9x*]+))?(\\.?(?[0-9x*]+))?") + elif type == "string" and . == "" then {major: null, minor:null, patch:null} + elif type == "number" then {minor:floor,patch:(.-floor)} + else {major: .} end; + +# Returns true if input version spec semantically matches the requested version +def semver($req): + if $req == null or $req == "" then false + elif . == $req then true + else _ver2obj|_semver_obj2obj($req|_ver2obj) end; diff --git a/4.4.36/bullseye/enterprise/local-package/utilities.sh b/4.4.36/bullseye/enterprise/local-package/utilities.sh new file mode 100644 index 0000000..4943afe --- /dev/null +++ b/4.4.36/bullseye/enterprise/local-package/utilities.sh @@ -0,0 +1,54 @@ + +function running_as_root +{ + test "$(id -u)" = "0" +} + +function secure_mode_enabled +{ + test "${SECURE_FILE_PERMISSIONS:=no}" = "yes" +} + +function debugging_enabled +{ + test "${NEO4J_DEBUG+yes}" = "yes" +} + +function debug_msg +{ + if debugging_enabled; then + echo "$@" + fi +} + +function containsElement +{ + local e match="$1" + shift + for e; do [[ "$e" == "$match" ]] && return 0; done + return 1 +} + +function is_writable +{ + ${exec_cmd} test -w "${1}" +} + +function print_permissions_advice_and_fail +{ + local _directory=${1} + local _userid=${2} + local _groupid=${3} + echo >&2 " +Folder ${_directory} is not accessible for user: ${_userid} or group ${_groupid}. This is commonly a file permissions issue on the mounted folder. + +Hints to solve the issue: +1) Make sure the folder exists before mounting it. Docker will create the folder using root permissions before starting the Neo4j container. The root permissions disallow Neo4j from writing to the mounted folder. +2) Pass the folder owner's user ID and group ID to docker run, so that docker runs as that user. +If the folder is owned by the current user, this can be done by adding this flag to your docker run command: + --user=\$(id -u):\$(id -g) + " + exit 1 +} + + diff --git a/4.4.36/ubi9/community/Dockerfile b/4.4.36/ubi9/community/Dockerfile new file mode 100644 index 0000000..47db4a0 --- /dev/null +++ b/4.4.36/ubi9/community/Dockerfile @@ -0,0 +1,96 @@ +FROM redhat/ubi9-minimal:latest +ENV JAVA_HOME=/usr + +# gather pre-requisite packages +RUN set -eux; \ + arch="$(rpm --query --queryformat='%{ARCH}' rpm)"; \ + case "${arch}" in \ + 'x86_64') \ + tini_url="https://github.com/krallin/tini/releases/download/v0.19.0/tini"; \ + tini_sha="93dcc18adc78c65a028a84799ecf8ad40c936fdfc5f2a57b1acda5a8117fa82c"; \ + ;; \ + 'aarch64') \ + tini_url="https://github.com/krallin/tini/releases/download/v0.19.0/tini-arm64"; \ + tini_sha="07952557df20bfd2a95f9bef198b445e006171969499a1d361bd9e6f8e5e0e81"; \ + ;; \ + *) echo >&2 "Neo4j does not currently have a docker image for architecture $arch"; exit 1 ;; \ + esac; \ + microdnf install -y dnf; \ + dnf install -y \ + findutils \ + gcc \ + git \ + gzip \ + hostname \ + java-11-openjdk-headless \ + jq \ + make \ + procps \ + shadow-utils \ + tar \ + wget \ + which; \ + # download tini and openssl + wget -q ${tini_url} -O /usr/bin/tini; \ + wget -q ${tini_url}.asc -O tini.asc; \ + echo "${tini_sha}" /usr/bin/tini | sha256sum -c --strict --quiet; \ + chmod a+x /usr/bin/tini; \ + export GNUPGHOME="$(mktemp -d)"; \ + gpg --batch --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys \ + 595E85A6B1B4779EA4DAAEC70B588DFF0527A9B7 \ + B42F6819007F00F88E364FD4036A9C25BF357DD4; \ + gpg --batch --verify tini.asc /usr/bin/tini; \ + git clone https://github.com/ncopa/su-exec.git; \ + cd su-exec; \ + git checkout 4c3bb42b093f14da70d8ab924b487ccfbb1397af; \ + echo d6c40440609a23483f12eb6295b5191e94baf08298a856bab6e15b10c3b82891 su-exec.c | sha256sum -c; \ + echo 2a87af245eb125aca9305a0b1025525ac80825590800f047419dc57bba36b334 Makefile | sha256sum -c; \ + make; \ + mv /su-exec/su-exec /usr/bin/su-exec; \ + gpgconf --kill all; \ + rm -rf "$GNUPGHOME" tini.asc /su-exec; \ + dnf remove -y gcc git make; \ + dnf autoremove; \ + dnf clean all + +ENV PATH="${JAVA_HOME}/bin:${PATH}" \ + NEO4J_SHA256=e870cd27e1c829998921addfc289dc0f44f838a4241e3cadf3acd94953a66a38 \ + NEO4J_TARBALL=neo4j-community-4.4.36-unix.tar.gz \ + NEO4J_EDITION=community \ + NEO4J_HOME="/var/lib/neo4j" \ + LANG=C.UTF-8 +ARG NEO4J_URI=https://dist.neo4j.org/neo4j-community-4.4.36-unix.tar.gz + +COPY ./local-package/* /startup/ + +RUN set -eux; \ + groupadd --gid 7474 --system neo4j && useradd --uid 7474 --system --no-create-home --home "${NEO4J_HOME}" --gid neo4j neo4j; \ + curl --fail --silent --show-error --location --remote-name ${NEO4J_URI}; \ + echo "${NEO4J_SHA256} ${NEO4J_TARBALL}" | sha256sum -c --strict --quiet; \ + tar --extract --file ${NEO4J_TARBALL} --directory /var/lib; \ + mv /var/lib/neo4j-* "${NEO4J_HOME}"; \ + rm ${NEO4J_TARBALL}; \ + mv /startup/neo4j-admin-report.sh "${NEO4J_HOME}"/bin/neo4j-admin-report; \ + mv "${NEO4J_HOME}"/data /data; \ + mv "${NEO4J_HOME}"/logs /logs; \ + chown -R neo4j:neo4j /data; \ + chmod -R 777 /data; \ + chown -R neo4j:neo4j /logs; \ + chmod -R 777 /logs; \ + chown -R neo4j:neo4j "${NEO4J_HOME}"; \ + chmod -R 777 "${NEO4J_HOME}"; \ + chmod -R 755 "${NEO4J_HOME}/bin"; \ + ln -s /data "${NEO4J_HOME}"/data; \ + ln -s /logs "${NEO4J_HOME}"/logs; \ + ln -s /startup/docker-entrypoint.sh /docker-entrypoint.sh + +ENV PATH "${NEO4J_HOME}"/bin:$PATH + +WORKDIR "${NEO4J_HOME}" + +VOLUME /data /logs + +EXPOSE 7474 7473 7687 + +ENTRYPOINT ["tini", "-g", "--", "/startup/docker-entrypoint.sh"] +CMD ["neo4j"] diff --git a/4.4.36/ubi9/community/local-package/docker-entrypoint.sh b/4.4.36/ubi9/community/local-package/docker-entrypoint.sh new file mode 100755 index 0000000..2b105f4 --- /dev/null +++ b/4.4.36/ubi9/community/local-package/docker-entrypoint.sh @@ -0,0 +1,647 @@ +#!/bin/bash -eu + +cmd="$1" + +# load useful utility functions +. /startup/utilities.sh + +function is_readable +{ + # this code is fairly ugly but works no matter who this script is running as. + # It would be nice if the writability tests could use this logic somehow. + local _file=${1} + perm=$(stat -c %a "${_file}") + + # everyone permission + if [[ ${perm:2:1} -ge 4 ]]; then + return 0 + fi + # owner permissions + if [[ ${perm:0:1} -ge 4 ]]; then + if [[ "$(stat -c %U ${_file})" = "${userid}" ]] || [[ "$(stat -c %u ${_file})" = "${userid}" ]]; then + return 0 + fi + fi + # group permissions + if [[ ${perm:1:1} -ge 4 ]]; then + if containsElement "$(stat -c %g ${_file})" "${groups[@]}" || containsElement "$(stat -c %G ${_file})" "${groups[@]}" ; then + return 0 + fi + fi + return 1 +} + +function is_writable +{ + # It would be nice if this and the is_readable function could combine somehow + local _file=${1} + perm=$(stat -c %a "${_file}") + + # everyone permission + if containsElement ${perm:2:1} 2 3 6 7; then + return 0 + fi + # owner permissions + if containsElement ${perm:0:1} 2 3 6 7; then + if [[ "$(stat -c %U ${_file})" = "${userid}" ]] || [[ "$(stat -c %u ${_file})" = "${userid}" ]]; then + return 0 + fi + fi + # group permissions + if containsElement ${perm:1:1} 2 3 6 7; then + if containsElement "$(stat -c %g ${_file})" "${groups[@]}" || containsElement "$(stat -c %G ${_file})" "${groups[@]}" ; then + return 0 + fi + fi + return 1 +} + +function check_mounted_folder_readable +{ + local _directory=${1} + debug_msg "checking ${_directory} is readable" + if ! is_readable "${_directory}"; then + print_permissions_advice_and_fail "${_directory}" "${userid}" "${groupid}" + fi +} + +function check_mounted_folder_writable_with_chown +{ +# The /data and /log directory are a bit different because they are very likely to be mounted by the user but not +# necessarily writable. +# This depends on whether a user ID is passed to the container and which folders are mounted. +# +# No user ID passed to container: +# 1) No folders are mounted. +# The /data and /log folder are owned by neo4j by default, so should be writable already. +# 2) Both /log and /data are mounted. +# This means on start up, /data and /logs are owned by an unknown user and we should chown them to neo4j for +# backwards compatibility. +# +# User ID passed to container: +# 1) Both /data and /logs are mounted +# The /data and /logs folders are owned by an unknown user but we *should* have rw permission to them. +# That should be verified and error (helpfully) if not. +# 2) User mounts /data or /logs *but not both* +# The unmounted folder is still owned by neo4j, which should already be writable. The mounted folder should +# have rw permissions through user id. This should be verified. +# 3) No folders are mounted. +# The /data and /log folder are owned by neo4j by default, and these are already writable by the user. +# (This is a very unlikely use case). + + local mountFolder=${1} + debug_msg "checking ${mountFolder} is writable" + if running_as_root && ! secure_mode_enabled; then + # check folder permissions + if ! is_writable "${mountFolder}" ; then + # warn that we're about to chown the folder and then chown it + echo "Warning: Folder mounted to \"${mountFolder}\" is not writable from inside container. Changing folder owner to ${userid}." + chown -R "${userid}":"${groupid}" "${mountFolder}" + # check permissions on files in the folder + elif [ $(su-exec "${userid}":"${groupid}" find "${mountFolder}" -not -writable | wc -l) -gt 0 ]; then + echo "Warning: Some files inside \"${mountFolder}\" are not writable from inside container. Changing folder owner to ${userid}." + chown -R "${userid}":"${groupid}" "${mountFolder}" + fi + else + if [[ ! -w "${mountFolder}" ]] && [[ "$(stat -c %U ${mountFolder})" != "neo4j" ]]; then + print_permissions_advice_and_fail "${mountFolder}" "${userid}" "${groupid}" + fi + fi +} + +function load_plugin_from_location +{ + # Install a plugin from location at runtime. + local _plugin_name="${1}" + local _location="${2}" + + local _plugins_dir="${NEO4J_HOME}/plugins" + if [ -d /plugins ]; then + local _plugins_dir="/plugins" + fi + + local _destination="${_plugins_dir}/${_plugin_name}.jar" + + # Now we install the plugin that is shipped with Neo4j + for filename in ${_location}; do + echo "Installing Plugin '${_plugin_name}' from ${_location} to ${_destination}" + cp --preserve "${filename}" "${_destination}" + chmod +rw ${_destination} + done + + if ! is_readable "${_destination}"; then + echo >&2 "Plugin at '${_destination}' is not readable" + exit 1 + fi +} + +function load_plugin_from_url +{ + # Load a plugin at runtime. The provided github repository must have a versions.json on the master branch with the + # correct format. + local _plugin_name="${1}" #e.g. apoc, graph-algorithms, graph-ql + + local _plugins_dir="${NEO4J_HOME}/plugins" + if [ -d /plugins ]; then + local _plugins_dir="/plugins" + fi + local _versions_json_url="$(jq --raw-output "with_entries( select(.key==\"${_plugin_name}\") ) | to_entries[] | .value.versions" /startup/neo4j-plugins.json )" + debug_msg "Will read ${_plugin_name} versions.json from ${_versions_json_url}" + # Using the same name for the plugin irrespective of version ensures we don't end up with different versions of the same plugin + local _destination="${_plugins_dir}/${_plugin_name}.jar" + local _neo4j_version="$(neo4j --version | cut -d' ' -f2)" + + # Now we call out to github to get the versions.json for this plugin and we parse that to find the url for the correct plugin jar for our neo4j version + echo "Fetching versions.json for Plugin '${_plugin_name}' from ${_versions_json_url}" + local _versions_json + if ! _versions_json="$(wget -q --timeout 300 --tries 30 -O - "${_versions_json_url}")"; then + debug_msg "ERROR: could not fetch '${_versions_json}'" + echo >&2 "ERROR: could not query ${_versions_json_url} for plugin compatibility information. + This could indicate a problem with your network or this container's network settings. + Neo4j will continue to start, but \"${_plugin_name}\" will not be loaded." + return + fi + local _plugin_jar_url="$(echo "${_versions_json}" | jq -L/startup --raw-output "import \"semver\" as lib; [ .[] | select(.neo4j|lib::semver(\"${_neo4j_version}\")) ] | min_by(.neo4j) | .jar")" + if [[ -z "${_plugin_jar_url}" ]] || [[ "${_plugin_jar_url}" == "null" ]]; then + debug_msg "ERROR: '${_versions_json_url}' does not contain an entry for ${_neo4j_version}" + echo >&2 "ERROR: No compatible \"${_plugin_name}\" plugin found for Neo4j ${_neo4j_version} ${NEO4J_EDITION}. + This can happen with the newest Neo4j versions when a compatible plugin has not yet been released. + You can either use an older version of Neo4j, or continue without ${_plugin_name}. + Neo4j will continue to start, but \"${_plugin_name}\" will not be loaded." + else + echo "Installing Plugin '${_plugin_name}' from ${_plugin_jar_url} to ${_destination} " + wget -q --timeout 300 --tries 30 --output-document="${_destination}" "${_plugin_jar_url}" + + if ! is_readable "${_destination}"; then + echo >&2 "Plugin at '${_destination}' is not readable" + exit 1 + fi + fi +} + +function apply_plugin_default_configuration +{ + # Set the correct Load a plugin at runtime. The provided github repository must have a versions.json on the master branch with the + # correct format. + local _plugin_name="${1}" #e.g. apoc, graph-algorithms, graphql + local _reference_conf="${2}" # used to determine if we can override properties + local _neo4j_conf="${NEO4J_HOME}/conf/neo4j.conf" + + local _property _value + echo "Applying default values for plugin ${_plugin_name} to neo4j.conf" + for _entry in $(jq --compact-output --raw-output "with_entries( select(.key==\"${_plugin_name}\") ) | to_entries[] | .value.properties | to_entries[]" /startup/neo4j-plugins.json); do + _property="$(jq --raw-output '.key' <<< "${_entry}")" + _value="$(jq --raw-output '.value' <<< "${_entry}")" + debug_msg "${_plugin_name} requires setting ${_property}=${_value}" + + # the first grep strips out comments + if grep -o "^[^#]*" "${_reference_conf}" | grep -q --fixed-strings "${_property}=" ; then + # property is already set in the user provided config. In this case we don't override what has been set explicitly by the user. + echo "Skipping ${_property} for plugin ${_plugin_name} because it is already set." + echo "You may need to add ${_value} to the ${_property} setting in your configuration file." + else + if grep -o "^[^#]*" "${_neo4j_conf}" | grep -q --fixed-strings "${_property}=" ; then + sed --in-place "s/${_property}=/&${_value},/" "${_neo4j_conf}" + debug_msg "${_property} was already in the configuration file, so ${_value} was added to it." + else + echo -e "\n${_property}=${_value}" >> "${_neo4j_conf}" + debug_msg "${_property}=${_value} has been added to the configuration file." + fi + fi + done +} + +function install_neo4j_labs_plugins +{ + # first verify that the requested plugins are valid. + debug_msg "One or more NEO4J_PLUGINS have been requested." + local _known_plugins=($(jq --raw-output "keys[]" /startup/neo4j-plugins.json)) + debug_msg "Checking requested plugins are known and can be installed." + for plugin_name in $(echo "${NEO4J_PLUGINS}" | jq --raw-output '.[]'); do + if ! containsElement "${plugin_name}" "${_known_plugins[@]}"; then + printf >&2 "\"%s\" is not a known Neo4j plugin. Options are:\n%s" "${plugin_name}" "$(jq --raw-output "keys[1:][]" /startup/neo4j-plugins.json)" + exit 1 + fi + done + + # We store a copy of the config before we modify it for the plugins to allow us to see if there are user-set values in the input config that we shouldn't override + local _old_config="$(mktemp)" + if [ -e "${NEO4J_HOME}"/conf/neo4j.conf ]; then + cp "${NEO4J_HOME}"/conf/neo4j.conf "${_old_config}" + else + touch "${NEO4J_HOME}"/conf/neo4j.conf + touch "${_old_config}" + fi + for plugin_name in $(echo "${NEO4J_PLUGINS}" | jq --raw-output '.[]'); do + debug_msg "Plugin ${plugin_name} has been requested" + local _location="$(jq --raw-output "with_entries( select(.key==\"${plugin_name}\") ) | to_entries[] | .value.location" /startup/neo4j-plugins.json )" + if [ "${_location}" != "null" -a -n "$(shopt -s nullglob; echo ${_location})" ]; then + debug_msg "$plugin_name is already in the container at ${_location}" + load_plugin_from_location "${plugin_name}" "${_location}" + else + debug_msg "$plugin_name must be downloaded." + load_plugin_from_url "${plugin_name}" + fi + debug_msg "Applying plugin specific configurations." + apply_plugin_default_configuration "${plugin_name}" "${_old_config}" + done + rm "${_old_config}" +} + +function add_docker_default_to_conf +{ + # docker defaults should NOT overwrite values already in the conf file + local _setting="${1}" + local _value="${2}" + + if ! grep -q "^${_setting}=" "${NEO4J_HOME}"/conf/neo4j.conf + then + debug_msg "Appended ${_setting}=${_value} to ${NEO4J_HOME}/conf/neo4j.conf" + echo -e "\n"${_setting}=${_value} >> "${NEO4J_HOME}"/conf/neo4j.conf + fi +} + +function add_env_setting_to_conf +{ + # settings from environment variables should overwrite values already in the conf + local _setting=${1} + local _value=${2} + local _append_not_replace_configs=("dbms.jvm.additional") + + if grep -q -F "${_setting}=" "${NEO4J_HOME}"/conf/neo4j.conf; then + if containsElement "${_setting}" "${_append_not_replace_configs[@]}"; then + debug_msg "${_setting} will be appended to neo4j.conf without replacing existing settings." + else + # Remove any lines containing the setting already + debug_msg "Removing existing setting for ${_setting}" + sed --in-place "/^${_setting}=.*/d" "${NEO4J_HOME}"/conf/neo4j.conf + fi + fi + # Then always append setting to file + debug_msg "Appended ${_setting}=${_value} to ${NEO4J_HOME}/conf/neo4j.conf" + echo "${_setting}=${_value}" >> "${NEO4J_HOME}"/conf/neo4j.conf +} + +function set_initial_password +{ + local _neo4j_auth="${1}" + + # set the neo4j initial password only if you run the database server + if [ "${cmd}" == "neo4j" ]; then + if [ "${_neo4j_auth:-}" == "none" ]; then + debug_msg "Authentication is requested to be unset" + add_env_setting_to_conf "dbms.security.auth_enabled" "false" + elif [[ "${_neo4j_auth:-}" =~ ^([^/]+)\/([^/]+)/?([tT][rR][uU][eE])?$ ]]; then + admin_user="${BASH_REMATCH[1]}" + password="${BASH_REMATCH[2]}" + do_reset="${BASH_REMATCH[3]}" + + if [ "${password}" == "neo4j" ]; then + echo >&2 "Invalid value for password. It cannot be 'neo4j', which is the default." + exit 1 + fi + if [ "${admin_user}" != "neo4j" ]; then + echo >&2 "Invalid admin username, it must be neo4j." + exit 1 + fi + + if running_as_root; then + # running set-initial-password as root will create subfolders to /data as root, causing startup fail when neo4j can't read or write the /data/dbms folder + # creating the folder first will avoid that + mkdir -p /data/dbms + debug_msg "Making sure /data/dbms is owned by ${userid}:${groupid}" + chown "${userid}":"${groupid}" /data/dbms + fi + + local extra_args=() + if [ "${do_reset}" == "true" ]; then + extra_args+=("--require-password-change") + fi + if [ "${EXTENDED_CONF+"yes"}" == "yes" ]; then + extra_args+=("--expand-commands") + fi + if debugging_enabled; then + extra_args+=("--verbose") + fi + debug_msg "Setting initial password" + debug_msg "${neo4j_admin_cmd} set-initial-password ***** ${extra_args[*]}" + if debugging_enabled; then + # don't suppress any output or errors in debugging mode + ${neo4j_admin_cmd} set-initial-password "${password}" "${extra_args[@]}" + else + # Will exit with error if users already exist (and print a message explaining that) + # we probably don't want the message though, since it throws an error message on restarting the container. + ${neo4j_admin_cmd} set-initial-password "${password}" "${extra_args[@]}" 2>/dev/null || true + fi + + elif [ -n "${_neo4j_auth:-}" ]; then + echo "$_neo4j_auth is invalid" + echo >&2 "Invalid value for NEO4J_AUTH: '${_neo4j_auth}'" + exit 1 + fi + fi +} + +# ==== CODE STARTS ==== +debug_msg "DEBUGGING ENABLED" + +# If we're running as root, then run as the neo4j user. Otherwise +# docker is running with --user and we simply use that user. Note +# that su-exec, despite its name, does not replicate the functionality +# of exec, so we need to use both +if running_as_root; then + userid="neo4j" + groupid="neo4j" + groups=($(id -G neo4j)) + exec_cmd="exec su-exec neo4j:neo4j" + neo4j_admin_cmd="su-exec neo4j:neo4j neo4j-admin" + debug_msg "Running as root user inside neo4j image" +else + userid="$(id -u)" + groupid="$(id -g)" + groups=($(id -G)) + exec_cmd="exec" + neo4j_admin_cmd="neo4j-admin" + debug_msg "Running as user ${userid}:${groupid} inside neo4j image" +fi +readonly userid +readonly groupid +readonly groups +readonly exec_cmd +readonly neo4j_admin_cmd + +# Need to chown the home directory +if running_as_root; then + debug_msg "chowning ${NEO4J_HOME} recursively to ${userid}":"${groupid}" + chown -R "${userid}":"${groupid}" "${NEO4J_HOME}" + chmod 700 "${NEO4J_HOME}" + find "${NEO4J_HOME}" -mindepth 1 -maxdepth 1 -type d -exec chmod -R 700 {} \; + debug_msg "Setting all files in ${NEO4J_HOME}/conf to permissions 600" + find "${NEO4J_HOME}"/conf -type f -exec chmod -R 600 {} \; +fi + +# ==== CHECK LICENSE AGREEMENT ==== + +# Only prompt for license agreement if command contains "neo4j" in it +if [[ "${cmd}" == *"neo4j"* ]]; then + if [ "${NEO4J_EDITION}" == "enterprise" ]; then + if [ "${NEO4J_ACCEPT_LICENSE_AGREEMENT:=no}" != "yes" ]; then + echo >&2 " +In order to use Neo4j Enterprise Edition you must accept the license agreement. + +The license agreement is available at https://neo4j.com/terms/licensing/ +If you have a support contract the following terms apply https://neo4j.com/terms/support-terms/ + +(c) Neo4j Sweden AB. All Rights Reserved. +Use of this Software without a proper commercial license +with Neo4j, Inc. or its affiliates is prohibited. +Neo4j has the right to terminate your usage if you are not compliant. + +More information is also available at: https://neo4j.com/licensing/ +If you have further inquiries about licensing, please contact us via https://neo4j.com/contact-us/ + +To accept the commercial license agreement set the environment variable +NEO4J_ACCEPT_LICENSE_AGREEMENT=yes + +To do this you can use the following docker argument: + + --env=NEO4J_ACCEPT_LICENSE_AGREEMENT=yes +" + exit 1 + fi + fi +fi + +# NEO4JLABS_PLUGINS is renamed to NEO4J_PLUGINS in 5.x, but we want the new name to work against 4.4 images too +if [ -n "${NEO4JLABS_PLUGINS:-}" ]; +then + : ${NEO4J_PLUGINS:=${NEO4JLABS_PLUGINS:-}} +fi + +# ==== RENAME LEGACY ENVIRONMENT CONF VARIABLES ==== + +# Env variable naming convention: +# - prefix NEO4J_ +# - double underscore char '__' instead of single underscore '_' char in the setting name +# - underscore char '_' instead of dot '.' char in the setting name +# Example: +# NEO4J_dbms_tx__log_rotation_retention__policy env variable to set +# dbms.tx_log.rotation.retention_policy setting + +# Backward compatibility - map old hardcoded env variables into new naming convention (if they aren't set already) +# Set some to default values if unset +: ${NEO4J_dbms_tx__log_rotation_retention__policy:=${NEO4J_dbms_txLog_rotation_retentionPolicy:-}} +: ${NEO4J_dbms_unmanaged__extension__classes:=${NEO4J_dbms_unmanagedExtensionClasses:-}} +: ${NEO4J_dbms_allow__format__migration:=${NEO4J_dbms_allowFormatMigration:-}} +: ${NEO4J_dbms_connectors_default__advertised__address:=${NEO4J_dbms_connectors_defaultAdvertisedAddress:-}} + +if [ "${NEO4J_EDITION}" == "enterprise" ]; + then + : ${NEO4J_causal__clustering_expected__core__cluster__size:=${NEO4J_causalClustering_expectedCoreClusterSize:-}} + : ${NEO4J_causal__clustering_initial__discovery__members:=${NEO4J_causalClustering_initialDiscoveryMembers:-}} + debug_msg "Copying contents of /conf to ${NEO4J_HOME}/conf/*" + : ${NEO4J_causal__clustering_discovery__advertised__address:=${NEO4J_causalClustering_discoveryAdvertisedAddress:-}} + : ${NEO4J_causal__clustering_transaction__advertised__address:=${NEO4J_causalClustering_transactionAdvertisedAddress:-}} + : ${NEO4J_causal__clustering_raft__advertised__address:=${NEO4J_causalClustering_raftAdvertisedAddress:-}} +fi + +# unset old hardcoded unsupported env variables +unset NEO4J_dbms_txLog_rotation_retentionPolicy NEO4J_UDC_SOURCE \ + NEO4J_dbms_unmanagedExtensionClasses NEO4J_dbms_allowFormatMigration \ + NEO4J_dbms_connectors_defaultAdvertisedAddress NEO4J_ha_serverId \ + NEO4J_ha_initialHosts NEO4J_causalClustering_expectedCoreClusterSize \ + NEO4J_causalClustering_initialDiscoveryMembers \ + NEO4J_causalClustering_discoveryListenAddress \ + NEO4J_causalClustering_discoveryAdvertisedAddress \ + NEO4J_causalClustering_transactionListenAddress \ + NEO4J_causalClustering_transactionAdvertisedAddress \ + NEO4J_causalClustering_raftListenAddress \ + NEO4J_causalClustering_raftAdvertisedAddress + +# ==== CHECK FILE PERMISSIONS ON MOUNTED FOLDERS ==== + + +if [ -d /conf ]; then + check_mounted_folder_readable "/conf" + rm -rf "${NEO4J_HOME}"/conf/* + debug_msg "Copying contents of /conf to ${NEO4J_HOME}/conf/*" + find /conf -type f -exec cp --preserve=ownership,mode {} "${NEO4J_HOME}"/conf \; +fi + +if [ -d /ssl ]; then + check_mounted_folder_readable "/ssl" + rm -rf "${NEO4J_HOME}"/certificates + ln -s /ssl "${NEO4J_HOME}"/certificates +fi + +if [ -d /plugins ]; then + if [[ -n "${NEO4J_PLUGINS:-}" ]]; then + # We need write permissions to write the required plugins to /plugins + debug_msg "Extra plugins were requested. Ensuring the mounted /plugins folder has the required write permissions." + check_mounted_folder_writable_with_chown "/plugins" + fi + check_mounted_folder_readable "/plugins" + : ${NEO4J_dbms_directories_plugins:="/plugins"} +fi + +if [ -d /import ]; then + check_mounted_folder_readable "/import" + : ${NEO4J_dbms_directories_import:="/import"} +fi + +if [ -d /metrics ]; then + # metrics is enterprise only + if [ "${NEO4J_EDITION}" == "enterprise" ]; + then + check_mounted_folder_writable_with_chown "/metrics" + : ${NEO4J_dbms_directories_metrics:="/metrics"} + fi +fi + +if [ -d /logs ]; then + check_mounted_folder_writable_with_chown "/logs" + : ${NEO4J_dbms_directories_logs:="/logs"} +fi + +if [ -d /data ]; then + check_mounted_folder_writable_with_chown "/data" + if [ -d /data/databases ]; then + check_mounted_folder_writable_with_chown "/data/databases" + fi + if [ -d /data/dbms ]; then + check_mounted_folder_writable_with_chown "/data/dbms" + fi + if [ -d /data/transactions ]; then + check_mounted_folder_writable_with_chown "/data/transactions" + fi +fi + +if [ -d /licenses ]; then + check_mounted_folder_readable "/licenses" + : ${NEO4J_dbms_directories_licenses:="/licenses"} +fi + +# ==== SET CONFIGURATIONS ==== + +## == DOCKER SPECIFIC DEFAULT CONFIGURATIONS === +## these should not override *any* configurations set by the user + +debug_msg "Setting docker specific configuration overrides" +add_docker_default_to_conf "dbms.memory.pagecache.size" "512M" +add_docker_default_to_conf "dbms.default_listen_address" "0.0.0.0" + +# set enterprise only docker defaults +if [ "${NEO4J_EDITION}" == "enterprise" ]; +then + debug_msg "Setting docker specific Enterprise Edition overrides" + add_docker_default_to_conf "causal_clustering.discovery_advertised_address" "$(hostname):5000" + add_docker_default_to_conf "causal_clustering.transaction_advertised_address" "$(hostname):6000" + add_docker_default_to_conf "causal_clustering.raft_advertised_address" "$(hostname):7000" + add_docker_default_to_conf "dbms.routing.advertised_address" "$(hostname):7688" +fi + +## == ENVIRONMENT VARIABLE CONFIGURATIONS === +## these override BOTH defaults and any existing values in the neo4j.conf file + +# these are docker control envs that have the NEO4J_ prefix but we don't want to add to the config. +not_configs=("NEO4J_ACCEPT_LICENSE_AGREEMENT" "NEO4J_AUTH" "NEO4J_AUTH_PATH" "NEO4J_DEBUG" "NEO4J_EDITION" \ + "NEO4J_HOME" "NEO4J_PLUGINS" "NEO4J_SHA256" "NEO4J_TARBALL") + +debug_msg "Applying configuration settings that have been set using environment variables." +# list env variables with prefix NEO4J_ and create settings from them +for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do + if containsElement "$i" "${not_configs[@]}"; then + continue + fi + setting=$(echo "${i}" | sed 's|^NEO4J_||' | sed 's|_|.|g' | sed 's|\.\.|_|g') + value=$(echo "${!i}") + # Don't allow settings with no value or settings that start with a number (neo4j converts settings to env variables and you cannot have an env variable that starts with a number) + if [[ -n ${value} ]]; then + if [[ ! "${setting}" =~ ^[0-9]+.*$ ]]; then + add_env_setting_to_conf "${setting}" "${value}" + else + echo >&2 "WARNING: ${setting} not written to conf file. Settings that start with a number are not permitted." + fi + fi +done + +# ==== SET PASSWORD AND PLUGINS ==== + +if [[ -n "${NEO4J_AUTH_PATH:-}" ]]; then + # Validate the existence of the password file + if [ ! -f "${NEO4J_AUTH_PATH}" ]; then + echo >&2 "The password file '${NEO4J_AUTH_PATH}' does not exist" + exit 1 + fi + # validate the password file is readable + check_mounted_folder_readable "${NEO4J_AUTH_PATH}" + + debug_msg "Setting initial password from file ${NEO4J_AUTH_PATH}" + set_initial_password "$(cat ${NEO4J_AUTH_PATH})" +else + debug_msg "Setting initial password from environment" + set_initial_password "${NEO4J_AUTH:-}" +fi + + +if [[ ! -z "${NEO4J_PLUGINS:-}" ]]; then + # NEO4J_PLUGINS should be a json array of plugins like '["graph-algorithms", "apoc", "streams", "graphql"]' + install_neo4j_labs_plugins +fi + +# ==== CLEANUP RUN FILE ==== + +if [ -f "${NEO4J_HOME}"/run/neo4j.pid ]; +then + rm "${NEO4J_HOME}"/run/neo4j.pid +fi + +# ==== INVOKE NEO4J STARTUP ==== + +[ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT} + +if [ "${cmd}" == "dump-config" ]; then + if [ ! -d "/conf" ]; then + echo >&2 "You must mount a folder to /conf so that the configuration file(s) can be dumped to there." + exit 1 + fi + check_mounted_folder_writable_with_chown "/conf" + cp --recursive "${NEO4J_HOME}"/conf/* /conf + echo "Config Dumped" + exit 0 +fi + +# this prints out a command for us to run. +# the command is something like: `java ...[lots of java options]... neo4j.mainClass ...[some neo4j options]...` +# putting debug messages here causes the function to break +function get_neo4j_run_cmd { + + local extra_args=() + + if [ "${EXTENDED_CONF+"yes"}" == "yes" ]; then + extra_args+=("--expand-commands") + fi + if debugging_enabled ; then + extra_args+=("--verbose") + fi + + if running_as_root; then + su-exec neo4j:neo4j neo4j console --dry-run "${extra_args[@]}" + else + neo4j console --dry-run "${extra_args[@]}" + fi +} + +# Use su-exec to drop privileges to neo4j user +# Note that su-exec, despite its name, does not replicate the +# functionality of exec, so we need to use both +if [ "${cmd}" == "neo4j" ]; then + # separate declaration and use of get_neo4j_run_cmd so that error codes are correctly surfaced + debug_msg "getting full neo4j run command" + neo4j_console_cmd="$(get_neo4j_run_cmd)" + debug_msg "${exec_cmd} ${neo4j_console_cmd}" + eval ${exec_cmd} ${neo4j_console_cmd?:No Neo4j command was generated} +else + debug_msg "${exec_cmd}" "$@" + ${exec_cmd} "$@" +fi diff --git a/4.4.36/ubi9/community/local-package/neo4j-admin-report.sh b/4.4.36/ubi9/community/local-package/neo4j-admin-report.sh new file mode 100755 index 0000000..c4c3915 --- /dev/null +++ b/4.4.36/ubi9/community/local-package/neo4j-admin-report.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# load useful utility functions +. /startup/utilities.sh + +function find_report_destination +{ + local to_flag="--to" + + while [[ $# -gt 0 ]]; do + case $1 in + # for arg in "$@"; do + # case $arg in + "${to_flag}"=*) + echo ${1#*=} + return + ;; + "${to_flag}") + echo ${2} + return + ;; + *) + shift + ;; + esac + done + mkdir -p /tmp/reports + echo "/tmp/reports" +} + +report_cmd=("neo4j-admin" "report") + +# note, these debug messages are unlikely to work in a docker exec, since environment isn't preserved. +debug_msg "Called ${0}" +debug_msg "neo4j-admin report command is:" "${report_cmd[@]}" "$@" + +# find report destination. This could be specified by argument to neo4j-admin or it could be the default location. +report_destination=$(find_report_destination "$@") +debug_msg "report_destination will be ${report_destination}" + +debug_msg "Determining which user to run neo4j-admin as." +if running_as_root; then + debug_msg "running neo4j-admin report as root" + if [[ ! $(su-exec neo4j:neo4j test -w "${report_destination}") ]]; then + debug_msg "reowning ${report_destination} to neo4j:neo4j" + chown neo4j:neo4j "${report_destination}" + fi + debug_msg su-exec neo4j:neo4j "${report_cmd[@]}" "$@" + su-exec neo4j:neo4j "${report_cmd[@]}" "$@" +else + debug_msg "running neo4j-admin report as user defined by --user flag" + if [[ ! -w "${report_destination}" ]]; then + print_permissions_advice_and_fail "${report_destination}" "$(id -u)" "$(id -g)" + fi + debug_msg "${report_cmd[@]}" "$@" + "${report_cmd[@]}" "$@" +fi \ No newline at end of file diff --git a/4.4.36/ubi9/community/local-package/neo4j-plugins.json b/4.4.36/ubi9/community/local-package/neo4j-plugins.json new file mode 100644 index 0000000..d738bd1 --- /dev/null +++ b/4.4.36/ubi9/community/local-package/neo4j-plugins.json @@ -0,0 +1,60 @@ +{ + "apoc": { + "versions": "https://neo4j-contrib.github.io/neo4j-apoc-procedures/versions.json", + "properties": { + "dbms.security.procedures.unrestricted": "apoc.*" + } + }, + "apoc-core": { + "location": "/var/lib/neo4j/labs/apoc-*-core.jar", + "versions": "https://neo4j-contrib.github.io/neo4j-apoc-procedures/versions.json", + "properties": { + "dbms.security.procedures.unrestricted": "apoc.*" + } + }, + "bloom": { + "location": "/var/lib/neo4j/products/bloom-plugin-*.jar", + "versions": "https://bloom-plugins.s3.eu-west-2.amazonaws.com/versions.json", + "properties": { + "dbms.unmanaged_extension_classes": "com.neo4j.bloom.server=/browser/bloom", + "dbms.security.procedures.unrestricted": "bloom.*", + "neo4j.bloom.license_file": "/licenses/bloom.license" + } + }, + "streams": { + "versions": "https://neo4j-contrib.github.io/neo4j-streams/versions.json", + "properties": {} + }, + "graphql": { + "versions": "https://neo4j-graphql.github.io/neo4j-graphql/versions.json", + "properties": { + "dbms.unmanaged_extension_classes": "org.neo4j.graphql=/graphql", + "dbms.security.procedures.unrestricted": "graphql.*" + } + }, + "graph-algorithms": { + "versions": "https://neo4j-contrib.github.io/neo4j-graph-algorithms/versions.json", + "properties": { + "dbms.security.procedures.unrestricted": "algo.*" + } + }, + "graph-data-science": { + "versions": "https://graphdatascience.ninja/versions.json", + "location": "/var/lib/neo4j/products/neo4j-graph-data-science-*.jar", + "properties": { + "dbms.security.procedures.unrestricted": "gds.*" + } + }, + "n10s": { + "versions": "https://neo4j-labs.github.io/neosemantics/versions.json", + "properties": { + "dbms.security.procedures.unrestricted":"semantics.*" + } + }, + "_testing": { + "versions": "http://host.testcontainers.internal:3000/versions.json", + "properties": { + "dbms.security.procedures.unrestricted": "com.neo4j.docker.neo4jserver.plugins.*" + } + } +} diff --git a/4.4.36/ubi9/community/local-package/semver.jq b/4.4.36/ubi9/community/local-package/semver.jq new file mode 100755 index 0000000..2d8c144 --- /dev/null +++ b/4.4.36/ubi9/community/local-package/semver.jq @@ -0,0 +1,22 @@ +def _semver_obj2obj($req): + if . == $req then true + elif .major != $req.major then false + elif .minor != $req.minor and .minor != "x" and .minor != "*" then false + elif .patch != $req.patch and .patch != "x" and .patch != "*" then false + elif $req.minor == null and ( .minor == "x" or .minor == "*" ) then false + elif $req.patch == null and ( .patch == "x" or .patch == "*" ) then false + elif $req.major == null and $req.minor == null and $req.patch == null then false + else true end; + +def _ver2obj: + if type == "object" then . + elif type == "string" and test("(?[0-9]+)(\\.(?[0-9x*]+))?(\\.?(?[0-9x*]+))?") then capture("(?[0-9]+)(\\.(?[0-9x*]+))?(\\.?(?[0-9x*]+))?") + elif type == "string" and . == "" then {major: null, minor:null, patch:null} + elif type == "number" then {minor:floor,patch:(.-floor)} + else {major: .} end; + +# Returns true if input version spec semantically matches the requested version +def semver($req): + if $req == null or $req == "" then false + elif . == $req then true + else _ver2obj|_semver_obj2obj($req|_ver2obj) end; diff --git a/4.4.36/ubi9/community/local-package/utilities.sh b/4.4.36/ubi9/community/local-package/utilities.sh new file mode 100644 index 0000000..4943afe --- /dev/null +++ b/4.4.36/ubi9/community/local-package/utilities.sh @@ -0,0 +1,54 @@ + +function running_as_root +{ + test "$(id -u)" = "0" +} + +function secure_mode_enabled +{ + test "${SECURE_FILE_PERMISSIONS:=no}" = "yes" +} + +function debugging_enabled +{ + test "${NEO4J_DEBUG+yes}" = "yes" +} + +function debug_msg +{ + if debugging_enabled; then + echo "$@" + fi +} + +function containsElement +{ + local e match="$1" + shift + for e; do [[ "$e" == "$match" ]] && return 0; done + return 1 +} + +function is_writable +{ + ${exec_cmd} test -w "${1}" +} + +function print_permissions_advice_and_fail +{ + local _directory=${1} + local _userid=${2} + local _groupid=${3} + echo >&2 " +Folder ${_directory} is not accessible for user: ${_userid} or group ${_groupid}. This is commonly a file permissions issue on the mounted folder. + +Hints to solve the issue: +1) Make sure the folder exists before mounting it. Docker will create the folder using root permissions before starting the Neo4j container. The root permissions disallow Neo4j from writing to the mounted folder. +2) Pass the folder owner's user ID and group ID to docker run, so that docker runs as that user. +If the folder is owned by the current user, this can be done by adding this flag to your docker run command: + --user=\$(id -u):\$(id -g) + " + exit 1 +} + + diff --git a/4.4.36/ubi9/enterprise/Dockerfile b/4.4.36/ubi9/enterprise/Dockerfile new file mode 100644 index 0000000..c8faa8e --- /dev/null +++ b/4.4.36/ubi9/enterprise/Dockerfile @@ -0,0 +1,96 @@ +FROM redhat/ubi9-minimal:latest +ENV JAVA_HOME=/usr + +# gather pre-requisite packages +RUN set -eux; \ + arch="$(rpm --query --queryformat='%{ARCH}' rpm)"; \ + case "${arch}" in \ + 'x86_64') \ + tini_url="https://github.com/krallin/tini/releases/download/v0.19.0/tini"; \ + tini_sha="93dcc18adc78c65a028a84799ecf8ad40c936fdfc5f2a57b1acda5a8117fa82c"; \ + ;; \ + 'aarch64') \ + tini_url="https://github.com/krallin/tini/releases/download/v0.19.0/tini-arm64"; \ + tini_sha="07952557df20bfd2a95f9bef198b445e006171969499a1d361bd9e6f8e5e0e81"; \ + ;; \ + *) echo >&2 "Neo4j does not currently have a docker image for architecture $arch"; exit 1 ;; \ + esac; \ + microdnf install -y dnf; \ + dnf install -y \ + findutils \ + gcc \ + git \ + gzip \ + hostname \ + java-11-openjdk-headless \ + jq \ + make \ + procps \ + shadow-utils \ + tar \ + wget \ + which; \ + # download tini and openssl + wget -q ${tini_url} -O /usr/bin/tini; \ + wget -q ${tini_url}.asc -O tini.asc; \ + echo "${tini_sha}" /usr/bin/tini | sha256sum -c --strict --quiet; \ + chmod a+x /usr/bin/tini; \ + export GNUPGHOME="$(mktemp -d)"; \ + gpg --batch --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys \ + 595E85A6B1B4779EA4DAAEC70B588DFF0527A9B7 \ + B42F6819007F00F88E364FD4036A9C25BF357DD4; \ + gpg --batch --verify tini.asc /usr/bin/tini; \ + git clone https://github.com/ncopa/su-exec.git; \ + cd su-exec; \ + git checkout 4c3bb42b093f14da70d8ab924b487ccfbb1397af; \ + echo d6c40440609a23483f12eb6295b5191e94baf08298a856bab6e15b10c3b82891 su-exec.c | sha256sum -c; \ + echo 2a87af245eb125aca9305a0b1025525ac80825590800f047419dc57bba36b334 Makefile | sha256sum -c; \ + make; \ + mv /su-exec/su-exec /usr/bin/su-exec; \ + gpgconf --kill all; \ + rm -rf "$GNUPGHOME" tini.asc /su-exec; \ + dnf remove -y gcc git make; \ + dnf autoremove; \ + dnf clean all + +ENV PATH="${JAVA_HOME}/bin:${PATH}" \ + NEO4J_SHA256=e6ec47c9a33aee60b3269d8b4e01f0b9ddd98a0520e4d15489a57b344bc43521 \ + NEO4J_TARBALL=neo4j-enterprise-4.4.36-unix.tar.gz \ + NEO4J_EDITION=enterprise \ + NEO4J_HOME="/var/lib/neo4j" \ + LANG=C.UTF-8 +ARG NEO4J_URI=https://dist.neo4j.org/neo4j-enterprise-4.4.36-unix.tar.gz + +COPY ./local-package/* /startup/ + +RUN set -eux; \ + groupadd --gid 7474 --system neo4j && useradd --uid 7474 --system --no-create-home --home "${NEO4J_HOME}" --gid neo4j neo4j; \ + curl --fail --silent --show-error --location --remote-name ${NEO4J_URI}; \ + echo "${NEO4J_SHA256} ${NEO4J_TARBALL}" | sha256sum -c --strict --quiet; \ + tar --extract --file ${NEO4J_TARBALL} --directory /var/lib; \ + mv /var/lib/neo4j-* "${NEO4J_HOME}"; \ + rm ${NEO4J_TARBALL}; \ + mv /startup/neo4j-admin-report.sh "${NEO4J_HOME}"/bin/neo4j-admin-report; \ + mv "${NEO4J_HOME}"/data /data; \ + mv "${NEO4J_HOME}"/logs /logs; \ + chown -R neo4j:neo4j /data; \ + chmod -R 777 /data; \ + chown -R neo4j:neo4j /logs; \ + chmod -R 777 /logs; \ + chown -R neo4j:neo4j "${NEO4J_HOME}"; \ + chmod -R 777 "${NEO4J_HOME}"; \ + chmod -R 755 "${NEO4J_HOME}/bin"; \ + ln -s /data "${NEO4J_HOME}"/data; \ + ln -s /logs "${NEO4J_HOME}"/logs; \ + ln -s /startup/docker-entrypoint.sh /docker-entrypoint.sh + +ENV PATH "${NEO4J_HOME}"/bin:$PATH + +WORKDIR "${NEO4J_HOME}" + +VOLUME /data /logs + +EXPOSE 7474 7473 7687 + +ENTRYPOINT ["tini", "-g", "--", "/startup/docker-entrypoint.sh"] +CMD ["neo4j"] diff --git a/4.4.36/ubi9/enterprise/local-package/docker-entrypoint.sh b/4.4.36/ubi9/enterprise/local-package/docker-entrypoint.sh new file mode 100755 index 0000000..2b105f4 --- /dev/null +++ b/4.4.36/ubi9/enterprise/local-package/docker-entrypoint.sh @@ -0,0 +1,647 @@ +#!/bin/bash -eu + +cmd="$1" + +# load useful utility functions +. /startup/utilities.sh + +function is_readable +{ + # this code is fairly ugly but works no matter who this script is running as. + # It would be nice if the writability tests could use this logic somehow. + local _file=${1} + perm=$(stat -c %a "${_file}") + + # everyone permission + if [[ ${perm:2:1} -ge 4 ]]; then + return 0 + fi + # owner permissions + if [[ ${perm:0:1} -ge 4 ]]; then + if [[ "$(stat -c %U ${_file})" = "${userid}" ]] || [[ "$(stat -c %u ${_file})" = "${userid}" ]]; then + return 0 + fi + fi + # group permissions + if [[ ${perm:1:1} -ge 4 ]]; then + if containsElement "$(stat -c %g ${_file})" "${groups[@]}" || containsElement "$(stat -c %G ${_file})" "${groups[@]}" ; then + return 0 + fi + fi + return 1 +} + +function is_writable +{ + # It would be nice if this and the is_readable function could combine somehow + local _file=${1} + perm=$(stat -c %a "${_file}") + + # everyone permission + if containsElement ${perm:2:1} 2 3 6 7; then + return 0 + fi + # owner permissions + if containsElement ${perm:0:1} 2 3 6 7; then + if [[ "$(stat -c %U ${_file})" = "${userid}" ]] || [[ "$(stat -c %u ${_file})" = "${userid}" ]]; then + return 0 + fi + fi + # group permissions + if containsElement ${perm:1:1} 2 3 6 7; then + if containsElement "$(stat -c %g ${_file})" "${groups[@]}" || containsElement "$(stat -c %G ${_file})" "${groups[@]}" ; then + return 0 + fi + fi + return 1 +} + +function check_mounted_folder_readable +{ + local _directory=${1} + debug_msg "checking ${_directory} is readable" + if ! is_readable "${_directory}"; then + print_permissions_advice_and_fail "${_directory}" "${userid}" "${groupid}" + fi +} + +function check_mounted_folder_writable_with_chown +{ +# The /data and /log directory are a bit different because they are very likely to be mounted by the user but not +# necessarily writable. +# This depends on whether a user ID is passed to the container and which folders are mounted. +# +# No user ID passed to container: +# 1) No folders are mounted. +# The /data and /log folder are owned by neo4j by default, so should be writable already. +# 2) Both /log and /data are mounted. +# This means on start up, /data and /logs are owned by an unknown user and we should chown them to neo4j for +# backwards compatibility. +# +# User ID passed to container: +# 1) Both /data and /logs are mounted +# The /data and /logs folders are owned by an unknown user but we *should* have rw permission to them. +# That should be verified and error (helpfully) if not. +# 2) User mounts /data or /logs *but not both* +# The unmounted folder is still owned by neo4j, which should already be writable. The mounted folder should +# have rw permissions through user id. This should be verified. +# 3) No folders are mounted. +# The /data and /log folder are owned by neo4j by default, and these are already writable by the user. +# (This is a very unlikely use case). + + local mountFolder=${1} + debug_msg "checking ${mountFolder} is writable" + if running_as_root && ! secure_mode_enabled; then + # check folder permissions + if ! is_writable "${mountFolder}" ; then + # warn that we're about to chown the folder and then chown it + echo "Warning: Folder mounted to \"${mountFolder}\" is not writable from inside container. Changing folder owner to ${userid}." + chown -R "${userid}":"${groupid}" "${mountFolder}" + # check permissions on files in the folder + elif [ $(su-exec "${userid}":"${groupid}" find "${mountFolder}" -not -writable | wc -l) -gt 0 ]; then + echo "Warning: Some files inside \"${mountFolder}\" are not writable from inside container. Changing folder owner to ${userid}." + chown -R "${userid}":"${groupid}" "${mountFolder}" + fi + else + if [[ ! -w "${mountFolder}" ]] && [[ "$(stat -c %U ${mountFolder})" != "neo4j" ]]; then + print_permissions_advice_and_fail "${mountFolder}" "${userid}" "${groupid}" + fi + fi +} + +function load_plugin_from_location +{ + # Install a plugin from location at runtime. + local _plugin_name="${1}" + local _location="${2}" + + local _plugins_dir="${NEO4J_HOME}/plugins" + if [ -d /plugins ]; then + local _plugins_dir="/plugins" + fi + + local _destination="${_plugins_dir}/${_plugin_name}.jar" + + # Now we install the plugin that is shipped with Neo4j + for filename in ${_location}; do + echo "Installing Plugin '${_plugin_name}' from ${_location} to ${_destination}" + cp --preserve "${filename}" "${_destination}" + chmod +rw ${_destination} + done + + if ! is_readable "${_destination}"; then + echo >&2 "Plugin at '${_destination}' is not readable" + exit 1 + fi +} + +function load_plugin_from_url +{ + # Load a plugin at runtime. The provided github repository must have a versions.json on the master branch with the + # correct format. + local _plugin_name="${1}" #e.g. apoc, graph-algorithms, graph-ql + + local _plugins_dir="${NEO4J_HOME}/plugins" + if [ -d /plugins ]; then + local _plugins_dir="/plugins" + fi + local _versions_json_url="$(jq --raw-output "with_entries( select(.key==\"${_plugin_name}\") ) | to_entries[] | .value.versions" /startup/neo4j-plugins.json )" + debug_msg "Will read ${_plugin_name} versions.json from ${_versions_json_url}" + # Using the same name for the plugin irrespective of version ensures we don't end up with different versions of the same plugin + local _destination="${_plugins_dir}/${_plugin_name}.jar" + local _neo4j_version="$(neo4j --version | cut -d' ' -f2)" + + # Now we call out to github to get the versions.json for this plugin and we parse that to find the url for the correct plugin jar for our neo4j version + echo "Fetching versions.json for Plugin '${_plugin_name}' from ${_versions_json_url}" + local _versions_json + if ! _versions_json="$(wget -q --timeout 300 --tries 30 -O - "${_versions_json_url}")"; then + debug_msg "ERROR: could not fetch '${_versions_json}'" + echo >&2 "ERROR: could not query ${_versions_json_url} for plugin compatibility information. + This could indicate a problem with your network or this container's network settings. + Neo4j will continue to start, but \"${_plugin_name}\" will not be loaded." + return + fi + local _plugin_jar_url="$(echo "${_versions_json}" | jq -L/startup --raw-output "import \"semver\" as lib; [ .[] | select(.neo4j|lib::semver(\"${_neo4j_version}\")) ] | min_by(.neo4j) | .jar")" + if [[ -z "${_plugin_jar_url}" ]] || [[ "${_plugin_jar_url}" == "null" ]]; then + debug_msg "ERROR: '${_versions_json_url}' does not contain an entry for ${_neo4j_version}" + echo >&2 "ERROR: No compatible \"${_plugin_name}\" plugin found for Neo4j ${_neo4j_version} ${NEO4J_EDITION}. + This can happen with the newest Neo4j versions when a compatible plugin has not yet been released. + You can either use an older version of Neo4j, or continue without ${_plugin_name}. + Neo4j will continue to start, but \"${_plugin_name}\" will not be loaded." + else + echo "Installing Plugin '${_plugin_name}' from ${_plugin_jar_url} to ${_destination} " + wget -q --timeout 300 --tries 30 --output-document="${_destination}" "${_plugin_jar_url}" + + if ! is_readable "${_destination}"; then + echo >&2 "Plugin at '${_destination}' is not readable" + exit 1 + fi + fi +} + +function apply_plugin_default_configuration +{ + # Set the correct Load a plugin at runtime. The provided github repository must have a versions.json on the master branch with the + # correct format. + local _plugin_name="${1}" #e.g. apoc, graph-algorithms, graphql + local _reference_conf="${2}" # used to determine if we can override properties + local _neo4j_conf="${NEO4J_HOME}/conf/neo4j.conf" + + local _property _value + echo "Applying default values for plugin ${_plugin_name} to neo4j.conf" + for _entry in $(jq --compact-output --raw-output "with_entries( select(.key==\"${_plugin_name}\") ) | to_entries[] | .value.properties | to_entries[]" /startup/neo4j-plugins.json); do + _property="$(jq --raw-output '.key' <<< "${_entry}")" + _value="$(jq --raw-output '.value' <<< "${_entry}")" + debug_msg "${_plugin_name} requires setting ${_property}=${_value}" + + # the first grep strips out comments + if grep -o "^[^#]*" "${_reference_conf}" | grep -q --fixed-strings "${_property}=" ; then + # property is already set in the user provided config. In this case we don't override what has been set explicitly by the user. + echo "Skipping ${_property} for plugin ${_plugin_name} because it is already set." + echo "You may need to add ${_value} to the ${_property} setting in your configuration file." + else + if grep -o "^[^#]*" "${_neo4j_conf}" | grep -q --fixed-strings "${_property}=" ; then + sed --in-place "s/${_property}=/&${_value},/" "${_neo4j_conf}" + debug_msg "${_property} was already in the configuration file, so ${_value} was added to it." + else + echo -e "\n${_property}=${_value}" >> "${_neo4j_conf}" + debug_msg "${_property}=${_value} has been added to the configuration file." + fi + fi + done +} + +function install_neo4j_labs_plugins +{ + # first verify that the requested plugins are valid. + debug_msg "One or more NEO4J_PLUGINS have been requested." + local _known_plugins=($(jq --raw-output "keys[]" /startup/neo4j-plugins.json)) + debug_msg "Checking requested plugins are known and can be installed." + for plugin_name in $(echo "${NEO4J_PLUGINS}" | jq --raw-output '.[]'); do + if ! containsElement "${plugin_name}" "${_known_plugins[@]}"; then + printf >&2 "\"%s\" is not a known Neo4j plugin. Options are:\n%s" "${plugin_name}" "$(jq --raw-output "keys[1:][]" /startup/neo4j-plugins.json)" + exit 1 + fi + done + + # We store a copy of the config before we modify it for the plugins to allow us to see if there are user-set values in the input config that we shouldn't override + local _old_config="$(mktemp)" + if [ -e "${NEO4J_HOME}"/conf/neo4j.conf ]; then + cp "${NEO4J_HOME}"/conf/neo4j.conf "${_old_config}" + else + touch "${NEO4J_HOME}"/conf/neo4j.conf + touch "${_old_config}" + fi + for plugin_name in $(echo "${NEO4J_PLUGINS}" | jq --raw-output '.[]'); do + debug_msg "Plugin ${plugin_name} has been requested" + local _location="$(jq --raw-output "with_entries( select(.key==\"${plugin_name}\") ) | to_entries[] | .value.location" /startup/neo4j-plugins.json )" + if [ "${_location}" != "null" -a -n "$(shopt -s nullglob; echo ${_location})" ]; then + debug_msg "$plugin_name is already in the container at ${_location}" + load_plugin_from_location "${plugin_name}" "${_location}" + else + debug_msg "$plugin_name must be downloaded." + load_plugin_from_url "${plugin_name}" + fi + debug_msg "Applying plugin specific configurations." + apply_plugin_default_configuration "${plugin_name}" "${_old_config}" + done + rm "${_old_config}" +} + +function add_docker_default_to_conf +{ + # docker defaults should NOT overwrite values already in the conf file + local _setting="${1}" + local _value="${2}" + + if ! grep -q "^${_setting}=" "${NEO4J_HOME}"/conf/neo4j.conf + then + debug_msg "Appended ${_setting}=${_value} to ${NEO4J_HOME}/conf/neo4j.conf" + echo -e "\n"${_setting}=${_value} >> "${NEO4J_HOME}"/conf/neo4j.conf + fi +} + +function add_env_setting_to_conf +{ + # settings from environment variables should overwrite values already in the conf + local _setting=${1} + local _value=${2} + local _append_not_replace_configs=("dbms.jvm.additional") + + if grep -q -F "${_setting}=" "${NEO4J_HOME}"/conf/neo4j.conf; then + if containsElement "${_setting}" "${_append_not_replace_configs[@]}"; then + debug_msg "${_setting} will be appended to neo4j.conf without replacing existing settings." + else + # Remove any lines containing the setting already + debug_msg "Removing existing setting for ${_setting}" + sed --in-place "/^${_setting}=.*/d" "${NEO4J_HOME}"/conf/neo4j.conf + fi + fi + # Then always append setting to file + debug_msg "Appended ${_setting}=${_value} to ${NEO4J_HOME}/conf/neo4j.conf" + echo "${_setting}=${_value}" >> "${NEO4J_HOME}"/conf/neo4j.conf +} + +function set_initial_password +{ + local _neo4j_auth="${1}" + + # set the neo4j initial password only if you run the database server + if [ "${cmd}" == "neo4j" ]; then + if [ "${_neo4j_auth:-}" == "none" ]; then + debug_msg "Authentication is requested to be unset" + add_env_setting_to_conf "dbms.security.auth_enabled" "false" + elif [[ "${_neo4j_auth:-}" =~ ^([^/]+)\/([^/]+)/?([tT][rR][uU][eE])?$ ]]; then + admin_user="${BASH_REMATCH[1]}" + password="${BASH_REMATCH[2]}" + do_reset="${BASH_REMATCH[3]}" + + if [ "${password}" == "neo4j" ]; then + echo >&2 "Invalid value for password. It cannot be 'neo4j', which is the default." + exit 1 + fi + if [ "${admin_user}" != "neo4j" ]; then + echo >&2 "Invalid admin username, it must be neo4j." + exit 1 + fi + + if running_as_root; then + # running set-initial-password as root will create subfolders to /data as root, causing startup fail when neo4j can't read or write the /data/dbms folder + # creating the folder first will avoid that + mkdir -p /data/dbms + debug_msg "Making sure /data/dbms is owned by ${userid}:${groupid}" + chown "${userid}":"${groupid}" /data/dbms + fi + + local extra_args=() + if [ "${do_reset}" == "true" ]; then + extra_args+=("--require-password-change") + fi + if [ "${EXTENDED_CONF+"yes"}" == "yes" ]; then + extra_args+=("--expand-commands") + fi + if debugging_enabled; then + extra_args+=("--verbose") + fi + debug_msg "Setting initial password" + debug_msg "${neo4j_admin_cmd} set-initial-password ***** ${extra_args[*]}" + if debugging_enabled; then + # don't suppress any output or errors in debugging mode + ${neo4j_admin_cmd} set-initial-password "${password}" "${extra_args[@]}" + else + # Will exit with error if users already exist (and print a message explaining that) + # we probably don't want the message though, since it throws an error message on restarting the container. + ${neo4j_admin_cmd} set-initial-password "${password}" "${extra_args[@]}" 2>/dev/null || true + fi + + elif [ -n "${_neo4j_auth:-}" ]; then + echo "$_neo4j_auth is invalid" + echo >&2 "Invalid value for NEO4J_AUTH: '${_neo4j_auth}'" + exit 1 + fi + fi +} + +# ==== CODE STARTS ==== +debug_msg "DEBUGGING ENABLED" + +# If we're running as root, then run as the neo4j user. Otherwise +# docker is running with --user and we simply use that user. Note +# that su-exec, despite its name, does not replicate the functionality +# of exec, so we need to use both +if running_as_root; then + userid="neo4j" + groupid="neo4j" + groups=($(id -G neo4j)) + exec_cmd="exec su-exec neo4j:neo4j" + neo4j_admin_cmd="su-exec neo4j:neo4j neo4j-admin" + debug_msg "Running as root user inside neo4j image" +else + userid="$(id -u)" + groupid="$(id -g)" + groups=($(id -G)) + exec_cmd="exec" + neo4j_admin_cmd="neo4j-admin" + debug_msg "Running as user ${userid}:${groupid} inside neo4j image" +fi +readonly userid +readonly groupid +readonly groups +readonly exec_cmd +readonly neo4j_admin_cmd + +# Need to chown the home directory +if running_as_root; then + debug_msg "chowning ${NEO4J_HOME} recursively to ${userid}":"${groupid}" + chown -R "${userid}":"${groupid}" "${NEO4J_HOME}" + chmod 700 "${NEO4J_HOME}" + find "${NEO4J_HOME}" -mindepth 1 -maxdepth 1 -type d -exec chmod -R 700 {} \; + debug_msg "Setting all files in ${NEO4J_HOME}/conf to permissions 600" + find "${NEO4J_HOME}"/conf -type f -exec chmod -R 600 {} \; +fi + +# ==== CHECK LICENSE AGREEMENT ==== + +# Only prompt for license agreement if command contains "neo4j" in it +if [[ "${cmd}" == *"neo4j"* ]]; then + if [ "${NEO4J_EDITION}" == "enterprise" ]; then + if [ "${NEO4J_ACCEPT_LICENSE_AGREEMENT:=no}" != "yes" ]; then + echo >&2 " +In order to use Neo4j Enterprise Edition you must accept the license agreement. + +The license agreement is available at https://neo4j.com/terms/licensing/ +If you have a support contract the following terms apply https://neo4j.com/terms/support-terms/ + +(c) Neo4j Sweden AB. All Rights Reserved. +Use of this Software without a proper commercial license +with Neo4j, Inc. or its affiliates is prohibited. +Neo4j has the right to terminate your usage if you are not compliant. + +More information is also available at: https://neo4j.com/licensing/ +If you have further inquiries about licensing, please contact us via https://neo4j.com/contact-us/ + +To accept the commercial license agreement set the environment variable +NEO4J_ACCEPT_LICENSE_AGREEMENT=yes + +To do this you can use the following docker argument: + + --env=NEO4J_ACCEPT_LICENSE_AGREEMENT=yes +" + exit 1 + fi + fi +fi + +# NEO4JLABS_PLUGINS is renamed to NEO4J_PLUGINS in 5.x, but we want the new name to work against 4.4 images too +if [ -n "${NEO4JLABS_PLUGINS:-}" ]; +then + : ${NEO4J_PLUGINS:=${NEO4JLABS_PLUGINS:-}} +fi + +# ==== RENAME LEGACY ENVIRONMENT CONF VARIABLES ==== + +# Env variable naming convention: +# - prefix NEO4J_ +# - double underscore char '__' instead of single underscore '_' char in the setting name +# - underscore char '_' instead of dot '.' char in the setting name +# Example: +# NEO4J_dbms_tx__log_rotation_retention__policy env variable to set +# dbms.tx_log.rotation.retention_policy setting + +# Backward compatibility - map old hardcoded env variables into new naming convention (if they aren't set already) +# Set some to default values if unset +: ${NEO4J_dbms_tx__log_rotation_retention__policy:=${NEO4J_dbms_txLog_rotation_retentionPolicy:-}} +: ${NEO4J_dbms_unmanaged__extension__classes:=${NEO4J_dbms_unmanagedExtensionClasses:-}} +: ${NEO4J_dbms_allow__format__migration:=${NEO4J_dbms_allowFormatMigration:-}} +: ${NEO4J_dbms_connectors_default__advertised__address:=${NEO4J_dbms_connectors_defaultAdvertisedAddress:-}} + +if [ "${NEO4J_EDITION}" == "enterprise" ]; + then + : ${NEO4J_causal__clustering_expected__core__cluster__size:=${NEO4J_causalClustering_expectedCoreClusterSize:-}} + : ${NEO4J_causal__clustering_initial__discovery__members:=${NEO4J_causalClustering_initialDiscoveryMembers:-}} + debug_msg "Copying contents of /conf to ${NEO4J_HOME}/conf/*" + : ${NEO4J_causal__clustering_discovery__advertised__address:=${NEO4J_causalClustering_discoveryAdvertisedAddress:-}} + : ${NEO4J_causal__clustering_transaction__advertised__address:=${NEO4J_causalClustering_transactionAdvertisedAddress:-}} + : ${NEO4J_causal__clustering_raft__advertised__address:=${NEO4J_causalClustering_raftAdvertisedAddress:-}} +fi + +# unset old hardcoded unsupported env variables +unset NEO4J_dbms_txLog_rotation_retentionPolicy NEO4J_UDC_SOURCE \ + NEO4J_dbms_unmanagedExtensionClasses NEO4J_dbms_allowFormatMigration \ + NEO4J_dbms_connectors_defaultAdvertisedAddress NEO4J_ha_serverId \ + NEO4J_ha_initialHosts NEO4J_causalClustering_expectedCoreClusterSize \ + NEO4J_causalClustering_initialDiscoveryMembers \ + NEO4J_causalClustering_discoveryListenAddress \ + NEO4J_causalClustering_discoveryAdvertisedAddress \ + NEO4J_causalClustering_transactionListenAddress \ + NEO4J_causalClustering_transactionAdvertisedAddress \ + NEO4J_causalClustering_raftListenAddress \ + NEO4J_causalClustering_raftAdvertisedAddress + +# ==== CHECK FILE PERMISSIONS ON MOUNTED FOLDERS ==== + + +if [ -d /conf ]; then + check_mounted_folder_readable "/conf" + rm -rf "${NEO4J_HOME}"/conf/* + debug_msg "Copying contents of /conf to ${NEO4J_HOME}/conf/*" + find /conf -type f -exec cp --preserve=ownership,mode {} "${NEO4J_HOME}"/conf \; +fi + +if [ -d /ssl ]; then + check_mounted_folder_readable "/ssl" + rm -rf "${NEO4J_HOME}"/certificates + ln -s /ssl "${NEO4J_HOME}"/certificates +fi + +if [ -d /plugins ]; then + if [[ -n "${NEO4J_PLUGINS:-}" ]]; then + # We need write permissions to write the required plugins to /plugins + debug_msg "Extra plugins were requested. Ensuring the mounted /plugins folder has the required write permissions." + check_mounted_folder_writable_with_chown "/plugins" + fi + check_mounted_folder_readable "/plugins" + : ${NEO4J_dbms_directories_plugins:="/plugins"} +fi + +if [ -d /import ]; then + check_mounted_folder_readable "/import" + : ${NEO4J_dbms_directories_import:="/import"} +fi + +if [ -d /metrics ]; then + # metrics is enterprise only + if [ "${NEO4J_EDITION}" == "enterprise" ]; + then + check_mounted_folder_writable_with_chown "/metrics" + : ${NEO4J_dbms_directories_metrics:="/metrics"} + fi +fi + +if [ -d /logs ]; then + check_mounted_folder_writable_with_chown "/logs" + : ${NEO4J_dbms_directories_logs:="/logs"} +fi + +if [ -d /data ]; then + check_mounted_folder_writable_with_chown "/data" + if [ -d /data/databases ]; then + check_mounted_folder_writable_with_chown "/data/databases" + fi + if [ -d /data/dbms ]; then + check_mounted_folder_writable_with_chown "/data/dbms" + fi + if [ -d /data/transactions ]; then + check_mounted_folder_writable_with_chown "/data/transactions" + fi +fi + +if [ -d /licenses ]; then + check_mounted_folder_readable "/licenses" + : ${NEO4J_dbms_directories_licenses:="/licenses"} +fi + +# ==== SET CONFIGURATIONS ==== + +## == DOCKER SPECIFIC DEFAULT CONFIGURATIONS === +## these should not override *any* configurations set by the user + +debug_msg "Setting docker specific configuration overrides" +add_docker_default_to_conf "dbms.memory.pagecache.size" "512M" +add_docker_default_to_conf "dbms.default_listen_address" "0.0.0.0" + +# set enterprise only docker defaults +if [ "${NEO4J_EDITION}" == "enterprise" ]; +then + debug_msg "Setting docker specific Enterprise Edition overrides" + add_docker_default_to_conf "causal_clustering.discovery_advertised_address" "$(hostname):5000" + add_docker_default_to_conf "causal_clustering.transaction_advertised_address" "$(hostname):6000" + add_docker_default_to_conf "causal_clustering.raft_advertised_address" "$(hostname):7000" + add_docker_default_to_conf "dbms.routing.advertised_address" "$(hostname):7688" +fi + +## == ENVIRONMENT VARIABLE CONFIGURATIONS === +## these override BOTH defaults and any existing values in the neo4j.conf file + +# these are docker control envs that have the NEO4J_ prefix but we don't want to add to the config. +not_configs=("NEO4J_ACCEPT_LICENSE_AGREEMENT" "NEO4J_AUTH" "NEO4J_AUTH_PATH" "NEO4J_DEBUG" "NEO4J_EDITION" \ + "NEO4J_HOME" "NEO4J_PLUGINS" "NEO4J_SHA256" "NEO4J_TARBALL") + +debug_msg "Applying configuration settings that have been set using environment variables." +# list env variables with prefix NEO4J_ and create settings from them +for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do + if containsElement "$i" "${not_configs[@]}"; then + continue + fi + setting=$(echo "${i}" | sed 's|^NEO4J_||' | sed 's|_|.|g' | sed 's|\.\.|_|g') + value=$(echo "${!i}") + # Don't allow settings with no value or settings that start with a number (neo4j converts settings to env variables and you cannot have an env variable that starts with a number) + if [[ -n ${value} ]]; then + if [[ ! "${setting}" =~ ^[0-9]+.*$ ]]; then + add_env_setting_to_conf "${setting}" "${value}" + else + echo >&2 "WARNING: ${setting} not written to conf file. Settings that start with a number are not permitted." + fi + fi +done + +# ==== SET PASSWORD AND PLUGINS ==== + +if [[ -n "${NEO4J_AUTH_PATH:-}" ]]; then + # Validate the existence of the password file + if [ ! -f "${NEO4J_AUTH_PATH}" ]; then + echo >&2 "The password file '${NEO4J_AUTH_PATH}' does not exist" + exit 1 + fi + # validate the password file is readable + check_mounted_folder_readable "${NEO4J_AUTH_PATH}" + + debug_msg "Setting initial password from file ${NEO4J_AUTH_PATH}" + set_initial_password "$(cat ${NEO4J_AUTH_PATH})" +else + debug_msg "Setting initial password from environment" + set_initial_password "${NEO4J_AUTH:-}" +fi + + +if [[ ! -z "${NEO4J_PLUGINS:-}" ]]; then + # NEO4J_PLUGINS should be a json array of plugins like '["graph-algorithms", "apoc", "streams", "graphql"]' + install_neo4j_labs_plugins +fi + +# ==== CLEANUP RUN FILE ==== + +if [ -f "${NEO4J_HOME}"/run/neo4j.pid ]; +then + rm "${NEO4J_HOME}"/run/neo4j.pid +fi + +# ==== INVOKE NEO4J STARTUP ==== + +[ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT} + +if [ "${cmd}" == "dump-config" ]; then + if [ ! -d "/conf" ]; then + echo >&2 "You must mount a folder to /conf so that the configuration file(s) can be dumped to there." + exit 1 + fi + check_mounted_folder_writable_with_chown "/conf" + cp --recursive "${NEO4J_HOME}"/conf/* /conf + echo "Config Dumped" + exit 0 +fi + +# this prints out a command for us to run. +# the command is something like: `java ...[lots of java options]... neo4j.mainClass ...[some neo4j options]...` +# putting debug messages here causes the function to break +function get_neo4j_run_cmd { + + local extra_args=() + + if [ "${EXTENDED_CONF+"yes"}" == "yes" ]; then + extra_args+=("--expand-commands") + fi + if debugging_enabled ; then + extra_args+=("--verbose") + fi + + if running_as_root; then + su-exec neo4j:neo4j neo4j console --dry-run "${extra_args[@]}" + else + neo4j console --dry-run "${extra_args[@]}" + fi +} + +# Use su-exec to drop privileges to neo4j user +# Note that su-exec, despite its name, does not replicate the +# functionality of exec, so we need to use both +if [ "${cmd}" == "neo4j" ]; then + # separate declaration and use of get_neo4j_run_cmd so that error codes are correctly surfaced + debug_msg "getting full neo4j run command" + neo4j_console_cmd="$(get_neo4j_run_cmd)" + debug_msg "${exec_cmd} ${neo4j_console_cmd}" + eval ${exec_cmd} ${neo4j_console_cmd?:No Neo4j command was generated} +else + debug_msg "${exec_cmd}" "$@" + ${exec_cmd} "$@" +fi diff --git a/4.4.36/ubi9/enterprise/local-package/neo4j-admin-report.sh b/4.4.36/ubi9/enterprise/local-package/neo4j-admin-report.sh new file mode 100755 index 0000000..c4c3915 --- /dev/null +++ b/4.4.36/ubi9/enterprise/local-package/neo4j-admin-report.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# load useful utility functions +. /startup/utilities.sh + +function find_report_destination +{ + local to_flag="--to" + + while [[ $# -gt 0 ]]; do + case $1 in + # for arg in "$@"; do + # case $arg in + "${to_flag}"=*) + echo ${1#*=} + return + ;; + "${to_flag}") + echo ${2} + return + ;; + *) + shift + ;; + esac + done + mkdir -p /tmp/reports + echo "/tmp/reports" +} + +report_cmd=("neo4j-admin" "report") + +# note, these debug messages are unlikely to work in a docker exec, since environment isn't preserved. +debug_msg "Called ${0}" +debug_msg "neo4j-admin report command is:" "${report_cmd[@]}" "$@" + +# find report destination. This could be specified by argument to neo4j-admin or it could be the default location. +report_destination=$(find_report_destination "$@") +debug_msg "report_destination will be ${report_destination}" + +debug_msg "Determining which user to run neo4j-admin as." +if running_as_root; then + debug_msg "running neo4j-admin report as root" + if [[ ! $(su-exec neo4j:neo4j test -w "${report_destination}") ]]; then + debug_msg "reowning ${report_destination} to neo4j:neo4j" + chown neo4j:neo4j "${report_destination}" + fi + debug_msg su-exec neo4j:neo4j "${report_cmd[@]}" "$@" + su-exec neo4j:neo4j "${report_cmd[@]}" "$@" +else + debug_msg "running neo4j-admin report as user defined by --user flag" + if [[ ! -w "${report_destination}" ]]; then + print_permissions_advice_and_fail "${report_destination}" "$(id -u)" "$(id -g)" + fi + debug_msg "${report_cmd[@]}" "$@" + "${report_cmd[@]}" "$@" +fi \ No newline at end of file diff --git a/4.4.36/ubi9/enterprise/local-package/neo4j-plugins.json b/4.4.36/ubi9/enterprise/local-package/neo4j-plugins.json new file mode 100644 index 0000000..d738bd1 --- /dev/null +++ b/4.4.36/ubi9/enterprise/local-package/neo4j-plugins.json @@ -0,0 +1,60 @@ +{ + "apoc": { + "versions": "https://neo4j-contrib.github.io/neo4j-apoc-procedures/versions.json", + "properties": { + "dbms.security.procedures.unrestricted": "apoc.*" + } + }, + "apoc-core": { + "location": "/var/lib/neo4j/labs/apoc-*-core.jar", + "versions": "https://neo4j-contrib.github.io/neo4j-apoc-procedures/versions.json", + "properties": { + "dbms.security.procedures.unrestricted": "apoc.*" + } + }, + "bloom": { + "location": "/var/lib/neo4j/products/bloom-plugin-*.jar", + "versions": "https://bloom-plugins.s3.eu-west-2.amazonaws.com/versions.json", + "properties": { + "dbms.unmanaged_extension_classes": "com.neo4j.bloom.server=/browser/bloom", + "dbms.security.procedures.unrestricted": "bloom.*", + "neo4j.bloom.license_file": "/licenses/bloom.license" + } + }, + "streams": { + "versions": "https://neo4j-contrib.github.io/neo4j-streams/versions.json", + "properties": {} + }, + "graphql": { + "versions": "https://neo4j-graphql.github.io/neo4j-graphql/versions.json", + "properties": { + "dbms.unmanaged_extension_classes": "org.neo4j.graphql=/graphql", + "dbms.security.procedures.unrestricted": "graphql.*" + } + }, + "graph-algorithms": { + "versions": "https://neo4j-contrib.github.io/neo4j-graph-algorithms/versions.json", + "properties": { + "dbms.security.procedures.unrestricted": "algo.*" + } + }, + "graph-data-science": { + "versions": "https://graphdatascience.ninja/versions.json", + "location": "/var/lib/neo4j/products/neo4j-graph-data-science-*.jar", + "properties": { + "dbms.security.procedures.unrestricted": "gds.*" + } + }, + "n10s": { + "versions": "https://neo4j-labs.github.io/neosemantics/versions.json", + "properties": { + "dbms.security.procedures.unrestricted":"semantics.*" + } + }, + "_testing": { + "versions": "http://host.testcontainers.internal:3000/versions.json", + "properties": { + "dbms.security.procedures.unrestricted": "com.neo4j.docker.neo4jserver.plugins.*" + } + } +} diff --git a/4.4.36/ubi9/enterprise/local-package/semver.jq b/4.4.36/ubi9/enterprise/local-package/semver.jq new file mode 100755 index 0000000..2d8c144 --- /dev/null +++ b/4.4.36/ubi9/enterprise/local-package/semver.jq @@ -0,0 +1,22 @@ +def _semver_obj2obj($req): + if . == $req then true + elif .major != $req.major then false + elif .minor != $req.minor and .minor != "x" and .minor != "*" then false + elif .patch != $req.patch and .patch != "x" and .patch != "*" then false + elif $req.minor == null and ( .minor == "x" or .minor == "*" ) then false + elif $req.patch == null and ( .patch == "x" or .patch == "*" ) then false + elif $req.major == null and $req.minor == null and $req.patch == null then false + else true end; + +def _ver2obj: + if type == "object" then . + elif type == "string" and test("(?[0-9]+)(\\.(?[0-9x*]+))?(\\.?(?[0-9x*]+))?") then capture("(?[0-9]+)(\\.(?[0-9x*]+))?(\\.?(?[0-9x*]+))?") + elif type == "string" and . == "" then {major: null, minor:null, patch:null} + elif type == "number" then {minor:floor,patch:(.-floor)} + else {major: .} end; + +# Returns true if input version spec semantically matches the requested version +def semver($req): + if $req == null or $req == "" then false + elif . == $req then true + else _ver2obj|_semver_obj2obj($req|_ver2obj) end; diff --git a/4.4.36/ubi9/enterprise/local-package/utilities.sh b/4.4.36/ubi9/enterprise/local-package/utilities.sh new file mode 100644 index 0000000..4943afe --- /dev/null +++ b/4.4.36/ubi9/enterprise/local-package/utilities.sh @@ -0,0 +1,54 @@ + +function running_as_root +{ + test "$(id -u)" = "0" +} + +function secure_mode_enabled +{ + test "${SECURE_FILE_PERMISSIONS:=no}" = "yes" +} + +function debugging_enabled +{ + test "${NEO4J_DEBUG+yes}" = "yes" +} + +function debug_msg +{ + if debugging_enabled; then + echo "$@" + fi +} + +function containsElement +{ + local e match="$1" + shift + for e; do [[ "$e" == "$match" ]] && return 0; done + return 1 +} + +function is_writable +{ + ${exec_cmd} test -w "${1}" +} + +function print_permissions_advice_and_fail +{ + local _directory=${1} + local _userid=${2} + local _groupid=${3} + echo >&2 " +Folder ${_directory} is not accessible for user: ${_userid} or group ${_groupid}. This is commonly a file permissions issue on the mounted folder. + +Hints to solve the issue: +1) Make sure the folder exists before mounting it. Docker will create the folder using root permissions before starting the Neo4j container. The root permissions disallow Neo4j from writing to the mounted folder. +2) Pass the folder owner's user ID and group ID to docker run, so that docker runs as that user. +If the folder is owned by the current user, this can be done by adding this flag to your docker run command: + --user=\$(id -u):\$(id -g) + " + exit 1 +} + +