diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..ce3e6c90 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,24 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: jennyowen, bfeshti + +--- + +## Guidelines + +Please note that GitHub issues are only meant for bug reports/feature requests. +If you have questions on how to use Neo4j, please ask on [Neo4j Community](https://community.neo4j.com/) or [StackOverflow](http://stackoverflow.com/questions/tagged/neo4j) instead of creating an issue here. + +To help us understand your issue, please specify important details, primarily: + +- **Steps to reproduce**. *Not including reproduction steps will mean your bug will take considerably longer to investigate and fix. Please don't skip this*. +- Expected behaviour +- Actual behaviour +- Neo4j image tag being used, eg `neo4j:latest`, `neo4j:enterprise-3.5` etc +- The output of the `docker version` command +- Operating system: (for example Windows 95/Ubuntu 16.04) + +Additionally, include (as appropriate) log-files, stacktraces, and other debug output. diff --git a/.gitignore b/.gitignore index 3cad29d3..66248f8d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,10 @@ /out/ /in/ /devenv.local +/local-mounts/ +/target/ + +# simlinks to dev scripts in https://github.com/neo-technology/teamcity-witchcraft +download_tool.py +docker_tests_get_installers.py +__pycache__/ diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 673f3da4..b3cde952 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -8,26 +8,129 @@ other Linuxes. Pull requests welcomed for other platforms. ## OSX only 1. install GNU Make (>=4.0) -1. install the Docker Toolbox +1. install the Docker Toolbox. See: https://docs.docker.com/install/ -## Debian +## Linux -1. install `uuid-runtime` +1. install the Docker Toolbox. See https://docs.docker.com/install/ -## All platforms +# Building the Image -1. download the Neo4j Community unix tarball -1. copy `devenv.local.template` as `devenv.local`; fill in the - directory to which you have downloaded the tarball and its version +The build will create two images (one for Enterprise and one for Community) for a single version of Neo4j. -# Build process +The make script will automatically download the source files needed to build the images. +You just need to specify the **full** Neo4j version including major, minor and patch numbers. For example: -## OSX only +```bash +NEO4JVERSION=3.5.11 make clean build +``` + +If you want to build an alpha/beta release, this will still work: + +```bash +NEO4JVERSION=3.5.0-alpha01 make clean build +``` + +When the make script is complete, the image name will be written to file in `tmp/.image-id-community` and `tmp/.image-id-enterprise`: + +```bash +$ cat tmp/.image-id-community +test/19564 + +$ cat tmp/.image-id-enterprise +test/13909 +``` + +## Building ARM64 based images + +There is a separate make script for building ARM64 based images. + +Like with `amd64` images, you must still specify the **full** Neo4j version including major, minor and patch numbers. For example: + +```bash +NEO4JVERSION=3.5.11 make -f make-arm64.mk clean tag-arm +``` + + +## If the Neo4j Version is not Publicly Available + +The make script cannot automatically download unreleased source files, so you need to manually download them before building the images. + +1. Assuming you cloned this repository to `$NEO4J_DOCKER_ROOT`, +download the community and enterprise unix tar.gz files and copy them to `$NEO4J_DOCKER_ROOT/in`. +1. Run the make script setting `NEO4JVERSION` to the version number in the files downloaded into the `in/` folder. + +For example: + +```bash +$ cd $NEO4J_DOCKER_ROOT +$ ls $NEO4J_DOCKER_ROOT/in + neo4j-community-4.0.0-alpha05-unix.tar.gz neo4j-enterprise-4.0.0-alpha05-unix.tar.gz + +$ NEO4JVERSION=4.0.0-alpha05 make clean build +``` + + +# Running the Tests + +The tests are written in java, and require Maven plus jdk 11 for Neo4j version 4.0 onwards or jdk 8 for earlier Neo4j versions. + +The tests require some information about the image before they can test it. +These can be passed as an environment variable or a command line parameter when invoking maven: + + +| Env Variable | Maven parameter | Description | +|-----------------|-----------------|------------------------------------------------------------| +| `NEO4JVERSION` | `-Dversion` | the Neo4j version of the image | +| `NEO4J_IMAGE` | `-Dimage` | the tag of the image to test | +| `NEO4J_EDITION` | `-Dedition` | Either `community` or `enterprise` depending on the image. | + + + +## Using Maven +The Makefile can run the entire test suite. +1. Make sure `java --version` is java 11 or java 8 as necessary. +2. `NEO4JVERSION= make test` This is a make target that will run these commands: +```bash +mvn test -Dimage=$(cat tmp/.image-id-community) -Dedition=community -Dversion=${NEO4JVERSION} +mvn test -Dimage=$(cat tmp/.image-id-enterprise) -Dedition=enterprise -Dversion=${NEO4JVERSION} +``` + +## In Intellij + +1. Make sure the project SDK is java 11 or java 8 as necessary. +1. Edit the [pom.xml file](../master/pom.xml) to replace `${env.NEO4JVERSION}` with the `NEO4JVERSION` you used to build the image. +*(Yes this is terrible, and we need to think of an alternative to this)*. + + For example: + ```xml + ${env.NEO4JVERSION} + ``` + becomes + ```xml + 4.0.0-alpha05 + ``` +1. Install the [EnvFile](https://plugins.jetbrains.com/plugin/7861-envfile) Intellij plugin. +2. Under Run Configurations edit the Template JUnit configuration: + 1. Select the "EnvFile" tab + 2. Make sure "Enable EnvFile" is checked. + 3. Click the `+` then click to add a `.env` file. + 4. In the file selection box select `./tmp/devenv-enterprise.env` or `./tmp/devenv-community.env` depending on which one you want to test. + 5. Rebuilding the Neo4j image will regenerate the `.env` files, so you don't need to worry about keeping the environment up to date. + + +### If the Neo4j Version is not Publicly Available + +1. Clone the Neo4j github repository and checkout the branch you want. +2. Make sure `java --version` returns java 11 if you're building Neo4j 4.0+, or java 8 if building an earlier branch. +1. Run `mvn install` plus whatever maven build flags you like. This should install the latest neo4j jars into the maven cache. +1. Follow instructions for [running tests in Intellij](#in-intellij), +use the `NEO4JVERSION` that is in the pom file of your Neo4j repository clone. + +### cannot find symbol `com.sun.security.auth.module.UnixSystem` -1. create a docker-machine VM -1. export the docker-machine environment for docker +This can happen if you switch from java 8 to java 11 and then try to rebuild the tests in Intellij. -## All platforms +Check that the `java.version` property in the [pom.xml file](../master/pom.xml) is set to 11 instead of 1.8. +DO NOT commit this set to 11 (yes this is a terrible solution). -1. `. devenv` -1. `make` diff --git a/Makefile b/Makefile index 554ee5e2..1a44209e 100644 --- a/Makefile +++ b/Makefile @@ -1,106 +1,68 @@ -SHELL := bash -.ONESHELL: -.SHELLFLAGS := -eu -o pipefail -c -.DELETE_ON_ERROR: -.SECONDEXPANSION: -.SECONDARY: +include make-common.mk -ifeq ($(origin .RECIPEPREFIX), undefined) - $(error This Make does not support .RECIPEPREFIX. Please use GNU Make 4.0 or later) -endif -.RECIPEPREFIX = > +NEO4J_BASE_IMAGE?="openjdk:11-jdk-slim" -ifndef NEO4J_VERSION - $(error NEO4J_VERSION is not set) -endif +# Use make test TESTS='' to run specific tests +# e.g. `make test TESTS='TestCausalCluster'` or `make test TESTS='*Cluster*'` +# the value of variable is passed to the maven test property. For more info see https://maven.apache.org/surefire/maven-surefire-plugin/examples/single-test.html +# by default this is empty which means all tests will be run +TESTS?="" -tarball = neo4j-$(1)-$(2)-unix.tar.gz -dist_site := http://dist.neo4j.org -series := $(shell echo "$(NEO4J_VERSION)" | sed -E 's/^([0-9]+\.[0-9]+)\..*/\1/') - -all: out/enterprise/.sentinel out/community/.sentinel +all: test .PHONY: all -test: test-community test-enterprise +test: test-enterprise test-community .PHONY: test -out/%/.sentinel: tmp/image-%/.sentinel tmp/.tests-pass-% -> mkdir -p $(@D) -> cp -r $( touch $@ +test-enterprise: build-enterprise +> mvn test -Dimage=$$(cat tmp/.image-id-enterprise) -Dadminimage=$$(cat tmp/.image-id-neo4j-admin-enterprise) -Dedition=enterprise -Dversion=$(NEO4JVERSION) -Dtest=$(TESTS) +.PHONY: test-enterprise -tmp/.tests-pass-%: tmp/.image-id-% $(shell find test -name 'test-*') $(shell find test -name '*.yml') $(shell find test -name '*.sh') -> mkdir -p $(@D) -> image_id=$$(cat $<) -> for test in $(filter test/test-%,$^); do -> echo "Running $${test}" -> "$${test}" "$${image_id}" "${series}" "$*" -> done -> touch $@ +test-community: build-community +> mvn test -Dimage=$$(cat tmp/.image-id-community) -Dadminimage=$$(cat tmp/.image-id-neo4j-admin-community) -Dedition=community -Dversion=$(NEO4JVERSION) -Dtest=$(TESTS) +.PHONY: test-community + +# just build the images, don't test or package +build: build-community build-enterprise +.PHONY: build + +build-community: tmp/.image-id-community tmp/.image-id-neo4j-admin-community tmp/devenv-community.env +.PHONY: build-community + +build-enterprise: tmp/.image-id-enterprise tmp/.image-id-neo4j-admin-enterprise tmp/devenv-enterprise.env +.PHONY: build-enterprise + +tmp/devenv-%.env: tmp/.image-id-% tmp/.image-id-neo4j-admin-% +> echo "NEO4JVERSION=$(NEO4JVERSION)" > ${@} +> echo "NEO4J_IMAGE=$$(cat tmp/.image-id-${*})" >> ${@} +> echo "NEO4JADMIN_IMAGE=$$(cat tmp/.image-id-neo4j-admin-${*})" >> ${@} +> echo "NEO4J_EDITION=${*}" >> ${@} + +# create release images and loadable images +package: package-community package-enterprise +.PHONY: package + +package-community: tmp/.image-id-community tmp/.image-id-neo4j-admin-community out/community/.sentinel +> mkdir -p out +> docker tag $$(cat $<) neo4j:$(NEO4JVERSION) +> docker save neo4j:$(NEO4JVERSION) > out/neo4j-community-$(NEO4JVERSION)-docker-loadable.tar +package-enterprise: tmp/.image-id-enterprise tmp/.image-id-neo4j-admin-enterprise out/enterprise/.sentinel +> mkdir -p out +> docker tag $$(cat $<) neo4j:$(NEO4JVERSION)-enterprise +> docker save neo4j:$(NEO4JVERSION)-enterprise > out/neo4j-enterprise-$(NEO4JVERSION)-docker-loadable.tar + +# create image from local build context tmp/.image-id-%: tmp/local-context-%/.sentinel > mkdir -p $(@D) > image=test/$$RANDOM > docker build --tag=$$image \ - --build-arg="NEO4J_URI=file:///tmp/$(call tarball,$*,$(NEO4J_VERSION))" \ + --build-arg="NEO4J_URI=file:///tmp/$(call tarball,$*,$(NEO4JVERSION))" \ $( echo -n $$image >$@ -tmp/local-context-%/.sentinel: tmp/image-%/.sentinel in/$(call tarball,%,$(NEO4J_VERSION)) -> rm -rf $(@D) +# copy the releaseable version of the image to the output folder. +out/%/.sentinel: tmp/image-%/.sentinel > mkdir -p $(@D) > cp -r $( cp $(filter %.tar.gz,$^) $(@D)/local-package -> touch $@ - -tmp/image-%/.sentinel: src/$(series)/Dockerfile src/$(series)/docker-entrypoint.sh \ - in/$(call tarball,%,$(NEO4J_VERSION)) -> mkdir -p $(@D) -> cp $(filter %/docker-entrypoint.sh,$^) $(@D)/docker-entrypoint.sh -> sha=$$(shasum --algorithm=256 $(filter %.tar.gz,$^) | cut -d' ' -f1) -> <$(filter %/Dockerfile,$^) sed \ - -e "s|%%NEO4J_SHA%%|$${sha}|" \ - -e "s|%%NEO4J_TARBALL%%|$(call tarball,$*,$(NEO4J_VERSION))|" \ - -e "s|%%NEO4J_EDITION%%|$*|" \ - -e "s|%%NEO4J_DIST_SITE%%|$(dist_site)|" \ - >$(@D)/Dockerfile -> mkdir -p $(@D)/local-package -> touch $(@D)/local-package/.sentinel -> touch $@ - -run = trapping-sigint \ - docker run --publish 7474:7474 --publish 7687:7687 \ - --env=NEO4J_ACCEPT_LICENSE_AGREEMENT=yes \ - --env=NEO4J_AUTH=neo4j/foo --rm $$(cat $1) -build-enterprise: tmp/.image-id-enterprise -> @echo "Neo4j $(NEO4J_VERSION)-enterprise available as: $$(cat $<)" -build-community: tmp/.image-id-community -> @echo "Neo4j $(NEO4J_VERSION)-community available as: $$(cat $<)" -run-enterprise: tmp/.image-id-enterprise -> $(call run,$<) -run-community: tmp/.image-id-community -> $(call run,$<) -test-enterprise: tmp/.tests-pass-enterprise -test-community: tmp/.tests-pass-community -.PHONY: run-enterprise run-community build-enterprise build-community test-enterprise test-community - -fetch_tarball = curl --fail --silent --show-error --location --remote-name \ - $(dist_site)/$(call tarball,$(1),$(NEO4J_VERSION)) - -cache: in/neo4j-%-$(NEO4J_VERSION)-unix.tar.gz -.PHONY: cache - -in/neo4j-community-$(NEO4J_VERSION)-unix.tar.gz: -> mkdir -p in -> cd in -> $(call fetch_tarball,community) - -in/neo4j-enterprise-$(NEO4J_VERSION)-unix.tar.gz: -> mkdir -p in -> cd in -> $(call fetch_tarball,enterprise) - -clean: -> rm -rf tmp -> rm -rf out -.PHONY: clean +> touch $@ \ No newline at end of file diff --git a/README.md b/README.md index 2c8d37fc..e053fdc3 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,62 @@ *NOTE:* Supported images are available in the [official image library](https://hub.docker.com/_/neo4j/) on Docker Hub. Please use those for production use. -# Using the Neo4j Docker image +# Using the Neo4j Docker Image -## Neo4j 2.3 +Documentation for the Neo4j image can be found [here](https://neo4j.com/docs/operations-manual/current/deployment/single-instance/docker/). -Documentation for the Neo4j 2.3 image can be found [here](https://neo4j.com/developer/docker-23/). +You can start a Neo4j container like this: -You can start a Neo4j 2.3 container like this: +``` +docker run \ + --publish=7474:7474 --publish=7687:7687 \ + --volume=$HOME/neo4j/data:/data \ + --volume=$HOME/neo4j/logs:/logs \ + neo4j:latest +``` + +To start a Neo4j Enterprise Edition container, you can run: ``` docker run \ - --publish=7474:7474 \ + --publish=7474:7474 --publish=7687:7687 \ + --env=NEO4J_ACCEPT_LICENSE_AGREEMENT=yes \ --volume=$HOME/neo4j/data:/data \ - neo4j:2.3 + --volume=$HOME/neo4j/logs:/logs \ + neo4j:enterprise ``` -## Neo4j 3.0 +Mounting the `/data` and `/logs` folder is optional, +but it means that data can persist between closing and reopening Neo4j containers. -Documentation for the Neo4j 3.0 image can be found [here](http://neo4j.com/docs/operations-manual/current/deployment/single-instance/docker/). +# Neo4j images for ARM64 -You can start a Neo4j 3.0 container like this: +We provide unsupported and untested builds of ARM64 Neo4j community edition from 4.0.0 and onwards. +These are unsuitable for production use, but may be useful for experimentation or hobbyists. -``` +They are available on Docker hub at: + +https://hub.docker.com/r/neo4j/neo4j-arm64-experimental + + +The images take the name format `neo4j/neo4j-arm64-experimental:-arm64`. +Example usage: + +```shell script docker run \ --publish=7474:7474 --publish=7687:7687 \ --volume=$HOME/neo4j/data:/data \ - neo4j:3.0 + --volume=$HOME/neo4j/logs:/logs \ + neo4j/neo4j-arm64-experimental:4.1.0-arm64 ``` + +# Building and Developing the Neo4j Docker Image + +See [DEVELOPMENT.md](DEVELOPMENT.md) + # Getting support and contributing -Please create issues and pull requests in the Github repository. +For bug reports and feature requests, please create issues and pull requests against this Github repository. + +If you need guidance with using Neo4j you can ask questions here: https://community.neo4j.com/ diff --git a/build/record-env b/build/record-env deleted file mode 100755 index ba9ba48d..00000000 --- a/build/record-env +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -eu - -var="$1" -[[ -z "${!var:-}" ]] && echo >&2 "Error: ${var} must be defined" && exit 1 -val="${!var}" -root=tmp/.env -path="${root}/${var}" - -mkdir -p "${root}" -grep --quiet --no-messages "^${val}$" "${path}" || echo "${val}" >"${path}" -echo "${path}" diff --git a/build/trapping-sigint b/build/trapping-sigint deleted file mode 100755 index b20c9b2c..00000000 --- a/build/trapping-sigint +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -em - -trap 'exit 0' SIGINT -set +m -$* diff --git a/devenv b/devenv index a11d13dd..a85e3317 100644 --- a/devenv +++ b/devenv @@ -2,6 +2,65 @@ PATH="./build:${PATH}" +# MacOS specific checks +if [ "$(uname)" == "Darwin" ] ; then + + # We use temporary directories during testing that need to be + # accessible to the Docker daemon. By default on Macs, $TMPDIR + # (which mktemp responds to) is set to a user-specific location that + # the Docker daemon cannot read from. + # + # In some environments, such as TeamCity, $TMPDIR is intentionally + # pointed elsewhere, so we only want to override the default + # value. (We don't currently run these builds on Macs, but you never + # know.) This default seems to be in /private/var/folders on some + # Macs and /var/folders on others, so we accommodate both. + if [[ "${TMPDIR}" == */var/folders* ]]; then + export TMPDIR=/tmp + fi + + echo "Setting PATH with MacOS specific locations" + export PATH="/usr/local/opt/coreutils/libexec/gnubin:$PATH" + export PATH="/usr/local/opt/gnu-sed/libexec/gnubin:$PATH" + export PATH="/usr/local/opt/make/libexec/gnubin:$PATH" + + if ! grep --version 2>/dev/null | grep -q "GNU grep" ; then + cat >&2 </dev/null | grep -q "GNU sed" ; then + cat >&2 </dev/null | grep -q "GNU coreutils" ; then + cat >&2 </dev/null 2>/dev/null || cat >&2 </dev/null; then if which apt-get >/dev/null; then sudo apt-get install make @@ -12,6 +71,9 @@ fi if [[ -f devenv.local ]]; then source devenv.local + # to be consistent with the rest of neo4j we should use NEO4JVERSION exclusively but unfortunately both with and without underscore are used in this repo + export NEO4JVERSION + NEO4J_VERSION="${NEO4JVERSION}" export NEO4J_VERSION else echo >&2 "Error: cannot find devenv.local" diff --git a/devenv.local.template b/devenv.local.template index 4ad8c6eb..9085a5d5 100644 --- a/devenv.local.template +++ b/devenv.local.template @@ -1,3 +1,3 @@ # -*- mode: shell-script -*- -NEO4J_VERSION= +NEO4JVERSION= \ No newline at end of file diff --git a/src/2.3/Dockerfile b/docker-image-src/2.3/Dockerfile similarity index 95% rename from src/2.3/Dockerfile rename to docker-image-src/2.3/Dockerfile index 8b9ccd1e..1a0bc8f5 100644 --- a/src/2.3/Dockerfile +++ b/docker-image-src/2.3/Dockerfile @@ -16,6 +16,8 @@ RUN curl --fail --silent --show-error --location --remote-name ${NEO4J_URI} \ && mv /var/lib/neo4j-* /var/lib/neo4j \ && rm ${NEO4J_TARBALL} +ENV PATH /var/lib/neo4j/bin:$PATH + WORKDIR /var/lib/neo4j RUN mv data /data \ diff --git a/src/2.3/docker-entrypoint.sh b/docker-image-src/2.3/docker-entrypoint.sh similarity index 88% rename from src/2.3/docker-entrypoint.sh rename to docker-image-src/2.3/docker-entrypoint.sh index fb1749be..dc1a61b9 100755 --- a/src/2.3/docker-entrypoint.sh +++ b/docker-image-src/2.3/docker-entrypoint.sh @@ -6,10 +6,10 @@ setting() { file="${3}" if [ -n "${value}" ]; then - if grep --quiet --fixed-strings "${setting}=" conf/"${file}"; then - sed --in-place "s|.*${setting}=.*|${setting}=${value}|" conf/"${file}" + if grep --quiet --fixed-strings "${setting}=" "${NEO4J_HOME}/conf/${file}"; then + sed --in-place "s|.*${setting}=.*|${setting}=${value}|" "${NEO4J_HOME}/conf/${file}" else - echo "${setting}=${value}" >>conf/"${file}" + echo "${setting}=${value}" >>"${NEO4J_HOME}/conf/${file}" fi fi } @@ -52,7 +52,7 @@ if [ "$1" == "neo4j" ]; then [ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT} if [ -d /conf ]; then - find /conf -type f -exec cp {} conf \; + find /conf -type f -exec cp {} "${NEO4J_HOME}/conf" \; fi if [ -d /ssl ]; then @@ -75,10 +75,10 @@ if [ "$1" == "neo4j" ]; then exec bin/neo4j console elif [ "$1" == "dump-config" ]; then - if [ -d /conf ]; then - cp --recursive conf/* /conf + if [ -d "${NEO4J_HOME}/conf" ]; then + cp --recursive "${NEO4J_HOME}"/conf/* /conf else - echo "You must provide a /conf volume" + echo >&2 "You must provide a /conf volume" exit 1 fi else diff --git a/src/3.0/Dockerfile b/docker-image-src/3.0/Dockerfile similarity index 95% rename from src/3.0/Dockerfile rename to docker-image-src/3.0/Dockerfile index e94dbaa8..8ea25127 100644 --- a/src/3.0/Dockerfile +++ b/docker-image-src/3.0/Dockerfile @@ -12,6 +12,8 @@ RUN curl --fail --silent --show-error --location --remote-name ${NEO4J_URI} \ && mv /var/lib/neo4j-* /var/lib/neo4j \ && rm ${NEO4J_TARBALL} +ENV PATH /var/lib/neo4j/bin:$PATH + WORKDIR /var/lib/neo4j RUN mv data /data \ diff --git a/docker-image-src/3.0/docker-entrypoint.sh b/docker-image-src/3.0/docker-entrypoint.sh new file mode 100755 index 00000000..41af739c --- /dev/null +++ b/docker-image-src/3.0/docker-entrypoint.sh @@ -0,0 +1,166 @@ +#!/bin/bash -eu + +setting() { + setting="${1}" + value="${2}" + file="${3:-neo4j.conf}" + + if [ ! -f "conf/${file}" ]; then + if [ -f "conf/neo4j.conf" ]; then + file="neo4j.conf" + fi + fi + + # 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 + if grep --quiet --fixed-strings "${setting}=" conf/"${file}"; then + sed --in-place "s|.*${setting}=.*|${setting}=${value}|" conf/"${file}" + else + echo "${setting}=${value}" >>conf/"${file}" + fi + else + echo >&2 "WARNING: ${setting} not written to conf file because settings that start with a number are not permitted" + fi + fi +} + +cmd="$1" + +if [ "${cmd}" == "dump-config" ]; then + if [ -d /conf ]; then + cp --recursive conf/* /conf + exit 0 + else + echo >&2 "You must provide a /conf volume" + exit 1 + fi +fi + +# 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:-"100M size"}} +: ${NEO4J_wrapper_java_additional:=${NEO4J_UDC_SOURCE:-"-Dneo4j.ext.udc.source=docker"}} +: ${NEO4J_dbms_memory_heap_initial__size:=${NEO4J_dbms_memory_heap_maxSize:-"512"}} +: ${NEO4J_dbms_memory_heap_max__size:=${NEO4J_dbms_memory_heap_maxSize:-"512"}} +: ${NEO4J_dbms_unmanaged__extension__classes:=${NEO4J_dbms_unmanagedExtensionClasses:-}} +: ${NEO4J_dbms_allow__format__migration:=${NEO4J_dbms_allowFormatMigration:-}} +: ${NEO4J_ha_server__id:=${NEO4J_ha_serverId:-}} +: ${NEO4J_ha_initial__hosts:=${NEO4J_ha_initialHosts:-}} + +: ${NEO4J_dbms_connector_http_address:="0.0.0.0:7474"} +: ${NEO4J_dbms_connector_https_address:="0.0.0.0:7473"} +: ${NEO4J_dbms_connector_bolt_address:="0.0.0.0:7687"} +: ${NEO4J_ha_host_coordination:="$(hostname):5001"} +: ${NEO4J_ha_host_data:="$(hostname):6001"} + +# unset old hardcoded unsupported env variables +unset NEO4J_dbms_txLog_rotation_retentionPolicy NEO4J_UDC_SOURCE \ + NEO4J_dbms_memory_heap_maxSize NEO4J_dbms_memory_heap_maxSize \ + NEO4J_dbms_unmanagedExtensionClasses NEO4J_dbms_allowFormatMigration \ + NEO4J_ha_initialHosts + +if [ -d /conf ]; then + find /conf -type f -exec cp {} conf \; +fi + +if [ -d /ssl ]; then + NEO4J_dbms_directories_certificates="/ssl" +fi + +if [ -d /plugins ]; then + NEO4J_dbms_directories_plugins="/plugins" +fi + +if [ -d /logs ]; then + NEO4J_dbms_directories_logs="/logs" +fi + +if [ -d /import ]; then + NEO4J_dbms_directories_import="/import" +fi + +if [ -d /metrics ]; then + NEO4J_dbms_directories_metrics="/metrics" +fi + +if [ "${cmd}" == "neo4j" ] ; then + if [ "${NEO4J_AUTH:-}" == "none" ]; then + NEO4J_dbms_security_auth__enabled=false + elif [[ "${NEO4J_AUTH:-}" == neo4j/* ]]; then + password="${NEO4J_AUTH#neo4j/}" + if [ "${password}" == "neo4j" ]; then + echo "Invalid value for password. It cannot be 'neo4j', which is the default." + exit 1 + fi + + setting "dbms.connector.http.address" "127.0.0.1:7474" + setting "dbms.connector.https.address" "127.0.0.1:7473" + setting "dbms.connector.bolt.address" "127.0.0.1:7687" + bin/neo4j start || \ + (cat logs/neo4j.log && echo "Neo4j failed to start for password change" && exit 1) + + end="$((SECONDS+100))" + while true; do + http_code="$(curl --silent --write-out %{http_code} --user "neo4j:${password}" --output /dev/null http://localhost:7474/db/data/ || true)" + + if [[ "${http_code}" = "200" ]]; then + break; + fi + + if [[ "${http_code}" = "401" ]]; then + curl --fail --silent --show-error --user neo4j:neo4j \ + --data '{"password": "'"${password}"'"}' \ + --header 'Content-Type: application/json' \ + http://localhost:7474/user/neo4j/password + break; + fi + + if [[ "${SECONDS}" -ge "${end}" ]]; then + (cat logs/neo4j.log && echo "Neo4j failed to start" && exit 1) + fi + + sleep 1 + done + + bin/neo4j stop + elif [ -n "${NEO4J_AUTH:-}" ]; then + echo "Invalid value for NEO4J_AUTH: '${NEO4J_AUTH}'" + exit 1 + fi +fi + +# list env variables with prefix NEO4J_ and create settings from them +unset NEO4J_AUTH NEO4J_SHA256 NEO4J_TARBALL +for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do + setting=$(echo ${i} | sed 's|^NEO4J_||' | sed 's|_|.|g' | sed 's|\.\.|_|g') + value=$(echo ${!i}) + if [[ -n ${value} ]]; then + if [[ ! "${setting}" =~ ^[0-9]+.*$ ]]; then + if grep -q -F "${setting}=" conf/neo4j.conf; then + # Remove any lines containing the setting already + sed --in-place "/^${setting}=.*/d" conf/neo4j.conf + fi + # Then always append setting to file + echo "${setting}=${value}" >> conf/neo4j.conf + else + echo >&2 "WARNING: ${setting} not written to conf file because settings that start with a number are not permitted" + fi + fi +done + +[ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT} + +if [ "${cmd}" == "neo4j" ] ; then + exec bin/neo4j console +else + exec "$@" +fi diff --git a/docker-image-src/3.1/Dockerfile b/docker-image-src/3.1/Dockerfile new file mode 100644 index 00000000..23a0a318 --- /dev/null +++ b/docker-image-src/3.1/Dockerfile @@ -0,0 +1,45 @@ +FROM adoptopenjdk/openjdk8:alpine-jre + +RUN addgroup -S neo4j && adduser -S -H -h /var/lib/neo4j -G neo4j neo4j + +ENV NEO4J_SHA256=%%NEO4J_SHA%% \ + NEO4J_TARBALL=%%NEO4J_TARBALL%% \ + NEO4J_HOME="/var/lib/neo4j" +ARG NEO4J_URI=%%NEO4J_DIST_SITE%%/%%NEO4J_TARBALL%% + +COPY ./local-package/* /tmp/ + +RUN apk add --no-cache --quiet \ + bash \ + curl \ + tini \ + su-exec \ + && curl --fail --silent --show-error --location --remote-name ${NEO4J_URI} \ + && echo "${NEO4J_SHA256} ${NEO4J_TARBALL}" | sha256sum -csw - \ + && tar --extract --file ${NEO4J_TARBALL} --directory /var/lib \ + && mv /var/lib/neo4j-* "${NEO4J_HOME}" \ + && rm ${NEO4J_TARBALL} \ + && mv "${NEO4J_HOME}"/data /data \ + && chown -R neo4j:neo4j /data \ + && chmod -R 777 /data \ + && mv "${NEO4J_HOME}"/logs /logs \ + && chown -R neo4j:neo4j /logs \ + && chmod -R 777 /logs \ + && chown -R neo4j:neo4j "${NEO4J_HOME}" \ + && chmod -R 777 "${NEO4J_HOME}" \ + && ln -s /data "${NEO4J_HOME}"/data \ + && ln -s /logs "${NEO4J_HOME}"/logs \ + && apk del curl + +ENV PATH "${NEO4J_HOME}"/bin:$PATH + +WORKDIR "${NEO4J_HOME}" + +VOLUME /data /logs + +COPY docker-entrypoint.sh /docker-entrypoint.sh + +EXPOSE 7474 7473 7687 + +ENTRYPOINT ["/sbin/tini", "-g", "--", "/docker-entrypoint.sh"] +CMD ["neo4j"] diff --git a/docker-image-src/3.1/docker-entrypoint.sh b/docker-image-src/3.1/docker-entrypoint.sh new file mode 100755 index 00000000..630d9172 --- /dev/null +++ b/docker-image-src/3.1/docker-entrypoint.sh @@ -0,0 +1,202 @@ +#!/bin/bash -eu + +cmd="$1" + +function running_as_root +{ + test "$(id -u)" = "0" +} + +# 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" + exec_cmd="exec su-exec neo4j" +else + userid="$(id -u)" + groupid="$(id -g)" + exec_cmd="exec" +fi +readonly userid +readonly groupid +readonly exec_cmd + +# Need to chown the home directory - but a user might have mounted a +# volume here (notably a conf volume). So take care not to chown +# volumes (stuff not owned by neo4j) +if running_as_root; then + # Non-recursive chown for the base directory + chown "${userid}":"${groupid}" "${NEO4J_HOME}" + chmod 700 "${NEO4J_HOME}" +fi + +while IFS= read -r -d '' dir +do + if running_as_root && [[ "$(stat -c %U "${dir}")" = "neo4j" ]]; then + # Using mindepth 1 to avoid the base directory here so recursive is OK + chown -R "${userid}":"${groupid}" "${dir}" + chmod -R 700 "${dir}" + fi +done < <(find "${NEO4J_HOME}" -type d -mindepth 1 -maxdepth 1 -print0) + +# Data dir is chowned later + +if [ "${cmd}" == "dump-config" ]; then + if [ -d /conf ]; then + ${exec_cmd} cp --recursive "${NEO4J_HOME}"/conf/* /conf + exit 0 + else + echo >&2 "You must provide a /conf volume" + exit 1 + fi +fi + +# 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:-"100M size"}} +: ${NEO4J_wrapper_java_additional:=${NEO4J_UDC_SOURCE:-"-Dneo4j.ext.udc.source=docker"}} +: ${NEO4J_dbms_memory_heap_initial__size:=${NEO4J_dbms_memory_heap_maxSize:-"512M"}} +: ${NEO4J_dbms_memory_heap_max__size:=${NEO4J_dbms_memory_heap_maxSize:-"512M"}} +: ${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:-}} +: ${NEO4J_ha_server__id:=${NEO4J_ha_serverId:-}} +: ${NEO4J_ha_initial__hosts:=${NEO4J_ha_initialHosts:-}} +: ${NEO4J_causal__clustering_expected__core__cluster__size:=${NEO4J_causalClustering_expectedCoreClusterSize:-}} +: ${NEO4J_causal__clustering_initial__discovery__members:=${NEO4J_causalClustering_initialDiscoveryMembers:-}} +: ${NEO4J_causal__clustering_discovery__listen__address:=${NEO4J_causalClustering_discoveryListenAddress:-"0.0.0.0:5000"}} +: ${NEO4J_causal__clustering_discovery__advertised__address:=${NEO4J_causalClustering_discoveryAdvertisedAddress:-"$(hostname):5000"}} +: ${NEO4J_causal__clustering_transaction__listen__address:=${NEO4J_causalClustering_transactionListenAddress:-"0.0.0.0:6000"}} +: ${NEO4J_causal__clustering_transaction__advertised__address:=${NEO4J_causalClustering_transactionAdvertisedAddress:-"$(hostname):6000"}} +: ${NEO4J_causal__clustering_raft__listen__address:=${NEO4J_causalClustering_raftListenAddress:-"0.0.0.0:7000"}} +: ${NEO4J_causal__clustering_raft__advertised__address:=${NEO4J_causalClustering_raftAdvertisedAddress:-"$(hostname):7000"}} + +: ${NEO4J_dbms_connectors_default__listen__address:="0.0.0.0"} +: ${NEO4J_dbms_connector_http_listen__address:="0.0.0.0:7474"} +: ${NEO4J_dbms_connector_https_listen__address:="0.0.0.0:7473"} +: ${NEO4J_dbms_connector_bolt_listen__address:="0.0.0.0:7687"} +: ${NEO4J_ha_host_coordination:="$(hostname):5001"} +: ${NEO4J_ha_host_data:="$(hostname):6001"} + +# unset old hardcoded unsupported env variables +unset NEO4J_dbms_txLog_rotation_retentionPolicy NEO4J_UDC_SOURCE \ + NEO4J_dbms_memory_heap_maxSize NEO4J_dbms_memory_heap_maxSize \ + 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 + +if [ -d /conf ]; then + find /conf -type f -exec cp {} "${NEO4J_HOME}"/conf \; +fi + +if [ -d /ssl ]; then + NEO4J_dbms_directories_certificates="/ssl" +fi + +if [ -d /plugins ]; then + NEO4J_dbms_directories_plugins="/plugins" +fi + +if [ -d /logs ]; then + NEO4J_dbms_directories_logs="/logs" +fi + +if [ -d /import ]; then + NEO4J_dbms_directories_import="/import" +fi + +if [ -d /metrics ]; then + NEO4J_dbms_directories_metrics="/metrics" +fi + +# set the neo4j initial password only if you run the database server +if [ "${cmd}" == "neo4j" ]; then + if [ "${NEO4J_AUTH:-}" == "none" ]; then + NEO4J_dbms_security_auth__enabled=false + elif [[ "${NEO4J_AUTH:-}" == neo4j/* ]]; then + password="${NEO4J_AUTH#neo4j/}" + if [ "${password}" == "neo4j" ]; then + echo >&2 "Invalid value for password. It cannot be 'neo4j', which is the default." + exit 1 + fi + # Will exit with error if users already exist (and print a message explaining that) + bin/neo4j-admin set-initial-password "${password}" || true + elif [ -n "${NEO4J_AUTH:-}" ]; then + echo >&2 "Invalid value for NEO4J_AUTH: '${NEO4J_AUTH}'" + exit 1 + fi +fi + +# list env variables with prefix NEO4J_ and create settings from them +unset NEO4J_AUTH NEO4J_SHA256 NEO4J_TARBALL +for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do + 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 + if grep -q -F "${setting}=" "${NEO4J_HOME}"/conf/neo4j.conf; then + # Remove any lines containing the setting already + sed --in-place "/^${setting}=.*/d" "${NEO4J_HOME}"/conf/neo4j.conf + fi + # Then always append setting to file + echo "${setting}=${value}" >> "${NEO4J_HOME}"/conf/neo4j.conf + else + echo >&2 "WARNING: ${setting} not written to conf file because settings that start with a number are not permitted" + fi + fi +done + +# Chown the data dir now that (maybe) an initial password has been +# set (this is a file in the data dir) +if running_as_root; then + chmod -R 777 /data + chown -R "${userid}":"${groupid}" /data +fi + +# if we're running as root and the logs directory is not writable by the neo4j user, then chown it. +# this situation happens if no user is passed to docker run and the /logs directory is mounted. +if running_as_root && [[ "$(stat -c %U /logs)" != "neo4j" ]]; then +#if [[ $(stat -c %u /logs) != $(id -u "${userid}") ]]; then + echo "/logs directory is not writable. Changing the directory owner to ${userid}:${groupid}" + # chown the log dir if it's not writable + chmod -R 777 /logs + chown -R "${userid}":"${groupid}" /logs +fi + +# If we're running as a non-default user and we can't write to the logs directory then user needs to change directory permissions manually. +# This happens if a user is passed to docker run and an unwritable log directory is mounted. +if ! running_as_root && [[ ! -w /logs ]]; then + echo "User does not have write permissions to mounted log directory." + echo "Manually grant write permissions for the directory and try again." + exit 1 +fi + +[ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT} + +# 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 + ${exec_cmd} neo4j console +else + ${exec_cmd} "$@" +fi diff --git a/docker-image-src/3.2/Dockerfile b/docker-image-src/3.2/Dockerfile new file mode 100644 index 00000000..23a0a318 --- /dev/null +++ b/docker-image-src/3.2/Dockerfile @@ -0,0 +1,45 @@ +FROM adoptopenjdk/openjdk8:alpine-jre + +RUN addgroup -S neo4j && adduser -S -H -h /var/lib/neo4j -G neo4j neo4j + +ENV NEO4J_SHA256=%%NEO4J_SHA%% \ + NEO4J_TARBALL=%%NEO4J_TARBALL%% \ + NEO4J_HOME="/var/lib/neo4j" +ARG NEO4J_URI=%%NEO4J_DIST_SITE%%/%%NEO4J_TARBALL%% + +COPY ./local-package/* /tmp/ + +RUN apk add --no-cache --quiet \ + bash \ + curl \ + tini \ + su-exec \ + && curl --fail --silent --show-error --location --remote-name ${NEO4J_URI} \ + && echo "${NEO4J_SHA256} ${NEO4J_TARBALL}" | sha256sum -csw - \ + && tar --extract --file ${NEO4J_TARBALL} --directory /var/lib \ + && mv /var/lib/neo4j-* "${NEO4J_HOME}" \ + && rm ${NEO4J_TARBALL} \ + && mv "${NEO4J_HOME}"/data /data \ + && chown -R neo4j:neo4j /data \ + && chmod -R 777 /data \ + && mv "${NEO4J_HOME}"/logs /logs \ + && chown -R neo4j:neo4j /logs \ + && chmod -R 777 /logs \ + && chown -R neo4j:neo4j "${NEO4J_HOME}" \ + && chmod -R 777 "${NEO4J_HOME}" \ + && ln -s /data "${NEO4J_HOME}"/data \ + && ln -s /logs "${NEO4J_HOME}"/logs \ + && apk del curl + +ENV PATH "${NEO4J_HOME}"/bin:$PATH + +WORKDIR "${NEO4J_HOME}" + +VOLUME /data /logs + +COPY docker-entrypoint.sh /docker-entrypoint.sh + +EXPOSE 7474 7473 7687 + +ENTRYPOINT ["/sbin/tini", "-g", "--", "/docker-entrypoint.sh"] +CMD ["neo4j"] diff --git a/docker-image-src/3.2/docker-entrypoint.sh b/docker-image-src/3.2/docker-entrypoint.sh new file mode 100755 index 00000000..630d9172 --- /dev/null +++ b/docker-image-src/3.2/docker-entrypoint.sh @@ -0,0 +1,202 @@ +#!/bin/bash -eu + +cmd="$1" + +function running_as_root +{ + test "$(id -u)" = "0" +} + +# 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" + exec_cmd="exec su-exec neo4j" +else + userid="$(id -u)" + groupid="$(id -g)" + exec_cmd="exec" +fi +readonly userid +readonly groupid +readonly exec_cmd + +# Need to chown the home directory - but a user might have mounted a +# volume here (notably a conf volume). So take care not to chown +# volumes (stuff not owned by neo4j) +if running_as_root; then + # Non-recursive chown for the base directory + chown "${userid}":"${groupid}" "${NEO4J_HOME}" + chmod 700 "${NEO4J_HOME}" +fi + +while IFS= read -r -d '' dir +do + if running_as_root && [[ "$(stat -c %U "${dir}")" = "neo4j" ]]; then + # Using mindepth 1 to avoid the base directory here so recursive is OK + chown -R "${userid}":"${groupid}" "${dir}" + chmod -R 700 "${dir}" + fi +done < <(find "${NEO4J_HOME}" -type d -mindepth 1 -maxdepth 1 -print0) + +# Data dir is chowned later + +if [ "${cmd}" == "dump-config" ]; then + if [ -d /conf ]; then + ${exec_cmd} cp --recursive "${NEO4J_HOME}"/conf/* /conf + exit 0 + else + echo >&2 "You must provide a /conf volume" + exit 1 + fi +fi + +# 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:-"100M size"}} +: ${NEO4J_wrapper_java_additional:=${NEO4J_UDC_SOURCE:-"-Dneo4j.ext.udc.source=docker"}} +: ${NEO4J_dbms_memory_heap_initial__size:=${NEO4J_dbms_memory_heap_maxSize:-"512M"}} +: ${NEO4J_dbms_memory_heap_max__size:=${NEO4J_dbms_memory_heap_maxSize:-"512M"}} +: ${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:-}} +: ${NEO4J_ha_server__id:=${NEO4J_ha_serverId:-}} +: ${NEO4J_ha_initial__hosts:=${NEO4J_ha_initialHosts:-}} +: ${NEO4J_causal__clustering_expected__core__cluster__size:=${NEO4J_causalClustering_expectedCoreClusterSize:-}} +: ${NEO4J_causal__clustering_initial__discovery__members:=${NEO4J_causalClustering_initialDiscoveryMembers:-}} +: ${NEO4J_causal__clustering_discovery__listen__address:=${NEO4J_causalClustering_discoveryListenAddress:-"0.0.0.0:5000"}} +: ${NEO4J_causal__clustering_discovery__advertised__address:=${NEO4J_causalClustering_discoveryAdvertisedAddress:-"$(hostname):5000"}} +: ${NEO4J_causal__clustering_transaction__listen__address:=${NEO4J_causalClustering_transactionListenAddress:-"0.0.0.0:6000"}} +: ${NEO4J_causal__clustering_transaction__advertised__address:=${NEO4J_causalClustering_transactionAdvertisedAddress:-"$(hostname):6000"}} +: ${NEO4J_causal__clustering_raft__listen__address:=${NEO4J_causalClustering_raftListenAddress:-"0.0.0.0:7000"}} +: ${NEO4J_causal__clustering_raft__advertised__address:=${NEO4J_causalClustering_raftAdvertisedAddress:-"$(hostname):7000"}} + +: ${NEO4J_dbms_connectors_default__listen__address:="0.0.0.0"} +: ${NEO4J_dbms_connector_http_listen__address:="0.0.0.0:7474"} +: ${NEO4J_dbms_connector_https_listen__address:="0.0.0.0:7473"} +: ${NEO4J_dbms_connector_bolt_listen__address:="0.0.0.0:7687"} +: ${NEO4J_ha_host_coordination:="$(hostname):5001"} +: ${NEO4J_ha_host_data:="$(hostname):6001"} + +# unset old hardcoded unsupported env variables +unset NEO4J_dbms_txLog_rotation_retentionPolicy NEO4J_UDC_SOURCE \ + NEO4J_dbms_memory_heap_maxSize NEO4J_dbms_memory_heap_maxSize \ + 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 + +if [ -d /conf ]; then + find /conf -type f -exec cp {} "${NEO4J_HOME}"/conf \; +fi + +if [ -d /ssl ]; then + NEO4J_dbms_directories_certificates="/ssl" +fi + +if [ -d /plugins ]; then + NEO4J_dbms_directories_plugins="/plugins" +fi + +if [ -d /logs ]; then + NEO4J_dbms_directories_logs="/logs" +fi + +if [ -d /import ]; then + NEO4J_dbms_directories_import="/import" +fi + +if [ -d /metrics ]; then + NEO4J_dbms_directories_metrics="/metrics" +fi + +# set the neo4j initial password only if you run the database server +if [ "${cmd}" == "neo4j" ]; then + if [ "${NEO4J_AUTH:-}" == "none" ]; then + NEO4J_dbms_security_auth__enabled=false + elif [[ "${NEO4J_AUTH:-}" == neo4j/* ]]; then + password="${NEO4J_AUTH#neo4j/}" + if [ "${password}" == "neo4j" ]; then + echo >&2 "Invalid value for password. It cannot be 'neo4j', which is the default." + exit 1 + fi + # Will exit with error if users already exist (and print a message explaining that) + bin/neo4j-admin set-initial-password "${password}" || true + elif [ -n "${NEO4J_AUTH:-}" ]; then + echo >&2 "Invalid value for NEO4J_AUTH: '${NEO4J_AUTH}'" + exit 1 + fi +fi + +# list env variables with prefix NEO4J_ and create settings from them +unset NEO4J_AUTH NEO4J_SHA256 NEO4J_TARBALL +for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do + 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 + if grep -q -F "${setting}=" "${NEO4J_HOME}"/conf/neo4j.conf; then + # Remove any lines containing the setting already + sed --in-place "/^${setting}=.*/d" "${NEO4J_HOME}"/conf/neo4j.conf + fi + # Then always append setting to file + echo "${setting}=${value}" >> "${NEO4J_HOME}"/conf/neo4j.conf + else + echo >&2 "WARNING: ${setting} not written to conf file because settings that start with a number are not permitted" + fi + fi +done + +# Chown the data dir now that (maybe) an initial password has been +# set (this is a file in the data dir) +if running_as_root; then + chmod -R 777 /data + chown -R "${userid}":"${groupid}" /data +fi + +# if we're running as root and the logs directory is not writable by the neo4j user, then chown it. +# this situation happens if no user is passed to docker run and the /logs directory is mounted. +if running_as_root && [[ "$(stat -c %U /logs)" != "neo4j" ]]; then +#if [[ $(stat -c %u /logs) != $(id -u "${userid}") ]]; then + echo "/logs directory is not writable. Changing the directory owner to ${userid}:${groupid}" + # chown the log dir if it's not writable + chmod -R 777 /logs + chown -R "${userid}":"${groupid}" /logs +fi + +# If we're running as a non-default user and we can't write to the logs directory then user needs to change directory permissions manually. +# This happens if a user is passed to docker run and an unwritable log directory is mounted. +if ! running_as_root && [[ ! -w /logs ]]; then + echo "User does not have write permissions to mounted log directory." + echo "Manually grant write permissions for the directory and try again." + exit 1 +fi + +[ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT} + +# 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 + ${exec_cmd} neo4j console +else + ${exec_cmd} "$@" +fi diff --git a/docker-image-src/3.3/Dockerfile b/docker-image-src/3.3/Dockerfile new file mode 100644 index 00000000..68922257 --- /dev/null +++ b/docker-image-src/3.3/Dockerfile @@ -0,0 +1,50 @@ +FROM openjdk:8-jre-slim + +ENV NEO4J_SHA256=%%NEO4J_SHA%% \ + NEO4J_TARBALL=%%NEO4J_TARBALL%% \ + NEO4J_EDITION=%%NEO4J_EDITION%% \ + NEO4J_HOME="/var/lib/neo4j" \ + TINI_VERSION="v0.18.0" \ + TINI_SHA256="12d20136605531b09a2c2dac02ccee85e1b874eb322ef6baf7561cd93f93c855" +ARG NEO4J_URI=%%NEO4J_DIST_SITE%%/%%NEO4J_TARBALL%% + +RUN addgroup --system neo4j && adduser --system --no-create-home --home "${NEO4J_HOME}" --ingroup neo4j neo4j + +COPY ./local-package/* /tmp/ + +RUN apt update \ + && apt install -y curl gosu jq \ + && curl -L --fail --silent --show-error "https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini" > /sbin/tini \ + && echo "${TINI_SHA256} /sbin/tini" | sha256sum -c --strict --quiet \ + && chmod +x /sbin/tini \ + && 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 "${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}" \ + && ln -s /data "${NEO4J_HOME}"/data \ + && ln -s /logs "${NEO4J_HOME}"/logs \ + && mv /tmp/neo4jlabs-plugins.json /neo4jlabs-plugins.json \ + && rm -rf /tmp/* \ + && rm -rf /var/lib/apt/lists/* + +ENV PATH "${NEO4J_HOME}"/bin:$PATH + +WORKDIR "${NEO4J_HOME}" + +VOLUME /data /logs + +COPY docker-entrypoint.sh /docker-entrypoint.sh + +EXPOSE 7474 7473 7687 + +ENTRYPOINT ["/sbin/tini", "-g", "--", "/docker-entrypoint.sh"] +CMD ["neo4j"] diff --git a/docker-image-src/3.3/docker-entrypoint.sh b/docker-image-src/3.3/docker-entrypoint.sh new file mode 100755 index 00000000..1a9938f1 --- /dev/null +++ b/docker-image-src/3.3/docker-entrypoint.sh @@ -0,0 +1,487 @@ +#!/bin/bash -eu + +cmd="$1" + +function running_as_root +{ + test "$(id -u)" = "0" +} + +function secure_mode_enabled +{ + test "${SECURE_FILE_PERMISSIONS:=no}" = "yes" +} + +function containsElement +{ + local e match="$1" + shift + for e; do [[ "$e" == "$match" ]] && return 0; done + return 1 +} + +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 print_permissions_advice_and_fail +{ + _directory=${1} + echo >&2 " +Folder ${_directory} is not accessible for user: ${userid} or group ${groupid} or groups ${groups[@]}, 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 +} + +function check_mounted_folder_readable +{ + local _directory=${1} + if ! is_readable "${_directory}"; then + print_permissions_advice_and_fail "${_directory}" + fi +} + +function check_mounted_folder_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} + if running_as_root; then + if ! is_writable "${mountFolder}" && ! secure_mode_enabled; 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}" + fi + else + if [[ ! -w "${mountFolder}" ]] && [[ "$(stat -c %U ${mountFolder})" != "neo4j" ]]; then + print_permissions_advice_and_fail "${mountFolder}" + fi + fi +} + +function load_plugin_from_github +{ + # 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" /neo4jlabs-plugins.json )" + # 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="$(curl --silent --show-error --fail --retry 30 --retry-max-time 300 -L "${_versions_json_url}")" + local _plugin_jar_url="$(echo "${_versions_json}" | jq --raw-output ".[] | select(.neo4j==\"${_neo4j_version}\") | .jar")" + if [[ -z "${_plugin_jar_url}" ]]; then + echo >&2 "Error: No jar URL found for version '${_neo4j_version}' in versions.json from '${_versions_json_url}'" + echo >&2 "${_versions_json}" + exit 1 + fi + echo "Installing Plugin '${_plugin_name}' from ${_plugin_jar_url} to ${_destination} " + curl --silent --show-error --fail --retry 30 --retry-max-time 300 -L -o "${_destination}" "${_plugin_jar_url}" + + if ! is_readable "${_destination}"; then + echo >&2 "Plugin at '${_destination}' is not readable" + exit 1 + 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, graph-ql + 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[]" /neo4jlabs-plugins.json); do + _property="$(jq --raw-output '.key' <<< "${_entry}")" + _value="$(jq --raw-output '.value' <<< "${_entry}")" + + # 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" + else + if grep -o "^[^#]*" "${_neo4j_conf}" | grep -q --fixed-strings "${_property}=" ; then + sed --in-place "s/${_property}=/&${_value},/" "${_neo4j_conf}" + else + echo "${_property}=${_value}" >> "${_neo4j_conf}" + fi + fi + done +} + +function install_neo4j_labs_plugins +{ + # 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)" + cp "${NEO4J_HOME}"/conf/neo4j.conf "${_old_config}" + for plugin_name in $(echo "${NEO4JLABS_PLUGINS}" | jq --raw-output '.[]'); do + load_plugin_from_github "${plugin_name}" + apply_plugin_default_configuration "${plugin_name}" "${_old_config}" + done + rm "${_old_config}" +} + +# 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 gosu neo4j:neo4j" +else + userid="$(id -u)" + groupid="$(id -g)" + groups=($(id -G)) + exec_cmd="exec" +fi +readonly userid +readonly groupid +readonly groups +readonly exec_cmd + + +# Need to chown the home directory - but a user might have mounted a +# volume here (notably a conf volume). So take care not to chown +# volumes (stuff not owned by neo4j) +if running_as_root; then + # Non-recursive chown for the base directory + chown "${userid}":"${groupid}" "${NEO4J_HOME}" + chmod 700 "${NEO4J_HOME}" + find "${NEO4J_HOME}" -mindepth 1 -maxdepth 1 -user root -type d -exec chown -R ${userid}:${groupid} {} \; + find "${NEO4J_HOME}" -mindepth 1 -maxdepth 1 -user root -type d -exec chmod -R 700 {} \; +fi + +# 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. + +(c) Neo4j Sweden AB. 2021. All Rights Reserved. +Use of this Software without a proper commercial license with Neo4j, +Inc. or its affiliates is prohibited. + +Email inquiries can be directed to: licensing@neo4j.com + +More information is also available at: https://neo4j.com/licensing/ + + +To accept the 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 + +# 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:-"100M size"}} +: ${NEO4J_wrapper_java_additional:=${NEO4J_UDC_SOURCE:-"-Dneo4j.ext.udc.source=docker"}} +: ${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:-}} +: ${NEO4J_ha_server__id:=${NEO4J_ha_serverId:-}} +: ${NEO4J_ha_initial__hosts:=${NEO4J_ha_initialHosts:-}} + +if [ "${NEO4J_EDITION}" == "enterprise" ]; + then + : ${NEO4J_causal__clustering_expected__core__cluster__size:=${NEO4J_causalClustering_expectedCoreClusterSize:-}} + : ${NEO4J_causal__clustering_initial__discovery__members:=${NEO4J_causalClustering_initialDiscoveryMembers:-}} + : ${NEO4J_causal__clustering_discovery__advertised__address:=${NEO4J_causalClustering_discoveryAdvertisedAddress:-"$(hostname):5000"}} + : ${NEO4J_causal__clustering_transaction__advertised__address:=${NEO4J_causalClustering_transactionAdvertisedAddress:-"$(hostname):6000"}} + : ${NEO4J_causal__clustering_raft__advertised__address:=${NEO4J_causalClustering_raftAdvertisedAddress:-"$(hostname):7000"}} + # Custom settings for dockerized neo4j + : ${NEO4J_ha_host_coordination:=$(hostname):5001} + : ${NEO4J_ha_host_data:=$(hostname):6001} + : ${NEO4J_causal__clustering_discovery__advertised__address:=$(hostname):5000} + : ${NEO4J_causal__clustering_transaction__advertised__address:=$(hostname):6000} + : ${NEO4J_causal__clustering_raft__advertised__address:=$(hostname):7000} +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 + +if [ -d /conf ]; then + if secure_mode_enabled; then + check_mounted_folder_readable "/conf" + fi + find /conf -type f -exec cp {} "${NEO4J_HOME}"/conf \; +fi + +if [ -d /ssl ]; then + if secure_mode_enabled; then + check_mounted_folder_readable "/ssl" + fi + : ${NEO4J_dbms_directories_certificates:="/ssl"} +fi + +if [ -d /plugins ]; then + if secure_mode_enabled; then + if [[ ! -z "${NEO4JLABS_PLUGINS:-}" ]]; then + # We need write permissions + check_mounted_folder_with_chown "/plugins" + fi + check_mounted_folder_readable "/plugins" + fi + : ${NEO4J_dbms_directories_plugins:="/plugins"} +fi + +if [ -d /import ]; then + if secure_mode_enabled; then + check_mounted_folder_readable "/import" + fi + : ${NEO4J_dbms_directories_import:="/import"} +fi + +if [ -d /metrics ]; then + if secure_mode_enabled; then + check_mounted_folder_readable "/metrics" + fi + : ${NEO4J_dbms_directories_metrics:="/metrics"} +fi + +if [ -d /logs ]; then + check_mounted_folder_with_chown "/logs" + : ${NEO4J_dbms_directories_logs:="/logs"} + if [ -d /data/databases ]; then + check_mounted_folder_with_chown "/data/databases" + fi + if [ -d /data/dbms ]; then + check_mounted_folder_with_chown "/data/dbms" + fi +fi + +if [ -d /data ]; then + check_mounted_folder_with_chown "/data" +fi + + +# set the neo4j initial password only if you run the database server +if [ "${cmd}" == "neo4j" ]; then + if [ "${NEO4J_AUTH:-}" == "none" ]; then + NEO4J_dbms_security_auth__enabled=false + elif [[ "${NEO4J_AUTH:-}" == neo4j/* ]]; then + password="${NEO4J_AUTH#neo4j/}" + if [ "${password}" == "neo4j" ]; then + echo >&2 "Invalid value for password. It cannot be 'neo4j', which is the default." + 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 + chown "${userid}":"${groupid}" /data/dbms + fi + # 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 set-initial-password "${password}" 2>/dev/null || true + elif [ -n "${NEO4J_AUTH:-}" ]; then + echo >&2 "Invalid value for NEO4J_AUTH: '${NEO4J_AUTH}'" + exit 1 + fi +fi + +declare -A COMMUNITY +declare -A ENTERPRISE + +COMMUNITY=( + [dbms.tx_log.rotation.retention_policy]="100M size" + [dbms.memory.pagecache.size]="512M" + [dbms.connectors.default_listen_address]="0.0.0.0" + [dbms.connector.https.listen_address]="0.0.0.0:7473" + [dbms.connector.http.listen_address]="0.0.0.0:7474" + [dbms.connector.bolt.listen_address]="0.0.0.0:7687" +) + +ENTERPRISE=( + [causal_clustering.transaction_listen_address]="0.0.0.0:6000" + [causal_clustering.raft_listen_address]="0.0.0.0:7000" + [causal_clustering.discovery_listen_address]="0.0.0.0:5000" +) + +for conf in ${!COMMUNITY[@]} ; do + + if ! grep -q "^$conf" "${NEO4J_HOME}"/conf/neo4j.conf + then + echo -e "\n"$conf=${COMMUNITY[$conf]} >> "${NEO4J_HOME}"/conf/neo4j.conf + fi +done + +for conf in ${!ENTERPRISE[@]} ; do + + if [ "${NEO4J_EDITION}" == "enterprise" ]; + then + if ! grep -q "^$conf" "${NEO4J_HOME}"/conf/neo4j.conf + then + echo -e "\n"$conf=${ENTERPRISE[$conf]} >> "${NEO4J_HOME}"/conf/neo4j.conf + fi + fi +done + +#The udc.source=tarball should be replaced by udc.source=docker in both dbms.jvm.additional and wrapper.java.additional +#Using sed to replace only this part will allow the custom configs to be added after, separated by a ,. +if grep -q "udc.source=tarball" "${NEO4J_HOME}"/conf/neo4j.conf; then + sed -i -e 's/udc.source=tarball/udc.source=docker/g' "${NEO4J_HOME}"/conf/neo4j.conf +fi +#The udc.source should always be set to docker by default and we have to allow also custom configs to be added after that. +#In this case, this piece of code helps to add the default value and a , to support custom configs after. +if ! grep -q "dbms.jvm.additional=-Dunsupported.dbms.udc.source=docker" "${NEO4J_HOME}"/conf/neo4j.conf; then + sed -i -e 's/dbms.jvm.additional=/dbms.jvm.additional=-Dunsupported.dbms.udc.source=docker,/g' "${NEO4J_HOME}"/conf/neo4j.conf +fi + +# list env variables with prefix NEO4J_ and create settings from them +unset NEO4J_AUTH NEO4J_SHA256 NEO4J_TARBALL +for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do + 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 + if grep -q -F "${setting}=" "${NEO4J_HOME}"/conf/neo4j.conf; then + # Remove any lines containing the setting already + sed --in-place "/^${setting}=.*/d" "${NEO4J_HOME}"/conf/neo4j.conf + fi + # Then always append setting to file + echo "${setting}=${value}" >> "${NEO4J_HOME}"/conf/neo4j.conf + else + echo >&2 "WARNING: ${setting} not written to conf file because settings that start with a number are not permitted" + fi + fi +done + +if [[ ! -z "${NEO4JLABS_PLUGINS:-}" ]]; then + # NEO4JLABS_PLUGINS should be a json array of plugins like '["graph-algorithms", "apoc", "streams", "graphql"]' + install_neo4j_labs_plugins +fi + +[ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT} + +if [ "${cmd}" == "dump-config" ]; then + if ! is_writable "/conf"; then + print_permissions_advice_and_fail "/conf" + fi + cp --recursive "${NEO4J_HOME}"/conf/* /conf + echo "Config Dumped" + exit 0 +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 + ${exec_cmd} neo4j console +else + ${exec_cmd} "$@" +fi diff --git a/docker-image-src/3.4/Dockerfile b/docker-image-src/3.4/Dockerfile new file mode 100644 index 00000000..ff6ccb18 --- /dev/null +++ b/docker-image-src/3.4/Dockerfile @@ -0,0 +1,51 @@ +FROM openjdk:8-jre-slim + +ENV NEO4J_SHA256=%%NEO4J_SHA%% \ + NEO4J_TARBALL=%%NEO4J_TARBALL%% \ + NEO4J_EDITION=%%NEO4J_EDITION%% \ + NEO4J_HOME="/var/lib/neo4j" \ + TINI_VERSION="v0.18.0" \ + TINI_SHA256="12d20136605531b09a2c2dac02ccee85e1b874eb322ef6baf7561cd93f93c855" +ARG NEO4J_URI=%%NEO4J_DIST_SITE%%/%%NEO4J_TARBALL%% + +RUN addgroup --system neo4j && adduser --system --no-create-home --home "${NEO4J_HOME}" --ingroup neo4j neo4j + +COPY ./local-package/* /tmp/ + +RUN apt update \ + && apt install -y curl wget gosu jq \ + && curl -L --fail --silent --show-error "https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini" > /sbin/tini \ + && echo "${TINI_SHA256} /sbin/tini" | sha256sum -c --strict --quiet \ + && chmod +x /sbin/tini \ + && 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 "${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}" \ + && ln -s /data "${NEO4J_HOME}"/data \ + && ln -s /logs "${NEO4J_HOME}"/logs \ + && mv /tmp/neo4jlabs-plugins.json /neo4jlabs-plugins.json \ + && rm -rf /tmp/* \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get -y purge --auto-remove curl + +ENV PATH "${NEO4J_HOME}"/bin:$PATH + +WORKDIR "${NEO4J_HOME}" + +VOLUME /data /logs + +COPY docker-entrypoint.sh /docker-entrypoint.sh + +EXPOSE 7474 7473 7687 + +ENTRYPOINT ["/sbin/tini", "-g", "--", "/docker-entrypoint.sh"] +CMD ["neo4j"] diff --git a/docker-image-src/3.4/docker-entrypoint.sh b/docker-image-src/3.4/docker-entrypoint.sh new file mode 100755 index 00000000..4a4603e5 --- /dev/null +++ b/docker-image-src/3.4/docker-entrypoint.sh @@ -0,0 +1,482 @@ +#!/bin/bash -eu + +cmd="$1" + +function running_as_root +{ + test "$(id -u)" = "0" +} + +function secure_mode_enabled +{ + test "${SECURE_FILE_PERMISSIONS:=no}" = "yes" +} + +function containsElement +{ + local e match="$1" + shift + for e; do [[ "$e" == "$match" ]] && return 0; done + return 1 +} + +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 print_permissions_advice_and_fail +{ + _directory=${1} + echo >&2 " +Folder ${_directory} is not accessible for user: ${userid} or group ${groupid} or groups ${groups[@]}, 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 +} + +function check_mounted_folder_readable +{ + local _directory=${1} + if ! is_readable "${_directory}"; then + print_permissions_advice_and_fail "${_directory}" + fi +} + +function check_mounted_folder_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} + if running_as_root; then + if ! is_writable "${mountFolder}" && ! secure_mode_enabled; 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}" + fi + else + if [[ ! -w "${mountFolder}" ]] && [[ "$(stat -c %U ${mountFolder})" != "neo4j" ]]; then + print_permissions_advice_and_fail "${mountFolder}" + fi + fi +} + +function load_plugin_from_github +{ + # 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" /neo4jlabs-plugins.json )" + # 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="$(wget -q --timeout 300 --tries 30 -O - "${_versions_json_url}")" + local _plugin_jar_url="$(echo "${_versions_json}" | jq --raw-output ".[] | select(.neo4j==\"${_neo4j_version}\") | .jar")" + if [[ -z "${_plugin_jar_url}" ]]; then + echo >&2 "Error: No jar URL found for version '${_neo4j_version}' in versions.json from '${_versions_json_url}'" + echo >&2 "${_versions_json}" + exit 1 + fi + 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 +} + +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, graph-ql + 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[]" /neo4jlabs-plugins.json); do + _property="$(jq --raw-output '.key' <<< "${_entry}")" + _value="$(jq --raw-output '.value' <<< "${_entry}")" + + # 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" + else + if grep -o "^[^#]*" "${_neo4j_conf}" | grep -q --fixed-strings "${_property}=" ; then + sed --in-place "s/${_property}=/&${_value},/" "${_neo4j_conf}" + else + echo "${_property}=${_value}" >> "${_neo4j_conf}" + fi + fi + done +} + +function install_neo4j_labs_plugins +{ + # 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)" + cp "${NEO4J_HOME}"/conf/neo4j.conf "${_old_config}" + for plugin_name in $(echo "${NEO4JLABS_PLUGINS}" | jq --raw-output '.[]'); do + load_plugin_from_github "${plugin_name}" + apply_plugin_default_configuration "${plugin_name}" "${_old_config}" + done + rm "${_old_config}" +} + +# 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 gosu neo4j:neo4j" +else + userid="$(id -u)" + groupid="$(id -g)" + groups=($(id -G)) + exec_cmd="exec" +fi +readonly userid +readonly groupid +readonly groups +readonly exec_cmd + + +# Need to chown the home directory - but a user might have mounted a +# volume here (notably a conf volume). So take care not to chown +# volumes (stuff not owned by neo4j) +if running_as_root; then + # Non-recursive chown for the base directory + chown "${userid}":"${groupid}" "${NEO4J_HOME}" + chmod 700 "${NEO4J_HOME}" + find "${NEO4J_HOME}" -mindepth 1 -maxdepth 1 -type d -exec chown -R ${userid}:${groupid} {} \; + find "${NEO4J_HOME}" -mindepth 1 -maxdepth 1 -type d -exec chmod -R 700 {} \; +fi + +# 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. + +(c) Neo4j Sweden AB. 2021. All Rights Reserved. +Use of this Software without a proper commercial license with Neo4j, +Inc. or its affiliates is prohibited. + +Email inquiries can be directed to: licensing@neo4j.com + +More information is also available at: https://neo4j.com/licensing/ + + +To accept the 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 + +# 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:-"100M size"}} +: ${NEO4J_wrapper_java_additional:=${NEO4J_UDC_SOURCE:-"-Dneo4j.ext.udc.source=docker"}} +: ${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:-}} +: ${NEO4J_ha_server__id:=${NEO4J_ha_serverId:-}} +: ${NEO4J_ha_initial__hosts:=${NEO4J_ha_initialHosts:-}} + +if [ "${NEO4J_EDITION}" == "enterprise" ]; + then + : ${NEO4J_causal__clustering_expected__core__cluster__size:=${NEO4J_causalClustering_expectedCoreClusterSize:-}} + : ${NEO4J_causal__clustering_initial__discovery__members:=${NEO4J_causalClustering_initialDiscoveryMembers:-}} + : ${NEO4J_causal__clustering_discovery__advertised__address:=${NEO4J_causalClustering_discoveryAdvertisedAddress:-"$(hostname):5000"}} + : ${NEO4J_causal__clustering_transaction__advertised__address:=${NEO4J_causalClustering_transactionAdvertisedAddress:-"$(hostname):6000"}} + : ${NEO4J_causal__clustering_raft__advertised__address:=${NEO4J_causalClustering_raftAdvertisedAddress:-"$(hostname):7000"}} + # Custom settings for dockerized neo4j + : ${NEO4J_ha_host_coordination:=$(hostname):5001} + : ${NEO4J_ha_host_data:=$(hostname):6001} + : ${NEO4J_causal__clustering_discovery__advertised__address:=$(hostname):5000} + : ${NEO4J_causal__clustering_transaction__advertised__address:=$(hostname):6000} + : ${NEO4J_causal__clustering_raft__advertised__address:=$(hostname):7000} +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 + +if [ -d /conf ]; then + if secure_mode_enabled; then + check_mounted_folder_readable "/conf" + fi + find /conf -type f -exec cp {} "${NEO4J_HOME}"/conf \; +fi + +if [ -d /ssl ]; then + if secure_mode_enabled; then + check_mounted_folder_readable "/ssl" + fi + : ${NEO4J_dbms_directories_certificates:="/ssl"} +fi + +if [ -d /plugins ]; then + if secure_mode_enabled; then + if [[ ! -z "${NEO4JLABS_PLUGINS:-}" ]]; then + # We need write permissions + check_mounted_folder_with_chown "/plugins" + fi + check_mounted_folder_readable "/plugins" + fi + : ${NEO4J_dbms_directories_plugins:="/plugins"} +fi + +if [ -d /import ]; then + if secure_mode_enabled; then + check_mounted_folder_readable "/import" + fi + : ${NEO4J_dbms_directories_import:="/import"} +fi + +if [ -d /metrics ]; then + if secure_mode_enabled; then + check_mounted_folder_readable "/metrics" + fi + : ${NEO4J_dbms_directories_metrics:="/metrics"} +fi + +if [ -d /logs ]; then + check_mounted_folder_with_chown "/logs" + : ${NEO4J_dbms_directories_logs:="/logs"} + if [ -d /data/databases ]; then + check_mounted_folder_with_chown "/data/databases" + fi + if [ -d /data/dbms ]; then + check_mounted_folder_with_chown "/data/dbms" + fi +fi + +if [ -d /data ]; then + check_mounted_folder_with_chown "/data" +fi + + +# set the neo4j initial password only if you run the database server +if [ "${cmd}" == "neo4j" ]; then + if [ "${NEO4J_AUTH:-}" == "none" ]; then + NEO4J_dbms_security_auth__enabled=false + elif [[ "${NEO4J_AUTH:-}" == neo4j/* ]]; then + password="${NEO4J_AUTH#neo4j/}" + if [ "${password}" == "neo4j" ]; then + echo >&2 "Invalid value for password. It cannot be 'neo4j', which is the default." + 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 + chown "${userid}":"${groupid}" /data/dbms + fi + # 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 set-initial-password "${password}" 2>/dev/null || true + elif [ -n "${NEO4J_AUTH:-}" ]; then + echo >&2 "Invalid value for NEO4J_AUTH: '${NEO4J_AUTH}'" + exit 1 + fi +fi + +declare -A COMMUNITY +declare -A ENTERPRISE + +COMMUNITY=( + [dbms.tx_log.rotation.retention_policy]="100M size" + [dbms.memory.pagecache.size]="512M" + [dbms.connectors.default_listen_address]="0.0.0.0" + [dbms.connector.https.listen_address]="0.0.0.0:7473" + [dbms.connector.http.listen_address]="0.0.0.0:7474" + [dbms.connector.bolt.listen_address]="0.0.0.0:7687" +) + +ENTERPRISE=( +) + +for conf in ${!COMMUNITY[@]} ; do + if ! grep -q "^$conf" "${NEO4J_HOME}"/conf/neo4j.conf + then + echo -e "\n"$conf=${COMMUNITY[$conf]} >> "${NEO4J_HOME}"/conf/neo4j.conf + fi +done + +for conf in ${!ENTERPRISE[@]} ; do + if [ "${NEO4J_EDITION}" == "enterprise" ]; + then + if ! grep -q "^$conf" "${NEO4J_HOME}"/conf/neo4j.conf + then + echo -e "\n"$conf=${ENTERPRISE[$conf]} >> "${NEO4J_HOME}"/conf/neo4j.conf + fi + fi +done + +#The udc.source=tarball should be replaced by udc.source=docker in both dbms.jvm.additional and wrapper.java.additional +#Using sed to replace only this part will allow the custom configs to be added after, separated by a ,. +if grep -q "udc.source=tarball" "${NEO4J_HOME}"/conf/neo4j.conf; then + sed -i -e 's/udc.source=tarball/udc.source=docker/g' "${NEO4J_HOME}"/conf/neo4j.conf +fi +#The udc.source should always be set to docker by default and we have to allow also custom configs to be added after that. +#In this case, this piece of code helps to add the default value and a , to support custom configs after. +if ! grep -q "dbms.jvm.additional=-Dunsupported.dbms.udc.source=docker" "${NEO4J_HOME}"/conf/neo4j.conf; then + sed -i -e 's/dbms.jvm.additional=/dbms.jvm.additional=-Dunsupported.dbms.udc.source=docker,/g' "${NEO4J_HOME}"/conf/neo4j.conf +fi + +# list env variables with prefix NEO4J_ and create settings from them +unset NEO4J_AUTH NEO4J_SHA256 NEO4J_TARBALL +for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do + 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 + if grep -q -F "${setting}=" "${NEO4J_HOME}"/conf/neo4j.conf; then + # Remove any lines containing the setting already + sed --in-place "/^${setting}=.*/d" "${NEO4J_HOME}"/conf/neo4j.conf + fi + # Then always append setting to file + echo "${setting}=${value}" >> "${NEO4J_HOME}"/conf/neo4j.conf + else + echo >&2 "WARNING: ${setting} not written to conf file because settings that start with a number are not permitted" + fi + fi +done + +if [[ ! -z "${NEO4JLABS_PLUGINS:-}" ]]; then + # NEO4JLABS_PLUGINS should be a json array of plugins like '["graph-algorithms", "apoc", "streams", "graphql"]' + install_neo4j_labs_plugins +fi + +[ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT} + +if [ "${cmd}" == "dump-config" ]; then + if ! is_writable "/conf"; then + print_permissions_advice_and_fail "/conf" + fi + cp --recursive "${NEO4J_HOME}"/conf/* /conf + echo "Config Dumped" + exit 0 +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 + ${exec_cmd} neo4j console +else + ${exec_cmd} "$@" +fi diff --git a/docker-image-src/3.5/Dockerfile b/docker-image-src/3.5/Dockerfile new file mode 100644 index 00000000..51c0a635 --- /dev/null +++ b/docker-image-src/3.5/Dockerfile @@ -0,0 +1,51 @@ +FROM openjdk:8-jre-slim + +ENV NEO4J_SHA256=%%NEO4J_SHA%% \ + NEO4J_TARBALL=%%NEO4J_TARBALL%% \ + NEO4J_EDITION=%%NEO4J_EDITION%% \ + NEO4J_HOME="/var/lib/neo4j" \ + TINI_VERSION="v0.18.0" \ + TINI_SHA256="12d20136605531b09a2c2dac02ccee85e1b874eb322ef6baf7561cd93f93c855" +ARG NEO4J_URI=%%NEO4J_DIST_SITE%%/%%NEO4J_TARBALL%% + +RUN addgroup --system neo4j && adduser --system --no-create-home --home "${NEO4J_HOME}" --ingroup neo4j neo4j + +COPY ./local-package/* /tmp/ + +RUN apt update \ + && apt install -y curl wget gosu jq \ + && curl -L --fail --silent --show-error "https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini" > /sbin/tini \ + && echo "${TINI_SHA256} /sbin/tini" | sha256sum -c --strict --quiet \ + && chmod +x /sbin/tini \ + && 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 "${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}" \ + && ln -s /data "${NEO4J_HOME}"/data \ + && ln -s /logs "${NEO4J_HOME}"/logs \ + && mv /tmp/neo4jlabs-plugins.json /neo4jlabs-plugins.json \ + && rm -rf /tmp/* \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get -y purge --auto-remove curl + +ENV PATH "${NEO4J_HOME}"/bin:$PATH + +WORKDIR "${NEO4J_HOME}" + +VOLUME /data /logs + +COPY docker-entrypoint.sh /docker-entrypoint.sh + +EXPOSE 7474 7473 7687 + +ENTRYPOINT ["/sbin/tini", "-g", "--", "/docker-entrypoint.sh"] +CMD ["neo4j"] diff --git a/docker-image-src/3.5/docker-entrypoint.sh b/docker-image-src/3.5/docker-entrypoint.sh new file mode 100755 index 00000000..ffeb76b5 --- /dev/null +++ b/docker-image-src/3.5/docker-entrypoint.sh @@ -0,0 +1,531 @@ +#!/bin/bash -eu + +cmd="$1" + +function running_as_root +{ + test "$(id -u)" = "0" +} + +function secure_mode_enabled +{ + test "${SECURE_FILE_PERMISSIONS:=no}" = "yes" +} + +function containsElement +{ + local e match="$1" + shift + for e; do [[ "$e" == "$match" ]] && return 0; done + return 1 +} + +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 print_permissions_advice_and_fail +{ + _directory=${1} + echo >&2 " +Folder ${_directory} is not accessible for user: ${userid} or group ${groupid} or groups ${groups[@]}, 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 +} + +function check_mounted_folder_readable +{ + local _directory=${1} + if ! is_readable "${_directory}"; then + print_permissions_advice_and_fail "${_directory}" + 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} + if running_as_root; then + if ! 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 [ $(gosu "${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 + fi + else + if [[ ! -w "${mountFolder}" ]] && [[ "$(stat -c %U ${mountFolder})" != "neo4j" ]]; then + print_permissions_advice_and_fail "${mountFolder}" + fi + fi +} + +function load_plugin_from_github +{ + # 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" /neo4jlabs-plugins.json )" + # 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="$(wget -q --timeout 300 --tries 30 -O - "${_versions_json_url}")" + local _plugin_jar_url="$(echo "${_versions_json}" | jq --raw-output ".[] | select(.neo4j==\"${_neo4j_version}\") | .jar")" + if [[ -z "${_plugin_jar_url}" ]]; then + echo >&2 "Error: No jar URL found for version '${_neo4j_version}' in versions.json from '${_versions_json_url}'" + echo >&2 "${_versions_json}" + exit 1 + fi + 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 +} + +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, graph-ql + 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[]" /neo4jlabs-plugins.json); do + _property="$(jq --raw-output '.key' <<< "${_entry}")" + _value="$(jq --raw-output '.value' <<< "${_entry}")" + + # 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" + else + if grep -o "^[^#]*" "${_neo4j_conf}" | grep -q --fixed-strings "${_property}=" ; then + sed --in-place "s/${_property}=/&${_value},/" "${_neo4j_conf}" + else + echo "${_property}=${_value}" >> "${_neo4j_conf}" + fi + fi + done +} + +function install_neo4j_labs_plugins +{ + # 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)" + cp "${NEO4J_HOME}"/conf/neo4j.conf "${_old_config}" + for plugin_name in $(echo "${NEO4JLABS_PLUGINS}" | jq --raw-output '.[]'); do + load_plugin_from_github "${plugin_name}" + 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}" + local _neo4j_home="${3}" + + if ! grep -q "^${_setting}=" "${_neo4j_home}"/conf/neo4j.conf + then + 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 _neo4j_home=${3} + + if grep -q -F "${_setting}=" "${_neo4j_home}"/conf/neo4j.conf; then + # Remove any lines containing the setting already + sed --in-place "/^${_setting}=.*/d" "${_neo4j_home}"/conf/neo4j.conf + fi + # Then always append setting to file + 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 + add_env_setting_to_conf "dbms.security.auth_enabled" "false" "${NEO4J_HOME}" + # NEO4J_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 + chown "${userid}":"${groupid}" /data/dbms + fi + + # 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. + if [ "${do_reset}" == "true" ]; then + neo4j-admin set-initial-password "${password}" --require-password-change 2>/dev/null || true + else + neo4j-admin set-initial-password "${password}" 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 +} + +# 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 gosu neo4j:neo4j" +else + userid="$(id -u)" + groupid="$(id -g)" + groups=($(id -G)) + exec_cmd="exec" +fi +readonly userid +readonly groupid +readonly groups +readonly exec_cmd + + +# Need to chown the home directory - but a user might have mounted a +# volume here (notably a conf volume). So take care not to chown +# volumes (stuff not owned by neo4j) +if running_as_root; then + # Non-recursive chown for the base directory + chown "${userid}":"${groupid}" "${NEO4J_HOME}" + chmod 700 "${NEO4J_HOME}" + find "${NEO4J_HOME}" -mindepth 1 -maxdepth 1 -type d -exec chown -R ${userid}:${groupid} {} \; + find "${NEO4J_HOME}" -mindepth 1 -maxdepth 1 -type d -exec chmod -R 700 {} \; +fi + +# Only prompt for license agreement if command contains "neo4j" in it +# ==== CHECK LICENSE AGREEMENT ==== + +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. + +(c) Neo4j Sweden AB. 2021. All Rights Reserved. +Use of this Software without a proper commercial license with Neo4j, +Inc. or its affiliates is prohibited. + +Email inquiries can be directed to: licensing@neo4j.com + +More information is also available at: https://neo4j.com/licensing/ + + +To accept the 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 + +# Env variable naming convention: +# ==== RENAME LEGACY ENVIRONMENT CONF VARIABLES ==== + +# - 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_wrapper_java_additional:=${NEO4J_UDC_SOURCE:-"-Dneo4j.ext.udc.source=docker"}} +: ${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:-}} + : ${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 + if secure_mode_enabled; then + check_mounted_folder_readable "/conf" + fi + find /conf -type f -exec cp {} "${NEO4J_HOME}"/conf \; +fi + +if [ -d /ssl ]; then + if secure_mode_enabled; then + check_mounted_folder_readable "/ssl" + fi + : ${NEO4J_dbms_directories_certificates:="/ssl"} +fi + +if [ -d /plugins ]; then + if secure_mode_enabled; then + if [[ ! -z "${NEO4JLABS_PLUGINS:-}" ]]; then + # We need write permissions + check_mounted_folder_writable_with_chown "/plugins" + fi + check_mounted_folder_readable "/plugins" + fi + : ${NEO4J_dbms_directories_plugins:="/plugins"} +fi + +if [ -d /import ]; then + if secure_mode_enabled; then + check_mounted_folder_readable "/import" + fi + : ${NEO4J_dbms_directories_import:="/import"} +fi + +if [ -d /metrics ]; then + if secure_mode_enabled; then + check_mounted_folder_readable "/metrics" + fi + : ${NEO4J_dbms_directories_metrics:="/metrics"} +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 +fi + +# ==== SET CONFIGURATIONS ==== + +## == DOCKER SPECIFIC DEFAULT CONFIGURATIONS === +## these should not override *any* configurations set by the user + +add_docker_default_to_conf "dbms.tx_log.rotation.retention_policy" "100M size" "${NEO4J_HOME}" +add_docker_default_to_conf "dbms.memory.pagecache.size" "512M" "${NEO4J_HOME}" +add_docker_default_to_conf "dbms.connectors.default_listen_address" "0.0.0.0" "${NEO4J_HOME}" +add_docker_default_to_conf "dbms.connector.https.listen_address" "0.0.0.0:7473" "${NEO4J_HOME}" +add_docker_default_to_conf "dbms.connector.http.listen_address" "0.0.0.0:7474" "${NEO4J_HOME}" +add_docker_default_to_conf "dbms.connector.bolt.listen_address" "0.0.0.0:7687" "${NEO4J_HOME}" +# set enterprise only docker defaults +if [ "${NEO4J_EDITION}" == "enterprise" ]; +then + add_docker_default_to_conf "causal_clustering.discovery_advertised_address" "$(hostname):5000" "${NEO4J_HOME}" + add_docker_default_to_conf "causal_clustering.transaction_advertised_address" "$(hostname):6000" "${NEO4J_HOME}" + add_docker_default_to_conf "causal_clustering.raft_advertised_address" "$(hostname):7000" "${NEO4J_HOME}" +fi + +## == ENVIRONMENT VARIABLE CONFIGURATIONS === +## these override BOTH defaults and any existing values in the neo4j.conf file + +#The udc.source=tarball should be replaced by udc.source=docker in both dbms.jvm.additional and wrapper.java.additional +#Using sed to replace only this part will allow the custom configs to be added after, separated by a ,. +if grep -q "udc.source=tarball" "${NEO4J_HOME}"/conf/neo4j.conf; then + sed -i -e 's/udc.source=tarball/udc.source=docker/g' "${NEO4J_HOME}"/conf/neo4j.conf +fi +#The udc.source should always be set to docker by default and we have to allow also custom configs to be added after that. +#In this case, this piece of code helps to add the default value and a , to support custom configs after. +if ! grep -q "dbms.jvm.additional=-Dunsupported.dbms.udc.source=docker" "${NEO4J_HOME}"/conf/neo4j.conf; then + sed -i -e 's/dbms.jvm.additional=/dbms.jvm.additional=-Dunsupported.dbms.udc.source=docker,/g' "${NEO4J_HOME}"/conf/neo4j.conf +fi + +# save NEO4J_HOME and NEO4J_AUTH to temp variables that don't begin with NEO4J_ so they don't get added to the conf +temp_neo4j_home="${NEO4J_HOME}" +temp_neo4j_auth="${NEO4J_AUTH:-}" +# list env variables with prefix NEO4J_ and create settings from them +unset NEO4J_AUTH NEO4J_SHA256 NEO4J_TARBALL NEO4J_EDITION NEO4J_ACCEPT_LICENSE_AGREEMENT NEO4J_HOME +for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do + 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}" "${temp_neo4j_home}" + else + echo >&2 "WARNING: ${setting} not written to conf file because settings that start with a number are not permitted" + fi + fi +done +export NEO4J_HOME="${temp_neo4j_home}" +unset temp_neo4j_home + +# ==== SET PASSWORD AND PLUGINS ==== + +set_initial_password "${temp_neo4j_auth}" + + +if [[ ! -z "${NEO4JLABS_PLUGINS:-}" ]]; then + # NEO4JLABS_PLUGINS should be a json array of plugins like '["graph-algorithms", "apoc", "streams", "graphql"]' + install_neo4j_labs_plugins +fi + +# ==== INVOKE NEO4J STARTUP ==== + +[ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT} + +if [ "${cmd}" == "dump-config" ]; then + if ! is_writable "/conf"; then + print_permissions_advice_and_fail "/conf" + fi + cp --recursive "${NEO4J_HOME}"/conf/* /conf + echo "Config Dumped" + exit 0 +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 + ${exec_cmd} neo4j console +else + ${exec_cmd} "$@" +fi diff --git a/docker-image-src/3.5/neo4j-admin/Dockerfile b/docker-image-src/3.5/neo4j-admin/Dockerfile new file mode 100644 index 00000000..142122fb --- /dev/null +++ b/docker-image-src/3.5/neo4j-admin/Dockerfile @@ -0,0 +1,42 @@ +FROM openjdk:8-jre-slim + +ENV NEO4J_SHA256=%%NEO4J_SHA%% \ + NEO4J_TARBALL=%%NEO4J_TARBALL%% \ + NEO4J_EDITION=%%NEO4J_EDITION%% \ + NEO4J_HOME="/var/lib/neo4j" +ARG NEO4J_URI=%%NEO4J_DIST_SITE%%/%%NEO4J_TARBALL%% + +RUN addgroup --gid 7474 --system neo4j && adduser --uid 7474 --system --no-create-home --home "${NEO4J_HOME}" --ingroup neo4j neo4j + +COPY ./local-package/* /tmp/ + +RUN apt update \ + && apt install -y curl gosu \ + && 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 "${NEO4J_HOME}"/data /data \ + && chown -R neo4j:neo4j /data \ + && chmod -R 777 /data \ + && mkdir -p /backup \ + && chown -R neo4j:neo4j /backup \ + && chmod -R 777 /backup \ + && chown -R neo4j:neo4j "${NEO4J_HOME}" \ + && chmod -R 777 "${NEO4J_HOME}" \ + && ln -s /data "${NEO4J_HOME}"/data \ + && rm -rf /tmp/* \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get -y purge --auto-remove curl + + +ENV PATH "${NEO4J_HOME}"/bin:$PATH +VOLUME /data /backup +WORKDIR "${NEO4J_HOME}" + +COPY docker-entrypoint.sh /docker-entrypoint.sh + +ENTRYPOINT ["/docker-entrypoint.sh"] + +CMD ["neo4j-admin"] diff --git a/docker-image-src/3.5/neo4j-admin/docker-entrypoint.sh b/docker-image-src/3.5/neo4j-admin/docker-entrypoint.sh new file mode 100755 index 00000000..55747ea9 --- /dev/null +++ b/docker-image-src/3.5/neo4j-admin/docker-entrypoint.sh @@ -0,0 +1,124 @@ +#!/bin/bash -eu + +function running_as_root +{ + test "$(id -u)" = "0" +} + +function is_writable +{ + gosu "${userid}":"${groupid}" test -w "${1}" +} + +function print_permissions_advice_and_fail +{ + local _directory=${1} + echo >&2 " +Folder ${_directory} is not accessible for user: ${userid} or group ${groupid} or groups ${groups[@]}, 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 +} + + +function check_mounted_folder_writable_with_chown +{ + local mountFolder=${1} + if running_as_root; 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 [ $(gosu "${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}" + fi + fi +} + +# ==== SETUP WHICH USER TO RUN AS ==== + +if running_as_root; then + userid="neo4j" + groupid="neo4j" + groups=($(id -G neo4j)) + exec_cmd="exec gosu neo4j:neo4j" +else + userid="$(id -u)" + groupid="$(id -g)" + groups=($(id -G)) + exec_cmd="exec" +fi + + +# ==== CHECK LICENSE AGREEMENT ==== + +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. + +(c) Neo4j Sweden AB. 2021. All Rights Reserved. +Use of this Software without a proper commercial license with Neo4j, +Inc. or its affiliates is prohibited. + +Email inquiries can be directed to: licensing@neo4j.com + +More information is also available at: https://neo4j.com/licensing/ + + +To accept the 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 + +# ==== ENSURE MOUNT FOLDER READ/WRITABILITY ==== + +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 +fi +if [ -d /backup ]; then + check_mounted_folder_writable_with_chown "/backup" +fi + +# ==== MAKE SURE NEO4J CANNOT BE RUN FROM THIS CONTAINER ==== + +rm ${NEO4J_HOME}/bin/neo4j + +if [[ "${1}" == "neo4j" ]]; then + correct_image="neo4j:"$(neo4j-admin --version)"-${NEO4J_EDITION}" + echo >&2 " +This is a neo4j-admin only image, and usage of Neo4j server is not supported from here. +If you wish to start a Neo4j database, use: + +docker run ${correct_image} + " + exit 1 +fi + +# ==== START NEO4J-ADMIN COMMAND ==== + +${exec_cmd} "${@}" diff --git a/docker-image-src/4.0/Dockerfile b/docker-image-src/4.0/Dockerfile new file mode 100644 index 00000000..f4b3aa85 --- /dev/null +++ b/docker-image-src/4.0/Dockerfile @@ -0,0 +1,51 @@ +FROM %%NEO4J_BASE_IMAGE%% + +ENV NEO4J_SHA256=%%NEO4J_SHA%% \ + NEO4J_TARBALL=%%NEO4J_TARBALL%% \ + NEO4J_EDITION=%%NEO4J_EDITION%% \ + NEO4J_HOME="/var/lib/neo4j" +ARG NEO4J_URI=%%NEO4J_DIST_SITE%%/%%NEO4J_TARBALL%% +ARG TINI_SHA256="12d20136605531b09a2c2dac02ccee85e1b874eb322ef6baf7561cd93f93c855" +ARG TINI_URI="https://github.com/krallin/tini/releases/download/v0.18.0/tini" + +RUN addgroup --gid 7474 --system neo4j && adduser --uid 7474 --system --no-create-home --home "${NEO4J_HOME}" --ingroup neo4j neo4j + +COPY ./local-package/* /tmp/ + +RUN apt update \ + && apt install -y curl wget gosu jq \ + && curl -L --fail --silent --show-error ${TINI_URI} > /sbin/tini \ + && echo "${TINI_SHA256} /sbin/tini" | sha256sum -c --strict --quiet \ + && chmod +x /sbin/tini \ + && 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 "${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}" \ + && ln -s /data "${NEO4J_HOME}"/data \ + && ln -s /logs "${NEO4J_HOME}"/logs \ + && mv /tmp/neo4jlabs-plugins.json /neo4jlabs-plugins.json \ + && rm -rf /tmp/* \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get -y purge --auto-remove curl + +ENV PATH "${NEO4J_HOME}"/bin:$PATH + +WORKDIR "${NEO4J_HOME}" + +VOLUME /data /logs + +COPY docker-entrypoint.sh /docker-entrypoint.sh + +EXPOSE 7474 7473 7687 + +ENTRYPOINT ["/sbin/tini", "-g", "--", "/docker-entrypoint.sh"] +CMD ["neo4j"] diff --git a/docker-image-src/4.0/docker-entrypoint.sh b/docker-image-src/4.0/docker-entrypoint.sh new file mode 100755 index 00000000..5eea5343 --- /dev/null +++ b/docker-image-src/4.0/docker-entrypoint.sh @@ -0,0 +1,506 @@ +#!/bin/bash -eu + +cmd="$1" + +function running_as_root +{ + test "$(id -u)" = "0" +} + +function secure_mode_enabled +{ + test "${SECURE_FILE_PERMISSIONS:=no}" = "yes" +} + +function containsElement +{ + local e match="$1" + shift + for e; do [[ "$e" == "$match" ]] && return 0; done + return 1 +} + +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 print_permissions_advice_and_fail +{ + _directory=${1} + echo >&2 " +Folder ${_directory} is not accessible for user: ${userid} or group ${groupid} or groups ${groups[@]}, 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 +} + +function check_mounted_folder_readable +{ + local _directory=${1} + if ! is_readable "${_directory}"; then + print_permissions_advice_and_fail "${_directory}" + 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} + if running_as_root; then + if ! 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 [ $(gosu "${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 + fi + else + if [[ ! -w "${mountFolder}" ]] && [[ "$(stat -c %U ${mountFolder})" != "neo4j" ]]; then + print_permissions_advice_and_fail "${mountFolder}" + fi + fi +} + +function load_plugin_from_github +{ + # 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" /neo4jlabs-plugins.json )" + # 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="$(wget -q --timeout 300 --tries 30 -O - "${_versions_json_url}")" + local _plugin_jar_url="$(echo "${_versions_json}" | jq --raw-output ".[] | select(.neo4j==\"${_neo4j_version}\") | .jar")" + if [[ -z "${_plugin_jar_url}" ]]; then + echo >&2 "Error: No jar URL found for version '${_neo4j_version}' in versions.json from '${_versions_json_url}'" + echo >&2 "${_versions_json}" + exit 1 + fi + 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 +} + +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, graph-ql + 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[]" /neo4jlabs-plugins.json); do + _property="$(jq --raw-output '.key' <<< "${_entry}")" + _value="$(jq --raw-output '.value' <<< "${_entry}")" + + # 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" + else + if grep -o "^[^#]*" "${_neo4j_conf}" | grep -q --fixed-strings "${_property}=" ; then + sed --in-place "s/${_property}=/&${_value},/" "${_neo4j_conf}" + else + echo "${_property}=${_value}" >> "${_neo4j_conf}" + fi + fi + done +} + +function install_neo4j_labs_plugins +{ + # 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)" + cp "${NEO4J_HOME}"/conf/neo4j.conf "${_old_config}" + for plugin_name in $(echo "${NEO4JLABS_PLUGINS}" | jq --raw-output '.[]'); do + load_plugin_from_github "${plugin_name}" + 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}" + local _neo4j_home="${3}" + + if ! grep -q "^${_setting}=" "${_neo4j_home}"/conf/neo4j.conf + then + 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 _neo4j_home=${3} + + if grep -q -F "${_setting}=" "${_neo4j_home}"/conf/neo4j.conf; then + # Remove any lines containing the setting already + sed --in-place "/^${_setting}=.*/d" "${_neo4j_home}"/conf/neo4j.conf + fi + # Then always append setting to file + 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 + add_env_setting_to_conf "dbms.security.auth_enabled" "false" "${NEO4J_HOME}" + # NEO4J_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 + chown "${userid}":"${groupid}" /data/dbms + fi + + # 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. + if [ "${do_reset}" == "true" ]; then + neo4j-admin set-initial-password "${password}" --require-password-change 2>/dev/null || true + else + neo4j-admin set-initial-password "${password}" 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 +} + +# 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 gosu neo4j:neo4j" +else + userid="$(id -u)" + groupid="$(id -g)" + groups=($(id -G)) + exec_cmd="exec" +fi +readonly userid +readonly groupid +readonly groups +readonly exec_cmd + + +# Need to chown the home directory +if running_as_root; then + chown -R "${userid}":"${groupid}" "${NEO4J_HOME}" + chmod 700 "${NEO4J_HOME}" + find "${NEO4J_HOME}" -mindepth 1 -maxdepth 1 -type d -exec chmod -R 700 {} \; +fi + +# Only prompt for license agreement if command contains "neo4j" in it +# ==== CHECK LICENSE AGREEMENT ==== + +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. + +(c) Neo4j Sweden AB. 2021. All Rights Reserved. +Use of this Software without a proper commercial license with Neo4j, +Inc. or its affiliates is prohibited. + +Email inquiries can be directed to: licensing@neo4j.com + +More information is also available at: https://neo4j.com/licensing/ + + +To accept the 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 + +# Env variable naming convention: +# ==== RENAME LEGACY ENVIRONMENT CONF VARIABLES ==== + +# - 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:-}} + : ${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" + find /conf -type f -exec cp {} "${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 "${NEO4JLABS_PLUGINS:-}" ]]; then + # We need 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 + check_mounted_folder_writable_with_chown "/metrics" + : ${NEO4J_dbms_directories_metrics:="/metrics"} +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 + +# ==== SET CONFIGURATIONS ==== + +## == DOCKER SPECIFIC DEFAULT CONFIGURATIONS === +## these should not override *any* configurations set by the user + +add_docker_default_to_conf "dbms.tx_log.rotation.retention_policy" "100M size" "${NEO4J_HOME}" +add_docker_default_to_conf "dbms.memory.pagecache.size" "512M" "${NEO4J_HOME}" +add_docker_default_to_conf "dbms.default_listen_address" "0.0.0.0" "${NEO4J_HOME}" +# set enterprise only docker defaults +if [ "${NEO4J_EDITION}" == "enterprise" ]; +then + add_docker_default_to_conf "causal_clustering.discovery_advertised_address" "$(hostname):5000" "${NEO4J_HOME}" + add_docker_default_to_conf "causal_clustering.transaction_advertised_address" "$(hostname):6000" "${NEO4J_HOME}" + add_docker_default_to_conf "causal_clustering.raft_advertised_address" "$(hostname):7000" "${NEO4J_HOME}" +fi + +## == ENVIRONMENT VARIABLE CONFIGURATIONS === +## these override BOTH defaults and any existing values in the neo4j.conf file + +# save NEO4J_HOME and NEO4J_AUTH to temp variables that don't begin with NEO4J_ so they don't get added to the conf +temp_neo4j_home="${NEO4J_HOME}" +temp_neo4j_auth="${NEO4J_AUTH:-}" +# list env variables with prefix NEO4J_ and create settings from them +unset NEO4J_AUTH NEO4J_SHA256 NEO4J_TARBALL NEO4J_EDITION NEO4J_ACCEPT_LICENSE_AGREEMENT NEO4J_HOME +for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do + 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}" "${temp_neo4j_home}" + else + echo >&2 "WARNING: ${setting} not written to conf file because settings that start with a number are not permitted" + fi + fi +done +export NEO4J_HOME="${temp_neo4j_home}" +unset temp_neo4j_home + +# ==== SET PASSWORD AND PLUGINS ==== + +set_initial_password "${temp_neo4j_auth}" + + +if [[ ! -z "${NEO4JLABS_PLUGINS:-}" ]]; then + # NEO4JLABS_PLUGINS should be a json array of plugins like '["graph-algorithms", "apoc", "streams", "graphql"]' + install_neo4j_labs_plugins +fi + +# ==== INVOKE NEO4J STARTUP ==== + +[ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT} + +if [ "${cmd}" == "dump-config" ]; then + if ! is_writable "/conf"; then + print_permissions_advice_and_fail "/conf" + fi + cp --recursive "${NEO4J_HOME}"/conf/* /conf + echo "Config Dumped" + exit 0 +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 + ${exec_cmd} neo4j console +else + ${exec_cmd} "$@" +fi diff --git a/docker-image-src/4.0/neo4j-admin/Dockerfile b/docker-image-src/4.0/neo4j-admin/Dockerfile new file mode 100644 index 00000000..cc6b6404 --- /dev/null +++ b/docker-image-src/4.0/neo4j-admin/Dockerfile @@ -0,0 +1,42 @@ +FROM %%NEO4J_BASE_IMAGE%% + +ENV NEO4J_SHA256=%%NEO4J_SHA%% \ + NEO4J_TARBALL=%%NEO4J_TARBALL%% \ + NEO4J_EDITION=%%NEO4J_EDITION%% \ + NEO4J_HOME="/var/lib/neo4j" +ARG NEO4J_URI=%%NEO4J_DIST_SITE%%/%%NEO4J_TARBALL%% + +RUN addgroup --gid 7474 --system neo4j && adduser --uid 7474 --system --no-create-home --home "${NEO4J_HOME}" --ingroup neo4j neo4j + +COPY ./local-package/* /tmp/ + +RUN apt update \ + && apt install -y curl gosu \ + && 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 "${NEO4J_HOME}"/data /data \ + && chown -R neo4j:neo4j /data \ + && chmod -R 777 /data \ + && mkdir -p /backup \ + && chown -R neo4j:neo4j /backup \ + && chmod -R 777 /backup \ + && chown -R neo4j:neo4j "${NEO4J_HOME}" \ + && chmod -R 777 "${NEO4J_HOME}" \ + && ln -s /data "${NEO4J_HOME}"/data \ + && rm -rf /tmp/* \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get -y purge --auto-remove curl + + +ENV PATH "${NEO4J_HOME}"/bin:$PATH +VOLUME /data /backup +WORKDIR "${NEO4J_HOME}" + +COPY docker-entrypoint.sh /docker-entrypoint.sh + +ENTRYPOINT ["/docker-entrypoint.sh"] + +CMD ["neo4j-admin"] diff --git a/docker-image-src/4.0/neo4j-admin/docker-entrypoint.sh b/docker-image-src/4.0/neo4j-admin/docker-entrypoint.sh new file mode 100755 index 00000000..cd1a5c7e --- /dev/null +++ b/docker-image-src/4.0/neo4j-admin/docker-entrypoint.sh @@ -0,0 +1,127 @@ +#!/bin/bash -eu + +function running_as_root +{ + test "$(id -u)" = "0" +} + +function is_writable +{ + gosu "${userid}":"${groupid}" test -w "${1}" +} + +function print_permissions_advice_and_fail +{ + local _directory=${1} + echo >&2 " +Folder ${_directory} is not accessible for user: ${userid} or group ${groupid} or groups ${groups[@]}, 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 +} + + +function check_mounted_folder_writable_with_chown +{ + local mountFolder=${1} + if running_as_root; 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 [ $(gosu "${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}" + fi + fi +} + +# ==== SETUP WHICH USER TO RUN AS ==== + +if running_as_root; then + userid="neo4j" + groupid="neo4j" + groups=($(id -G neo4j)) + exec_cmd="exec gosu neo4j:neo4j" +else + userid="$(id -u)" + groupid="$(id -g)" + groups=($(id -G)) + exec_cmd="exec" +fi + + +# ==== CHECK LICENSE AGREEMENT ==== + +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. + +(c) Neo4j Sweden AB. 2021. All Rights Reserved. +Use of this Software without a proper commercial license with Neo4j, +Inc. or its affiliates is prohibited. + +Email inquiries can be directed to: licensing@neo4j.com + +More information is also available at: https://neo4j.com/licensing/ + + +To accept the 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 + +# ==== ENSURE MOUNT FOLDER READ/WRITABILITY ==== + +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 /backup ]; then + check_mounted_folder_writable_with_chown "/backup" +fi + +# ==== MAKE SURE NEO4J CANNOT BE RUN FROM THIS CONTAINER ==== + +rm ${NEO4J_HOME}/bin/neo4j + +if [[ "${1}" == "neo4j" ]]; then + correct_image="neo4j:"$(neo4j-admin --version)"-${NEO4J_EDITION}" + echo >&2 " +This is a neo4j-admin only image, and usage of Neo4j server is not supported from here. +If you wish to start a Neo4j database, use: + +docker run ${correct_image} + " + exit 1 +fi + +# ==== START NEO4J-ADMIN COMMAND ==== + +${exec_cmd} "${@}" diff --git a/docker-image-src/4.1/Dockerfile b/docker-image-src/4.1/Dockerfile new file mode 100644 index 00000000..f4b3aa85 --- /dev/null +++ b/docker-image-src/4.1/Dockerfile @@ -0,0 +1,51 @@ +FROM %%NEO4J_BASE_IMAGE%% + +ENV NEO4J_SHA256=%%NEO4J_SHA%% \ + NEO4J_TARBALL=%%NEO4J_TARBALL%% \ + NEO4J_EDITION=%%NEO4J_EDITION%% \ + NEO4J_HOME="/var/lib/neo4j" +ARG NEO4J_URI=%%NEO4J_DIST_SITE%%/%%NEO4J_TARBALL%% +ARG TINI_SHA256="12d20136605531b09a2c2dac02ccee85e1b874eb322ef6baf7561cd93f93c855" +ARG TINI_URI="https://github.com/krallin/tini/releases/download/v0.18.0/tini" + +RUN addgroup --gid 7474 --system neo4j && adduser --uid 7474 --system --no-create-home --home "${NEO4J_HOME}" --ingroup neo4j neo4j + +COPY ./local-package/* /tmp/ + +RUN apt update \ + && apt install -y curl wget gosu jq \ + && curl -L --fail --silent --show-error ${TINI_URI} > /sbin/tini \ + && echo "${TINI_SHA256} /sbin/tini" | sha256sum -c --strict --quiet \ + && chmod +x /sbin/tini \ + && 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 "${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}" \ + && ln -s /data "${NEO4J_HOME}"/data \ + && ln -s /logs "${NEO4J_HOME}"/logs \ + && mv /tmp/neo4jlabs-plugins.json /neo4jlabs-plugins.json \ + && rm -rf /tmp/* \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get -y purge --auto-remove curl + +ENV PATH "${NEO4J_HOME}"/bin:$PATH + +WORKDIR "${NEO4J_HOME}" + +VOLUME /data /logs + +COPY docker-entrypoint.sh /docker-entrypoint.sh + +EXPOSE 7474 7473 7687 + +ENTRYPOINT ["/sbin/tini", "-g", "--", "/docker-entrypoint.sh"] +CMD ["neo4j"] diff --git a/docker-image-src/4.1/docker-entrypoint.sh b/docker-image-src/4.1/docker-entrypoint.sh new file mode 100755 index 00000000..5eea5343 --- /dev/null +++ b/docker-image-src/4.1/docker-entrypoint.sh @@ -0,0 +1,506 @@ +#!/bin/bash -eu + +cmd="$1" + +function running_as_root +{ + test "$(id -u)" = "0" +} + +function secure_mode_enabled +{ + test "${SECURE_FILE_PERMISSIONS:=no}" = "yes" +} + +function containsElement +{ + local e match="$1" + shift + for e; do [[ "$e" == "$match" ]] && return 0; done + return 1 +} + +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 print_permissions_advice_and_fail +{ + _directory=${1} + echo >&2 " +Folder ${_directory} is not accessible for user: ${userid} or group ${groupid} or groups ${groups[@]}, 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 +} + +function check_mounted_folder_readable +{ + local _directory=${1} + if ! is_readable "${_directory}"; then + print_permissions_advice_and_fail "${_directory}" + 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} + if running_as_root; then + if ! 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 [ $(gosu "${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 + fi + else + if [[ ! -w "${mountFolder}" ]] && [[ "$(stat -c %U ${mountFolder})" != "neo4j" ]]; then + print_permissions_advice_and_fail "${mountFolder}" + fi + fi +} + +function load_plugin_from_github +{ + # 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" /neo4jlabs-plugins.json )" + # 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="$(wget -q --timeout 300 --tries 30 -O - "${_versions_json_url}")" + local _plugin_jar_url="$(echo "${_versions_json}" | jq --raw-output ".[] | select(.neo4j==\"${_neo4j_version}\") | .jar")" + if [[ -z "${_plugin_jar_url}" ]]; then + echo >&2 "Error: No jar URL found for version '${_neo4j_version}' in versions.json from '${_versions_json_url}'" + echo >&2 "${_versions_json}" + exit 1 + fi + 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 +} + +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, graph-ql + 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[]" /neo4jlabs-plugins.json); do + _property="$(jq --raw-output '.key' <<< "${_entry}")" + _value="$(jq --raw-output '.value' <<< "${_entry}")" + + # 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" + else + if grep -o "^[^#]*" "${_neo4j_conf}" | grep -q --fixed-strings "${_property}=" ; then + sed --in-place "s/${_property}=/&${_value},/" "${_neo4j_conf}" + else + echo "${_property}=${_value}" >> "${_neo4j_conf}" + fi + fi + done +} + +function install_neo4j_labs_plugins +{ + # 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)" + cp "${NEO4J_HOME}"/conf/neo4j.conf "${_old_config}" + for plugin_name in $(echo "${NEO4JLABS_PLUGINS}" | jq --raw-output '.[]'); do + load_plugin_from_github "${plugin_name}" + 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}" + local _neo4j_home="${3}" + + if ! grep -q "^${_setting}=" "${_neo4j_home}"/conf/neo4j.conf + then + 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 _neo4j_home=${3} + + if grep -q -F "${_setting}=" "${_neo4j_home}"/conf/neo4j.conf; then + # Remove any lines containing the setting already + sed --in-place "/^${_setting}=.*/d" "${_neo4j_home}"/conf/neo4j.conf + fi + # Then always append setting to file + 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 + add_env_setting_to_conf "dbms.security.auth_enabled" "false" "${NEO4J_HOME}" + # NEO4J_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 + chown "${userid}":"${groupid}" /data/dbms + fi + + # 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. + if [ "${do_reset}" == "true" ]; then + neo4j-admin set-initial-password "${password}" --require-password-change 2>/dev/null || true + else + neo4j-admin set-initial-password "${password}" 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 +} + +# 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 gosu neo4j:neo4j" +else + userid="$(id -u)" + groupid="$(id -g)" + groups=($(id -G)) + exec_cmd="exec" +fi +readonly userid +readonly groupid +readonly groups +readonly exec_cmd + + +# Need to chown the home directory +if running_as_root; then + chown -R "${userid}":"${groupid}" "${NEO4J_HOME}" + chmod 700 "${NEO4J_HOME}" + find "${NEO4J_HOME}" -mindepth 1 -maxdepth 1 -type d -exec chmod -R 700 {} \; +fi + +# Only prompt for license agreement if command contains "neo4j" in it +# ==== CHECK LICENSE AGREEMENT ==== + +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. + +(c) Neo4j Sweden AB. 2021. All Rights Reserved. +Use of this Software without a proper commercial license with Neo4j, +Inc. or its affiliates is prohibited. + +Email inquiries can be directed to: licensing@neo4j.com + +More information is also available at: https://neo4j.com/licensing/ + + +To accept the 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 + +# Env variable naming convention: +# ==== RENAME LEGACY ENVIRONMENT CONF VARIABLES ==== + +# - 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:-}} + : ${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" + find /conf -type f -exec cp {} "${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 "${NEO4JLABS_PLUGINS:-}" ]]; then + # We need 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 + check_mounted_folder_writable_with_chown "/metrics" + : ${NEO4J_dbms_directories_metrics:="/metrics"} +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 + +# ==== SET CONFIGURATIONS ==== + +## == DOCKER SPECIFIC DEFAULT CONFIGURATIONS === +## these should not override *any* configurations set by the user + +add_docker_default_to_conf "dbms.tx_log.rotation.retention_policy" "100M size" "${NEO4J_HOME}" +add_docker_default_to_conf "dbms.memory.pagecache.size" "512M" "${NEO4J_HOME}" +add_docker_default_to_conf "dbms.default_listen_address" "0.0.0.0" "${NEO4J_HOME}" +# set enterprise only docker defaults +if [ "${NEO4J_EDITION}" == "enterprise" ]; +then + add_docker_default_to_conf "causal_clustering.discovery_advertised_address" "$(hostname):5000" "${NEO4J_HOME}" + add_docker_default_to_conf "causal_clustering.transaction_advertised_address" "$(hostname):6000" "${NEO4J_HOME}" + add_docker_default_to_conf "causal_clustering.raft_advertised_address" "$(hostname):7000" "${NEO4J_HOME}" +fi + +## == ENVIRONMENT VARIABLE CONFIGURATIONS === +## these override BOTH defaults and any existing values in the neo4j.conf file + +# save NEO4J_HOME and NEO4J_AUTH to temp variables that don't begin with NEO4J_ so they don't get added to the conf +temp_neo4j_home="${NEO4J_HOME}" +temp_neo4j_auth="${NEO4J_AUTH:-}" +# list env variables with prefix NEO4J_ and create settings from them +unset NEO4J_AUTH NEO4J_SHA256 NEO4J_TARBALL NEO4J_EDITION NEO4J_ACCEPT_LICENSE_AGREEMENT NEO4J_HOME +for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do + 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}" "${temp_neo4j_home}" + else + echo >&2 "WARNING: ${setting} not written to conf file because settings that start with a number are not permitted" + fi + fi +done +export NEO4J_HOME="${temp_neo4j_home}" +unset temp_neo4j_home + +# ==== SET PASSWORD AND PLUGINS ==== + +set_initial_password "${temp_neo4j_auth}" + + +if [[ ! -z "${NEO4JLABS_PLUGINS:-}" ]]; then + # NEO4JLABS_PLUGINS should be a json array of plugins like '["graph-algorithms", "apoc", "streams", "graphql"]' + install_neo4j_labs_plugins +fi + +# ==== INVOKE NEO4J STARTUP ==== + +[ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT} + +if [ "${cmd}" == "dump-config" ]; then + if ! is_writable "/conf"; then + print_permissions_advice_and_fail "/conf" + fi + cp --recursive "${NEO4J_HOME}"/conf/* /conf + echo "Config Dumped" + exit 0 +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 + ${exec_cmd} neo4j console +else + ${exec_cmd} "$@" +fi diff --git a/docker-image-src/4.1/neo4j-admin/Dockerfile b/docker-image-src/4.1/neo4j-admin/Dockerfile new file mode 100644 index 00000000..cc6b6404 --- /dev/null +++ b/docker-image-src/4.1/neo4j-admin/Dockerfile @@ -0,0 +1,42 @@ +FROM %%NEO4J_BASE_IMAGE%% + +ENV NEO4J_SHA256=%%NEO4J_SHA%% \ + NEO4J_TARBALL=%%NEO4J_TARBALL%% \ + NEO4J_EDITION=%%NEO4J_EDITION%% \ + NEO4J_HOME="/var/lib/neo4j" +ARG NEO4J_URI=%%NEO4J_DIST_SITE%%/%%NEO4J_TARBALL%% + +RUN addgroup --gid 7474 --system neo4j && adduser --uid 7474 --system --no-create-home --home "${NEO4J_HOME}" --ingroup neo4j neo4j + +COPY ./local-package/* /tmp/ + +RUN apt update \ + && apt install -y curl gosu \ + && 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 "${NEO4J_HOME}"/data /data \ + && chown -R neo4j:neo4j /data \ + && chmod -R 777 /data \ + && mkdir -p /backup \ + && chown -R neo4j:neo4j /backup \ + && chmod -R 777 /backup \ + && chown -R neo4j:neo4j "${NEO4J_HOME}" \ + && chmod -R 777 "${NEO4J_HOME}" \ + && ln -s /data "${NEO4J_HOME}"/data \ + && rm -rf /tmp/* \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get -y purge --auto-remove curl + + +ENV PATH "${NEO4J_HOME}"/bin:$PATH +VOLUME /data /backup +WORKDIR "${NEO4J_HOME}" + +COPY docker-entrypoint.sh /docker-entrypoint.sh + +ENTRYPOINT ["/docker-entrypoint.sh"] + +CMD ["neo4j-admin"] diff --git a/docker-image-src/4.1/neo4j-admin/docker-entrypoint.sh b/docker-image-src/4.1/neo4j-admin/docker-entrypoint.sh new file mode 100755 index 00000000..cd1a5c7e --- /dev/null +++ b/docker-image-src/4.1/neo4j-admin/docker-entrypoint.sh @@ -0,0 +1,127 @@ +#!/bin/bash -eu + +function running_as_root +{ + test "$(id -u)" = "0" +} + +function is_writable +{ + gosu "${userid}":"${groupid}" test -w "${1}" +} + +function print_permissions_advice_and_fail +{ + local _directory=${1} + echo >&2 " +Folder ${_directory} is not accessible for user: ${userid} or group ${groupid} or groups ${groups[@]}, 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 +} + + +function check_mounted_folder_writable_with_chown +{ + local mountFolder=${1} + if running_as_root; 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 [ $(gosu "${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}" + fi + fi +} + +# ==== SETUP WHICH USER TO RUN AS ==== + +if running_as_root; then + userid="neo4j" + groupid="neo4j" + groups=($(id -G neo4j)) + exec_cmd="exec gosu neo4j:neo4j" +else + userid="$(id -u)" + groupid="$(id -g)" + groups=($(id -G)) + exec_cmd="exec" +fi + + +# ==== CHECK LICENSE AGREEMENT ==== + +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. + +(c) Neo4j Sweden AB. 2021. All Rights Reserved. +Use of this Software without a proper commercial license with Neo4j, +Inc. or its affiliates is prohibited. + +Email inquiries can be directed to: licensing@neo4j.com + +More information is also available at: https://neo4j.com/licensing/ + + +To accept the 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 + +# ==== ENSURE MOUNT FOLDER READ/WRITABILITY ==== + +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 /backup ]; then + check_mounted_folder_writable_with_chown "/backup" +fi + +# ==== MAKE SURE NEO4J CANNOT BE RUN FROM THIS CONTAINER ==== + +rm ${NEO4J_HOME}/bin/neo4j + +if [[ "${1}" == "neo4j" ]]; then + correct_image="neo4j:"$(neo4j-admin --version)"-${NEO4J_EDITION}" + echo >&2 " +This is a neo4j-admin only image, and usage of Neo4j server is not supported from here. +If you wish to start a Neo4j database, use: + +docker run ${correct_image} + " + exit 1 +fi + +# ==== START NEO4J-ADMIN COMMAND ==== + +${exec_cmd} "${@}" diff --git a/docker-image-src/4.2/Dockerfile b/docker-image-src/4.2/Dockerfile new file mode 100644 index 00000000..f4b3aa85 --- /dev/null +++ b/docker-image-src/4.2/Dockerfile @@ -0,0 +1,51 @@ +FROM %%NEO4J_BASE_IMAGE%% + +ENV NEO4J_SHA256=%%NEO4J_SHA%% \ + NEO4J_TARBALL=%%NEO4J_TARBALL%% \ + NEO4J_EDITION=%%NEO4J_EDITION%% \ + NEO4J_HOME="/var/lib/neo4j" +ARG NEO4J_URI=%%NEO4J_DIST_SITE%%/%%NEO4J_TARBALL%% +ARG TINI_SHA256="12d20136605531b09a2c2dac02ccee85e1b874eb322ef6baf7561cd93f93c855" +ARG TINI_URI="https://github.com/krallin/tini/releases/download/v0.18.0/tini" + +RUN addgroup --gid 7474 --system neo4j && adduser --uid 7474 --system --no-create-home --home "${NEO4J_HOME}" --ingroup neo4j neo4j + +COPY ./local-package/* /tmp/ + +RUN apt update \ + && apt install -y curl wget gosu jq \ + && curl -L --fail --silent --show-error ${TINI_URI} > /sbin/tini \ + && echo "${TINI_SHA256} /sbin/tini" | sha256sum -c --strict --quiet \ + && chmod +x /sbin/tini \ + && 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 "${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}" \ + && ln -s /data "${NEO4J_HOME}"/data \ + && ln -s /logs "${NEO4J_HOME}"/logs \ + && mv /tmp/neo4jlabs-plugins.json /neo4jlabs-plugins.json \ + && rm -rf /tmp/* \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get -y purge --auto-remove curl + +ENV PATH "${NEO4J_HOME}"/bin:$PATH + +WORKDIR "${NEO4J_HOME}" + +VOLUME /data /logs + +COPY docker-entrypoint.sh /docker-entrypoint.sh + +EXPOSE 7474 7473 7687 + +ENTRYPOINT ["/sbin/tini", "-g", "--", "/docker-entrypoint.sh"] +CMD ["neo4j"] diff --git a/docker-image-src/4.2/docker-entrypoint.sh b/docker-image-src/4.2/docker-entrypoint.sh new file mode 100755 index 00000000..6a724445 --- /dev/null +++ b/docker-image-src/4.2/docker-entrypoint.sh @@ -0,0 +1,526 @@ +#!/bin/bash -eu + +cmd="$1" + +function running_as_root +{ + test "$(id -u)" = "0" +} + +function secure_mode_enabled +{ + test "${SECURE_FILE_PERMISSIONS:=no}" = "yes" +} + +function containsElement +{ + local e match="$1" + shift + for e; do [[ "$e" == "$match" ]] && return 0; done + return 1 +} + +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 expand_commands_optionally +{ + if [ "${EXTENDED_CONF+"yes"}" == "yes" ]; then + echo "--expand-commands" + fi +} + +function print_permissions_advice_and_fail +{ + _directory=${1} + echo >&2 " +Folder ${_directory} is not accessible for user: ${userid} or group ${groupid} or groups ${groups[@]}, 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 +} + +function check_mounted_folder_readable +{ + local _directory=${1} + if ! is_readable "${_directory}"; then + print_permissions_advice_and_fail "${_directory}" + 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} + if running_as_root; then + if ! 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 [ $(gosu "${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 + fi + else + if [[ ! -w "${mountFolder}" ]] && [[ "$(stat -c %U ${mountFolder})" != "neo4j" ]]; then + print_permissions_advice_and_fail "${mountFolder}" + fi + fi +} + +function load_plugin_from_github +{ + # 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" /neo4jlabs-plugins.json )" + # 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="$(wget -q --timeout 300 --tries 30 -O - "${_versions_json_url}")" + local _plugin_jar_url="$(echo "${_versions_json}" | jq --raw-output ".[] | select(.neo4j==\"${_neo4j_version}\") | .jar")" + if [[ -z "${_plugin_jar_url}" ]]; then + echo >&2 "Error: No jar URL found for version '${_neo4j_version}' in versions.json from '${_versions_json_url}'" + echo >&2 "${_versions_json}" + exit 1 + fi + 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 +} + +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, graph-ql + 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[]" /neo4jlabs-plugins.json); do + _property="$(jq --raw-output '.key' <<< "${_entry}")" + _value="$(jq --raw-output '.value' <<< "${_entry}")" + + # 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" + else + if grep -o "^[^#]*" "${_neo4j_conf}" | grep -q --fixed-strings "${_property}=" ; then + sed --in-place "s/${_property}=/&${_value},/" "${_neo4j_conf}" + else + echo "${_property}=${_value}" >> "${_neo4j_conf}" + fi + fi + done +} + +function install_neo4j_labs_plugins +{ + # 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)" + cp "${NEO4J_HOME}"/conf/neo4j.conf "${_old_config}" + for plugin_name in $(echo "${NEO4JLABS_PLUGINS}" | jq --raw-output '.[]'); do + load_plugin_from_github "${plugin_name}" + 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}" + local _neo4j_home="${3}" + + if ! grep -q "^${_setting}=" "${_neo4j_home}"/conf/neo4j.conf + then + 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 _neo4j_home=${3} + + if grep -q -F "${_setting}=" "${_neo4j_home}"/conf/neo4j.conf; then + # Remove any lines containing the setting already + sed --in-place "/^${_setting}=.*/d" "${_neo4j_home}"/conf/neo4j.conf + fi + # Then always append setting to file + 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 + add_env_setting_to_conf "dbms.security.auth_enabled" "false" "${NEO4J_HOME}" + # NEO4J_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 + chown "${userid}":"${groupid}" /data/dbms + fi + + # 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. + if [ "${do_reset}" == "true" ]; then + ${neo4j_admin_cmd} set-initial-password "${password}" --require-password-change $(expand_commands_optionally) 2>/dev/null || true + else + ${neo4j_admin_cmd} set-initial-password "${password}" $(expand_commands_optionally) 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 +} + +# 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 gosu neo4j:neo4j" + neo4j_admin_cmd="gosu neo4j:neo4j neo4j-admin" +else + userid="$(id -u)" + groupid="$(id -g)" + groups=($(id -G)) + exec_cmd="exec" + neo4j_admin_cmd="neo4j-admin" +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 + chown -R "${userid}":"${groupid}" "${NEO4J_HOME}" + chmod 700 "${NEO4J_HOME}" + find "${NEO4J_HOME}" -mindepth 1 -maxdepth 1 -type d -exec chmod -R 700 {} \; + 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. + +(c) Neo4j Sweden AB. 2021. All Rights Reserved. +Use of this Software without a proper commercial license with Neo4j, +Inc. or its affiliates is prohibited. + +Email inquiries can be directed to: licensing@neo4j.com + +More information is also available at: https://neo4j.com/licensing/ + + +To accept the 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 + +# ==== 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:-}} + : ${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/* + 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 "${NEO4JLABS_PLUGINS:-}" ]]; then + # We need 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 + check_mounted_folder_writable_with_chown "/metrics" + : ${NEO4J_dbms_directories_metrics:="/metrics"} +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 + +add_docker_default_to_conf "dbms.tx_log.rotation.retention_policy" "100M size" "${NEO4J_HOME}" +add_docker_default_to_conf "dbms.memory.pagecache.size" "512M" "${NEO4J_HOME}" +add_docker_default_to_conf "dbms.default_listen_address" "0.0.0.0" "${NEO4J_HOME}" +# set enterprise only docker defaults +if [ "${NEO4J_EDITION}" == "enterprise" ]; +then + add_docker_default_to_conf "causal_clustering.discovery_advertised_address" "$(hostname):5000" "${NEO4J_HOME}" + add_docker_default_to_conf "causal_clustering.transaction_advertised_address" "$(hostname):6000" "${NEO4J_HOME}" + add_docker_default_to_conf "causal_clustering.raft_advertised_address" "$(hostname):7000" "${NEO4J_HOME}" +fi + +## == ENVIRONMENT VARIABLE CONFIGURATIONS === +## these override BOTH defaults and any existing values in the neo4j.conf file + +# save NEO4J_HOME and NEO4J_AUTH to temp variables that don't begin with NEO4J_ so they don't get added to the conf +temp_neo4j_home="${NEO4J_HOME}" +temp_neo4j_auth="${NEO4J_AUTH:-}" +# list env variables with prefix NEO4J_ and create settings from them +unset NEO4J_AUTH NEO4J_SHA256 NEO4J_TARBALL NEO4J_EDITION NEO4J_ACCEPT_LICENSE_AGREEMENT NEO4J_HOME +for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do + 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}" "${temp_neo4j_home}" + else + echo >&2 "WARNING: ${setting} not written to conf file because settings that start with a number are not permitted" + fi + fi +done +export NEO4J_HOME="${temp_neo4j_home}" +unset temp_neo4j_home + +# ==== SET PASSWORD AND PLUGINS ==== + +set_initial_password "${temp_neo4j_auth}" + + +if [[ ! -z "${NEO4JLABS_PLUGINS:-}" ]]; then + # NEO4JLABS_PLUGINS should be a json array of plugins like '["graph-algorithms", "apoc", "streams", "graphql"]' + install_neo4j_labs_plugins +fi + +# ==== INVOKE NEO4J STARTUP ==== + +[ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT} + +if [ "${cmd}" == "dump-config" ]; then + if ! is_writable "/conf"; then + print_permissions_advice_and_fail "/conf" + fi + cp --recursive "${NEO4J_HOME}"/conf/* /conf + echo "Config Dumped" + exit 0 +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 + if [ "${EXTENDED_CONF+"yes"}" == "yes" ]; then + ${exec_cmd} neo4j console --expand-commands + else + ${exec_cmd} neo4j console + fi +else + ${exec_cmd} "$@" +fi diff --git a/docker-image-src/4.2/neo4j-admin/Dockerfile b/docker-image-src/4.2/neo4j-admin/Dockerfile new file mode 100644 index 00000000..cc6b6404 --- /dev/null +++ b/docker-image-src/4.2/neo4j-admin/Dockerfile @@ -0,0 +1,42 @@ +FROM %%NEO4J_BASE_IMAGE%% + +ENV NEO4J_SHA256=%%NEO4J_SHA%% \ + NEO4J_TARBALL=%%NEO4J_TARBALL%% \ + NEO4J_EDITION=%%NEO4J_EDITION%% \ + NEO4J_HOME="/var/lib/neo4j" +ARG NEO4J_URI=%%NEO4J_DIST_SITE%%/%%NEO4J_TARBALL%% + +RUN addgroup --gid 7474 --system neo4j && adduser --uid 7474 --system --no-create-home --home "${NEO4J_HOME}" --ingroup neo4j neo4j + +COPY ./local-package/* /tmp/ + +RUN apt update \ + && apt install -y curl gosu \ + && 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 "${NEO4J_HOME}"/data /data \ + && chown -R neo4j:neo4j /data \ + && chmod -R 777 /data \ + && mkdir -p /backup \ + && chown -R neo4j:neo4j /backup \ + && chmod -R 777 /backup \ + && chown -R neo4j:neo4j "${NEO4J_HOME}" \ + && chmod -R 777 "${NEO4J_HOME}" \ + && ln -s /data "${NEO4J_HOME}"/data \ + && rm -rf /tmp/* \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get -y purge --auto-remove curl + + +ENV PATH "${NEO4J_HOME}"/bin:$PATH +VOLUME /data /backup +WORKDIR "${NEO4J_HOME}" + +COPY docker-entrypoint.sh /docker-entrypoint.sh + +ENTRYPOINT ["/docker-entrypoint.sh"] + +CMD ["neo4j-admin"] diff --git a/docker-image-src/4.2/neo4j-admin/docker-entrypoint.sh b/docker-image-src/4.2/neo4j-admin/docker-entrypoint.sh new file mode 100755 index 00000000..cd1a5c7e --- /dev/null +++ b/docker-image-src/4.2/neo4j-admin/docker-entrypoint.sh @@ -0,0 +1,127 @@ +#!/bin/bash -eu + +function running_as_root +{ + test "$(id -u)" = "0" +} + +function is_writable +{ + gosu "${userid}":"${groupid}" test -w "${1}" +} + +function print_permissions_advice_and_fail +{ + local _directory=${1} + echo >&2 " +Folder ${_directory} is not accessible for user: ${userid} or group ${groupid} or groups ${groups[@]}, 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 +} + + +function check_mounted_folder_writable_with_chown +{ + local mountFolder=${1} + if running_as_root; 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 [ $(gosu "${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}" + fi + fi +} + +# ==== SETUP WHICH USER TO RUN AS ==== + +if running_as_root; then + userid="neo4j" + groupid="neo4j" + groups=($(id -G neo4j)) + exec_cmd="exec gosu neo4j:neo4j" +else + userid="$(id -u)" + groupid="$(id -g)" + groups=($(id -G)) + exec_cmd="exec" +fi + + +# ==== CHECK LICENSE AGREEMENT ==== + +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. + +(c) Neo4j Sweden AB. 2021. All Rights Reserved. +Use of this Software without a proper commercial license with Neo4j, +Inc. or its affiliates is prohibited. + +Email inquiries can be directed to: licensing@neo4j.com + +More information is also available at: https://neo4j.com/licensing/ + + +To accept the 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 + +# ==== ENSURE MOUNT FOLDER READ/WRITABILITY ==== + +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 /backup ]; then + check_mounted_folder_writable_with_chown "/backup" +fi + +# ==== MAKE SURE NEO4J CANNOT BE RUN FROM THIS CONTAINER ==== + +rm ${NEO4J_HOME}/bin/neo4j + +if [[ "${1}" == "neo4j" ]]; then + correct_image="neo4j:"$(neo4j-admin --version)"-${NEO4J_EDITION}" + echo >&2 " +This is a neo4j-admin only image, and usage of Neo4j server is not supported from here. +If you wish to start a Neo4j database, use: + +docker run ${correct_image} + " + exit 1 +fi + +# ==== START NEO4J-ADMIN COMMAND ==== + +${exec_cmd} "${@}" diff --git a/docker-image-src/4.3/Dockerfile b/docker-image-src/4.3/Dockerfile new file mode 100644 index 00000000..f4b3aa85 --- /dev/null +++ b/docker-image-src/4.3/Dockerfile @@ -0,0 +1,51 @@ +FROM %%NEO4J_BASE_IMAGE%% + +ENV NEO4J_SHA256=%%NEO4J_SHA%% \ + NEO4J_TARBALL=%%NEO4J_TARBALL%% \ + NEO4J_EDITION=%%NEO4J_EDITION%% \ + NEO4J_HOME="/var/lib/neo4j" +ARG NEO4J_URI=%%NEO4J_DIST_SITE%%/%%NEO4J_TARBALL%% +ARG TINI_SHA256="12d20136605531b09a2c2dac02ccee85e1b874eb322ef6baf7561cd93f93c855" +ARG TINI_URI="https://github.com/krallin/tini/releases/download/v0.18.0/tini" + +RUN addgroup --gid 7474 --system neo4j && adduser --uid 7474 --system --no-create-home --home "${NEO4J_HOME}" --ingroup neo4j neo4j + +COPY ./local-package/* /tmp/ + +RUN apt update \ + && apt install -y curl wget gosu jq \ + && curl -L --fail --silent --show-error ${TINI_URI} > /sbin/tini \ + && echo "${TINI_SHA256} /sbin/tini" | sha256sum -c --strict --quiet \ + && chmod +x /sbin/tini \ + && 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 "${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}" \ + && ln -s /data "${NEO4J_HOME}"/data \ + && ln -s /logs "${NEO4J_HOME}"/logs \ + && mv /tmp/neo4jlabs-plugins.json /neo4jlabs-plugins.json \ + && rm -rf /tmp/* \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get -y purge --auto-remove curl + +ENV PATH "${NEO4J_HOME}"/bin:$PATH + +WORKDIR "${NEO4J_HOME}" + +VOLUME /data /logs + +COPY docker-entrypoint.sh /docker-entrypoint.sh + +EXPOSE 7474 7473 7687 + +ENTRYPOINT ["/sbin/tini", "-g", "--", "/docker-entrypoint.sh"] +CMD ["neo4j"] diff --git a/docker-image-src/4.3/docker-entrypoint.sh b/docker-image-src/4.3/docker-entrypoint.sh new file mode 100755 index 00000000..dd80bb88 --- /dev/null +++ b/docker-image-src/4.3/docker-entrypoint.sh @@ -0,0 +1,541 @@ +#!/bin/bash -eu + +cmd="$1" + +function running_as_root +{ + test "$(id -u)" = "0" +} + +function secure_mode_enabled +{ + test "${SECURE_FILE_PERMISSIONS:=no}" = "yes" +} + +function containsElement +{ + local e match="$1" + shift + for e; do [[ "$e" == "$match" ]] && return 0; done + return 1 +} + +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 expand_commands_optionally +{ + if [ "${EXTENDED_CONF+"yes"}" == "yes" ]; then + echo "--expand-commands" + fi +} + +function print_permissions_advice_and_fail +{ + _directory=${1} + echo >&2 " +Folder ${_directory} is not accessible for user: ${userid} or group ${groupid} or groups ${groups[@]}, 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 +} + +function check_mounted_folder_readable +{ + local _directory=${1} + if ! is_readable "${_directory}"; then + print_permissions_advice_and_fail "${_directory}" + 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} + if running_as_root; then + if ! 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 [ $(gosu "${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 + fi + else + if [[ ! -w "${mountFolder}" ]] && [[ "$(stat -c %U ${mountFolder})" != "neo4j" ]]; then + print_permissions_advice_and_fail "${mountFolder}" + fi + fi +} + +function load_plugin_from_github +{ + # 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" /neo4jlabs-plugins.json )" + # 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="$(wget -q --timeout 300 --tries 30 -O - "${_versions_json_url}")" + local _plugin_jar_url="$(echo "${_versions_json}" | jq --raw-output ".[] | select(.neo4j==\"${_neo4j_version}\") | .jar")" + if [[ -z "${_plugin_jar_url}" ]]; then + echo >&2 "Error: No jar URL found for version '${_neo4j_version}' in versions.json from '${_versions_json_url}'" + echo >&2 "${_versions_json}" + exit 1 + fi + 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 +} + +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, graph-ql + 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[]" /neo4jlabs-plugins.json); do + _property="$(jq --raw-output '.key' <<< "${_entry}")" + _value="$(jq --raw-output '.value' <<< "${_entry}")" + + # 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" + else + if grep -o "^[^#]*" "${_neo4j_conf}" | grep -q --fixed-strings "${_property}=" ; then + sed --in-place "s/${_property}=/&${_value},/" "${_neo4j_conf}" + else + echo "${_property}=${_value}" >> "${_neo4j_conf}" + fi + fi + done +} + +function install_neo4j_labs_plugins +{ + # 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)" + cp "${NEO4J_HOME}"/conf/neo4j.conf "${_old_config}" + for plugin_name in $(echo "${NEO4JLABS_PLUGINS}" | jq --raw-output '.[]'); do + load_plugin_from_github "${plugin_name}" + 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}" + local _neo4j_home="${3}" + + if ! grep -q "^${_setting}=" "${_neo4j_home}"/conf/neo4j.conf + then + 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 _neo4j_home=${3} + + if grep -q -F "${_setting}=" "${_neo4j_home}"/conf/neo4j.conf; then + # Remove any lines containing the setting already + sed --in-place "/^${_setting}=.*/d" "${_neo4j_home}"/conf/neo4j.conf + fi + # Then always append setting to file + 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 + add_env_setting_to_conf "dbms.security.auth_enabled" "false" "${NEO4J_HOME}" + # NEO4J_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 + chown "${userid}":"${groupid}" /data/dbms + fi + + # 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. + if [ "${do_reset}" == "true" ]; then + ${neo4j_admin_cmd} set-initial-password "${password}" --require-password-change $(expand_commands_optionally) 2>/dev/null || true + else + ${neo4j_admin_cmd} set-initial-password "${password}" $(expand_commands_optionally) 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 +} + +# 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 gosu neo4j:neo4j" + neo4j_admin_cmd="gosu neo4j:neo4j neo4j-admin" +else + userid="$(id -u)" + groupid="$(id -g)" + groups=($(id -G)) + exec_cmd="exec" + neo4j_admin_cmd="neo4j-admin" +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 + chown -R "${userid}":"${groupid}" "${NEO4J_HOME}" + chmod 700 "${NEO4J_HOME}" + find "${NEO4J_HOME}" -mindepth 1 -maxdepth 1 -type d -exec chmod -R 700 {} \; + 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. + +(c) Neo4j Sweden AB. 2021. All Rights Reserved. +Use of this Software without a proper commercial license with Neo4j, +Inc. or its affiliates is prohibited. + +Email inquiries can be directed to: licensing@neo4j.com + +More information is also available at: https://neo4j.com/licensing/ + + +To accept the 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 + +# ==== 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:-}} + : ${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/* + 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 "${NEO4JLABS_PLUGINS:-}" ]]; then + # We need 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 + check_mounted_folder_writable_with_chown "/metrics" + : ${NEO4J_dbms_directories_metrics:="/metrics"} +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 + +add_docker_default_to_conf "dbms.tx_log.rotation.retention_policy" "100M size" "${NEO4J_HOME}" +add_docker_default_to_conf "dbms.memory.pagecache.size" "512M" "${NEO4J_HOME}" +add_docker_default_to_conf "dbms.default_listen_address" "0.0.0.0" "${NEO4J_HOME}" +# set enterprise only docker defaults +if [ "${NEO4J_EDITION}" == "enterprise" ]; +then + add_docker_default_to_conf "causal_clustering.discovery_advertised_address" "$(hostname):5000" "${NEO4J_HOME}" + add_docker_default_to_conf "causal_clustering.transaction_advertised_address" "$(hostname):6000" "${NEO4J_HOME}" + add_docker_default_to_conf "causal_clustering.raft_advertised_address" "$(hostname):7000" "${NEO4J_HOME}" +fi + +## == ENVIRONMENT VARIABLE CONFIGURATIONS === +## these override BOTH defaults and any existing values in the neo4j.conf file + +# save NEO4J_HOME and NEO4J_AUTH to temp variables that don't begin with NEO4J_ so they don't get added to the conf +temp_neo4j_home="${NEO4J_HOME}" +temp_neo4j_auth="${NEO4J_AUTH:-}" +# list env variables with prefix NEO4J_ and create settings from them +unset NEO4J_AUTH NEO4J_SHA256 NEO4J_TARBALL NEO4J_EDITION NEO4J_ACCEPT_LICENSE_AGREEMENT NEO4J_HOME +for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do + 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}" "${temp_neo4j_home}" + else + echo >&2 "WARNING: ${setting} not written to conf file because settings that start with a number are not permitted" + fi + fi +done +export NEO4J_HOME="${temp_neo4j_home}" +unset temp_neo4j_home + +# ==== SET PASSWORD AND PLUGINS ==== + +set_initial_password "${temp_neo4j_auth}" + + +if [[ ! -z "${NEO4JLABS_PLUGINS:-}" ]]; then + # NEO4JLABS_PLUGINS should be a json array of plugins like '["graph-algorithms", "apoc", "streams", "graphql"]' + install_neo4j_labs_plugins +fi + +# ==== INVOKE NEO4J STARTUP ==== + +[ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT} + +if [ "${cmd}" == "dump-config" ]; then + if ! is_writable "/conf"; then + print_permissions_advice_and_fail "/conf" + fi + 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]...` +function get_neo4j_run_cmd { + + local extraArgs=() + + if [ "${EXTENDED_CONF+"yes"}" == "yes" ]; then + extraArgs+=("--expand-commands") + fi + + if running_as_root; then + gosu neo4j:neo4j neo4j console --dry-run "${extraArgs[@]}" + else + neo4j console --dry-run "${extraArgs[@]}" + 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 + neo4j_console_cmd="$(get_neo4j_run_cmd)" + eval "${exec_cmd} ${neo4j_console_cmd?:No Neo4j command was generated}" +else + ${exec_cmd} "$@" +fi diff --git a/docker-image-src/4.3/neo4j-admin/Dockerfile b/docker-image-src/4.3/neo4j-admin/Dockerfile new file mode 100644 index 00000000..cc6b6404 --- /dev/null +++ b/docker-image-src/4.3/neo4j-admin/Dockerfile @@ -0,0 +1,42 @@ +FROM %%NEO4J_BASE_IMAGE%% + +ENV NEO4J_SHA256=%%NEO4J_SHA%% \ + NEO4J_TARBALL=%%NEO4J_TARBALL%% \ + NEO4J_EDITION=%%NEO4J_EDITION%% \ + NEO4J_HOME="/var/lib/neo4j" +ARG NEO4J_URI=%%NEO4J_DIST_SITE%%/%%NEO4J_TARBALL%% + +RUN addgroup --gid 7474 --system neo4j && adduser --uid 7474 --system --no-create-home --home "${NEO4J_HOME}" --ingroup neo4j neo4j + +COPY ./local-package/* /tmp/ + +RUN apt update \ + && apt install -y curl gosu \ + && 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 "${NEO4J_HOME}"/data /data \ + && chown -R neo4j:neo4j /data \ + && chmod -R 777 /data \ + && mkdir -p /backup \ + && chown -R neo4j:neo4j /backup \ + && chmod -R 777 /backup \ + && chown -R neo4j:neo4j "${NEO4J_HOME}" \ + && chmod -R 777 "${NEO4J_HOME}" \ + && ln -s /data "${NEO4J_HOME}"/data \ + && rm -rf /tmp/* \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get -y purge --auto-remove curl + + +ENV PATH "${NEO4J_HOME}"/bin:$PATH +VOLUME /data /backup +WORKDIR "${NEO4J_HOME}" + +COPY docker-entrypoint.sh /docker-entrypoint.sh + +ENTRYPOINT ["/docker-entrypoint.sh"] + +CMD ["neo4j-admin"] diff --git a/docker-image-src/4.3/neo4j-admin/docker-entrypoint.sh b/docker-image-src/4.3/neo4j-admin/docker-entrypoint.sh new file mode 100755 index 00000000..cd1a5c7e --- /dev/null +++ b/docker-image-src/4.3/neo4j-admin/docker-entrypoint.sh @@ -0,0 +1,127 @@ +#!/bin/bash -eu + +function running_as_root +{ + test "$(id -u)" = "0" +} + +function is_writable +{ + gosu "${userid}":"${groupid}" test -w "${1}" +} + +function print_permissions_advice_and_fail +{ + local _directory=${1} + echo >&2 " +Folder ${_directory} is not accessible for user: ${userid} or group ${groupid} or groups ${groups[@]}, 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 +} + + +function check_mounted_folder_writable_with_chown +{ + local mountFolder=${1} + if running_as_root; 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 [ $(gosu "${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}" + fi + fi +} + +# ==== SETUP WHICH USER TO RUN AS ==== + +if running_as_root; then + userid="neo4j" + groupid="neo4j" + groups=($(id -G neo4j)) + exec_cmd="exec gosu neo4j:neo4j" +else + userid="$(id -u)" + groupid="$(id -g)" + groups=($(id -G)) + exec_cmd="exec" +fi + + +# ==== CHECK LICENSE AGREEMENT ==== + +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. + +(c) Neo4j Sweden AB. 2021. All Rights Reserved. +Use of this Software without a proper commercial license with Neo4j, +Inc. or its affiliates is prohibited. + +Email inquiries can be directed to: licensing@neo4j.com + +More information is also available at: https://neo4j.com/licensing/ + + +To accept the 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 + +# ==== ENSURE MOUNT FOLDER READ/WRITABILITY ==== + +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 /backup ]; then + check_mounted_folder_writable_with_chown "/backup" +fi + +# ==== MAKE SURE NEO4J CANNOT BE RUN FROM THIS CONTAINER ==== + +rm ${NEO4J_HOME}/bin/neo4j + +if [[ "${1}" == "neo4j" ]]; then + correct_image="neo4j:"$(neo4j-admin --version)"-${NEO4J_EDITION}" + echo >&2 " +This is a neo4j-admin only image, and usage of Neo4j server is not supported from here. +If you wish to start a Neo4j database, use: + +docker run ${correct_image} + " + exit 1 +fi + +# ==== START NEO4J-ADMIN COMMAND ==== + +${exec_cmd} "${@}" diff --git a/docker-image-src/4.4/Dockerfile b/docker-image-src/4.4/Dockerfile new file mode 100644 index 00000000..f4b3aa85 --- /dev/null +++ b/docker-image-src/4.4/Dockerfile @@ -0,0 +1,51 @@ +FROM %%NEO4J_BASE_IMAGE%% + +ENV NEO4J_SHA256=%%NEO4J_SHA%% \ + NEO4J_TARBALL=%%NEO4J_TARBALL%% \ + NEO4J_EDITION=%%NEO4J_EDITION%% \ + NEO4J_HOME="/var/lib/neo4j" +ARG NEO4J_URI=%%NEO4J_DIST_SITE%%/%%NEO4J_TARBALL%% +ARG TINI_SHA256="12d20136605531b09a2c2dac02ccee85e1b874eb322ef6baf7561cd93f93c855" +ARG TINI_URI="https://github.com/krallin/tini/releases/download/v0.18.0/tini" + +RUN addgroup --gid 7474 --system neo4j && adduser --uid 7474 --system --no-create-home --home "${NEO4J_HOME}" --ingroup neo4j neo4j + +COPY ./local-package/* /tmp/ + +RUN apt update \ + && apt install -y curl wget gosu jq \ + && curl -L --fail --silent --show-error ${TINI_URI} > /sbin/tini \ + && echo "${TINI_SHA256} /sbin/tini" | sha256sum -c --strict --quiet \ + && chmod +x /sbin/tini \ + && 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 "${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}" \ + && ln -s /data "${NEO4J_HOME}"/data \ + && ln -s /logs "${NEO4J_HOME}"/logs \ + && mv /tmp/neo4jlabs-plugins.json /neo4jlabs-plugins.json \ + && rm -rf /tmp/* \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get -y purge --auto-remove curl + +ENV PATH "${NEO4J_HOME}"/bin:$PATH + +WORKDIR "${NEO4J_HOME}" + +VOLUME /data /logs + +COPY docker-entrypoint.sh /docker-entrypoint.sh + +EXPOSE 7474 7473 7687 + +ENTRYPOINT ["/sbin/tini", "-g", "--", "/docker-entrypoint.sh"] +CMD ["neo4j"] diff --git a/docker-image-src/4.4/docker-entrypoint.sh b/docker-image-src/4.4/docker-entrypoint.sh new file mode 100755 index 00000000..612aa3e5 --- /dev/null +++ b/docker-image-src/4.4/docker-entrypoint.sh @@ -0,0 +1,571 @@ +#!/bin/bash -eu + +cmd="$1" + +function running_as_root +{ + test "$(id -u)" = "0" +} + +function secure_mode_enabled +{ + test "${SECURE_FILE_PERMISSIONS:=no}" = "yes" +} + +function containsElement +{ + local e match="$1" + shift + for e; do [[ "$e" == "$match" ]] && return 0; done + return 1 +} + +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 expand_commands_optionally +{ + if [ "${EXTENDED_CONF+"yes"}" == "yes" ]; then + echo "--expand-commands" + fi +} + +function print_permissions_advice_and_fail +{ + _directory=${1} + echo >&2 " +Folder ${_directory} is not accessible for user: ${userid} or group ${groupid} or groups ${groups[@]}, 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 +} + +function check_mounted_folder_readable +{ + local _directory=${1} + if ! is_readable "${_directory}"; then + print_permissions_advice_and_fail "${_directory}" + 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} + if running_as_root; then + if ! 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 [ $(gosu "${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 + fi + else + if [[ ! -w "${mountFolder}" ]] && [[ "$(stat -c %U ${mountFolder})" != "neo4j" ]]; then + print_permissions_advice_and_fail "${mountFolder}" + 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}" + done + + if ! is_readable "${_destination}"; then + echo >&2 "Plugin at '${_destination}' is not readable" + exit 1 + fi +} + +function load_plugin_from_github +{ + # 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" /neo4jlabs-plugins.json )" + # 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="$(wget -q --timeout 300 --tries 30 -O - "${_versions_json_url}")" + local _plugin_jar_url="$(echo "${_versions_json}" | jq --raw-output ".[] | select(.neo4j==\"${_neo4j_version}\") | .jar")" + if [[ -z "${_plugin_jar_url}" ]]; then + echo >&2 "Error: No jar URL found for version '${_neo4j_version}' in versions.json from '${_versions_json_url}'" + echo >&2 "${_versions_json}" + exit 1 + fi + 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 +} + +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, graph-ql + 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[]" /neo4jlabs-plugins.json); do + _property="$(jq --raw-output '.key' <<< "${_entry}")" + _value="$(jq --raw-output '.value' <<< "${_entry}")" + + # 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" + else + if grep -o "^[^#]*" "${_neo4j_conf}" | grep -q --fixed-strings "${_property}=" ; then + sed --in-place "s/${_property}=/&${_value},/" "${_neo4j_conf}" + else + echo "${_property}=${_value}" >> "${_neo4j_conf}" + fi + fi + done +} + +function install_neo4j_labs_plugins +{ + # 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)" + cp "${NEO4J_HOME}"/conf/neo4j.conf "${_old_config}" + for plugin_name in $(echo "${NEO4JLABS_PLUGINS}" | jq --raw-output '.[]'); do + local _location="$(jq --raw-output "with_entries( select(.key==\"${plugin_name}\") ) | to_entries[] | .value.location" /neo4jlabs-plugins.json )" + if [ "${_location}" != "null" ]; then + load_plugin_from_location "${plugin_name}" "${_location}" + else + load_plugin_from_github "${plugin_name}" + fi + 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}" + local _neo4j_home="${3}" + + if ! grep -q "^${_setting}=" "${_neo4j_home}"/conf/neo4j.conf + then + 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 _neo4j_home=${3} + + if grep -q -F "${_setting}=" "${_neo4j_home}"/conf/neo4j.conf; then + # Remove any lines containing the setting already + sed --in-place "/^${_setting}=.*/d" "${_neo4j_home}"/conf/neo4j.conf + fi + # Then always append setting to file + 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 + add_env_setting_to_conf "dbms.security.auth_enabled" "false" "${NEO4J_HOME}" + # NEO4J_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 + chown "${userid}":"${groupid}" /data/dbms + fi + + # 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. + if [ "${do_reset}" == "true" ]; then + ${neo4j_admin_cmd} set-initial-password "${password}" --require-password-change $(expand_commands_optionally) 2>/dev/null || true + else + ${neo4j_admin_cmd} set-initial-password "${password}" $(expand_commands_optionally) 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 +} + +# 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 gosu neo4j:neo4j" + neo4j_admin_cmd="gosu neo4j:neo4j neo4j-admin" +else + userid="$(id -u)" + groupid="$(id -g)" + groups=($(id -G)) + exec_cmd="exec" + neo4j_admin_cmd="neo4j-admin" +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 + chown -R "${userid}":"${groupid}" "${NEO4J_HOME}" + chmod 700 "${NEO4J_HOME}" + find "${NEO4J_HOME}" -mindepth 1 -maxdepth 1 -type d -exec chmod -R 700 {} \; + 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. + +(c) Neo4j Sweden AB. 2021. All Rights Reserved. +Use of this Software without a proper commercial license with Neo4j, +Inc. or its affiliates is prohibited. + +Email inquiries can be directed to: licensing@neo4j.com + +More information is also available at: https://neo4j.com/licensing/ + + +To accept the 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 + +# ==== 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:-}} + : ${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/* + 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 "${NEO4JLABS_PLUGINS:-}" ]]; then + # We need 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 + check_mounted_folder_writable_with_chown "/metrics" + : ${NEO4J_dbms_directories_metrics:="/metrics"} +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 + +add_docker_default_to_conf "dbms.tx_log.rotation.retention_policy" "100M size" "${NEO4J_HOME}" +add_docker_default_to_conf "dbms.memory.pagecache.size" "512M" "${NEO4J_HOME}" +add_docker_default_to_conf "dbms.default_listen_address" "0.0.0.0" "${NEO4J_HOME}" +# set enterprise only docker defaults +if [ "${NEO4J_EDITION}" == "enterprise" ]; +then + add_docker_default_to_conf "causal_clustering.discovery_advertised_address" "$(hostname):5000" "${NEO4J_HOME}" + add_docker_default_to_conf "causal_clustering.transaction_advertised_address" "$(hostname):6000" "${NEO4J_HOME}" + add_docker_default_to_conf "causal_clustering.raft_advertised_address" "$(hostname):7000" "${NEO4J_HOME}" +fi + +## == ENVIRONMENT VARIABLE CONFIGURATIONS === +## these override BOTH defaults and any existing values in the neo4j.conf file + +# save NEO4J_HOME and NEO4J_AUTH to temp variables that don't begin with NEO4J_ so they don't get added to the conf +temp_neo4j_home="${NEO4J_HOME}" +temp_neo4j_auth="${NEO4J_AUTH:-}" +# list env variables with prefix NEO4J_ and create settings from them +unset NEO4J_AUTH NEO4J_SHA256 NEO4J_TARBALL NEO4J_EDITION NEO4J_ACCEPT_LICENSE_AGREEMENT NEO4J_HOME +for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do + 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}" "${temp_neo4j_home}" + else + echo >&2 "WARNING: ${setting} not written to conf file because settings that start with a number are not permitted" + fi + fi +done +export NEO4J_HOME="${temp_neo4j_home}" +unset temp_neo4j_home + +# ==== SET PASSWORD AND PLUGINS ==== + +set_initial_password "${temp_neo4j_auth}" + + +if [[ ! -z "${NEO4JLABS_PLUGINS:-}" ]]; then + # NEO4JLABS_PLUGINS should be a json array of plugins like '["graph-algorithms", "apoc", "streams", "graphql"]' + install_neo4j_labs_plugins +fi + +# ==== INVOKE NEO4J STARTUP ==== + +[ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT} + +if [ "${cmd}" == "dump-config" ]; then + if ! is_writable "/conf"; then + print_permissions_advice_and_fail "/conf" + fi + 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]...` +function get_neo4j_run_cmd { + + local extraArgs=() + + if [ "${EXTENDED_CONF+"yes"}" == "yes" ]; then + extraArgs+=("--expand-commands") + fi + + if running_as_root; then + gosu neo4j:neo4j neo4j console --dry-run "${extraArgs[@]}" + else + neo4j console --dry-run "${extraArgs[@]}" + 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 + neo4j_console_cmd="$(get_neo4j_run_cmd)" + eval "${exec_cmd} ${neo4j_console_cmd?:No Neo4j command was generated}" +else + ${exec_cmd} "$@" +fi diff --git a/docker-image-src/4.4/neo4j-admin/Dockerfile b/docker-image-src/4.4/neo4j-admin/Dockerfile new file mode 100644 index 00000000..cc6b6404 --- /dev/null +++ b/docker-image-src/4.4/neo4j-admin/Dockerfile @@ -0,0 +1,42 @@ +FROM %%NEO4J_BASE_IMAGE%% + +ENV NEO4J_SHA256=%%NEO4J_SHA%% \ + NEO4J_TARBALL=%%NEO4J_TARBALL%% \ + NEO4J_EDITION=%%NEO4J_EDITION%% \ + NEO4J_HOME="/var/lib/neo4j" +ARG NEO4J_URI=%%NEO4J_DIST_SITE%%/%%NEO4J_TARBALL%% + +RUN addgroup --gid 7474 --system neo4j && adduser --uid 7474 --system --no-create-home --home "${NEO4J_HOME}" --ingroup neo4j neo4j + +COPY ./local-package/* /tmp/ + +RUN apt update \ + && apt install -y curl gosu \ + && 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 "${NEO4J_HOME}"/data /data \ + && chown -R neo4j:neo4j /data \ + && chmod -R 777 /data \ + && mkdir -p /backup \ + && chown -R neo4j:neo4j /backup \ + && chmod -R 777 /backup \ + && chown -R neo4j:neo4j "${NEO4J_HOME}" \ + && chmod -R 777 "${NEO4J_HOME}" \ + && ln -s /data "${NEO4J_HOME}"/data \ + && rm -rf /tmp/* \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get -y purge --auto-remove curl + + +ENV PATH "${NEO4J_HOME}"/bin:$PATH +VOLUME /data /backup +WORKDIR "${NEO4J_HOME}" + +COPY docker-entrypoint.sh /docker-entrypoint.sh + +ENTRYPOINT ["/docker-entrypoint.sh"] + +CMD ["neo4j-admin"] diff --git a/docker-image-src/4.4/neo4j-admin/docker-entrypoint.sh b/docker-image-src/4.4/neo4j-admin/docker-entrypoint.sh new file mode 100755 index 00000000..cd1a5c7e --- /dev/null +++ b/docker-image-src/4.4/neo4j-admin/docker-entrypoint.sh @@ -0,0 +1,127 @@ +#!/bin/bash -eu + +function running_as_root +{ + test "$(id -u)" = "0" +} + +function is_writable +{ + gosu "${userid}":"${groupid}" test -w "${1}" +} + +function print_permissions_advice_and_fail +{ + local _directory=${1} + echo >&2 " +Folder ${_directory} is not accessible for user: ${userid} or group ${groupid} or groups ${groups[@]}, 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 +} + + +function check_mounted_folder_writable_with_chown +{ + local mountFolder=${1} + if running_as_root; 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 [ $(gosu "${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}" + fi + fi +} + +# ==== SETUP WHICH USER TO RUN AS ==== + +if running_as_root; then + userid="neo4j" + groupid="neo4j" + groups=($(id -G neo4j)) + exec_cmd="exec gosu neo4j:neo4j" +else + userid="$(id -u)" + groupid="$(id -g)" + groups=($(id -G)) + exec_cmd="exec" +fi + + +# ==== CHECK LICENSE AGREEMENT ==== + +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. + +(c) Neo4j Sweden AB. 2021. All Rights Reserved. +Use of this Software without a proper commercial license with Neo4j, +Inc. or its affiliates is prohibited. + +Email inquiries can be directed to: licensing@neo4j.com + +More information is also available at: https://neo4j.com/licensing/ + + +To accept the 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 + +# ==== ENSURE MOUNT FOLDER READ/WRITABILITY ==== + +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 /backup ]; then + check_mounted_folder_writable_with_chown "/backup" +fi + +# ==== MAKE SURE NEO4J CANNOT BE RUN FROM THIS CONTAINER ==== + +rm ${NEO4J_HOME}/bin/neo4j + +if [[ "${1}" == "neo4j" ]]; then + correct_image="neo4j:"$(neo4j-admin --version)"-${NEO4J_EDITION}" + echo >&2 " +This is a neo4j-admin only image, and usage of Neo4j server is not supported from here. +If you wish to start a Neo4j database, use: + +docker run ${correct_image} + " + exit 1 +fi + +# ==== START NEO4J-ADMIN COMMAND ==== + +${exec_cmd} "${@}" diff --git a/docker-image-src/5.0/Dockerfile b/docker-image-src/5.0/Dockerfile new file mode 100644 index 00000000..f4b3aa85 --- /dev/null +++ b/docker-image-src/5.0/Dockerfile @@ -0,0 +1,51 @@ +FROM %%NEO4J_BASE_IMAGE%% + +ENV NEO4J_SHA256=%%NEO4J_SHA%% \ + NEO4J_TARBALL=%%NEO4J_TARBALL%% \ + NEO4J_EDITION=%%NEO4J_EDITION%% \ + NEO4J_HOME="/var/lib/neo4j" +ARG NEO4J_URI=%%NEO4J_DIST_SITE%%/%%NEO4J_TARBALL%% +ARG TINI_SHA256="12d20136605531b09a2c2dac02ccee85e1b874eb322ef6baf7561cd93f93c855" +ARG TINI_URI="https://github.com/krallin/tini/releases/download/v0.18.0/tini" + +RUN addgroup --gid 7474 --system neo4j && adduser --uid 7474 --system --no-create-home --home "${NEO4J_HOME}" --ingroup neo4j neo4j + +COPY ./local-package/* /tmp/ + +RUN apt update \ + && apt install -y curl wget gosu jq \ + && curl -L --fail --silent --show-error ${TINI_URI} > /sbin/tini \ + && echo "${TINI_SHA256} /sbin/tini" | sha256sum -c --strict --quiet \ + && chmod +x /sbin/tini \ + && 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 "${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}" \ + && ln -s /data "${NEO4J_HOME}"/data \ + && ln -s /logs "${NEO4J_HOME}"/logs \ + && mv /tmp/neo4jlabs-plugins.json /neo4jlabs-plugins.json \ + && rm -rf /tmp/* \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get -y purge --auto-remove curl + +ENV PATH "${NEO4J_HOME}"/bin:$PATH + +WORKDIR "${NEO4J_HOME}" + +VOLUME /data /logs + +COPY docker-entrypoint.sh /docker-entrypoint.sh + +EXPOSE 7474 7473 7687 + +ENTRYPOINT ["/sbin/tini", "-g", "--", "/docker-entrypoint.sh"] +CMD ["neo4j"] diff --git a/docker-image-src/5.0/docker-entrypoint.sh b/docker-image-src/5.0/docker-entrypoint.sh new file mode 100755 index 00000000..612aa3e5 --- /dev/null +++ b/docker-image-src/5.0/docker-entrypoint.sh @@ -0,0 +1,571 @@ +#!/bin/bash -eu + +cmd="$1" + +function running_as_root +{ + test "$(id -u)" = "0" +} + +function secure_mode_enabled +{ + test "${SECURE_FILE_PERMISSIONS:=no}" = "yes" +} + +function containsElement +{ + local e match="$1" + shift + for e; do [[ "$e" == "$match" ]] && return 0; done + return 1 +} + +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 expand_commands_optionally +{ + if [ "${EXTENDED_CONF+"yes"}" == "yes" ]; then + echo "--expand-commands" + fi +} + +function print_permissions_advice_and_fail +{ + _directory=${1} + echo >&2 " +Folder ${_directory} is not accessible for user: ${userid} or group ${groupid} or groups ${groups[@]}, 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 +} + +function check_mounted_folder_readable +{ + local _directory=${1} + if ! is_readable "${_directory}"; then + print_permissions_advice_and_fail "${_directory}" + 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} + if running_as_root; then + if ! 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 [ $(gosu "${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 + fi + else + if [[ ! -w "${mountFolder}" ]] && [[ "$(stat -c %U ${mountFolder})" != "neo4j" ]]; then + print_permissions_advice_and_fail "${mountFolder}" + 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}" + done + + if ! is_readable "${_destination}"; then + echo >&2 "Plugin at '${_destination}' is not readable" + exit 1 + fi +} + +function load_plugin_from_github +{ + # 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" /neo4jlabs-plugins.json )" + # 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="$(wget -q --timeout 300 --tries 30 -O - "${_versions_json_url}")" + local _plugin_jar_url="$(echo "${_versions_json}" | jq --raw-output ".[] | select(.neo4j==\"${_neo4j_version}\") | .jar")" + if [[ -z "${_plugin_jar_url}" ]]; then + echo >&2 "Error: No jar URL found for version '${_neo4j_version}' in versions.json from '${_versions_json_url}'" + echo >&2 "${_versions_json}" + exit 1 + fi + 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 +} + +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, graph-ql + 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[]" /neo4jlabs-plugins.json); do + _property="$(jq --raw-output '.key' <<< "${_entry}")" + _value="$(jq --raw-output '.value' <<< "${_entry}")" + + # 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" + else + if grep -o "^[^#]*" "${_neo4j_conf}" | grep -q --fixed-strings "${_property}=" ; then + sed --in-place "s/${_property}=/&${_value},/" "${_neo4j_conf}" + else + echo "${_property}=${_value}" >> "${_neo4j_conf}" + fi + fi + done +} + +function install_neo4j_labs_plugins +{ + # 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)" + cp "${NEO4J_HOME}"/conf/neo4j.conf "${_old_config}" + for plugin_name in $(echo "${NEO4JLABS_PLUGINS}" | jq --raw-output '.[]'); do + local _location="$(jq --raw-output "with_entries( select(.key==\"${plugin_name}\") ) | to_entries[] | .value.location" /neo4jlabs-plugins.json )" + if [ "${_location}" != "null" ]; then + load_plugin_from_location "${plugin_name}" "${_location}" + else + load_plugin_from_github "${plugin_name}" + fi + 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}" + local _neo4j_home="${3}" + + if ! grep -q "^${_setting}=" "${_neo4j_home}"/conf/neo4j.conf + then + 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 _neo4j_home=${3} + + if grep -q -F "${_setting}=" "${_neo4j_home}"/conf/neo4j.conf; then + # Remove any lines containing the setting already + sed --in-place "/^${_setting}=.*/d" "${_neo4j_home}"/conf/neo4j.conf + fi + # Then always append setting to file + 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 + add_env_setting_to_conf "dbms.security.auth_enabled" "false" "${NEO4J_HOME}" + # NEO4J_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 + chown "${userid}":"${groupid}" /data/dbms + fi + + # 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. + if [ "${do_reset}" == "true" ]; then + ${neo4j_admin_cmd} set-initial-password "${password}" --require-password-change $(expand_commands_optionally) 2>/dev/null || true + else + ${neo4j_admin_cmd} set-initial-password "${password}" $(expand_commands_optionally) 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 +} + +# 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 gosu neo4j:neo4j" + neo4j_admin_cmd="gosu neo4j:neo4j neo4j-admin" +else + userid="$(id -u)" + groupid="$(id -g)" + groups=($(id -G)) + exec_cmd="exec" + neo4j_admin_cmd="neo4j-admin" +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 + chown -R "${userid}":"${groupid}" "${NEO4J_HOME}" + chmod 700 "${NEO4J_HOME}" + find "${NEO4J_HOME}" -mindepth 1 -maxdepth 1 -type d -exec chmod -R 700 {} \; + 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. + +(c) Neo4j Sweden AB. 2021. All Rights Reserved. +Use of this Software without a proper commercial license with Neo4j, +Inc. or its affiliates is prohibited. + +Email inquiries can be directed to: licensing@neo4j.com + +More information is also available at: https://neo4j.com/licensing/ + + +To accept the 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 + +# ==== 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:-}} + : ${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/* + 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 "${NEO4JLABS_PLUGINS:-}" ]]; then + # We need 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 + check_mounted_folder_writable_with_chown "/metrics" + : ${NEO4J_dbms_directories_metrics:="/metrics"} +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 + +add_docker_default_to_conf "dbms.tx_log.rotation.retention_policy" "100M size" "${NEO4J_HOME}" +add_docker_default_to_conf "dbms.memory.pagecache.size" "512M" "${NEO4J_HOME}" +add_docker_default_to_conf "dbms.default_listen_address" "0.0.0.0" "${NEO4J_HOME}" +# set enterprise only docker defaults +if [ "${NEO4J_EDITION}" == "enterprise" ]; +then + add_docker_default_to_conf "causal_clustering.discovery_advertised_address" "$(hostname):5000" "${NEO4J_HOME}" + add_docker_default_to_conf "causal_clustering.transaction_advertised_address" "$(hostname):6000" "${NEO4J_HOME}" + add_docker_default_to_conf "causal_clustering.raft_advertised_address" "$(hostname):7000" "${NEO4J_HOME}" +fi + +## == ENVIRONMENT VARIABLE CONFIGURATIONS === +## these override BOTH defaults and any existing values in the neo4j.conf file + +# save NEO4J_HOME and NEO4J_AUTH to temp variables that don't begin with NEO4J_ so they don't get added to the conf +temp_neo4j_home="${NEO4J_HOME}" +temp_neo4j_auth="${NEO4J_AUTH:-}" +# list env variables with prefix NEO4J_ and create settings from them +unset NEO4J_AUTH NEO4J_SHA256 NEO4J_TARBALL NEO4J_EDITION NEO4J_ACCEPT_LICENSE_AGREEMENT NEO4J_HOME +for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do + 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}" "${temp_neo4j_home}" + else + echo >&2 "WARNING: ${setting} not written to conf file because settings that start with a number are not permitted" + fi + fi +done +export NEO4J_HOME="${temp_neo4j_home}" +unset temp_neo4j_home + +# ==== SET PASSWORD AND PLUGINS ==== + +set_initial_password "${temp_neo4j_auth}" + + +if [[ ! -z "${NEO4JLABS_PLUGINS:-}" ]]; then + # NEO4JLABS_PLUGINS should be a json array of plugins like '["graph-algorithms", "apoc", "streams", "graphql"]' + install_neo4j_labs_plugins +fi + +# ==== INVOKE NEO4J STARTUP ==== + +[ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT} + +if [ "${cmd}" == "dump-config" ]; then + if ! is_writable "/conf"; then + print_permissions_advice_and_fail "/conf" + fi + 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]...` +function get_neo4j_run_cmd { + + local extraArgs=() + + if [ "${EXTENDED_CONF+"yes"}" == "yes" ]; then + extraArgs+=("--expand-commands") + fi + + if running_as_root; then + gosu neo4j:neo4j neo4j console --dry-run "${extraArgs[@]}" + else + neo4j console --dry-run "${extraArgs[@]}" + 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 + neo4j_console_cmd="$(get_neo4j_run_cmd)" + eval "${exec_cmd} ${neo4j_console_cmd?:No Neo4j command was generated}" +else + ${exec_cmd} "$@" +fi diff --git a/docker-image-src/5.0/neo4j-admin/Dockerfile b/docker-image-src/5.0/neo4j-admin/Dockerfile new file mode 100644 index 00000000..cc6b6404 --- /dev/null +++ b/docker-image-src/5.0/neo4j-admin/Dockerfile @@ -0,0 +1,42 @@ +FROM %%NEO4J_BASE_IMAGE%% + +ENV NEO4J_SHA256=%%NEO4J_SHA%% \ + NEO4J_TARBALL=%%NEO4J_TARBALL%% \ + NEO4J_EDITION=%%NEO4J_EDITION%% \ + NEO4J_HOME="/var/lib/neo4j" +ARG NEO4J_URI=%%NEO4J_DIST_SITE%%/%%NEO4J_TARBALL%% + +RUN addgroup --gid 7474 --system neo4j && adduser --uid 7474 --system --no-create-home --home "${NEO4J_HOME}" --ingroup neo4j neo4j + +COPY ./local-package/* /tmp/ + +RUN apt update \ + && apt install -y curl gosu \ + && 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 "${NEO4J_HOME}"/data /data \ + && chown -R neo4j:neo4j /data \ + && chmod -R 777 /data \ + && mkdir -p /backup \ + && chown -R neo4j:neo4j /backup \ + && chmod -R 777 /backup \ + && chown -R neo4j:neo4j "${NEO4J_HOME}" \ + && chmod -R 777 "${NEO4J_HOME}" \ + && ln -s /data "${NEO4J_HOME}"/data \ + && rm -rf /tmp/* \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get -y purge --auto-remove curl + + +ENV PATH "${NEO4J_HOME}"/bin:$PATH +VOLUME /data /backup +WORKDIR "${NEO4J_HOME}" + +COPY docker-entrypoint.sh /docker-entrypoint.sh + +ENTRYPOINT ["/docker-entrypoint.sh"] + +CMD ["neo4j-admin"] diff --git a/docker-image-src/5.0/neo4j-admin/docker-entrypoint.sh b/docker-image-src/5.0/neo4j-admin/docker-entrypoint.sh new file mode 100755 index 00000000..cd1a5c7e --- /dev/null +++ b/docker-image-src/5.0/neo4j-admin/docker-entrypoint.sh @@ -0,0 +1,127 @@ +#!/bin/bash -eu + +function running_as_root +{ + test "$(id -u)" = "0" +} + +function is_writable +{ + gosu "${userid}":"${groupid}" test -w "${1}" +} + +function print_permissions_advice_and_fail +{ + local _directory=${1} + echo >&2 " +Folder ${_directory} is not accessible for user: ${userid} or group ${groupid} or groups ${groups[@]}, 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 +} + + +function check_mounted_folder_writable_with_chown +{ + local mountFolder=${1} + if running_as_root; 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 [ $(gosu "${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}" + fi + fi +} + +# ==== SETUP WHICH USER TO RUN AS ==== + +if running_as_root; then + userid="neo4j" + groupid="neo4j" + groups=($(id -G neo4j)) + exec_cmd="exec gosu neo4j:neo4j" +else + userid="$(id -u)" + groupid="$(id -g)" + groups=($(id -G)) + exec_cmd="exec" +fi + + +# ==== CHECK LICENSE AGREEMENT ==== + +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. + +(c) Neo4j Sweden AB. 2021. All Rights Reserved. +Use of this Software without a proper commercial license with Neo4j, +Inc. or its affiliates is prohibited. + +Email inquiries can be directed to: licensing@neo4j.com + +More information is also available at: https://neo4j.com/licensing/ + + +To accept the 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 + +# ==== ENSURE MOUNT FOLDER READ/WRITABILITY ==== + +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 /backup ]; then + check_mounted_folder_writable_with_chown "/backup" +fi + +# ==== MAKE SURE NEO4J CANNOT BE RUN FROM THIS CONTAINER ==== + +rm ${NEO4J_HOME}/bin/neo4j + +if [[ "${1}" == "neo4j" ]]; then + correct_image="neo4j:"$(neo4j-admin --version)"-${NEO4J_EDITION}" + echo >&2 " +This is a neo4j-admin only image, and usage of Neo4j server is not supported from here. +If you wish to start a Neo4j database, use: + +docker run ${correct_image} + " + exit 1 +fi + +# ==== START NEO4J-ADMIN COMMAND ==== + +${exec_cmd} "${@}" diff --git a/make-arm64.mk b/make-arm64.mk new file mode 100644 index 00000000..0c190cb9 --- /dev/null +++ b/make-arm64.mk @@ -0,0 +1,43 @@ +include make-common.mk + +NEO4J_BASE_IMAGE?="arm64v8/openjdk:11-jdk-slim" +TAG ?= neo4j + +package-arm-experimental: TAG:=neo4j/neo4j-arm64-experimental +package-arm-experimental: tag-arm +> mkdir -p out +> docker save $(TAG):$(NEO4JVERSION) > out/neo4j-community-$(NEO4JVERSION)-arm64-docker-loadable.tar +> docker save $(TAG):$(NEO4JVERSION)-enterprise > out/neo4j-enterprise-$(NEO4JVERSION)-arm64-docker-loadable.tar +.PHONY: package-arm-experimental + +package-arm: TAG:=neo4j +package-arm: tag-arm out/community/.sentinel out/enterprise/.sentinel +> mkdir -p out +> docker save $(TAG):$(NEO4JVERSION) > out/neo4j-community-$(NEO4JVERSION)-arm64-docker-loadable.tar +> docker save $(TAG):$(NEO4JVERSION)-enterprise > out/neo4j-enterprise-$(NEO4JVERSION)-arm64-docker-loadable.tar +.PHONY: package-arm + +tag-arm: build-arm +> docker tag $$(cat tmp/.image-id-community-arm) $(TAG):$(NEO4JVERSION) +> docker tag $$(cat tmp/.image-id-enterprise-arm) $(TAG):$(NEO4JVERSION)-enterprise +.PHONY: tag-arm + +test-arm: build-arm +> mvn test -Dimage=$$(cat tmp/.image-id-community-arm) -Dadminimage=$$(cat tmp/.image-id-neo4j-admin-community) -Dedition=community -Dversion=$(NEO4JVERSION) -Dtest=com.neo4j.docker.neo4jserver.TestBasic +> mvn test -Dimage=$$(cat tmp/.image-id-enterprise-arm) -Dadminimage=$$(cat tmp/.image-id-neo4j-admin-enterprise) -Dedition=enterprise -Dversion=$(NEO4JVERSION) -Dtest=com.neo4j.docker.neo4jserver.TestBasic +.PHONY: test-arm + +# neo4j-admin builds don't need tini so we don't need to have an architecture specific target for them +build-arm: tmp/.image-id-community-arm tmp/.image-id-enterprise-arm tmp/.image-id-neo4j-admin-community tmp/.image-id-neo4j-admin-enterprise +.PHONY: build-arm + +tmp/.image-id-%-arm: tmp/local-context-%/.sentinel in/$(call tarball,%,$(NEO4JVERSION)) +> image=test/$$RANDOM-arm +> docker build --tag=$$image \ + --build-arg="NEO4J_URI=file:///tmp/$(call tarball,$*,$(NEO4JVERSION))" \ + --build-arg="TINI_URI=https://github.com/krallin/tini/releases/download/v0.18.0/tini-arm64" \ + --build-arg="TINI_SHA256=7c5463f55393985ee22357d976758aaaecd08defb3c5294d353732018169b019" \ + $( echo -n $$image >$@ + + diff --git a/make-common.mk b/make-common.mk new file mode 100644 index 00000000..e2add3ae --- /dev/null +++ b/make-common.mk @@ -0,0 +1,112 @@ +SHELL := bash +.ONESHELL: +.SHELLFLAGS := -eu -o pipefail -c +.DELETE_ON_ERROR: +.SECONDEXPANSION: +.SECONDARY: + +ifeq ($(origin .RECIPEPREFIX), undefined) + $(error This Make does not support .RECIPEPREFIX. Please use GNU Make 4.0 or later) +endif +.RECIPEPREFIX = > + +ifndef NEO4JVERSION + $(error NEO4JVERSION is not set) +endif + +tarball = neo4j-$(1)-$(2)-unix.tar.gz +dist_site := https://dist.neo4j.org +series := $(shell echo "$(NEO4JVERSION)" | sed -E 's/^([0-9]+\.[0-9]+)\..*/\1/') + +out/%/.sentinel: tmp/image-%/.sentinel +> mkdir -p $(@D) +> cp -r $( touch $@ + +## building the image ## + +tmp/.image-id-neo4j-admin-%: tmp/local-context-neo4j-admin-%/.sentinel +> mkdir -p $(@D) +> image=test/admin-$$RANDOM +> docker build --tag=$$image \ + --build-arg="NEO4J_URI=file:///tmp/$(call tarball,$*,$(NEO4JVERSION))" $( echo -n $$image >$@ + +# tmp/local-context-{community,enterprise} is a local folder containing the +# Dockerfile/entrypoint/Neo4j/etc required to build a complete image locally. +tmp/local-context-%/.sentinel: tmp/image-%/.sentinel in/$(call tarball,%,$(NEO4JVERSION)) tmp/neo4jlabs-plugins.json +> rm -rf $(@D) +> mkdir -p $(@D) +> cp -r $( cp $(filter %.tar.gz,$^) $(@D)/local-package +> cp $(filter %.json,$^) $(@D)/local-package +> touch $@ + +tmp/local-context-neo4j-admin-%/.sentinel: tmp/image-neo4j-admin-%/.sentinel in/$(call tarball,%,$(NEO4JVERSION)) +> rm -rf $(@D) +> mkdir -p $(@D) +> cp -r $( cp $(filter %.tar.gz,$^) $(@D)/local-package +> touch $@ + +# tmp/image-{community,enterprise} contains the Dockerfile, docker-entrypoint.sh and plugins.json +# with all the variables (eg tini) filled in, but *NO Neo4j tar*. This is what gets released to dockerhub. +# You can successfully do `docker build tmp/image-{community,enterprise}` so long as the Neo4j is a released version. +tmp/image-%/.sentinel: docker-image-src/$(series)/Dockerfile docker-image-src/$(series)/docker-entrypoint.sh \ + in/$(call tarball,%,$(NEO4JVERSION)) tmp/neo4jlabs-plugins.json +> mkdir -p $(@D) +> cp $(filter %/docker-entrypoint.sh,$^) $(@D)/docker-entrypoint.sh +> sha=$$(shasum --algorithm=256 $(filter %.tar.gz,$^) | cut -d' ' -f1) +> <$(filter %/Dockerfile,$^) sed \ + -e "s|%%NEO4J_BASE_IMAGE%%|${NEO4J_BASE_IMAGE}|" \ + -e "s|%%NEO4J_SHA%%|$${sha}|" \ + -e "s|%%NEO4J_TARBALL%%|$(call tarball,$*,$(NEO4JVERSION))|" \ + -e "s|%%NEO4J_EDITION%%|$*|" \ + -e "s|%%NEO4J_DIST_SITE%%|$(dist_site)|" \ + >$(@D)/Dockerfile +> mkdir -p $(@D)/local-package +> cp $(filter %.json,$^) $(@D)/local-package +> touch $(@D)/local-package/.sentinel +> touch $@ + +tmp/image-neo4j-admin-%/.sentinel: docker-image-src/$(series)/neo4j-admin/Dockerfile \ + docker-image-src/$(series)/neo4j-admin/docker-entrypoint.sh \ + in/$(call tarball,%,$(NEO4JVERSION)) +> mkdir -p $(@D) +> cp $(filter %/docker-entrypoint.sh,$^) $(@D)/docker-entrypoint.sh +> sha=$$(shasum --algorithm=256 $(filter %.tar.gz,$^) | cut -d' ' -f1) +> <$(filter %/Dockerfile,$^) sed \ + -e "s|%%NEO4J_BASE_IMAGE%%|${NEO4J_BASE_IMAGE}|" \ + -e "s|%%NEO4J_SHA%%|$${sha}|" \ + -e "s|%%NEO4J_TARBALL%%|$(call tarball,$*,$(NEO4JVERSION))|" \ + -e "s|%%NEO4J_EDITION%%|$*|" \ + -e "s|%%NEO4J_DIST_SITE%%|$(dist_site)|" \ + >$(@D)/Dockerfile +> mkdir -p $(@D)/local-package +> touch $(@D)/local-package/.sentinel +> touch $@ + +tmp/neo4jlabs-plugins.json: ./neo4jlabs-plugins.json +> mkdir -p $(@D) +> cp $< $@ + +fetch_tarball = curl --fail --silent --show-error --location --remote-name \ + $(dist_site)/$(call tarball,$(1),$(NEO4JVERSION)) + +cache: in/neo4j-%-$(NEO4JVERSION)-unix.tar.gz +.PHONY: cache + +in/neo4j-community-$(NEO4JVERSION)-unix.tar.gz: +> mkdir -p in +> cd in +> $(call fetch_tarball,community) + +in/neo4j-enterprise-$(NEO4JVERSION)-unix.tar.gz: +> mkdir -p in +> cd in +> $(call fetch_tarball,enterprise) + +clean: +> rm -rf tmp +> rm -rf out +.PHONY: clean diff --git a/neo4jlabs-plugins.json b/neo4jlabs-plugins.json new file mode 100644 index 00000000..20982de2 --- /dev/null +++ b/neo4jlabs-plugins.json @@ -0,0 +1,57 @@ +{ + "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", + "properties": { + "dbms.security.procedures.unrestricted": "apoc.*" + } + }, + "bloom": { + "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://s3-eu-west-1.amazonaws.com/com.neo4j.graphalgorithms.dist/graph-data-science/versions.json", + "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/pom.xml b/pom.xml new file mode 100644 index 00000000..3be51d29 --- /dev/null +++ b/pom.xml @@ -0,0 +1,108 @@ + + + 4.0.0 + + com.neo4j + docker-neo4j-tests + 1.0-SNAPSHOT + jar + + + ${env.NEO4JVERSION} + + 1.8 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.7.0 + + ${java.version} + ${java.version} + + + + maven-surefire-plugin + 2.22.0 + + + + + + + + org.neo4j + neo4j + ${neo4j.version} + provided + + + org.slf4j + slf4j-nop + + + + + + org.slf4j + slf4j-api + 1.7.32 + + + org.slf4j + slf4j-log4j12 + 1.7.32 + + + org.junit.jupiter + junit-jupiter-api + 5.6.0 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.6.0 + test + + + org.junit.jupiter + junit-jupiter-params + 5.6.0 + test + + + org.junit.jupiter + junit-jupiter-migrationsupport + 5.6.0 + test + + + org.testcontainers + junit-jupiter + 1.16.0 + test + + + org.testcontainers + testcontainers + 1.16.0 + test + + + org.neo4j.driver + neo4j-java-driver + 4.0.0 + test + + + + \ No newline at end of file diff --git a/src/3.0/docker-entrypoint.sh b/src/3.0/docker-entrypoint.sh deleted file mode 100755 index 722e4133..00000000 --- a/src/3.0/docker-entrypoint.sh +++ /dev/null @@ -1,151 +0,0 @@ -#!/bin/bash -eu - -setting() { - setting="${1}" - value="${2}" - file="${3:-neo4j.conf}" - - if [ ! -f "conf/${file}" ]; then - if [ -f "conf/neo4j.conf" ]; then - file="neo4j.conf" - fi - fi - - if [ -n "${value}" ]; then - if grep --quiet --fixed-strings "${setting}=" conf/"${file}"; then - sed --in-place "s|.*${setting}=.*|${setting}=${value}|" conf/"${file}" - else - echo "${setting}=${value}" >>conf/"${file}" - fi - fi -} - -if [ "$1" == "neo4j" ]; then - - # 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:-"100M size"}} - : ${NEO4J_wrapper_java_additional:=${NEO4J_UDC_SOURCE:-"-Dneo4j.ext.udc.source=docker"}} - : ${NEO4J_dbms_memory_heap_initial__size:=${NEO4J_dbms_memory_heap_maxSize:-"512"}} - : ${NEO4J_dbms_memory_heap_max__size:=${NEO4J_dbms_memory_heap_maxSize:-"512"}} - : ${NEO4J_dbms_unmanaged__extension__classes:=${NEO4J_dbms_unmanagedExtensionClasses:-}} - : ${NEO4J_dbms_allow__format__migration:=${NEO4J_dbms_allowFormatMigration:-}} - : ${NEO4J_ha_server__id:=${NEO4J_ha_serverId:-}} - : ${NEO4J_ha_initial__hosts:=${NEO4J_ha_initialHosts:-}} - - : ${NEO4J_dbms_connector_http_address:="0.0.0.0:7474"} - : ${NEO4J_dbms_connector_https_address:="0.0.0.0:7473"} - : ${NEO4J_dbms_connector_bolt_address:="0.0.0.0:7687"} - : ${NEO4J_ha_host_coordination:="$(hostname):5001"} - : ${NEO4J_ha_host_data:="$(hostname):6001"} - - # unset old hardcoded unsupported env variables - unset NEO4J_dbms_txLog_rotation_retentionPolicy NEO4J_UDC_SOURCE \ - NEO4J_dbms_memory_heap_maxSize NEO4J_dbms_memory_heap_maxSize \ - NEO4J_dbms_unmanagedExtensionClasses NEO4J_dbms_allowFormatMigration \ - NEO4J_ha_initialHosts - - if [ -d /conf ]; then - find /conf -type f -exec cp {} conf \; - fi - - if [ -d /ssl ]; then - NEO4J_dbms_directories_certificates="/ssl" - fi - - if [ -d /plugins ]; then - NEO4J_dbms_directories_plugins="/plugins" - fi - - if [ -d /logs ]; then - NEO4J_dbms_directories_logs="/logs" - fi - - if [ -d /import ]; then - NEO4J_dbms_directories_import="/import" - fi - - if [ -d /metrics ]; then - NEO4J_dbms_directories_metrics="/metrics" - fi - - if [ "${NEO4J_AUTH:-}" == "none" ]; then - NEO4J_dbms_security_auth__enabled=false - elif [[ "${NEO4J_AUTH:-}" == neo4j/* ]]; then - password="${NEO4J_AUTH#neo4j/}" - if [ "${password}" == "neo4j" ]; then - echo "Invalid value for password. It cannot be 'neo4j', which is the default." - exit 1 - fi - - setting "dbms.connector.http.address" "127.0.0.1:7474" - setting "dbms.connector.https.address" "127.0.0.1:7473" - setting "dbms.connector.bolt.address" "127.0.0.1:7687" - bin/neo4j start || \ - (cat logs/neo4j.log && echo "Neo4j failed to start for password change" && exit 1) - - end="$((SECONDS+100))" - while true; do - http_code="$(curl --silent --write-out %{http_code} --user "neo4j:${password}" --output /dev/null http://localhost:7474/db/data/ || true)" - - if [[ "${http_code}" = "200" ]]; then - break; - fi - - if [[ "${http_code}" = "401" ]]; then - curl --fail --silent --show-error --user neo4j:neo4j \ - --data '{"password": "'"${password}"'"}' \ - --header 'Content-Type: application/json' \ - http://localhost:7474/user/neo4j/password - break; - fi - - if [[ "${SECONDS}" -ge "${end}" ]]; then - (cat logs/neo4j.log && echo "Neo4j failed to start" && exit 1) - fi - - sleep 1 - done - - bin/neo4j stop - elif [ -n "${NEO4J_AUTH:-}" ]; then - echo "Invalid value for NEO4J_AUTH: '${NEO4J_AUTH}'" - exit 1 - fi - - # list env variables with prefix NEO4J_ and create settings from them - unset NEO4J_AUTH NEO4J_SHA256 NEO4J_TARBALL - for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do - setting=$(echo ${i} | sed 's|^NEO4J_||' | sed 's|_|.|g' | sed 's|\.\.|_|g') - value=$(echo ${!i}) - if [[ -n ${value} ]]; then - if grep -q -F "${setting}=" conf/neo4j.conf; then - # Remove any lines containing the setting already - sed --in-place "/${setting}=.*/d" conf/neo4j.conf - fi - # Then always append setting to file - echo "${setting}=${value}" >> conf/neo4j.conf - fi - done - - [ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT} - - exec bin/neo4j console -elif [ "$1" == "dump-config" ]; then - if [ -d /conf ]; then - cp --recursive conf/* /conf - else - echo "You must provide a /conf volume" - exit 1 - fi -else - exec "$@" -fi diff --git a/src/3.1/Dockerfile b/src/3.1/Dockerfile deleted file mode 100644 index 5c7e40cc..00000000 --- a/src/3.1/Dockerfile +++ /dev/null @@ -1,31 +0,0 @@ -FROM openjdk:8-jre-alpine - -RUN apk add --no-cache --quiet \ - bash \ - curl - -ENV NEO4J_SHA256=%%NEO4J_SHA%% \ - NEO4J_TARBALL=%%NEO4J_TARBALL%% -ARG NEO4J_URI=%%NEO4J_DIST_SITE%%/%%NEO4J_TARBALL%% - -COPY ./local-package/* /tmp/ - -RUN curl --fail --silent --show-error --location --remote-name ${NEO4J_URI} \ - && echo "${NEO4J_SHA256} ${NEO4J_TARBALL}" | sha256sum -csw - \ - && tar --extract --file ${NEO4J_TARBALL} --directory /var/lib \ - && mv /var/lib/neo4j-* /var/lib/neo4j \ - && rm ${NEO4J_TARBALL} - -WORKDIR /var/lib/neo4j - -RUN mv data /data \ - && ln -s /data - -VOLUME /data - -COPY docker-entrypoint.sh /docker-entrypoint.sh - -EXPOSE 7474 7473 7687 - -ENTRYPOINT ["/docker-entrypoint.sh"] -CMD ["neo4j"] diff --git a/src/3.1/docker-entrypoint.sh b/src/3.1/docker-entrypoint.sh deleted file mode 100755 index 1fac31e9..00000000 --- a/src/3.1/docker-entrypoint.sh +++ /dev/null @@ -1,120 +0,0 @@ -#!/bin/bash -eu - -if [ "$1" == "neo4j" ]; then - - # 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:-"100M size"}} - : ${NEO4J_wrapper_java_additional:=${NEO4J_UDC_SOURCE:-"-Dneo4j.ext.udc.source=docker"}} - : ${NEO4J_dbms_memory_heap_initial__size:=${NEO4J_dbms_memory_heap_maxSize:-"512M"}} - : ${NEO4J_dbms_memory_heap_max__size:=${NEO4J_dbms_memory_heap_maxSize:-"512M"}} - : ${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:-}} - : ${NEO4J_ha_server__id:=${NEO4J_ha_serverId:-}} - : ${NEO4J_ha_initial__hosts:=${NEO4J_ha_initialHosts:-}} - : ${NEO4J_causal__clustering_expected__core__cluster__size:=${NEO4J_causalClustering_expectedCoreClusterSize:-}} - : ${NEO4J_causal__clustering_initial__discovery__members:=${NEO4J_causalClustering_initialDiscoveryMembers:-}} - : ${NEO4J_causal__clustering_discovery__listen__address:=${NEO4J_causalClustering_discoveryListenAddress:-"0.0.0.0:5000"}} - : ${NEO4J_causal__clustering_discovery__advertised__address:=${NEO4J_causalClustering_discoveryAdvertisedAddress:-"$(hostname):5000"}} - : ${NEO4J_causal__clustering_transaction__listen__address:=${NEO4J_causalClustering_transactionListenAddress:-"0.0.0.0:6000"}} - : ${NEO4J_causal__clustering_transaction__advertised__address:=${NEO4J_causalClustering_transactionAdvertisedAddress:-"$(hostname):6000"}} - : ${NEO4J_causal__clustering_raft__listen__address:=${NEO4J_causalClustering_raftListenAddress:-"0.0.0.0:7000"}} - : ${NEO4J_causal__clustering_raft__advertised__address:=${NEO4J_causalClustering_raftAdvertisedAddress:-"$(hostname):7000"}} - - : ${NEO4J_dbms_connectors_default__listen__address:="0.0.0.0"} - : ${NEO4J_dbms_connector_http_listen__address:="0.0.0.0:7474"} - : ${NEO4J_dbms_connector_https_listen__address:="0.0.0.0:7473"} - : ${NEO4J_dbms_connector_bolt_listen__address:="0.0.0.0:7687"} - : ${NEO4J_ha_host_coordination:="$(hostname):5001"} - : ${NEO4J_ha_host_data:="$(hostname):6001"} - - # unset old hardcoded unsupported env variables - unset NEO4J_dbms_txLog_rotation_retentionPolicy NEO4J_UDC_SOURCE \ - NEO4J_dbms_memory_heap_maxSize NEO4J_dbms_memory_heap_maxSize \ - 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 - - if [ -d /conf ]; then - find /conf -type f -exec cp {} conf \; - fi - - if [ -d /ssl ]; then - NEO4J_dbms_directories_certificates="/ssl" - fi - - if [ -d /plugins ]; then - NEO4J_dbms_directories_plugins="/plugins" - fi - - if [ -d /logs ]; then - NEO4J_dbms_directories_logs="/logs" - fi - - if [ -d /import ]; then - NEO4J_dbms_directories_import="/import" - fi - - if [ -d /metrics ]; then - NEO4J_dbms_directories_metrics="/metrics" - fi - - if [ "${NEO4J_AUTH:-}" == "none" ]; then - NEO4J_dbms_security_auth__enabled=false - elif [[ "${NEO4J_AUTH:-}" == neo4j/* ]]; then - password="${NEO4J_AUTH#neo4j/}" - if [ "${password}" == "neo4j" ]; then - echo "Invalid value for password. It cannot be 'neo4j', which is the default." - exit 1 - fi - # Will exit with error if users already exist (and print a message explaining that) - bin/neo4j-admin set-initial-password "${password}" || true - elif [ -n "${NEO4J_AUTH:-}" ]; then - echo "Invalid value for NEO4J_AUTH: '${NEO4J_AUTH}'" - exit 1 - fi - - # list env variables with prefix NEO4J_ and create settings from them - unset NEO4J_AUTH NEO4J_SHA256 NEO4J_TARBALL - for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do - setting=$(echo ${i} | sed 's|^NEO4J_||' | sed 's|_|.|g' | sed 's|\.\.|_|g') - value=$(echo ${!i}) - if [[ -n ${value} ]]; then - if grep -q -F "${setting}=" conf/neo4j.conf; then - # Remove any lines containing the setting already - sed --in-place "/${setting}=.*/d" conf/neo4j.conf - fi - # Then always append setting to file - echo "${setting}=${value}" >> conf/neo4j.conf - fi - done - - [ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT} - - exec bin/neo4j console -elif [ "$1" == "dump-config" ]; then - if [ -d /conf ]; then - cp --recursive conf/* /conf - else - echo "You must provide a /conf volume" - exit 1 - fi -else - exec "$@" -fi diff --git a/src/3.2/Dockerfile b/src/3.2/Dockerfile deleted file mode 100644 index 6bf83a68..00000000 --- a/src/3.2/Dockerfile +++ /dev/null @@ -1,31 +0,0 @@ -FROM openjdk:8-jre-alpine - -RUN apk add --no-cache --quiet \ - bash \ - curl - -ENV NEO4J_SHA256=%%NEO4J_SHA%% \ - NEO4J_TARBALL=%%NEO4J_TARBALL%% -ARG NEO4J_URI=%%NEO4J_DIST_SITE%%/%%NEO4J_TARBALL%% - -COPY ./local-package/* /tmp/ - -RUN curl --fail --silent --show-error --location --remote-name ${NEO4J_URI} \ - && echo "${NEO4J_SHA256} ${NEO4J_TARBALL}" | sha256sum -csw - \ - && tar --extract --file ${NEO4J_TARBALL} --directory /var/lib \ - && mv /var/lib/neo4j-* /var/lib/neo4j \ - && rm ${NEO4J_TARBALL} \ - && mv /var/lib/neo4j/data /data \ - && ln -s /data /var/lib/neo4j/data \ - && apk del curl - -WORKDIR /var/lib/neo4j - -VOLUME /data - -COPY docker-entrypoint.sh /docker-entrypoint.sh - -EXPOSE 7474 7473 7687 - -ENTRYPOINT ["/docker-entrypoint.sh"] -CMD ["neo4j"] diff --git a/src/3.2/docker-entrypoint.sh b/src/3.2/docker-entrypoint.sh deleted file mode 100755 index 1fac31e9..00000000 --- a/src/3.2/docker-entrypoint.sh +++ /dev/null @@ -1,120 +0,0 @@ -#!/bin/bash -eu - -if [ "$1" == "neo4j" ]; then - - # 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:-"100M size"}} - : ${NEO4J_wrapper_java_additional:=${NEO4J_UDC_SOURCE:-"-Dneo4j.ext.udc.source=docker"}} - : ${NEO4J_dbms_memory_heap_initial__size:=${NEO4J_dbms_memory_heap_maxSize:-"512M"}} - : ${NEO4J_dbms_memory_heap_max__size:=${NEO4J_dbms_memory_heap_maxSize:-"512M"}} - : ${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:-}} - : ${NEO4J_ha_server__id:=${NEO4J_ha_serverId:-}} - : ${NEO4J_ha_initial__hosts:=${NEO4J_ha_initialHosts:-}} - : ${NEO4J_causal__clustering_expected__core__cluster__size:=${NEO4J_causalClustering_expectedCoreClusterSize:-}} - : ${NEO4J_causal__clustering_initial__discovery__members:=${NEO4J_causalClustering_initialDiscoveryMembers:-}} - : ${NEO4J_causal__clustering_discovery__listen__address:=${NEO4J_causalClustering_discoveryListenAddress:-"0.0.0.0:5000"}} - : ${NEO4J_causal__clustering_discovery__advertised__address:=${NEO4J_causalClustering_discoveryAdvertisedAddress:-"$(hostname):5000"}} - : ${NEO4J_causal__clustering_transaction__listen__address:=${NEO4J_causalClustering_transactionListenAddress:-"0.0.0.0:6000"}} - : ${NEO4J_causal__clustering_transaction__advertised__address:=${NEO4J_causalClustering_transactionAdvertisedAddress:-"$(hostname):6000"}} - : ${NEO4J_causal__clustering_raft__listen__address:=${NEO4J_causalClustering_raftListenAddress:-"0.0.0.0:7000"}} - : ${NEO4J_causal__clustering_raft__advertised__address:=${NEO4J_causalClustering_raftAdvertisedAddress:-"$(hostname):7000"}} - - : ${NEO4J_dbms_connectors_default__listen__address:="0.0.0.0"} - : ${NEO4J_dbms_connector_http_listen__address:="0.0.0.0:7474"} - : ${NEO4J_dbms_connector_https_listen__address:="0.0.0.0:7473"} - : ${NEO4J_dbms_connector_bolt_listen__address:="0.0.0.0:7687"} - : ${NEO4J_ha_host_coordination:="$(hostname):5001"} - : ${NEO4J_ha_host_data:="$(hostname):6001"} - - # unset old hardcoded unsupported env variables - unset NEO4J_dbms_txLog_rotation_retentionPolicy NEO4J_UDC_SOURCE \ - NEO4J_dbms_memory_heap_maxSize NEO4J_dbms_memory_heap_maxSize \ - 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 - - if [ -d /conf ]; then - find /conf -type f -exec cp {} conf \; - fi - - if [ -d /ssl ]; then - NEO4J_dbms_directories_certificates="/ssl" - fi - - if [ -d /plugins ]; then - NEO4J_dbms_directories_plugins="/plugins" - fi - - if [ -d /logs ]; then - NEO4J_dbms_directories_logs="/logs" - fi - - if [ -d /import ]; then - NEO4J_dbms_directories_import="/import" - fi - - if [ -d /metrics ]; then - NEO4J_dbms_directories_metrics="/metrics" - fi - - if [ "${NEO4J_AUTH:-}" == "none" ]; then - NEO4J_dbms_security_auth__enabled=false - elif [[ "${NEO4J_AUTH:-}" == neo4j/* ]]; then - password="${NEO4J_AUTH#neo4j/}" - if [ "${password}" == "neo4j" ]; then - echo "Invalid value for password. It cannot be 'neo4j', which is the default." - exit 1 - fi - # Will exit with error if users already exist (and print a message explaining that) - bin/neo4j-admin set-initial-password "${password}" || true - elif [ -n "${NEO4J_AUTH:-}" ]; then - echo "Invalid value for NEO4J_AUTH: '${NEO4J_AUTH}'" - exit 1 - fi - - # list env variables with prefix NEO4J_ and create settings from them - unset NEO4J_AUTH NEO4J_SHA256 NEO4J_TARBALL - for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do - setting=$(echo ${i} | sed 's|^NEO4J_||' | sed 's|_|.|g' | sed 's|\.\.|_|g') - value=$(echo ${!i}) - if [[ -n ${value} ]]; then - if grep -q -F "${setting}=" conf/neo4j.conf; then - # Remove any lines containing the setting already - sed --in-place "/${setting}=.*/d" conf/neo4j.conf - fi - # Then always append setting to file - echo "${setting}=${value}" >> conf/neo4j.conf - fi - done - - [ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT} - - exec bin/neo4j console -elif [ "$1" == "dump-config" ]; then - if [ -d /conf ]; then - cp --recursive conf/* /conf - else - echo "You must provide a /conf volume" - exit 1 - fi -else - exec "$@" -fi diff --git a/src/3.3/Dockerfile b/src/3.3/Dockerfile deleted file mode 100644 index 1524dead..00000000 --- a/src/3.3/Dockerfile +++ /dev/null @@ -1,32 +0,0 @@ -FROM openjdk:8-jre-alpine - -RUN apk add --no-cache --quiet \ - bash \ - curl - -ENV NEO4J_SHA256=%%NEO4J_SHA%% \ - NEO4J_TARBALL=%%NEO4J_TARBALL%% \ - NEO4J_EDITION=%%NEO4J_EDITION%% -ARG NEO4J_URI=%%NEO4J_DIST_SITE%%/%%NEO4J_TARBALL%% - -COPY ./local-package/* /tmp/ - -RUN curl --fail --silent --show-error --location --remote-name ${NEO4J_URI} \ - && echo "${NEO4J_SHA256} ${NEO4J_TARBALL}" | sha256sum -csw - \ - && tar --extract --file ${NEO4J_TARBALL} --directory /var/lib \ - && mv /var/lib/neo4j-* /var/lib/neo4j \ - && rm ${NEO4J_TARBALL} \ - && mv /var/lib/neo4j/data /data \ - && ln -s /data /var/lib/neo4j/data \ - && apk del curl - -WORKDIR /var/lib/neo4j - -VOLUME /data - -COPY docker-entrypoint.sh /docker-entrypoint.sh - -EXPOSE 7474 7473 7687 - -ENTRYPOINT ["/docker-entrypoint.sh"] -CMD ["neo4j"] diff --git a/src/3.3/docker-entrypoint.sh b/src/3.3/docker-entrypoint.sh deleted file mode 100755 index 220844c3..00000000 --- a/src/3.3/docker-entrypoint.sh +++ /dev/null @@ -1,163 +0,0 @@ -#!/bin/bash -eu - -if [ "$1" == "neo4j" ]; then - if [ "$NEO4J_EDITION" == "enterprise" ]; then - if [ "${NEO4J_ACCEPT_LICENSE_AGREEMENT:=no}" != "yes" ]; then - echo " -In order to use Neo4j Enterprise Edition you must accept the license agreement. - -(c) Network Engine for Objects in Lund AB. 2017. All Rights Reserved. -Use of this Software without a proper commercial license with Neo4j, -Inc. or its affiliates is prohibited. - -Email inquiries can be directed to: licensing@neo4j.com - -More information is also available at: https://neo4j.com/licensing/ - - -To accept the license agreemnt 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 - - # 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:-"100M size"}} - : ${NEO4J_wrapper_java_additional:=${NEO4J_UDC_SOURCE:-"-Dneo4j.ext.udc.source=docker"}} - : ${NEO4J_dbms_memory_heap_initial__size:=${NEO4J_dbms_memory_heap_maxSize:-"512M"}} - : ${NEO4J_dbms_memory_heap_max__size:=${NEO4J_dbms_memory_heap_maxSize:-"512M"}} - : ${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:-}} - : ${NEO4J_ha_server__id:=${NEO4J_ha_serverId:-}} - : ${NEO4J_ha_initial__hosts:=${NEO4J_ha_initialHosts:-}} - : ${NEO4J_causal__clustering_expected__core__cluster__size:=${NEO4J_causalClustering_expectedCoreClusterSize:-}} - : ${NEO4J_causal__clustering_initial__discovery__members:=${NEO4J_causalClustering_initialDiscoveryMembers:-}} - : ${NEO4J_causal__clustering_discovery__listen__address:=${NEO4J_causalClustering_discoveryListenAddress:-"0.0.0.0:5000"}} - : ${NEO4J_causal__clustering_discovery__advertised__address:=${NEO4J_causalClustering_discoveryAdvertisedAddress:-"$(hostname):5000"}} - : ${NEO4J_causal__clustering_transaction__listen__address:=${NEO4J_causalClustering_transactionListenAddress:-"0.0.0.0:6000"}} - : ${NEO4J_causal__clustering_transaction__advertised__address:=${NEO4J_causalClustering_transactionAdvertisedAddress:-"$(hostname):6000"}} - : ${NEO4J_causal__clustering_raft__listen__address:=${NEO4J_causalClustering_raftListenAddress:-"0.0.0.0:7000"}} - : ${NEO4J_causal__clustering_raft__advertised__address:=${NEO4J_causalClustering_raftAdvertisedAddress:-"$(hostname):7000"}} - - : ${NEO4J_dbms_connectors_default__listen__address:="0.0.0.0"} - : ${NEO4J_dbms_connector_http_listen__address:="0.0.0.0:7474"} - : ${NEO4J_dbms_connector_https_listen__address:="0.0.0.0:7473"} - : ${NEO4J_dbms_connector_bolt_listen__address:="0.0.0.0:7687"} - : ${NEO4J_ha_host_coordination:="$(hostname):5001"} - : ${NEO4J_ha_host_data:="$(hostname):6001"} - - # unset old hardcoded unsupported env variables - unset NEO4J_dbms_txLog_rotation_retentionPolicy NEO4J_UDC_SOURCE \ - NEO4J_dbms_memory_heap_maxSize NEO4J_dbms_memory_heap_maxSize \ - 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 - - # Custom settings for dockerized neo4j - : ${NEO4J_dbms_tx__log_rotation_retention__policy:=100M size} - : ${NEO4J_dbms_memory_pagecache_size:=512M} - : ${NEO4J_wrapper_java_additional:=-Dneo4j.ext.udc.source=docker} - : ${NEO4J_dbms_memory_heap_initial__size:=512M} - : ${NEO4J_dbms_memory_heap_max__size:=512M} - : ${NEO4J_dbms_connectors_default__listen__address:=0.0.0.0} - : ${NEO4J_dbms_connector_http_listen__address:=0.0.0.0:7474} - : ${NEO4J_dbms_connector_https_listen__address:=0.0.0.0:7473} - : ${NEO4J_dbms_connector_bolt_listen__address:=0.0.0.0:7687} - : ${NEO4J_ha_host_coordination:=$(hostname):5001} - : ${NEO4J_ha_host_data:=$(hostname):6001} - : ${NEO4J_causal__clustering_discovery__listen__address:=0.0.0.0:5000} - : ${NEO4J_causal__clustering_discovery__advertised__address:=$(hostname):5000} - : ${NEO4J_causal__clustering_transaction__listen__address:=0.0.0.0:6000} - : ${NEO4J_causal__clustering_transaction__advertised__address:=$(hostname):6000} - : ${NEO4J_causal__clustering_raft__listen__address:=0.0.0.0:7000} - : ${NEO4J_causal__clustering_raft__advertised__address:=$(hostname):7000} - - if [ -d /conf ]; then - find /conf -type f -exec cp {} conf \; - fi - - if [ -d /ssl ]; then - NEO4J_dbms_directories_certificates="/ssl" - fi - - if [ -d /plugins ]; then - NEO4J_dbms_directories_plugins="/plugins" - fi - - if [ -d /logs ]; then - NEO4J_dbms_directories_logs="/logs" - fi - - if [ -d /import ]; then - NEO4J_dbms_directories_import="/import" - fi - - if [ -d /metrics ]; then - NEO4J_dbms_directories_metrics="/metrics" - fi - - if [ "${NEO4J_AUTH:-}" == "none" ]; then - NEO4J_dbms_security_auth__enabled=false - elif [[ "${NEO4J_AUTH:-}" == neo4j/* ]]; then - password="${NEO4J_AUTH#neo4j/}" - if [ "${password}" == "neo4j" ]; then - echo "Invalid value for password. It cannot be 'neo4j', which is the default." - exit 1 - fi - # Will exit with error if users already exist (and print a message explaining that) - bin/neo4j-admin set-initial-password "${password}" || true - elif [ -n "${NEO4J_AUTH:-}" ]; then - echo "Invalid value for NEO4J_AUTH: '${NEO4J_AUTH}'" - exit 1 - fi - - # list env variables with prefix NEO4J_ and create settings from them - unset NEO4J_AUTH NEO4J_SHA256 NEO4J_TARBALL - for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do - setting=$(echo ${i} | sed 's|^NEO4J_||' | sed 's|_|.|g' | sed 's|\.\.|_|g') - value=$(echo ${!i}) - if [[ -n ${value} ]]; then - if grep -q -F "${setting}=" conf/neo4j.conf; then - # Remove any lines containing the setting already - sed --in-place "/${setting}=.*/d" conf/neo4j.conf - fi - # Then always append setting to file - echo "${setting}=${value}" >> conf/neo4j.conf - fi - done - - [ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT} - - exec bin/neo4j console -elif [ "$1" == "dump-config" ]; then - if [ -d /conf ]; then - cp --recursive conf/* /conf - else - echo "You must provide a /conf volume" - exit 1 - fi -else - exec "$@" -fi diff --git a/src/3.4/Dockerfile b/src/3.4/Dockerfile deleted file mode 100644 index 1524dead..00000000 --- a/src/3.4/Dockerfile +++ /dev/null @@ -1,32 +0,0 @@ -FROM openjdk:8-jre-alpine - -RUN apk add --no-cache --quiet \ - bash \ - curl - -ENV NEO4J_SHA256=%%NEO4J_SHA%% \ - NEO4J_TARBALL=%%NEO4J_TARBALL%% \ - NEO4J_EDITION=%%NEO4J_EDITION%% -ARG NEO4J_URI=%%NEO4J_DIST_SITE%%/%%NEO4J_TARBALL%% - -COPY ./local-package/* /tmp/ - -RUN curl --fail --silent --show-error --location --remote-name ${NEO4J_URI} \ - && echo "${NEO4J_SHA256} ${NEO4J_TARBALL}" | sha256sum -csw - \ - && tar --extract --file ${NEO4J_TARBALL} --directory /var/lib \ - && mv /var/lib/neo4j-* /var/lib/neo4j \ - && rm ${NEO4J_TARBALL} \ - && mv /var/lib/neo4j/data /data \ - && ln -s /data /var/lib/neo4j/data \ - && apk del curl - -WORKDIR /var/lib/neo4j - -VOLUME /data - -COPY docker-entrypoint.sh /docker-entrypoint.sh - -EXPOSE 7474 7473 7687 - -ENTRYPOINT ["/docker-entrypoint.sh"] -CMD ["neo4j"] diff --git a/src/3.4/docker-entrypoint.sh b/src/3.4/docker-entrypoint.sh deleted file mode 100755 index 7e31aa72..00000000 --- a/src/3.4/docker-entrypoint.sh +++ /dev/null @@ -1,155 +0,0 @@ -#!/bin/bash -eu - -if [ "$1" == "neo4j" ]; then - if [ "$NEO4J_EDITION" == "enterprise" ]; then - if [ "${NEO4J_ACCEPT_LICENSE_AGREEMENT:=no}" != "yes" ]; then - echo " -In order to use Neo4j Enterprise Edition you must accept the license agreement. - -(c) Network Engine for Objects in Lund AB. 2017. All Rights Reserved. -Use of this Software without a proper commercial license with Neo4j, -Inc. or its affiliates is prohibited. - -Email inquiries can be directed to: licensing@neo4j.com - -More information is also available at: https://neo4j.com/licensing/ - - -To accept the license agreemnt 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 - - # 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 - NEO4J_dbms_tx__log_rotation_retention__policy=${NEO4J_dbms_txLog_rotation_retentionPolicy:-} - NEO4J_wrapper_java_additional=${NEO4J_UDC_SOURCE:-} - NEO4J_dbms_memory_heap_initial__size=${NEO4J_dbms_memory_heap_maxSize:-} - NEO4J_dbms_memory_heap_max__size=${NEO4J_dbms_memory_heap_maxSize:-} - 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:-} - NEO4J_ha_server__id=${NEO4J_ha_serverId:-} - NEO4J_ha_initial__hosts=${NEO4J_ha_initialHosts:-} - NEO4J_causal__clustering_expected__core__cluster__size=${NEO4J_causalClustering_expectedCoreClusterSize:-} - NEO4J_causal__clustering_initial__discovery__members=${NEO4J_causalClustering_initialDiscoveryMembers:-} - NEO4J_causal__clustering_discovery__listen__address=${NEO4J_causalClustering_discoveryListenAddress:-} - NEO4J_causal__clustering_discovery__advertised__address=${NEO4J_causalClustering_discoveryAdvertisedAddress:-} - NEO4J_causal__clustering_transaction__listen__address=${NEO4J_causalClustering_transactionListenAddress:-} - NEO4J_causal__clustering_transaction__advertised__address=${NEO4J_causalClustering_transactionAdvertisedAddress:-} - NEO4J_causal__clustering_raft__listen__address=${NEO4J_causalClustering_raftListenAddress:-} - NEO4J_causal__clustering_raft__advertised__address=${NEO4J_causalClustering_raftAdvertisedAddress:-} - - # unset old hardcoded unsupported env variables - unset NEO4J_dbms_txLog_rotation_retentionPolicy NEO4J_UDC_SOURCE \ - NEO4J_dbms_memory_heap_maxSize NEO4J_dbms_memory_heap_maxSize \ - 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 - - # Custom settings for dockerized neo4j - : ${NEO4J_dbms_tx__log_rotation_retention__policy:=100M size} - : ${NEO4J_dbms_memory_pagecache_size:=512M} - : ${NEO4J_wrapper_java_additional:=-Dneo4j.ext.udc.source=docker} - : ${NEO4J_dbms_memory_heap_initial__size:=512M} - : ${NEO4J_dbms_memory_heap_max__size:=512M} - : ${NEO4J_dbms_connectors_default__listen__address:=0.0.0.0} - : ${NEO4J_dbms_connector_http_listen__address:=0.0.0.0:7474} - : ${NEO4J_dbms_connector_https_listen__address:=0.0.0.0:7473} - : ${NEO4J_dbms_connector_bolt_listen__address:=0.0.0.0:7687} - : ${NEO4J_ha_host_coordination:=$(hostname):5001} - : ${NEO4J_ha_host_data:=$(hostname):6001} - : ${NEO4J_causal__clustering_discovery__listen__address:=0.0.0.0:5000} - : ${NEO4J_causal__clustering_discovery__advertised__address:=$(hostname):5000} - : ${NEO4J_causal__clustering_transaction__listen__address:=0.0.0.0:6000} - : ${NEO4J_causal__clustering_transaction__advertised__address:=$(hostname):6000} - : ${NEO4J_causal__clustering_raft__listen__address:=0.0.0.0:7000} - : ${NEO4J_causal__clustering_raft__advertised__address:=$(hostname):7000} - - if [ -d /conf ]; then - find /conf -type f -exec cp {} conf \; - fi - - if [ -d /ssl ]; then - NEO4J_dbms_directories_certificates="/ssl" - fi - - if [ -d /plugins ]; then - NEO4J_dbms_directories_plugins="/plugins" - fi - - if [ -d /logs ]; then - NEO4J_dbms_directories_logs="/logs" - fi - - if [ -d /import ]; then - NEO4J_dbms_directories_import="/import" - fi - - if [ -d /metrics ]; then - NEO4J_dbms_directories_metrics="/metrics" - fi - - if [ "${NEO4J_AUTH:-}" == "none" ]; then - NEO4J_dbms_security_auth__enabled=false - elif [[ "${NEO4J_AUTH:-}" == neo4j/* ]]; then - password="${NEO4J_AUTH#neo4j/}" - if [ "${password}" == "neo4j" ]; then - echo "Invalid value for password. It cannot be 'neo4j', which is the default." - exit 1 - fi - # Will exit with error if users already exist (and print a message explaining that) - bin/neo4j-admin set-initial-password "${password}" || true - elif [ -n "${NEO4J_AUTH:-}" ]; then - echo "Invalid value for NEO4J_AUTH: '${NEO4J_AUTH}'" - exit 1 - fi - - # list env variables with prefix NEO4J_ and create settings from them - unset NEO4J_AUTH NEO4J_SHA256 NEO4J_TARBALL - for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do - setting=$(echo ${i} | sed 's|^NEO4J_||' | sed 's|_|.|g' | sed 's|\.\.|_|g') - value=$(echo ${!i}) - if [[ -n ${value} ]]; then - if grep -q -F "${setting}=" conf/neo4j.conf; then - # Remove any lines containing the setting already - sed --in-place "/${setting}=.*/d" conf/neo4j.conf - fi - # Then always append setting to file - echo "${setting}=${value}" >> conf/neo4j.conf - fi - done - - [ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT} - - exec bin/neo4j console -elif [ "$1" == "dump-config" ]; then - if [ -d /conf ]; then - cp --recursive conf/* /conf - else - echo "You must provide a /conf volume" - exit 1 - fi -else - exec "$@" -fi diff --git a/src/main/resources/log4j.properties b/src/main/resources/log4j.properties new file mode 100644 index 00000000..43fc63ed --- /dev/null +++ b/src/main/resources/log4j.properties @@ -0,0 +1,3 @@ +log4j.rootLogger=INFO, console +log4j.appender.console=org.apache.log4j.ConsoleAppender +log4j.appender.console.layout=org.apache.log4j.PatternLayout \ No newline at end of file diff --git a/src/test/java/com/neo4j/docker/neo4jadmin/TestAdminBasic.java b/src/test/java/com/neo4j/docker/neo4jadmin/TestAdminBasic.java new file mode 100644 index 00000000..0e31c3e4 --- /dev/null +++ b/src/test/java/com/neo4j/docker/neo4jadmin/TestAdminBasic.java @@ -0,0 +1,34 @@ +package com.neo4j.docker.neo4jadmin; + +import com.neo4j.docker.utils.TestSettings; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.ContainerLaunchException; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; +import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; + +import java.time.Duration; + +public class TestAdminBasic +{ + private static final Logger log = LoggerFactory.getLogger( TestAdminBasic.class ); + + @Test + void testCannotRunNeo4j() + { + GenericContainer admin = new GenericContainer( TestSettings.ADMIN_IMAGE_ID ); + admin.withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" ) + .withExposedPorts( 7474 ) + .withLogConsumer( new Slf4jLogConsumer( log ) ) + .withStartupCheckStrategy( new OneShotStartupCheckStrategy().withTimeout( Duration.ofSeconds( 15 ) ) ) + .waitingFor( new HttpWaitStrategy().forPort( 7474 ).forStatusCode( 200 ) ) + .withCommand( "neo4j", "console" ); + + Assertions.assertThrows( ContainerLaunchException.class, () -> admin.start() ); + admin.stop(); + } +} diff --git a/src/test/java/com/neo4j/docker/neo4jadmin/TestBackupRestore.java b/src/test/java/com/neo4j/docker/neo4jadmin/TestBackupRestore.java new file mode 100644 index 00000000..7d53dd04 --- /dev/null +++ b/src/test/java/com/neo4j/docker/neo4jadmin/TestBackupRestore.java @@ -0,0 +1,150 @@ +package com.neo4j.docker.neo4jadmin; + +import com.neo4j.docker.utils.DatabaseIO; +import com.neo4j.docker.utils.HostFileSystemOperations; +import com.neo4j.docker.utils.Neo4jVersion; +import com.neo4j.docker.utils.SetContainerUser; +import com.neo4j.docker.utils.TestSettings; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; +import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; +import org.testcontainers.containers.wait.strategy.Wait; + +import java.nio.file.Path; +import java.time.Duration; + +public class TestBackupRestore +{ + // with authentication + // with non-default user + private static final Logger log = LoggerFactory.getLogger( TestBackupRestore.class ); + + @BeforeAll + static void beforeAll() + { + Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 4,4,0 )), + "Neo4j admin image not available before 4.4.0"); + Assumptions.assumeTrue( TestSettings.EDITION == TestSettings.Edition.ENTERPRISE, + "backup and restore only available in Neo4j Enterprise" ); + } + + private GenericContainer createDBContainer( boolean asDefaultUser, String password ) + { + String auth = "none"; + if(!password.equalsIgnoreCase("none")) + { + auth = "neo4j/"+password; + } + + GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID ); + container.withEnv( "NEO4J_AUTH", auth ) + .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" ) + .withEnv( "NEO4J_dbms_backup_enabled", "true" ) + .withEnv( "NEO4J_dbms_backup_listen__address", "0.0.0.0:6362" ) + .withExposedPorts( 7474, 7687, 6362 ) + .withLogConsumer( new Slf4jLogConsumer( log ) ) + .waitingFor( Wait.forHttp( "/" ) + .forPort( 7474 ) + .forStatusCode( 200 ) + .withStartupTimeout( Duration.ofSeconds( 90 ) ) ); + if(!asDefaultUser) + { + SetContainerUser.nonRootUser( container ); + } + return container; + } + + private GenericContainer createAdminContainer( boolean asDefaultUser ) + { + GenericContainer container = new GenericContainer( TestSettings.ADMIN_IMAGE_ID ); + container.withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" ) + .withLogConsumer( new Slf4jLogConsumer( log ) ) + .withStartupCheckStrategy( new OneShotStartupCheckStrategy().withTimeout( Duration.ofSeconds( 90 ) ) ); + if(!asDefaultUser) + { + SetContainerUser.nonRootUser( container ); + } + return container; + } + + @Test + void shouldBackupAndRestore_defaultUser_noAuth() throws Exception + { + testCanBackupAndRestore( true, "none" ); + } + @Test + void shouldBackupAndRestore_nonDefaultUser_noAuth() throws Exception + { + testCanBackupAndRestore( false, "none" ); + } + @Test + void shouldBackupAndRestore_defaultUser_withAuth() throws Exception + { + testCanBackupAndRestore( true, "secretpassword" ); + } + @Test + void shouldBackupAndRestore_nonDefaultUser_withAuth() throws Exception + { + testCanBackupAndRestore( false, "secretpassword" ); + } + + private void testCanBackupAndRestore(boolean asDefaultUser, String password) throws Exception + { + String dbUser = "neo4j"; + Path testOutputFolder = HostFileSystemOperations.createTempFolder( "backupRestore-" ); + + // BACKUP + // start a database and populate data + GenericContainer neo4j = createDBContainer( asDefaultUser, password ); + Path dataDir = HostFileSystemOperations.createTempFolderAndMountAsVolume( + neo4j, "data-", "/data", testOutputFolder ); + neo4j.start(); + DatabaseIO dbio = new DatabaseIO( neo4j ); + dbio.putInitialDataIntoContainer( dbUser, password ); + dbio.verifyInitialDataInContainer( dbUser, password ); + + // start admin container to initiate backup + GenericContainer adminBackup = createAdminContainer( asDefaultUser ) + .withNetworkMode( "container:"+neo4j.getContainerId() ) + .waitingFor( new LogMessageWaitStrategy().withRegEx( "^Backup complete successful.*" ) ) + .withCommand( "neo4j-admin", "backup", "--database=neo4j", "--backup-dir=/backup"); + + Path backupDir = HostFileSystemOperations.createTempFolderAndMountAsVolume( + adminBackup, "backup-", "/backup", testOutputFolder ); + adminBackup.start(); + + Assertions.assertTrue( neo4j.isRunning(), "neo4j container should still be running" ); + dbio.verifyInitialDataInContainer( dbUser, password ); + adminBackup.stop(); + + // RESTORE + + // write more stuff + dbio.putMoreDataIntoContainer( dbUser, password ); + dbio.verifyMoreDataIntoContainer( dbUser, password, true ); + + // do restore + dbio.runCypherQuery( dbUser, password, "STOP DATABASE neo4j", "system" ); + GenericContainer adminRestore = createAdminContainer( asDefaultUser ) + .waitingFor( new LogMessageWaitStrategy().withRegEx( "^.*restoreStatus=successful.*" ) ) + .withCommand( "neo4j-admin", "restore", "--database=neo4j", "--from=/backup/neo4j", "--force"); + HostFileSystemOperations.mountHostFolderAsVolume( adminRestore, backupDir, "/backup" ); + HostFileSystemOperations.mountHostFolderAsVolume( adminRestore, dataDir, "/data" ); + adminRestore.start(); + dbio.runCypherQuery( dbUser, password, "START DATABASE neo4j", "system" ); + + // verify new stuff is missing + dbio.verifyMoreDataIntoContainer( dbUser, password, false ); + + // clean up + adminRestore.stop(); + neo4j.stop(); + } +} diff --git a/src/test/java/com/neo4j/docker/neo4jadmin/TestDumpLoad.java b/src/test/java/com/neo4j/docker/neo4jadmin/TestDumpLoad.java new file mode 100644 index 00000000..d58c4b00 --- /dev/null +++ b/src/test/java/com/neo4j/docker/neo4jadmin/TestDumpLoad.java @@ -0,0 +1,161 @@ +package com.neo4j.docker.neo4jadmin; + +import com.github.dockerjava.api.command.CreateContainerCmd; +import com.neo4j.docker.utils.DatabaseIO; +import com.neo4j.docker.utils.HostFileSystemOperations; +import com.neo4j.docker.utils.Neo4jVersion; +import com.neo4j.docker.utils.SetContainerUser; +import com.neo4j.docker.utils.TestSettings; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; +import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; +import org.testcontainers.containers.wait.strategy.Wait; + +import java.nio.file.Path; +import java.time.Duration; +import java.util.function.Consumer; + +public class TestDumpLoad +{ + private static Logger log = LoggerFactory.getLogger( TestDumpLoad.class ); + + @BeforeAll + static void beforeAll() + { + Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 4, 4, 0 )), + "Neo4j admin image not available before 4.4.0"); + } + + private GenericContainer createDBContainer( boolean asDefaultUser, String password ) + { + String auth = "none"; + if(!password.equalsIgnoreCase("none")) + { + auth = "neo4j/"+password; + } + + GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID ); + container.withEnv( "NEO4J_AUTH", auth ) + .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" ) + .withExposedPorts( 7474, 7687 ) + .withLogConsumer( new Slf4jLogConsumer( log ) ) + .waitingFor( Wait.forHttp( "/" ) + .forPort( 7474 ) + .forStatusCode( 200 ) + .withStartupTimeout( Duration.ofSeconds( 90 ) ) ) + // the default testcontainer framework behaviour is to just stop the process entirely, + // preventing clean shutdown. This means we can run the stop command and + // it'll send a SIGTERM to initiate neo4j shutdown. See also stopContainer method. + .withCreateContainerCmdModifier( + (Consumer) cmd -> cmd.withStopSignal( "SIGTERM" ).withStopTimeout( 20 )); + if(!asDefaultUser) + { + SetContainerUser.nonRootUser( container ); + } + return container; + } + + private GenericContainer createAdminContainer( boolean asDefaultUser ) + { + GenericContainer container = new GenericContainer( TestSettings.ADMIN_IMAGE_ID ); + container.withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" ) + .withExposedPorts( 7474, 7687 ) + .withLogConsumer( new Slf4jLogConsumer( log ) ) + .waitingFor( new LogMessageWaitStrategy().withRegEx( "^Done: \\d+ files, [\\d\\.,]+[KMGi]+B processed\\..*" ) ) +// .waitingFor( new LogMessageWaitStrategy().withRegEx( "^Done: .*" ) ) + .withStartupCheckStrategy( new OneShotStartupCheckStrategy().withTimeout( Duration.ofSeconds( 90 ) ) ); + if(!asDefaultUser) + { + SetContainerUser.nonRootUser( container ); + } + return container; + } + + @Test + void shouldDumpAndLoad_defaultUser_noAuth() throws Exception + { + shouldCreateDumpAndLoadDump( true, "none" ); + } + + @Test + void shouldDumpAndLoad_nonDefaultUser_noAuth() throws Exception + { + shouldCreateDumpAndLoadDump( false, "none" ); + } + + @Test + void shouldDumpAndLoad_defaultUser_withAuth() throws Exception + { + shouldCreateDumpAndLoadDump( true, "verysecretpassword" ); + } + + @Test + void shouldDumpAndLoad_nonDefaultUser_withAuth() throws Exception + { + shouldCreateDumpAndLoadDump( false, "verysecretpassword" ); + } + + //container.stop() actually runs the killContainer Command, preventing clean shutdown. + // This runs the actual stop command. Which we set up in createDBContainer to send SIGTERM + private void stopContainer(GenericContainer container) + { + container.getDockerClient().stopContainerCmd( container.getContainerId() ).exec(); + } + + private void shouldCreateDumpAndLoadDump( boolean asDefaultUser, String password ) throws Exception + { + Path testOutputFolder = HostFileSystemOperations.createTempFolder( "dumpandload-" ); + Path firstDataDir; + Path secondDataDir; + Path backupDir; + + // start a database and populate it + try(GenericContainer container = createDBContainer( asDefaultUser, password )) + { + firstDataDir = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, "data1-", "/data", testOutputFolder ); + container.start(); + DatabaseIO dbio = new DatabaseIO( container ); + dbio.putInitialDataIntoContainer( "neo4j", password ); + stopContainer( container ); + } + + // use admin container to create dump + try(GenericContainer admin = createAdminContainer( asDefaultUser )) + { + HostFileSystemOperations.mountHostFolderAsVolume( admin, firstDataDir, "/data" ); + backupDir = HostFileSystemOperations.createTempFolderAndMountAsVolume( + admin, "dump-", "/backup", testOutputFolder ); + admin.withCommand( "neo4j-admin", "dump", "--database=neo4j", "--to=/backup/neo4j.dump" ); + admin.start(); + } + Assertions.assertTrue( backupDir.resolve( "neo4j.dump" ).toFile().exists(), "dump file not created"); + + // dump file exists. Now try to load it into a new database. + // use admin container to create dump + try(GenericContainer admin = createAdminContainer( asDefaultUser )) + { + secondDataDir = HostFileSystemOperations.createTempFolderAndMountAsVolume( + admin, "data2-", "/data", testOutputFolder ); + HostFileSystemOperations.mountHostFolderAsVolume( admin, backupDir, "/backup" ); + admin.withCommand( "neo4j-admin", "load", "--database=neo4j", "--from=/backup/neo4j.dump" ); + admin.start(); + } + + // verify data in 2nd data directory by starting a database and verifying data we populated earlier + try(GenericContainer container = createDBContainer( asDefaultUser, password )) + { + HostFileSystemOperations.mountHostFolderAsVolume( container, secondDataDir, "/data" ); + container.start(); + DatabaseIO dbio = new DatabaseIO( container ); + dbio.verifyInitialDataInContainer( "neo4j", password ); + } + } +} diff --git a/src/test/java/com/neo4j/docker/neo4jserver/TestBasic.java b/src/test/java/com/neo4j/docker/neo4jserver/TestBasic.java new file mode 100644 index 00000000..0b603dba --- /dev/null +++ b/src/test/java/com/neo4j/docker/neo4jserver/TestBasic.java @@ -0,0 +1,130 @@ +package com.neo4j.docker.neo4jserver; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.Container; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.OutputFrame; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import com.neo4j.docker.utils.Neo4jVersion; +import com.neo4j.docker.utils.TestSettings; +import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; +import org.testcontainers.containers.wait.strategy.Wait; + +import java.time.Duration; + +public class TestBasic +{ + private static Logger log = LoggerFactory.getLogger( TestBasic.class ); + + private GenericContainer createBasicContainer() + { + GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID ); + container.withEnv( "NEO4J_AUTH", "none" ) + .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" ) + .withExposedPorts( 7474 ) + .withLogConsumer( new Slf4jLogConsumer( log ) ); + return container; + } + + + @Test + void testListensOn7474() + { + try(GenericContainer container = createBasicContainer()) + { + container.setWaitStrategy( Wait.forHttp( "/" ).forPort( 7474 ).forStatusCode( 200 ) ); + container.start(); + Assertions.assertTrue( container.isRunning() ); + } + } + + @Test + void testNoUnexpectedErrors() throws Exception + { + try(GenericContainer container = createBasicContainer()) + { + container.setWaitStrategy( Wait.forHttp( "/" ).forPort( 7474 ).forStatusCode( 200 ) ); + container.start(); + Assertions.assertTrue( container.isRunning() ); + + String stderr = container.getLogs(OutputFrame.OutputType.STDERR); + Assertions.assertEquals( "", stderr, + "Unexpected errors in stderr from container!\n" + + stderr ); + } + } + + @Test + void testLicenseAcceptanceRequired_Neo4jServer() + { + Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3,3,0 ) ), + "No license checks before version 3.3.0"); + testLicenseAcceptance( TestSettings.IMAGE_ID ); + } + + @Test + void testLicenseAcceptanceRequired_Neo4jAdmin() + { + Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 4,4,0 ) ), + "No Neo4j admin image before version 4.4.0"); + testLicenseAcceptance( TestSettings.ADMIN_IMAGE_ID ); + } + + private void testLicenseAcceptance(String image) + { + Assumptions.assumeTrue( TestSettings.EDITION == TestSettings.Edition.ENTERPRISE, + "No license checks for community edition"); + + String logsOut; + try(GenericContainer container = new GenericContainer( image ) + .withLogConsumer( new Slf4jLogConsumer( log ) ) ) + { + container.waitingFor( Wait.forLogMessage( ".*must accept the license.*", 1 ) + .withStartupTimeout( Duration.ofSeconds( 30 ) ) ); + container.setStartupCheckStrategy( new OneShotStartupCheckStrategy() ); + // container start should fail due to licensing. + Assertions.assertThrows( Exception.class, () -> container.start(), + "Neo4j did not notify about accepting the license agreement" ); + logsOut = container.getLogs(); + } + // double check the container didn't warn and start neo4j anyway + Assertions.assertTrue( logsOut.contains( "must accept the license" ), + "Neo4j did not notify about accepting the license agreement" ); + Assertions.assertFalse( logsOut.contains( "Remote interface available" ), + "Neo4j was started even though the license was not accepted" ); + } + + @Test + void testCypherShellOnPath() throws Exception + { + String expectedCypherShellPath = "/var/lib/neo4j/bin/cypher-shell"; + try(GenericContainer container = createBasicContainer()) + { + container.setWaitStrategy( Wait.forHttp( "/" ).forPort( 7474 ).forStatusCode( 200 ) ); + container.start(); + + Container.ExecResult whichResult = container.execInContainer( "which", "cypher-shell" ); + Assertions.assertTrue( whichResult.getStdout().contains( expectedCypherShellPath ), + "cypher-shell not on path" ); + } + } + + @Test + void testCanChangeWorkDir() throws Exception + { + try(GenericContainer container = createBasicContainer()) + { + container.setWorkingDirectory( "/tmp" ); + container.setWaitStrategy( Wait.forHttp( "/" ) + .forPort( 7474 ) + .forStatusCode( 200 ) + .withStartupTimeout( Duration.ofSeconds( 60 ) ) ); + Assertions.assertDoesNotThrow( () -> container.start(), + "Could not start neo4j from workdir NEO4J_HOME" ); + } + } +} diff --git a/src/test/java/com/neo4j/docker/neo4jserver/TestBundledPluginInstallation.java b/src/test/java/com/neo4j/docker/neo4jserver/TestBundledPluginInstallation.java new file mode 100644 index 00000000..9b8c562f --- /dev/null +++ b/src/test/java/com/neo4j/docker/neo4jserver/TestBundledPluginInstallation.java @@ -0,0 +1,112 @@ +package com.neo4j.docker.neo4jserver; + +import com.neo4j.docker.utils.Neo4jVersion; +import com.neo4j.docker.utils.TestSettings; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; + +import java.util.LinkedList; +import java.util.List; + +import org.neo4j.driver.AuthTokens; +import org.neo4j.driver.Driver; +import org.neo4j.driver.GraphDatabase; +import org.neo4j.driver.Record; +import org.neo4j.driver.Result; +import org.neo4j.driver.Session; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TestBundledPluginInstallation +{ + private static final int DEFAULT_BROWSER_PORT = 7474; + private static final int DEFAULT_BOLT_PORT = 7687; + + private static final Logger log = LoggerFactory.getLogger( TestBundledPluginInstallation.class ); + + private GenericContainer container; + + @BeforeAll + public static void checkVersionIsCompatibleWithTest() + { + Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 4, 4, 0 ) ) ); + } + + private void createContainerWithBundledPlugin() + { + container = new GenericContainer( TestSettings.IMAGE_ID ); + + container.withEnv( "NEO4J_AUTH", "neo4j/neo" ) + .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" ) + .withEnv( "NEO4JLABS_PLUGINS", "[\"apoc-core\"]" ) + .withExposedPorts( DEFAULT_BROWSER_PORT, DEFAULT_BOLT_PORT ) + .withLogConsumer( new Slf4jLogConsumer( log ) ); + } + + @BeforeEach + public void setUp() + { + createContainerWithBundledPlugin(); + container.setWaitStrategy( Wait.forHttp( "/" ).forPort( DEFAULT_BROWSER_PORT ).forStatusCode( 200 ) ); + } + + @Test + public void testBundledPlugin() throws Exception + { + // When we start the neo4j docker container + container.start(); + + // Then the plugin is copied to the plugins directory + List pluginJars = new LinkedList<>(); + for ( String filename : container.execInContainer( "ls", "-1", "/var/lib/neo4j/plugins" ).getStdout().split( "\n" ) ) + { + if ( filename.endsWith( "jar" ) ) + { + pluginJars.add( filename ); + } + } + + assertTrue( pluginJars.size() == 1 ); + assertTrue( pluginJars.get( 0 ).contains( "apoc" ) ); + assertTrue( pluginJars.get( 0 ).contains( "-core" ) ); + + // When we connect to the database with the plugin + String boltAddress = "bolt://" + container.getContainerIpAddress() + ":" + container.getMappedPort( DEFAULT_BOLT_PORT ); + try ( Driver coreDriver = GraphDatabase.driver( boltAddress, AuthTokens.basic( "neo4j", "neo" ) ) ) + { + Session session = coreDriver.session(); + Result res = session.run( "CALL apoc.help(\"apoc.version\")" ); + + // Then something is returned + assertTrue( res.stream().anyMatch( x -> x.get( "name" ).asString().equals( "apoc.version" ) ) ); + + // When we call the procedure from the plugin + res = session.run( "RETURN apoc.version()" ); + + // Then we get the response we expect + Record record = res.single(); + String version = record.get( 0 ).asString(); + assertTrue( version.startsWith( String.format( "%d.", TestSettings.NEO4J_VERSION.major ) ), "unexpected version: " + version ); + assertFalse( res.hasNext(), "Our procedure should only return a single result" ); + + // Check that the config has been set + res = session.run( "CALL dbms.listConfig() YIELD name, value WHERE name='dbms.security.procedures.unrestricted' RETURN value" ); + record = res.single(); + assertEquals( "apoc.*", record.get( "value" ).asString(), "neo4j config not updated for plugin" ); + assertFalse( res.hasNext(), "Config lookup should only return a single result" ); + } + finally + { + container.stop(); + } + } +} diff --git a/src/test/java/com/neo4j/docker/neo4jserver/TestCausalCluster.java b/src/test/java/com/neo4j/docker/neo4jserver/TestCausalCluster.java new file mode 100644 index 00000000..fffe9789 --- /dev/null +++ b/src/test/java/com/neo4j/docker/neo4jserver/TestCausalCluster.java @@ -0,0 +1,115 @@ +package com.neo4j.docker.neo4jserver; + +import com.neo4j.docker.utils.HostFileSystemOperations; +import com.neo4j.docker.utils.SetContainerUser; +import com.neo4j.docker.utils.TestSettings; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.neo4j.driver.*; +import org.testcontainers.containers.DockerComposeContainer; +import org.testcontainers.containers.wait.strategy.*; + +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; + +@Disabled +public class TestCausalCluster +{ + private static final int DEFAULT_BOLT_PORT = 7687; + + @Disabled + @Test + void testCausalClusteringBasic() throws Exception + { + Assumptions.assumeTrue(TestSettings.EDITION == TestSettings.Edition.ENTERPRISE, + "No causal clustering for community edition"); + + Path tmpDir = HostFileSystemOperations.createTempFolder( "CC_cluster_" ); + + File compose_file = new File(tmpDir.toString(), "causal-cluster-compose.yml"); + Files.copy(getResource("causal-cluster-compose.yml"), Paths.get(compose_file.getPath())); + + Files.createDirectories( tmpDir.resolve( "core1" ) ); + Files.createDirectories( tmpDir.resolve( "core2" ) ); + Files.createDirectories( tmpDir.resolve( "core3" ) ); + Files.createDirectories( tmpDir.resolve( "readreplica1" ) ); + + String content = new String(Files.readAllBytes(Paths.get(compose_file.getPath()))); + String[] contentLines = content.split(System.getProperty("line.separator")); + String[] editedLines = new String[contentLines.length]; + int i = 0; + + for (String line : contentLines) { + editedLines[i] = line.replaceAll("%%IMAGE%%", TestSettings.IMAGE_ID); + editedLines[i] = editedLines[i].replaceAll("%%LOGS_DIR%%", tmpDir.toAbsolutePath().toString()); + editedLines[i] = editedLines[i].replaceAll("%%USERIDGROUPID%%", SetContainerUser.getNonRootUserString()); + i++; + } + + String editedContent = String.join("\n", editedLines); + + DataOutputStream outstream = new DataOutputStream(new FileOutputStream(compose_file,false)); + outstream.write(editedContent.getBytes()); + outstream.close(); + System.out.println("logs: " + compose_file.getName() + " and " + tmpDir.toString()); + + DockerComposeContainer clusteringContainer = new DockerComposeContainer(compose_file) + .withLocalCompose(true) + .withExposedService("core1", DEFAULT_BOLT_PORT ) + .withExposedService("core1", 7474, + Wait.forHttp( "/" ) + .forPort( 7474 ) + .forStatusCode( 200 ) + .withStartupTimeout( Duration.ofSeconds( 300 ) )) + .withExposedService("readreplica1", DEFAULT_BOLT_PORT); + + clusteringContainer.start(); + + String core1Uri = "bolt://" + clusteringContainer.getServiceHost("core1", DEFAULT_BOLT_PORT) + + ":" + + clusteringContainer.getServicePort("core1", DEFAULT_BOLT_PORT); + String rrUri = "bolt://" + clusteringContainer.getServiceHost("readreplica1", DEFAULT_BOLT_PORT) + + ":" + + clusteringContainer.getServicePort("readreplica1", DEFAULT_BOLT_PORT); + + try ( Driver coreDriver = GraphDatabase.driver( core1Uri, AuthTokens.basic( "neo4j", "neo"))) + { + Session session = coreDriver.session(); + Result rs = session.run( "CREATE (arne:dog {name:'Arne'})-[:SNIFFS]->(bosse:dog {name:'Bosse'}) RETURN arne.name"); + Assertions.assertEquals( "Arne", rs.single().get( 0 ).asString(), "did not receive expected result from cypher CREATE query" ); + } + catch (Exception e) + { + clusteringContainer.stop(); + return; + } + + try ( Driver rrDriver = GraphDatabase.driver(rrUri, AuthTokens.basic("neo4j", "neo"))) + { + Session session = rrDriver.session(); + Result rs = session.run( "MATCH (a:dog)-[:SNIFFS]->(b:dog) RETURN a.name"); + Assertions.assertEquals( "Arne", rs.single().get( 0 ).asString(), "did not receive expected result from cypher MATCH query" ); + } + catch (Exception e) + { + clusteringContainer.stop(); + return; + } + + clusteringContainer.stop(); + + } + + private InputStream getResource(String path) { + InputStream resource = getClass().getClassLoader().getResourceAsStream(path); + return resource; + } +} diff --git a/src/test/java/com/neo4j/docker/neo4jserver/TestConfSettings.java b/src/test/java/com/neo4j/docker/neo4jserver/TestConfSettings.java new file mode 100644 index 00000000..3e09818a --- /dev/null +++ b/src/test/java/com/neo4j/docker/neo4jserver/TestConfSettings.java @@ -0,0 +1,453 @@ +package com.neo4j.docker.neo4jserver; + +import com.neo4j.docker.utils.HostFileSystemOperations; +import com.neo4j.docker.utils.Neo4jVersion; +import com.neo4j.docker.utils.SetContainerUser; +import com.neo4j.docker.utils.TestSettings; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.output.WaitingConsumer; +import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; +import org.testcontainers.containers.wait.strategy.Wait; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +public class TestConfSettings { + private static Logger log = LoggerFactory.getLogger(TestConfSettings.class); + + private GenericContainer createContainer() + { + return new GenericContainer(TestSettings.IMAGE_ID) + .withEnv("NEO4J_AUTH", "none") + .withEnv("NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes") + .withExposedPorts(7474, 7687) + .withLogConsumer(new Slf4jLogConsumer(log)); + } + + private Map parseConfFile(File conf) throws FileNotFoundException + { + Map configurations = new HashMap<>(); + Scanner scanner = new Scanner(conf); + while ( scanner.hasNextLine() ) + { + String[] params = scanner.nextLine().split( "=", 2 ); + if(params.length < 2) + { + continue; + } + log.debug( params[0] + "\t:\t" + params[1] ); + configurations.put( params[0], params[1] ); + } + return configurations; + } + + private void assertConfigurationPresentInDebugLog( Path debugLog, String setting, String value, boolean shouldBeFound ) throws IOException + { + assertConfigurationPresentInDebugLog( debugLog, setting, new String[]{value}, shouldBeFound ); + } + + private void assertConfigurationPresentInDebugLog( Path debugLog, String setting, String[] eitherOfValues, boolean shouldBeFound ) throws IOException + { + // searches the debug log for the given string, returns true if present + Stream lines = Files.lines(debugLog); + String actualSetting = lines.filter(s -> s.contains( setting )).findFirst().orElse( "" ); + lines.close(); + if(shouldBeFound) + { + Assertions.assertTrue( !actualSetting.isEmpty(), setting+" was never set" ); + Assertions.assertTrue( Arrays.stream( eitherOfValues ).anyMatch( actualSetting::contains ), + setting +" is set to the wrong value. Expected either of: "+ Arrays.toString( eitherOfValues ) +" Actual: " + actualSetting ); + } + else + { + Assertions.assertTrue( actualSetting.isEmpty(), + setting+" was set when it should not have been. Actual value: "+actualSetting ); + } + } + + @Test + void testIgnoreNumericVars() + { + try(GenericContainer container = createContainer()) + { + container.withEnv( "NEO4J_1a", "1" ); + container.start(); + Assertions.assertTrue( container.isRunning() ); + + WaitingConsumer waitingConsumer = new WaitingConsumer(); + container.followOutput( waitingConsumer ); + + Assertions.assertDoesNotThrow( () -> waitingConsumer.waitUntil( frame -> frame.getUtf8String() + .contains( "WARNING: 1a not written to conf file because settings that start with a number are not permitted" ), + 15, TimeUnit.SECONDS ), + "Neo4j did not warn about invalid numeric config variable `Neo4j_1a`" ); + } + } + + @Test + void testEnvVarsOverrideDefaultConfigurations() throws Exception + { + Assumptions.assumeTrue(TestSettings.NEO4J_VERSION.isAtLeastVersion(new Neo4jVersion(3, 0, 0)), + "No neo4j-admin in 2.3: skipping neo4j-admin-conf-override test"); + + File conf; + try(GenericContainer container = createContainer() + .withEnv("NEO4J_dbms_memory_pagecache_size", "1000m") + .withEnv("NEO4J_dbms_memory_heap_initial__size", "2000m") + .withEnv("NEO4J_dbms_memory_heap_max__size", "3000m") + .withEnv( "NEO4J_dbms_directories_logs", "/notdefaultlogs" ) + .withEnv( "NEO4J_dbms_directories_data", "/notdefaultdata" ) + .withCommand("dump-config") ) + { + Path confMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "overriddenbyenv-conf-", + "/conf" ); + conf = confMount.resolve( "neo4j.conf" ).toFile(); + container.setWaitStrategy( + Wait.forLogMessage( ".*Config Dumped.*", 1 ) + .withStartupTimeout( Duration.ofSeconds( 30 ) ) ); + container.setStartupCheckStrategy( new OneShotStartupCheckStrategy() ); + SetContainerUser.nonRootUser( container ); + container.start(); + } + + // now check the settings we set via env are in the new conf file + Assertions.assertTrue( conf.exists(), "configuration file not written" ); + Assertions.assertTrue( conf.canRead(), "configuration file not readable for some reason?" ); + + Map configurations = parseConfFile( conf ); + Assertions.assertTrue( configurations.containsKey( "dbms.memory.pagecache.size" ), "pagecache size not overridden" ); + Assertions.assertEquals( "1000m", + configurations.get( "dbms.memory.pagecache.size" ), + "pagecache size not overridden" ); + + Assertions.assertTrue( configurations.containsKey( "dbms.memory.heap.initial_size" ), "initial heap size not overridden" ); + Assertions.assertEquals( "2000m", + configurations.get( "dbms.memory.heap.initial_size" ), + "initial heap size not overridden" ); + + Assertions.assertTrue( configurations.containsKey( "dbms.memory.heap.max_size" ), "maximum heap size not overridden" ); + Assertions.assertEquals( "3000m", + configurations.get( "dbms.memory.heap.max_size" ), + "maximum heap size not overridden" ); + + Assertions.assertTrue( configurations.containsKey( "dbms.directories.logs" ), "log folder not overridden" ); + Assertions.assertEquals( "/notdefaultlogs", + configurations.get( "dbms.directories.logs" ), + "log directory not overridden" ); + Assertions.assertTrue( configurations.containsKey( "dbms.directories.data" ), "data folder not overridden" ); + Assertions.assertEquals( "/notdefaultdata", + configurations.get( "dbms.directories.data" ), + "data directory not overridden" ); + } + + @Test + void testReadsTheConfFile() throws Exception + { + Path debugLog; + + try(GenericContainer container = createContainer()) + { + Path testOutputFolder = HostFileSystemOperations.createTempFolder( "confIsRead-" ); + //Mount /conf + Path confMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "conf-", + "/conf", + testOutputFolder); + Path logMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "logs-", + "/logs", + testOutputFolder); + debugLog = logMount.resolve("debug.log"); + SetContainerUser.nonRootUser( container ); + //Create ReadConf.conf file with the custom env variables + Path confFile = Paths.get( "src", "test", "resources", "confs", "ReadConf.conf" ); + Files.copy( confFile, confMount.resolve( "neo4j.conf" ) ); + //Start the container + container.setWaitStrategy( Wait.forHttp( "/" ).forPort( 7474 ).forStatusCode( 200 ) ); + container.start(); + } + + //Check if the container reads the conf file + assertConfigurationPresentInDebugLog( debugLog, "dbms.memory.heap.max_size", "512", true ); + } + + @Test + void testCommentedConfigsAreReplacedByDefaultOnes() throws Exception + { + File conf; + try(GenericContainer container = createContainer()) + { + //Mount /conf + Path confMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "replacedbydefault-conf-", + "/conf" ); + conf = confMount.resolve( "neo4j.conf" ).toFile(); + SetContainerUser.nonRootUser( container ); + //Create ConfsReplaced.conf file + Path confFile = Paths.get( "src", "test", "resources", "confs", "ConfsReplaced.conf" ); + Files.copy( confFile, confMount.resolve( "neo4j.conf" ) ); + //Start the container + container.setWaitStrategy( + Wait.forLogMessage( ".*Config Dumped.*", 1 ) + .withStartupTimeout( Duration.ofSeconds( 30 ) ) ); + // what is StartupCheckStrategy you wonder. Well, let me tell you a story. + // There was a time all these tests were failing because the config file was being dumped + // and the container closed so quickly. So quickly that it exposed a race condition between the container + // and the TestContainers library. The container could start and finish before the container library + // got around to checking if the container had started. + // The default "Has the container started" check strategy is to see if the container is running. + // But our container wasn't running because it was so quick it had already finished! The check failed and we had flaky tests :( + // This strategy here will check to see if the container is running OR if it exited with status code 0. + // It seems to do what we need... FOR NOW?? + container.setStartupCheckStrategy( new OneShotStartupCheckStrategy() ); + container.setCommand( "dump-config" ); + container.start(); + } + //Read the config file to check if the config is set correctly + Map configurations = parseConfFile( conf ); + Assertions.assertTrue( configurations.containsKey( "dbms.memory.pagecache.size" ), + "conf settings not set correctly by docker-entrypoint" ); + Assertions.assertEquals( "512M", + configurations.get( "dbms.memory.pagecache.size" ), + "conf settings not appended correctly by docker-entrypoint" ); + } + + @Test + void testConfigsAreNotOverridenByDockerentrypoint() throws Exception + { + File conf; + try(GenericContainer container = createContainer()) + { + //Mount /conf + Path confMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "notoverriddenbydefault-conf-", + "/conf" ); + conf = confMount.resolve( "neo4j.conf" ).toFile(); + SetContainerUser.nonRootUser( container ); + //Create ConfsNotOverriden.conf file + Path confFile = Paths.get( "src", "test", "resources", "confs", "ConfsNotOverriden.conf" ); + Files.copy( confFile, confMount.resolve( "neo4j.conf" ) ); + //Start the container + container.setWaitStrategy( + Wait.forLogMessage( ".*Config Dumped.*", 1 ) + .withStartupTimeout( Duration.ofSeconds( 30 ) ) ); + container.setStartupCheckStrategy( new OneShotStartupCheckStrategy() ); + container.setCommand( "dump-config" ); + container.start(); + } + + //Read the config file to check if the config is not overriden + Map configurations = parseConfFile(conf); + Assertions.assertTrue(configurations.containsKey("dbms.memory.pagecache.size"), "conf settings not set correctly by docker-entrypoint"); + Assertions.assertEquals("1024M", + configurations.get("dbms.memory.pagecache.size"), + "docker-entrypoint has overriden custom setting set from user's conf"); + } + + @Test + void testEnvVarsOverride() throws Exception + { + Path debugLog; + try(GenericContainer container = createContainer().withEnv("NEO4J_dbms_memory_pagecache_size", "512m")) + { + Path testOutputFolder = HostFileSystemOperations.createTempFolder( "envoverrideworks-" ); + Path confMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "conf-", + "/conf", + testOutputFolder ); + Path logMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "logs-", + "/logs", + testOutputFolder ); + debugLog = logMount.resolve( "debug.log" ); + SetContainerUser.nonRootUser( container ); + //Create EnvVarsOverride.conf file + Path confFile = Paths.get( "src", "test", "resources", "confs", "EnvVarsOverride.conf" ); + Files.copy( confFile, confMount.resolve( "neo4j.conf" ) ); + //Start the container + container.setWaitStrategy( Wait.forHttp( "/" ).forPort( 7474 ).forStatusCode( 200 ) ); + container.start(); + } + + assertConfigurationPresentInDebugLog( debugLog, "dbms.memory.pagecache.size", new String[]{"512m", "512.00MiB"}, true ); + } + + @Test + void testEnterpriseOnlyDefaultsConfigsAreSet () throws Exception + { + Assumptions.assumeTrue(TestSettings.EDITION == TestSettings.Edition.ENTERPRISE, + "This is testing only ENTERPRISE EDITION configs"); + + try(GenericContainer container = createContainer().withEnv("NEO4J_dbms_memory_pagecache_size", "512m")) + { + //Mount /logs + Path logMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "enterpriseonlysettings-logs-", + "/logs" ); + SetContainerUser.nonRootUser( container ); + //Start the container + container.setWaitStrategy( Wait.forHttp( "/" ).forPort( 7474 ).forStatusCode( 200 ) ); + container.start(); + //Read debug.log to check that causal_clustering confs are set successfully + String expectedTxAddress = container.getContainerId().substring( 0, 12 ) + ":6000"; + + assertConfigurationPresentInDebugLog( logMount.resolve( "debug.log" ), + "causal_clustering.transaction_advertised_address", + expectedTxAddress, + true ); + } + } + + @Test + void testEnterpriseOnlyDefaultsDontOverrideConfFile () throws Exception + { + Assumptions.assumeTrue(TestSettings.EDITION == TestSettings.Edition.ENTERPRISE, + "This is testing only ENTERPRISE EDITION configs"); + + try(GenericContainer container = createContainer()) + { + Path testOutputFolder = HostFileSystemOperations.createTempFolder( "ee-only-not-ovewritten-" ); + Path confMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "conf-", + "/conf", + testOutputFolder ); + Path logMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "logs-", + "/logs", + testOutputFolder ); + // mount a configuration file with enterprise only sttings already set + Path confFile = Paths.get( "src", "test", "resources", "confs", "EnterpriseOnlyNotOverwritten.conf"); + Files.copy( confFile, confMount.resolve( "neo4j.conf" ) ); + + //Start the container + SetContainerUser.nonRootUser( container ); + container.setWaitStrategy( Wait.forHttp( "/" ).forPort( 7474 ).forStatusCode( 200 ) ); + container.start(); + //Read debug.log to check that causal_clustering confs are set successfully + + assertConfigurationPresentInDebugLog( logMount.resolve( "debug.log" ), + "causal_clustering.transaction_advertised_address", + "localhost:6060", + true ); + } + } + + @Test + void testCommunityDoesNotHaveEnterpriseConfigs() throws Exception + { + Assumptions.assumeTrue(TestSettings.EDITION == TestSettings.Edition.COMMUNITY, + "This is testing only COMMUNITY EDITION configs"); + Path debugLog; + try(GenericContainer container = createContainer().withEnv("NEO4J_dbms_memory_pagecache_size", "512m")) + { + //Mount /logs + Path logMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "enterprisesettingsnotincommunity-logs-", + "/logs" ); + debugLog = logMount.resolve( "debug.log" ); + SetContainerUser.nonRootUser( container ); + //Start the container + container.setWaitStrategy( Wait.forHttp( "/" ).forPort( 7474 ).forStatusCode( 200 ) ); + container.start(); + } + + //Read debug.log to check that causal_clustering confs are not present + assertConfigurationPresentInDebugLog( debugLog, "causal_clustering.transaction_listen_address", + "*", + false ); + } + + @Test + void testJvmAdditionalNotOverridden() throws Exception + { + Path logMount; + + try(GenericContainer container = createContainer()) + { + Assumptions.assumeFalse( TestSettings.NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_400), "test not applicable in versions newer than 4.0." ); + + Path testOutputFolder = HostFileSystemOperations.createTempFolder( "jvmaddnotoverridden-" ); + //Mount /conf + Path confMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "conf-", + "/conf", testOutputFolder); + logMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "logs-", + "/logs", + testOutputFolder); + SetContainerUser.nonRootUser( container ); + //Create JvmAdditionalNotOverriden.conf file + Path confFile = Paths.get( "src", "test", "resources", "confs", "JvmAdditionalNotOverriden.conf" ); + Files.copy( confFile, confMount.resolve( "neo4j.conf" ) ); + //Start the container + container.setWaitStrategy( Wait.forHttp( "/" ).forPort( 7474 ).forStatusCode( 200 ) ); + container.start(); + } + + assertConfigurationPresentInDebugLog( logMount.resolve( "debug.log"), + "dbms.jvm.additional", + "-Dunsupported.dbms.udc.source=docker,-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005", + true ); + } + + @Test + void testShellExpansionAvoided() throws Exception + { + Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_400), "test only applicable to 4.0 and beyond." ); + + Path confMount; + try(GenericContainer container = createContainer().withEnv("NEO4J_dbms_security_procedures_unrestricted", "*")) + { + confMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "shellexpansionavoided-conf-", + "/conf" ); + + SetContainerUser.nonRootUser( container ); + //Start the container + container.setWaitStrategy( + Wait.forLogMessage( ".*Config Dumped.*", 1 ) + .withStartupTimeout( Duration.ofSeconds( 30 ) ) ); + container.setStartupCheckStrategy( new OneShotStartupCheckStrategy() ); + container.setCommand( "dump-config" ); + container.start(); + } + File conf = confMount.resolve( "neo4j.conf" ).toFile(); + Map configurations = parseConfFile(conf); + Assertions.assertTrue(configurations.containsKey("dbms.security.procedures.unrestricted"), "configuration not set from env var"); + Assertions.assertEquals("*", + configurations.get("dbms.security.procedures.unrestricted"), + "Configuration value should be *. If it's not docker-entrypoint.sh probably evaluated it as a glob expression."); + } +} diff --git a/src/test/java/com/neo4j/docker/neo4jserver/TestExtendedConf.java b/src/test/java/com/neo4j/docker/neo4jserver/TestExtendedConf.java new file mode 100644 index 00000000..6208fc27 --- /dev/null +++ b/src/test/java/com/neo4j/docker/neo4jserver/TestExtendedConf.java @@ -0,0 +1,231 @@ +package com.neo4j.docker.neo4jserver; + +import com.neo4j.docker.utils.HostFileSystemOperations; +import com.neo4j.docker.utils.Neo4jVersion; +import com.neo4j.docker.utils.SetContainerUser; +import com.neo4j.docker.utils.TestSettings; +import org.junit.Assert; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.ContainerLaunchException; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.OutputFrame; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; +import org.testcontainers.containers.wait.strategy.Wait; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.PosixFilePermission; +import java.time.Duration; +import java.util.HashSet; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class TestExtendedConf +{ + private static final Logger log = LoggerFactory.getLogger( TestExtendedConf.class ); + + @BeforeAll + static void ensureFeaturePresent() + { + Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 4,2,1 ) ), + "Extended configuration feature not available before 4.2" ); + } + + protected GenericContainer createContainer(String password) + { + return new GenericContainer(TestSettings.IMAGE_ID) + .withEnv("NEO4J_AUTH", password == null || password.isEmpty() ? "none" : "neo4j/" + password) + .withEnv("NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes") + .withEnv( "EXTENDED_CONF", "yeppers" ) + .withExposedPorts(7474, 7687) + .waitingFor( Wait.forHttp( "/" ).forPort( 7474 ).forStatusCode( 200 ) ) + .withLogConsumer(new Slf4jLogConsumer( log )); + } + + + @ParameterizedTest + @ValueSource(strings = {"", "secretN30"}) + public void shouldStartWithExtendedConf(String password) + { + try(GenericContainer container = createContainer(password)) + { + container.setWaitStrategy( Wait.forHttp( "/" ).forPort( 7474 ).forStatusCode( 200 ) ); + container.start(); + + Assertions.assertTrue( container.isRunning() ); + assertPasswordChangedLogIsCorrect( password, container ); + } + } + + private void assertPasswordChangedLogIsCorrect( String password, GenericContainer container ) + { + if ( password.isEmpty()) { + Assertions.assertFalse( container.getLogs( OutputFrame.OutputType.STDOUT).contains( "Changed password for user 'neo4j'." ) ); + } else { + Assertions.assertTrue( container.getLogs( OutputFrame.OutputType.STDOUT).contains( "Changed password for user 'neo4j'." ) ); + } + } + + @ParameterizedTest + @ValueSource(strings = {"", "secretN30"}) + void testReadsTheExtendedConfFile_defaultUser(String password) throws Exception + { + // set up test folders + Path testOutputFolder = HostFileSystemOperations.createTempFolder( "extendedConfIsRead-" ); + Path confFolder = HostFileSystemOperations.createTempFolder( "conf-", testOutputFolder ); + Path logsFolder = HostFileSystemOperations.createTempFolder( "logs-", testOutputFolder ); + + // copy configuration file and set permissions + Path confFile = Paths.get( "src", "test", "resources", "confs", "ExtendedConf.conf" ); + Files.copy( confFile, confFolder.resolve( "neo4j.conf" ) ); + setFileOwnerToNeo4j( confFolder.resolve( "neo4j.conf" ) ); + chmodConfFilePermissions( confFolder.resolve( "neo4j.conf" ) ); + + // start container + try(GenericContainer container = createContainer(password)) + { + runContainerAndVerify( container, confFolder, logsFolder, password ); + } + } + + @ParameterizedTest + @ValueSource( strings = {"", "secretN30"} ) + void testInvalidExtendedConfFile_nonRootUser( String password ) throws Exception + { + // set up test folders + Path testOutputFolder = HostFileSystemOperations.createTempFolder( "extendedConfIsRead-" ); + Path confFolder = HostFileSystemOperations.createTempFolder( "conf-", testOutputFolder ); + + // copy configuration file and set permissions + Path confFile = Paths.get( "src", "test", "resources", "confs", "InvalidExtendedConf.conf" ); + Files.copy( confFile, confFolder.resolve( "neo4j.conf" ) ); + chmodConfFilePermissions( confFolder.resolve( "neo4j.conf" ) ); + + try(GenericContainer container = createContainer( password )) + { + SetContainerUser.nonRootUser( container ); + container.withFileSystemBind( "/etc/passwd", "/etc/passwd", BindMode.READ_ONLY ); + container.withFileSystemBind( "/etc/group", "/etc/group", BindMode.READ_ONLY ); + HostFileSystemOperations.mountHostFolderAsVolume( container, confFolder, "/conf" ); + container.setStartupCheckStrategy( new OneShotStartupCheckStrategy().withTimeout( Duration.ofSeconds( 30 ) ) ); + container.setWaitStrategy( + Wait.forLogMessage( ".*this is an error message from inside neo4j config command expansion.*", 1 ) + .withStartupTimeout( Duration.ofSeconds( 30 ) ) ); + + Assert.assertThrows( "Container should have errored on start", + ContainerLaunchException.class, + () -> container.start() ); + + String logs = container.getLogs(); + // check that error messages from neo4j are visible in docker logs + Assertions.assertTrue( logs.contains( "Error evaluating value for setting 'dbms.logs.http.rotation.keep_number'" ) ); + // check that error messages from the command that failed are visible in docker logs + Assertions.assertTrue( logs.contains( "this is an error message from inside neo4j config command expansion" ) ); + // check that the error is only encountered once (i.e. we quit the docker entrypoint the first time it was encountered) + Assertions.assertEquals( 1, countOccurrences( Pattern.compile( "Error evaluating value for setting" ), logs ) ); + } + } + + private int countOccurrences( Pattern pattern, String inString ) + { + Matcher matcher = pattern.matcher( inString ); + int count = 0; + while ( matcher.find() ) + { + count = count + 1; + } + return count; + } + + @ParameterizedTest + @ValueSource(strings = {"", "secretN30"}) + void testReadsTheExtendedConfFile_nonRootUser(String password) throws Exception + { + // set up test folders + Path testOutputFolder = HostFileSystemOperations.createTempFolder( "extendedConfIsRead-" ); + Path confFolder = HostFileSystemOperations.createTempFolder( "conf-", testOutputFolder ); + Path logsFolder = HostFileSystemOperations.createTempFolder( "logs-", testOutputFolder ); + + // copy configuration file and set permissions + Path confFile = Paths.get( "src", "test", "resources", "confs", "ExtendedConf.conf" ); + Files.copy( confFile, confFolder.resolve( "neo4j.conf" ) ); + chmodConfFilePermissions( confFolder.resolve( "neo4j.conf" ) ); + + try(GenericContainer container = createContainer(password)) + { + SetContainerUser.nonRootUser( container ); + container.withFileSystemBind( "/etc/passwd", "/etc/passwd", BindMode.READ_ONLY ); + container.withFileSystemBind( "/etc/group", "/etc/group", BindMode.READ_ONLY ); + runContainerAndVerify( container, confFolder, logsFolder, password ); + } + } + + private void runContainerAndVerify(GenericContainer container, Path confFolder, Path logsFolder, String password) throws Exception + { + HostFileSystemOperations.mountHostFolderAsVolume( container, confFolder, "/conf" ); + HostFileSystemOperations.mountHostFolderAsVolume( container, logsFolder, "/logs" ); + + container.start(); + + Path debugLog = logsFolder.resolve("debug.log"); + Assert.assertTrue("Did not write debug log", debugLog.toFile().exists()); + + //Check if the container reads the conf file + Stream lines = Files.lines( debugLog); + Optional isMatch = lines.filter( s -> s.contains("dbms.logs.http.rotation.keep_number=20")).findFirst(); + lines.close(); + Assertions.assertTrue( isMatch.isPresent(), "dbms.max_databases was not set correctly"); + + //Check the password was changed if set + assertPasswordChangedLogIsCorrect( password, container ); + } + + private void chmodConfFilePermissions( Path file ) throws IOException + { + + HashSet permissions = new HashSet() + {{ + add( PosixFilePermission.OWNER_READ ); + add( PosixFilePermission.OWNER_WRITE ); + }}; + + if ( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 4, 3, 0 ) ) ) + { + permissions.add( PosixFilePermission.GROUP_READ ); + } + Files.setPosixFilePermissions( file, permissions ); + } + + private void setFileOwnerToNeo4j(Path file) throws Exception + { + ProcessBuilder pb = new ProcessBuilder( "chown", "7474:7474", file.toAbsolutePath().toString() ).redirectErrorStream( true ); + Process proc = pb.start(); + proc.waitFor(); + if(proc.exitValue() != 0) + { + String errorMsg = new BufferedReader( new InputStreamReader( proc.getInputStream() ) ) + .lines() + .collect( Collectors.joining() ); + // if we cannot set up test conditions properly, abort test but don't register a test failure. + Assumptions.assumeTrue( false, + "Could not change owner of test file to 7474. User needs to be in sudoers list. Error:\n" + + errorMsg ); + } + return; + } +} diff --git a/src/test/java/com/neo4j/docker/neo4jserver/TestHACluster.java b/src/test/java/com/neo4j/docker/neo4jserver/TestHACluster.java new file mode 100644 index 00000000..82b2769b --- /dev/null +++ b/src/test/java/com/neo4j/docker/neo4jserver/TestHACluster.java @@ -0,0 +1,115 @@ +package com.neo4j.docker.neo4jserver; + +import com.neo4j.docker.utils.Neo4jVersion; +import com.neo4j.docker.utils.SetContainerUser; +import com.neo4j.docker.utils.TestSettings; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.DockerComposeContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.containers.wait.strategy.WaitStrategy; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.Random; + +import org.neo4j.driver.AuthTokens; +import org.neo4j.driver.Driver; +import org.neo4j.driver.GraphDatabase; +import org.neo4j.driver.Session; +import org.neo4j.driver.Result; + +@Disabled +public class TestHACluster +{ + private Random rng = new Random( ); + private static Logger log = LoggerFactory.getLogger( TestHACluster.class); + private String dbPassword = "neo"; + + private void putInitialDataIntoContainer( String boltUri ) + { + Driver driver = GraphDatabase.driver( boltUri, AuthTokens.basic( "neo4j", dbPassword)); + try ( Session session = driver.session()) + { + Result rs = session.run( "CREATE (arne:dog {name:'Arne'})-[:SNIFFS]->(bosse:dog {name:'Bosse'}) RETURN arne.name"); + Assertions.assertEquals( "Arne", rs.single().get( 0 ).asString(), "did not receive expected result from cypher CREATE query" ); + } + driver.close(); + } + + private void verifyDataInContainer( String boltUri ) + { + Driver driver = GraphDatabase.driver( boltUri, AuthTokens.basic( "neo4j", dbPassword)); + try ( Session session = driver.session()) + { + Result rs = session.run( "MATCH (a:dog)-[:SNIFFS]->(b:dog) RETURN a.name"); + Assertions.assertEquals( "Arne", rs.single().get( 0 ).asString(), "did not receive expected result from cypher CREATE query" ); + } + driver.close(); + } + + private String getBoltUriForService(DockerComposeContainer container, String service) + { + return "bolt://" + container.getServiceHost( service, 7687 ) + + ":" + container.getServicePort( service, 7687 ); + } + + @Test + void testHAStartsOK() throws Exception + { + Assumptions.assumeTrue( TestSettings.EDITION == TestSettings.Edition.ENTERPRISE, + "HA Tests don't apply to community version"); + Assumptions.assumeFalse( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3,5,0 ) ), + "HA Tests don't apply to versions 3.5 and later"); + Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isNewerThan( new Neo4jVersion( 3,0,0 )), + "HA Tests don't apply before 3.0"); + + Path logDir = TestSettings.TEST_TMP_FOLDER.resolve( String.format( "HA_cluster_%04d", rng.nextInt(10000 ) )); + log.info( "writing HA test logs into "+logDir.toString() ); + + Path composeTemplate = Paths.get( "src", "test", "resources", "ha-cluster-compose.yml" ); + Path composeFile = logDir.resolve( "ha-cluster-compose.yml" ); + + // read the HA compose file template and replace placeholders + String composeContent = new String( Files.readAllBytes( composeTemplate ) ); + composeContent = composeContent + .replaceAll( "%%USERIDGROUPID%%", SetContainerUser.getNonRootUserString() ) + .replaceAll( "%%IMAGE%%", TestSettings.IMAGE_ID ) + .replaceAll( "%%LOGS_DIR%%", logDir.toAbsolutePath().toString() ); + + // create log folders + Files.createDirectories( logDir.resolve( "master" ) ); + Files.createDirectories( logDir.resolve( "slave1" ) ); + Files.createDirectories( logDir.resolve( "slave2" ) ); + + // save new compose file + Files.write( composeFile, composeContent.getBytes() ); + + // now actually start the cluster + WaitStrategy waiter = Wait.forListeningPort().withStartupTimeout( Duration.ofSeconds( 90 ) ); + DockerComposeContainer clusteringContainer = new DockerComposeContainer( composeFile.toFile() ) + .withLocalCompose(true) + .withExposedService( "master", 7687 ) + .withExposedService( "slave1", 7687 ) + .waitingFor( "master", waiter) + .waitingFor( "slave1", waiter); + clusteringContainer.start(); + + // write some data + log.info( "Cluster started, writing data to master" ); + putInitialDataIntoContainer( getBoltUriForService( clusteringContainer, "master" ) ); + + // read some data + log.info( "Reading data from slave" ); + verifyDataInContainer( getBoltUriForService( clusteringContainer, "slave1" ) ); + + // teardown resources + clusteringContainer.stop(); + } +} diff --git a/src/test/java/com/neo4j/docker/neo4jserver/TestMounting.java b/src/test/java/com/neo4j/docker/neo4jserver/TestMounting.java new file mode 100644 index 00000000..d6436ecb --- /dev/null +++ b/src/test/java/com/neo4j/docker/neo4jserver/TestMounting.java @@ -0,0 +1,291 @@ +package com.neo4j.docker.neo4jserver; + +import com.github.dockerjava.api.command.CreateContainerCmd; +import com.github.dockerjava.api.model.Bind; +import com.neo4j.docker.utils.DatabaseIO; +import com.neo4j.docker.utils.HostFileSystemOperations; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import com.neo4j.docker.utils.SetContainerUser; +import com.neo4j.docker.utils.Neo4jVersion; +import com.neo4j.docker.utils.TestSettings; +import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; +import org.testcontainers.containers.wait.strategy.Wait; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.Random; +import java.util.function.Consumer; +import java.util.stream.Stream; + + +public class TestMounting +{ + private static Logger log = LoggerFactory.getLogger( TestMounting.class ); + + static Stream defaultUserFlagSecurePermissionsFlag() + { + // "asUser={0}, secureFlag={1}" + // expected behaviour is that if you set --user flag, your data should be read/writable + // if you don't set --user flag then read/writability should be controlled by the secure file permissions flag + // the asUser=true, secureflag=false combination is tested separately because the container should fail to start. + return Stream.of( + Arguments.arguments( false, false ), + Arguments.arguments( true, false ), + Arguments.arguments( true, true )); + } + + private GenericContainer setupBasicContainer( boolean asCurrentUser, boolean isSecurityFlagSet ) + { + log.info( "Running as user {}, {}", + asCurrentUser?"non-root":"root", + isSecurityFlagSet?"with secure file permissions":"with unsecured file permissions" ); + + GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID ); + container.withExposedPorts( 7474, 7687 ) + .withLogConsumer( new Slf4jLogConsumer( log ) ) + .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" ) + .withEnv( "NEO4J_AUTH", "none" ); + if(asCurrentUser) + { + SetContainerUser.nonRootUser( container ); + } + if(isSecurityFlagSet) + { + container.withEnv( "SECURE_FILE_PERMISSIONS", "yes" ); + } + return container; + } + + private void verifySingleFolder( Path folderToCheck, boolean shouldBeWritable ) + { + String folderForDiagnostics = folderToCheck.toAbsolutePath().toString(); + + Assertions.assertTrue( folderToCheck.toFile().exists(), "did not create " + folderForDiagnostics + " folder on host" ); + if( shouldBeWritable ) + { + Assertions.assertTrue( folderToCheck.toFile().canRead(), "cannot read host "+folderForDiagnostics+" folder" ); + Assertions.assertTrue(folderToCheck.toFile().canWrite(), "cannot write to host "+folderForDiagnostics+" folder" ); + } + } + + private void verifyDataFolderContentsArePresentOnHost( Path dataMount, boolean shouldBeWritable ) + { + //verifySingleFolder( dataMount.resolve( "dbms" ), shouldBeWritable ); + verifySingleFolder( dataMount.resolve( "databases" ), shouldBeWritable ); + + if(TestSettings.NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_400 )) + { + verifySingleFolder( dataMount.resolve( "transactions" ), shouldBeWritable ); + } + } + + private void verifyLogsFolderContentsArePresentOnHost( Path logsMount, boolean shouldBeWritable ) + { + verifySingleFolder( logsMount, shouldBeWritable ); + Assertions.assertTrue( logsMount.resolve( "debug.log" ).toFile().exists(), + "Neo4j did not write a debug.log file to "+logsMount.toString() ); + Assertions.assertEquals( shouldBeWritable, + logsMount.resolve( "debug.log" ).toFile().canWrite(), + String.format( "The debug.log file should %sbe writable", shouldBeWritable ? "" : "not ") ); + } + + + @Test + void testDumpConfig( ) throws Exception + { + try(GenericContainer container = setupBasicContainer( true, false )) + { + Path confMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "dumpconfig-conf-", + "/conf" ); + container.setWaitStrategy( + Wait.forLogMessage( ".*Config Dumped.*", 1 ) + .withStartupTimeout( Duration.ofSeconds( 30 ) ) ); + container.setStartupCheckStrategy( new OneShotStartupCheckStrategy() ); + container.withCommand( "dump-config" ); + container.start(); + + Path expectedConfDumpFile = confMount.resolve( "neo4j.conf" ); + Assertions.assertTrue( expectedConfDumpFile.toFile().exists(), + "dump-config did not dump the config file to " + confMount.toString() ); + } + } + + + @ParameterizedTest(name = "asUser={0}, secureFlag={1}") + @MethodSource( "defaultUserFlagSecurePermissionsFlag" ) + void testCanMountJustDataFolder(boolean asCurrentUser, boolean isSecurityFlagSet) throws IOException + { + Assumptions.assumeTrue(TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3,1,0 ) ), + "User checks not valid before 3.1" ); + + try(GenericContainer container = setupBasicContainer( asCurrentUser, isSecurityFlagSet )) + { + Path dataMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "canmountjustdata-", + "/data" ); + container.start(); + + // neo4j should now have started, so there'll be stuff in the data folder + // we need to check that stuff is readable and owned by the correct user + verifyDataFolderContentsArePresentOnHost( dataMount, asCurrentUser ); + } + } + + @ParameterizedTest(name = "asUser={0}, secureFlag={1}") + @MethodSource( "defaultUserFlagSecurePermissionsFlag" ) + void testCanMountJustLogsFolder(boolean asCurrentUser, boolean isSecurityFlagSet) throws IOException + { + Assumptions.assumeTrue(TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3,1,0 ) ), + "User checks not valid before 3.1" ); + + try(GenericContainer container = setupBasicContainer( asCurrentUser, isSecurityFlagSet )) + { + Path logsMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "canmountjustlogs-", + "/logs" ); + container.start(); + + verifyLogsFolderContentsArePresentOnHost( logsMount, asCurrentUser ); + } + } + + @ParameterizedTest(name = "asUser={0}, secureFlag={1}") + @MethodSource( "defaultUserFlagSecurePermissionsFlag" ) + void testCanMountDataAndLogsFolder(boolean asCurrentUser, boolean isSecurityFlagSet) throws IOException + { + Assumptions.assumeTrue(TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3,1,0 ) ), + "User checks not valid before 3.1" ); + + try(GenericContainer container = setupBasicContainer( asCurrentUser, isSecurityFlagSet )) + { + Path testOutputFolder = HostFileSystemOperations.createTempFolder( "canmountdataandlogs-" ); + Path dataMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "data-", "/data", testOutputFolder + ); + Path logsMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "logs-", "/logs", testOutputFolder + ); + container.start(); + + verifyDataFolderContentsArePresentOnHost( dataMount, asCurrentUser ); + verifyLogsFolderContentsArePresentOnHost( logsMount, asCurrentUser ); + } + } + + @Test + void testCantWriteIfSecureEnabledAndNoPermissions_data() throws IOException + { + Assumptions.assumeTrue(TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3,1,0 ) ), + "User checks not valid before 3.1" ); + + try(GenericContainer container = setupBasicContainer( false, true )) + { + HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "nopermissioninsecuremode-data-", + "/data" ); + + // currently Neo4j will try to start and fail. It should be fixed to throw an error and not try starting + // container.setWaitStrategy( Wait.forLogMessage( "[fF]older /data is not accessible for user", 1 ).withStartupTimeout( Duration.ofSeconds( 20 ) ) ); + container.setWaitStrategy( Wait.forListeningPort() + .withStartupTimeout( Duration.ofSeconds( 20 ) ) ); + Assertions.assertThrows( org.testcontainers.containers.ContainerLaunchException.class, + () -> container.start(), + "Neo4j should not start in secure mode if data folder is unwritable" ); + } + } + + @Test + void testCantWriteIfSecureEnabledAndNoPermissions_logs() throws IOException + { + Assumptions.assumeTrue(TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3,1,0 ) ), + "User checks not valid before 3.1" ); + + try(GenericContainer container = setupBasicContainer( false, true )) + { + HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "nopermissioninsecuremode-logs-", + "/logs" ); + + // currently Neo4j will try to start and fail. It should be fixed to throw an error and not try starting + // container.setWaitStrategy( Wait.forLogMessage( "[fF]older /logs is not accessible for user", 1 ).withStartupTimeout( Duration.ofSeconds( 20 ) ) ); + container.setWaitStrategy( Wait.forListeningPort() + .withStartupTimeout( Duration.ofSeconds( 20 ) ) ); + Assertions.assertThrows( org.testcontainers.containers.ContainerLaunchException.class, + () -> container.start(), + "Neo4j should not start in secure mode if logs folder is unwritable" ); + } + } + + @ParameterizedTest(name = "as current user={0}") + @ValueSource(booleans = {true, false}) + void canMountAllTheThings_fileMounts(boolean asCurrentUser) throws Exception + { + Path testOutputFolder = HostFileSystemOperations.createTempFolder( "mount-everything-" ); + + try(GenericContainer container = setupBasicContainer( asCurrentUser, false )) + { + var configMountPath = HostFileSystemOperations.createTempFolderAndMountAsVolume( container, "conf", "/conf", testOutputFolder ); + Path confFile = Paths.get( "src", "test", "resources", "confs", "MountConf.conf" ); + Files.copy( confFile, configMountPath.resolve( "neo4j.conf" ) ); + + HostFileSystemOperations.createTempFolderAndMountAsVolume( container, "data", "/data", testOutputFolder ); + HostFileSystemOperations.createTempFolderAndMountAsVolume( container, "import", "/import", + testOutputFolder ); + HostFileSystemOperations.createTempFolderAndMountAsVolume( container, "logs", "/logs", testOutputFolder ); + HostFileSystemOperations.createTempFolderAndMountAsVolume( container, "metrics", "/metrics", + testOutputFolder ); + HostFileSystemOperations.createTempFolderAndMountAsVolume( container, "plugins", "/plugins", + testOutputFolder ); + container.start(); + DatabaseIO databaseIO = new DatabaseIO( container ); + // do some database writes so that we try writing to writable folders. + databaseIO.putInitialDataIntoContainer( "neo4j", "none" ); + databaseIO.verifyInitialDataInContainer( "neo4j", "none" ); + } + } + + @ParameterizedTest(name = "as current user={0}") + @ValueSource(booleans = {true, false}) + void canMountAllTheThings_namedVolumes(boolean asCurrentUser) throws Exception + { + String id = String.format( "%04d", new Random().nextInt( 10000 )); + try(GenericContainer container = setupBasicContainer( asCurrentUser, false )) + { + container.withCreateContainerCmdModifier( + (Consumer) cmd -> cmd.getHostConfig().withBinds( + Bind.parse( "conf-" + id + ":/conf" ), // not sure what todo here? + Bind.parse("data-"+id+":/data"), + Bind.parse("import-"+id+":/import"), + Bind.parse("logs-"+id+":/logs"), + //Bind.parse("metrics-"+id+":/metrics"), //todo metrics needs to be writable but we aren't chowning in the dockerfile, so a named volume for metrics will fail + Bind.parse("plugins-"+id+":/plugins") + )); + container.start(); + DatabaseIO databaseIO = new DatabaseIO( container ); + // do some database writes so that we try writing to writable folders. + databaseIO.putInitialDataIntoContainer( "neo4j", "none" ); + databaseIO.verifyInitialDataInContainer( "neo4j", "none" ); + } + } +} diff --git a/src/test/java/com/neo4j/docker/neo4jserver/TestPasswords.java b/src/test/java/com/neo4j/docker/neo4jserver/TestPasswords.java new file mode 100644 index 00000000..2a610929 --- /dev/null +++ b/src/test/java/com/neo4j/docker/neo4jserver/TestPasswords.java @@ -0,0 +1,195 @@ +package com.neo4j.docker.neo4jserver; + +import com.neo4j.docker.utils.DatabaseIO; +import com.neo4j.docker.utils.HostFileSystemOperations; +import com.neo4j.docker.utils.Neo4jVersion; +import com.neo4j.docker.utils.SetContainerUser; +import com.neo4j.docker.utils.TestSettings; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.output.WaitingConsumer; +import org.testcontainers.containers.wait.strategy.Wait; + +import java.nio.file.Path; +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +public class TestPasswords +{ + private static Logger log = LoggerFactory.getLogger( TestPasswords.class); + + private GenericContainer createContainer( boolean asCurrentUser ) + { + GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID ); + container.withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" ) + .withExposedPorts( 7474, 7687 ) + .withLogConsumer( new Slf4jLogConsumer( log ) ) + .waitingFor( Wait.forHttp( "/" ) + .forPort( 7474 ) + .forStatusCode( 200 ) + .withStartupTimeout( Duration.ofSeconds( 90 ) ) ); + if(asCurrentUser) + { + SetContainerUser.nonRootUser( container ); + } + return container; + } + + @Test + void testNoPassword() + { + // we test that setting NEO4J_AUTH to none lets the database start in TestBasic.java but not that we can read/write the database + try(GenericContainer container = createContainer( false )) + { + container.withEnv( "NEO4J_AUTH", "none" ); + container.start(); + DatabaseIO db = new DatabaseIO(container); + db.putInitialDataIntoContainer( "neo4j", "none" ); + db.verifyInitialDataInContainer( "neo4j", "none" ); + } + } + + @Test + void testPasswordCantBeNeo4j() throws Exception + { + try(GenericContainer failContainer = new GenericContainer( TestSettings.IMAGE_ID ).withLogConsumer( new Slf4jLogConsumer( log ) )) + { + if ( TestSettings.EDITION == TestSettings.Edition.ENTERPRISE ) + { + failContainer.withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" ); + } + failContainer.withEnv( "NEO4J_AUTH", "neo4j/neo4j" ); + failContainer.start(); + + WaitingConsumer waitingConsumer = new WaitingConsumer(); + failContainer.followOutput( waitingConsumer ); + + Assertions.assertDoesNotThrow( () -> waitingConsumer.waitUntil( + frame -> frame.getUtf8String().contains("Invalid value for password" ), 10, TimeUnit.SECONDS ), + "did not error due to invalid password" ); + } + } + + @Test + void testDefaultPasswordAndPasswordResetIfNoNeo4jAuthSet() + { + try(GenericContainer container = createContainer( true )) + { + log.info( "Starting first container as current user and not specifying NEO4J_AUTH" ); + container.start(); + DatabaseIO db = new DatabaseIO(container); + // try with no password, this should fail because the default password should be applied with no NEO4J_AUTH env variable + Assertions.assertThrows( org.neo4j.driver.exceptions.AuthenticationException.class, + () -> db.putInitialDataIntoContainer( "neo4j", "" ), + "Able to access database with no password, even though NEO4J_AUTH=none was not specified!"); + Assertions.assertThrows( org.neo4j.driver.exceptions.ClientException.class, + () -> db.putInitialDataIntoContainer( "neo4j", "neo4j" ), + "Was not prompted for a new password when using default"); + db.changePassword( "neo4j", "neo4j", "newpassword" ); + db.putInitialDataIntoContainer( "neo4j", "newpassword" ); + } + } + + @ParameterizedTest(name = "as current user={0}") + @ValueSource(booleans = {true, false}) + void testCanSetPassword( boolean asCurrentUser ) throws Exception + { + // create container and mount /data folder so that data can persist between sessions + String password = "some_valid_password"; + Path dataMount; + + try(GenericContainer firstContainer = createContainer( asCurrentUser )) + { + firstContainer.withEnv( "NEO4J_AUTH", "neo4j/"+password ); + dataMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + firstContainer, + "password-defaultuser-data-", + "/data" ); + log.info( String.format( "Starting first container as %s user and setting password", + asCurrentUser? "current" : "default" ) ); + // create a database with stuff in + firstContainer.start(); + DatabaseIO db = new DatabaseIO(firstContainer); + db.putInitialDataIntoContainer( "neo4j", password ); + } + + // with a new container, check the database data. + try(GenericContainer secondContainer = createContainer( asCurrentUser )) + { + HostFileSystemOperations.mountHostFolderAsVolume( secondContainer, dataMount, "/data" ); + log.info( "starting new container with same /data mount as same user without setting password" ); + secondContainer.start(); + DatabaseIO db = new DatabaseIO(secondContainer); + db.verifyInitialDataInContainer( "neo4j", password ); + } + } + + + @ParameterizedTest(name = "as current user={0}") + @ValueSource(booleans = {true, false}) + void testSettingNeo4jAuthDoesntOverrideExistingPassword( boolean asCurrentUser ) throws Exception + { + String password = "some_valid_password"; + Path dataMount; + + try(GenericContainer firstContainer = createContainer( asCurrentUser )) + { + firstContainer.withEnv( "NEO4J_AUTH", "neo4j/"+password ); + dataMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + firstContainer, + "password-envoverride-data-", + "/data" ); + + // create a database with stuff in + log.info( String.format( "Starting first container as %s user and setting password", + asCurrentUser? "current" : "default" ) ); + firstContainer.start(); + DatabaseIO db = new DatabaseIO(firstContainer); + db.putInitialDataIntoContainer( "neo4j", password ); + } + + // with a new container, check the database data. + try(GenericContainer secondContainer = createContainer( asCurrentUser )) + { + String wrongPassword = "not_the_password"; + secondContainer.withEnv( "NEO4J_AUTH", "neo4j/"+wrongPassword ); + HostFileSystemOperations.mountHostFolderAsVolume( secondContainer, dataMount, "/data" ); + log.info( "starting new container with same /data mount as same user without setting password" ); + secondContainer.start(); + DatabaseIO db = new DatabaseIO(secondContainer); + db.verifyInitialDataInContainer( "neo4j", password ); + Assertions.assertThrows( org.neo4j.driver.exceptions.AuthenticationException.class, + () -> db.verifyConnectivity( "neo4j", wrongPassword) ); + } + } + + @Test + void testPromptsForPasswordReset() + { + Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3,6,0 ) ), + "Require password reset is only a feature in 3.6 onwards"); + try(GenericContainer container = createContainer( false )) + { + String user = "neo4j"; + String intialPass = "apassword"; + String resetPass = "new_password"; + container.withEnv("NEO4J_AUTH", user+"/"+intialPass+"/true" ); + container.start(); + DatabaseIO db = new DatabaseIO(container); + Assertions.assertThrows( org.neo4j.driver.exceptions.ClientException.class, + () -> db.putInitialDataIntoContainer( user, intialPass ), + "Neo4j did not error because of password reset requirement"); + + db.changePassword( user, intialPass, resetPass ); + db.putInitialDataIntoContainer( user, resetPass ); + db.verifyInitialDataInContainer( user, resetPass ); + } + } +} diff --git a/src/test/java/com/neo4j/docker/neo4jserver/TestPluginInstallation.java b/src/test/java/com/neo4j/docker/neo4jserver/TestPluginInstallation.java new file mode 100644 index 00000000..f8920b66 --- /dev/null +++ b/src/test/java/com/neo4j/docker/neo4jserver/TestPluginInstallation.java @@ -0,0 +1,182 @@ +package com.neo4j.docker.neo4jserver; + +import com.neo4j.docker.neo4jserver.plugins.ExampleNeo4jPlugin; +import com.neo4j.docker.utils.HostFileHttpHandler; +import com.neo4j.docker.utils.HttpServerRule; +import com.neo4j.docker.neo4jserver.plugins.JarBuilder; +import com.neo4j.docker.utils.SetContainerUser; +import com.neo4j.docker.utils.TestSettings; +import org.junit.Rule; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.migrationsupport.rules.EnableRuleMigrationSupport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.Testcontainers; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.shaded.com.google.common.io.Files; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; + +import org.neo4j.driver.AuthTokens; +import org.neo4j.driver.Driver; +import org.neo4j.driver.GraphDatabase; +import org.neo4j.driver.Record; +import org.neo4j.driver.Session; +import org.neo4j.driver.Result; + +import static com.neo4j.docker.utils.TestSettings.NEO4J_VERSION; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@EnableRuleMigrationSupport +public class TestPluginInstallation +{ + private static final int DEFAULT_BROWSER_PORT = 7474; + private static final int DEFAULT_BOLT_PORT = 7687; + + private static final String versions = "versions.json"; + private static final String myPlugin = "myPlugin.jar"; + + private static final Logger log = LoggerFactory.getLogger( TestPluginInstallation.class ); + + @Rule + public HttpServerRule httpServer = new HttpServerRule(); + + private GenericContainer container; + + @BeforeAll + public static void checkVersionIsCompatibleWithTest() + { + // Should work for all versions + } + + private void createContainerWithTestingPlugin() + { + Testcontainers.exposeHostPorts( httpServer.PORT ); + container = new GenericContainer( TestSettings.IMAGE_ID ); + + container.withEnv( "NEO4J_AUTH", "neo4j/neo" ) + .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" ) + .withEnv( "NEO4JLABS_PLUGINS", "[\"_testing\"]" ) + .withExposedPorts( DEFAULT_BROWSER_PORT, DEFAULT_BOLT_PORT ) + .withLogConsumer( new Slf4jLogConsumer( log ) ); + + SetContainerUser.nonRootUser( container ); + } + + @BeforeEach + public void setUp( @TempDir Path pluginsDir ) throws Exception + { + File versionsJson = pluginsDir.resolve( versions ).toFile(); + + Files.write( getResource( "versions.json" ).replace( "$NEO4JVERSION", NEO4J_VERSION.toString() ), versionsJson, StandardCharsets.UTF_8 ); + + File myPluginJar = pluginsDir.resolve( myPlugin ).toFile(); + + new JarBuilder().createJarFor( myPluginJar, ExampleNeo4jPlugin.class, ExampleNeo4jPlugin.PrimitiveOutput.class ); + + httpServer.registerHandler( versions, new HostFileHttpHandler( versionsJson, "application/json" ) ); + httpServer.registerHandler( myPlugin, new HostFileHttpHandler( myPluginJar, "application/java-archive" ) ); + + createContainerWithTestingPlugin(); + container.setWaitStrategy( Wait.forHttp( "/" ).forPort( DEFAULT_BROWSER_PORT ).forStatusCode( 200 ) ); + } + + @Test + @DisabledIfEnvironmentVariable(named = "NEO4J_DOCKER_TESTS_TestPluginInstallation", matches = "ignore") + public void testPlugin() throws Exception + { + // When we start the neo4j docker container + container.start(); + + // Then the plugin is downloaded and placed in the plugins directory + String lsPluginsDir = container.execInContainer( "ls", "/var/lib/neo4j/plugins" ).getStdout(); + // Two options here because it varies depending on whether the plugins dir _only_ contains our file or if it contains multiple files + assertTrue( lsPluginsDir.contains( "\n_testing.jar\n" ) || lsPluginsDir.equals( "_testing.jar\n" ), "Plugin jar file not found in plugins directory" ); + + // When we connect to the database with the plugin + String boltAddress = "bolt://" + container.getContainerIpAddress() + ":" + container.getMappedPort( DEFAULT_BOLT_PORT ); + try ( Driver coreDriver = GraphDatabase.driver( boltAddress, AuthTokens.basic( "neo4j", "neo" ) ) ) + { + Session session = coreDriver.session(); + Result res = session.run( "CALL dbms.procedures() YIELD name, signature RETURN name, signature" ); + + // Then the procedure from the plugin is listed + assertTrue( res.stream().anyMatch( x -> x.get( "name" ).asString().equals( "com.neo4j.docker.neo4jserver.plugins.defaultValues" ) ), + "Missing procedure provided by our plugin" ); + + // When we call the procedure from the plugin + res = session.run( "CALL com.neo4j.docker.neo4jserver.plugins.defaultValues" ); + + // Then we get the response we expect + Record record = res.single(); + String message = "Result from calling our procedure doesnt match our expectations"; + assertEquals( "a string", record.get( "string" ).asString(), message ); + assertEquals( 42L, record.get( "integer" ).asInt(), message ); + assertEquals( 3.14d, record.get( "aFloat" ).asDouble(), 0.000001, message ); + assertEquals( true, record.get( "aBoolean" ).asBoolean(), message ); + assertFalse( res.hasNext(), "Our procedure should only return a single result" ); + + // Check that the config has been set + res = session.run ( "CALL dbms.listConfig() YIELD name, value WHERE name='dbms.security.procedures.unrestricted' RETURN value" ); + record = res.single(); + assertEquals( "com.neo4j.docker.neo4jserver.plugins.*", record.get( "value" ).asString(), "neo4j config not updated for plugin" ); + assertFalse( res.hasNext(), "Config lookup should only return a single result" ); + } + finally + { + container.stop(); + } + } + + @Test + @DisabledIfEnvironmentVariable(named = "NEO4J_DOCKER_TESTS_TestPluginInstallation", matches = "ignore") + public void testPluginConfigurationDoesNotOverrideUserSetValues() throws Exception + { + // When we set a config value explicitly + container = container.withEnv ("NEO4J_dbms_security_procedures_unrestricted", "foo" ); + // When we start the neo4j docker container + container.start(); + + // When we connect to the database with the plugin + String boltAddress = "bolt://" + container.getContainerIpAddress() + ":" + container.getMappedPort( DEFAULT_BOLT_PORT ); + try ( Driver coreDriver = GraphDatabase.driver( boltAddress, AuthTokens.basic( "neo4j", "neo" ) ) ) + { + Session session = coreDriver.session(); + // Check that the config remains as set by our env var and is not overriden by the plugin defaults + Result res = session.run ( "CALL dbms.listConfig() YIELD name, value WHERE name='dbms.security.procedures.unrestricted' RETURN value" ); + Record record = res.single(); + assertEquals( "foo", record.get( "value" ).asString(), "neo4j config should not be overriden by plugin" ); + assertFalse( res.hasNext(), "Config lookup should only return a single result" ); + } + finally + { + container.stop(); + } + } + + private String getResource( String path ) throws IOException + { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream( path ); + ByteArrayOutputStream result = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int length; + while ( (length = inputStream.read( buffer )) != -1 ) + { + result.write( buffer, 0, length ); + } + return result.toString( "UTF-8" ); + } +} diff --git a/src/test/java/com/neo4j/docker/neo4jserver/TestUpgrade.java b/src/test/java/com/neo4j/docker/neo4jserver/TestUpgrade.java new file mode 100644 index 00000000..364b69ee --- /dev/null +++ b/src/test/java/com/neo4j/docker/neo4jserver/TestUpgrade.java @@ -0,0 +1,146 @@ +package com.neo4j.docker.neo4jserver; + +import com.github.dockerjava.api.command.CreateContainerCmd; +import com.github.dockerjava.api.model.Bind; +import com.neo4j.docker.utils.DatabaseIO; +import com.neo4j.docker.utils.HostFileSystemOperations; +import com.neo4j.docker.utils.Neo4jVersion; +import com.neo4j.docker.utils.TestSettings; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; + +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import java.util.function.Consumer; + +@Disabled +public class TestUpgrade +{ + private static final Logger log = LoggerFactory.getLogger( TestUpgrade.class ); + private final String user = "neo4j"; + private final String password = "quality"; + + private GenericContainer makeContainer(String image) + { + GenericContainer container = new GenericContainer( image ); + container.withEnv( "NEO4J_AUTH", user + "/" + password ) + .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" ) + .withExposedPorts( 7474 ) + .withExposedPorts( 7687 ) + .withLogConsumer( new Slf4jLogConsumer( log ) ); + return container; + } + + private static List upgradableNeo4jVersions() + { + return Arrays.asList( new Neo4jVersion( 3, 5, 3 ), // 3.5.6 image introduced file permission changes, so we need to test upgrades before that version + new Neo4jVersion( 3, 5, 7 ), + Neo4jVersion.NEO4J_VERSION_400, + new Neo4jVersion( 4,1,0 )); + } + + + @ParameterizedTest(name = "upgrade from {0}") + @MethodSource("upgradableNeo4jVersions") + void canUpgradeNeo4j_fileMounts(Neo4jVersion upgradeFrom) throws Exception + { + Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isNewerThan( upgradeFrom ), "cannot upgrade from newer version "+upgradeFrom.toString() ); + String upgradeFromImage = getUpgradeFromImage( upgradeFrom ); + Path tmpMountFolder = HostFileSystemOperations.createTempFolder( "upgrade-"+upgradeFrom.major+upgradeFrom.minor+"-" ); + Path data, logs, imports, metrics; + + try(GenericContainer container = makeContainer( upgradeFromImage )) + { + data = HostFileSystemOperations.createTempFolderAndMountAsVolume( container, "data-", "/data", + tmpMountFolder ); + logs = HostFileSystemOperations.createTempFolderAndMountAsVolume( container, "logs-", "/logs", + tmpMountFolder ); + imports = HostFileSystemOperations.createTempFolderAndMountAsVolume( container, "import-", "/import", + tmpMountFolder ); + metrics = HostFileSystemOperations.createTempFolderAndMountAsVolume( container, "metrics-", "/metrics", + tmpMountFolder ); + container.start(); + DatabaseIO db = new DatabaseIO( container ); + db.putInitialDataIntoContainer( user, password ); + // stops container cleanly so that neo4j process has enough time to end. The autoclose doesn't seem to block. + container.getDockerClient().stopContainerCmd( container.getContainerId() ).exec(); + } + + try(GenericContainer container = makeContainer( TestSettings.IMAGE_ID )) + { + HostFileSystemOperations.mountHostFolderAsVolume( container, data, "/data" ); + HostFileSystemOperations.mountHostFolderAsVolume( container, logs, "/logs" ); + HostFileSystemOperations.mountHostFolderAsVolume( container, imports, "/import" ); + HostFileSystemOperations.mountHostFolderAsVolume( container, metrics, "/metrics" ); + container.withEnv( "NEO4J_dbms_allow__upgrade", "true" ); + container.start(); + DatabaseIO db = new DatabaseIO( container ); + db.verifyInitialDataInContainer( user, password ); + } + } + + @ParameterizedTest(name = "upgrade from {0}") + @MethodSource("upgradableNeo4jVersions") + void canUpgradeNeo4j_namedVolumes(Neo4jVersion upgradeFrom) throws Exception + { + Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isNewerThan( upgradeFrom ), "cannot upgrade from newer version "+upgradeFrom.toString() ); + String upgradeFromImage = getUpgradeFromImage( upgradeFrom ); + String id = String.format( "%04d", new Random().nextInt( 10000 )); + log.info( "creating volumes with id: "+id ); + + try(GenericContainer container = makeContainer( upgradeFromImage )) + { + container.withCreateContainerCmdModifier( + (Consumer) cmd -> cmd.getHostConfig().withBinds( + Bind.parse("upgrade-conf-"+id+":/conf"), + Bind.parse("upgrade-data-"+id+":/data"), + Bind.parse("upgrade-import-"+id+":/import"), + Bind.parse("upgrade-logs-"+id+":/logs"), + Bind.parse("upgrade-metrics-"+id+":/metrics"), + Bind.parse("upgrade-plugins-"+id+":/plugins") + )); + container.start(); + DatabaseIO db = new DatabaseIO( container ); + db.putInitialDataIntoContainer( user, password ); + container.getDockerClient().stopContainerCmd( container.getContainerId() ).exec(); + } + + try(GenericContainer container = makeContainer( TestSettings.IMAGE_ID )) + { + container.withCreateContainerCmdModifier( + (Consumer) cmd -> cmd.getHostConfig().withBinds( + Bind.parse("upgrade-conf-"+id+":/conf"), + Bind.parse("upgrade-data-"+id+":/data"), + Bind.parse("upgrade-import-"+id+":/import"), + Bind.parse("upgrade-logs-"+id+":/logs"), + Bind.parse("upgrade-metrics-"+id+":/metrics"), + Bind.parse("upgrade-plugins-"+id+":/plugins") + )); + container.withEnv( "NEO4J_dbms_allow__upgrade", "true" ); + container.start(); + DatabaseIO db = new DatabaseIO( container ); + db.verifyInitialDataInContainer( user, password ); + } + } + + + private String getUpgradeFromImage(Neo4jVersion ver) + { + if(TestSettings.EDITION == TestSettings.Edition.ENTERPRISE) + { + return "neo4j:" + ver.toString() + "-enterprise"; + } + else + { + return "neo4j:" + ver.toString(); + } + } +} diff --git a/src/test/java/com/neo4j/docker/neo4jserver/plugins/ExampleNeo4jPlugin.java b/src/test/java/com/neo4j/docker/neo4jserver/plugins/ExampleNeo4jPlugin.java new file mode 100644 index 00000000..08a84fb6 --- /dev/null +++ b/src/test/java/com/neo4j/docker/neo4jserver/plugins/ExampleNeo4jPlugin.java @@ -0,0 +1,47 @@ +package com.neo4j.docker.neo4jserver.plugins; + +import java.util.stream.Stream; + +import org.neo4j.graphdb.GraphDatabaseService; +import org.neo4j.logging.Log; +import org.neo4j.procedure.Context; +import org.neo4j.procedure.Name; +import org.neo4j.procedure.Procedure; + +/* +This class is a basic Neo4J plugin that defines a procedure which can be called via Cypher. + */ +public class ExampleNeo4jPlugin +{ + // Output data class containing primitive types + public static class PrimitiveOutput + { + public String string; + public long integer; + public double aFloat; + public boolean aBoolean; + + public PrimitiveOutput( String string, long integer, double aFloat, boolean aBoolean ) + { + this.string = string; + this.integer = integer; + this.aFloat = aFloat; + this.aBoolean = aBoolean; + } + } + + @Context + public GraphDatabaseService db; + + @Context + public Log log; + + // A Neo4j procedure that always returns fixed values + @Procedure + public Stream defaultValues( @Name( value = "string", defaultValue = "a string" ) String string, + @Name( value = "integer", defaultValue = "42" ) long integer, @Name( value = "float", defaultValue = "3.14" ) double aFloat, + @Name( value = "boolean", defaultValue = "true" ) boolean aBoolean ) + { + return Stream.of( new PrimitiveOutput( string, integer, aFloat, aBoolean ) ); + } +} \ No newline at end of file diff --git a/src/test/java/com/neo4j/docker/neo4jserver/plugins/JarBuilder.java b/src/test/java/com/neo4j/docker/neo4jserver/plugins/JarBuilder.java new file mode 100644 index 00000000..bcff5f19 --- /dev/null +++ b/src/test/java/com/neo4j/docker/neo4jserver/plugins/JarBuilder.java @@ -0,0 +1,45 @@ +package com.neo4j.docker.neo4jserver.plugins; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.jar.JarOutputStream; +import java.util.zip.ZipEntry; + +/** + * Utility to create jar files containing classes from the current classpath. + */ +public class JarBuilder +{ + public URL createJarFor( File f, Class... classesToInclude ) throws IOException + { + try ( FileOutputStream fout = new FileOutputStream( f ); JarOutputStream jarOut = new JarOutputStream( fout ) ) + { + for ( Class target : classesToInclude ) + { + String fileName = target.getName().replace( ".", "/" ) + ".class"; + jarOut.putNextEntry( new ZipEntry( fileName ) ); + jarOut.write( classCompiledBytes( fileName ) ); + jarOut.closeEntry(); + } + } + return f.toURI().toURL(); + } + + private byte[] classCompiledBytes( String fileName ) throws IOException + { + try ( InputStream in = getClass().getClassLoader().getResourceAsStream( fileName ) ) + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + while ( in.available() > 0 ) + { + out.write( in.read() ); + } + + return out.toByteArray(); + } + } +} diff --git a/src/test/java/com/neo4j/docker/utils/DatabaseIO.java b/src/test/java/com/neo4j/docker/utils/DatabaseIO.java new file mode 100644 index 00000000..624acf47 --- /dev/null +++ b/src/test/java/com/neo4j/docker/utils/DatabaseIO.java @@ -0,0 +1,149 @@ +package com.neo4j.docker.utils; + +import org.junit.jupiter.api.Assertions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import org.neo4j.driver.AuthToken; +import org.neo4j.driver.AuthTokens; +import org.neo4j.driver.Config; +import org.neo4j.driver.Driver; +import org.neo4j.driver.GraphDatabase; +import org.neo4j.driver.Record; +import org.neo4j.driver.Session; +import org.neo4j.driver.SessionConfig; +import org.neo4j.driver.Result; + +public class DatabaseIO +{ + private static Config TEST_DRIVER_CONFIG = Config.builder().withoutEncryption().build(); + private static final Logger log = LoggerFactory.getLogger( DatabaseIO.class ); + + private GenericContainer container; + private String boltUri; + + public DatabaseIO( GenericContainer container ) + { + this.container = container; +// this.boltUri = getBoltURIFromContainer( container ); + this.boltUri = "bolt://"+container.getContainerIpAddress()+":"+container.getMappedPort( 7687 ); + } + +// public static String getBoltURIFromContainer( GenericContainer container ) +// { +// return "bolt://"+container.getContainerIpAddress()+":"+container.getMappedPort( 7687 ); +// } + + public void putInitialDataIntoContainer( String user, String password ) + { + log.info( "Writing data into database" ); + List result = runCypherQuery( user, password,"CREATE (arne:dog {name:'Arne'})-[:SNIFFS]->(bosse:dog {name:'Bosse'}) RETURN arne.name" ); + Assertions.assertEquals( "Arne", result.get( 0 ).get( "arne.name" ).asString(), "did not receive expected result from cypher CREATE query" ); + } + + public void verifyInitialDataInContainer( String user, String password ) + { + log.info( "verifying data is present in the database" ); + List result = runCypherQuery( user, password,"MATCH (a:dog)-[:SNIFFS]->(b:dog) RETURN a.name"); + Assertions.assertEquals( "Arne", result.get( 0 ).get("a.name").asString(), "did not receive expected result from cypher MATCH query" ); + } + + public void putMoreDataIntoContainer( String user, String password ) + { + log.info( "Writing more data into database" ); + List result = runCypherQuery( user, password, + "MATCH (a:dog {name:'Arne'}) CREATE (armstrong:dog {name:'Armstrong'})-[:SNIFFS]->(a) return a.name, armstrong.name" ); + Assertions.assertEquals( "Arne", result.get( 0 ).get("a.name").asString(), + "did not receive expected result from cypher MATCH query" ); + Assertions.assertEquals( "Armstrong", result.get( 0 ).get( "armstrong.name" ).asString(), + "did not receive expected result from cypher CREATE query" ); + } + + public void verifyMoreDataIntoContainer( String user, String password, boolean extraDataShouldBeThere ) + { + log.info( "Verifying extra data is {}in database", extraDataShouldBeThere? "":"not " ); + List result = runCypherQuery( user, password,"MATCH (a:dog)-[:SNIFFS]->(b:dog) RETURN a.name"); + String dogs = result.stream() + .map( record -> record.get( 0 ).asString() ) + .sorted() + .collect( Collectors.joining(",")); + // dogs should now be a String which is a comma delimited list of dog names + + if(extraDataShouldBeThere) + { + Assertions.assertEquals( "Armstrong,Arne", dogs, "cypher query did not return correct data" ); + } + else + { + Assertions.assertEquals( "Arne", dogs, "cypher query did not return correct data" ); + } + } + + + + public void changePassword(String user, String oldPassword, String newPassword) + { + if(TestSettings.NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_400 )) + { + String cypher = "ALTER CURRENT USER SET PASSWORD FROM '"+oldPassword+"' TO '"+newPassword+"'"; + runCypherQuery( user, oldPassword, cypher, "system" ); + } + else + { + runCypherQuery( user, oldPassword, "CALL dbms.changePassword('"+newPassword+"')" ); + } + } + + public List runCypherQuery( String user, String password, String cypher) + { + // we don't just do runCypherQuery( user, password, cypher, "neo4j") because + // it breaks the upgrade tests from 3.5.x + List records; + Driver driver = GraphDatabase.driver( boltUri, getToken( user, password ), TEST_DRIVER_CONFIG ); + try ( Session session = driver.session()) + { + Result rs = session.run( cypher ); + records = rs.list(); + } + driver.close(); + return records; + } + + public List runCypherQuery( String user, String password, String cypher, String database) + { + List records; + Driver driver = GraphDatabase.driver( boltUri, getToken( user, password ), TEST_DRIVER_CONFIG ); + try ( Session session = driver.session(SessionConfig.forDatabase( database ))) + { + Result rs = session.run( cypher ); + records = rs.list(); + } + driver.close(); + return records; + } + + public void verifyConnectivity( String user, String password ) + { + GraphDatabase.driver( boltUri, + getToken( user, password ), + TEST_DRIVER_CONFIG ) + .verifyConnectivity(); + } + + private AuthToken getToken(String user, String password) + { + if(password.equals( "none" )) + { + return AuthTokens.none(); + } + else + { + return AuthTokens.basic( user, password ); + } + } +} diff --git a/src/test/java/com/neo4j/docker/utils/HostFileHttpHandler.java b/src/test/java/com/neo4j/docker/utils/HostFileHttpHandler.java new file mode 100644 index 00000000..5a1b8586 --- /dev/null +++ b/src/test/java/com/neo4j/docker/utils/HostFileHttpHandler.java @@ -0,0 +1,33 @@ +package com.neo4j.docker.utils; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; + +import java.io.File; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.nio.file.Files; + +/** + * HttpHandler that responds to all hhtp requests with the given file from the file system + */ +public class HostFileHttpHandler implements HttpHandler +{ + private final File file; + private final String contentType; + + public HostFileHttpHandler( File fileToDownload, String contentType ) + { + this.file = fileToDownload; + this.contentType = contentType; + } + + @Override + public void handle( HttpExchange exchange ) throws IOException + { + exchange.getResponseHeaders().add( "Content-Type", contentType ); + exchange.sendResponseHeaders( HttpURLConnection.HTTP_OK, file.length() ); + Files.copy( this.file.toPath(), exchange.getResponseBody() ); + exchange.close(); + } +} \ No newline at end of file diff --git a/src/test/java/com/neo4j/docker/utils/HostFileSystemOperations.java b/src/test/java/com/neo4j/docker/utils/HostFileSystemOperations.java new file mode 100644 index 00000000..d2cc9b37 --- /dev/null +++ b/src/test/java/com/neo4j/docker/utils/HostFileSystemOperations.java @@ -0,0 +1,74 @@ +package com.neo4j.docker.utils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.GenericContainer; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Random; + +public class HostFileSystemOperations +{ + private static Logger log = LoggerFactory.getLogger( HostFileSystemOperations.class); + private static Random rng = new Random( ); + + public static Path createTempFolderAndMountAsVolume( GenericContainer container, String hostFolderNamePrefix, + String containerMountPoint ) throws IOException + { + return createTempFolderAndMountAsVolume( container, hostFolderNamePrefix, containerMountPoint, + TestSettings.TEST_TMP_FOLDER ); + } + + public static Path createTempFolderAndMountAsVolume( GenericContainer container, String hostFolderNamePrefix, + String containerMountPoint, Path parentFolder ) throws IOException + { + String randomStr = String.format( "%04d", rng.nextInt(10000 ) ); // random 4 digit number + Path hostFolder = parentFolder.resolve( hostFolderNamePrefix + randomStr); + try + { + Files.createDirectories( hostFolder ); + } + catch ( IOException e ) + { + log.error( "could not create directory: " + hostFolder.toAbsolutePath().toString() ); + e.printStackTrace(); + throw e; + } + log.info( "Created folder "+hostFolder.toString() ); + mountHostFolderAsVolume( container, hostFolder, containerMountPoint ); + return hostFolder; + } + + public static void mountHostFolderAsVolume(GenericContainer container, Path hostFolder, String containerMountPoint) + { + container.withFileSystemBind( hostFolder.toAbsolutePath().toString(), + containerMountPoint, + BindMode.READ_WRITE ); + } + + public static Path createTempFolder( String folderNamePrefix ) throws IOException + { + return createTempFolder( folderNamePrefix, TestSettings.TEST_TMP_FOLDER ); + } + + public static Path createTempFolder( String folderNamePrefix, Path parentFolder ) throws IOException + { + String randomStr = String.format( "%04d", rng.nextInt(10000 ) ); // random 4 digit number + Path hostFolder = parentFolder.resolve( folderNamePrefix + randomStr); + try + { + Files.createDirectories( hostFolder ); + } + catch ( IOException e ) + { + log.error( "could not create directory: " + hostFolder.toAbsolutePath().toString() ); + e.printStackTrace(); + throw e; + } + + return hostFolder; + } +} diff --git a/src/test/java/com/neo4j/docker/utils/HttpServerRule.java b/src/test/java/com/neo4j/docker/utils/HttpServerRule.java new file mode 100644 index 00000000..146fc850 --- /dev/null +++ b/src/test/java/com/neo4j/docker/utils/HttpServerRule.java @@ -0,0 +1,42 @@ +package com.neo4j.docker.utils; + +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import org.junit.rules.ExternalResource; + +import java.net.InetSocketAddress; + +/** + * Runs a HTTP Server with to allow integration testing + */ +public class HttpServerRule extends ExternalResource +{ + public final int PORT = 3000; + private HttpServer server; + + @Override + protected void before() throws Throwable + { + server = HttpServer.create( new InetSocketAddress( PORT ), 0 ); + server.setExecutor( null ); // creates a default executor + server.start(); + } + + @Override + protected void after() + { + if ( server != null ) + { + server.stop( 0 ); // doesn't wait all current exchange handlers complete + } + } + + // Register a handler to provide desired behaviour on a specific uri path + public void registerHandler( String uriToHandle, HttpHandler httpHandler ) + { + if (!uriToHandle.startsWith( "/" )){ + uriToHandle = '/' + uriToHandle; + } + server.createContext( uriToHandle, httpHandler ); + } +} \ No newline at end of file diff --git a/src/test/java/com/neo4j/docker/utils/Neo4jVersion.java b/src/test/java/com/neo4j/docker/utils/Neo4jVersion.java new file mode 100644 index 00000000..6a2244f7 --- /dev/null +++ b/src/test/java/com/neo4j/docker/utils/Neo4jVersion.java @@ -0,0 +1,96 @@ +package com.neo4j.docker.utils; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +public class Neo4jVersion +{ + //public static final Neo4jVersion EXPECTED_NEO4J_VERSION = Neo4jVersion.fromVersionString( System.getenv( "NEO4J_VERSION" ) ); + //public static final Neo4jVersion LATEST_2X_VERSION = new Neo4jVersion(2,3,12); + //public static final Neo4jVersion LATEST_32_VERSION = new Neo4jVersion(3,2,14); + public static final Neo4jVersion NEO4J_VERSION_400 = new Neo4jVersion(4,0,0); + + public final int major; + public final int minor; + public final int patch; + public final String label; + + public static Neo4jVersion fromVersionString(String version) + { + // Could be one of the forms: + // A.B.C, A.B.C-alphaDD, A.B.C-betaDD, A.B.C-rcDD + // (?\d)\.(?\d)\.(?[\d]+)(?