diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..52b36886 --- /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 + +--- + +## 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) error messages, 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..0e1a2d65 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -8,26 +8,148 @@ 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 + +From Neo4j 4.4.0 onwards, the Neo4j image should be buildable on any architecture using the same build commands as [Building the Image](#building-the-image). + +For earlier versions of Neo4j, you may need to set the variable `NEO4J_BASE_IMAGE` to your architecture specific version of `openjdk:11-jdk-slim` (or `openjdk:8-jdk-slim` for versions before 4.0.0). + +Like with `amd64` images, you must still specify the **full** Neo4j version including major, minor and patch numbers. For example: + +```bash +NEO4J_BASE_IMAGE=arm64v8/openjdk:11-jdk-slim +NEO4JVERSION=4.3.7 make clean build +``` + + +## 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. | + + + + +### Running with podman + +Tests in this module are using testcontainers. The framework expects you to have docker available on your system. +And there are some issues like described here: https://github.com/testcontainers/testcontainers-java/issues/2088 + +TLDR on what you need to do to be able to use podman: + +1. Make sure you have podman service running. For example: ```podman system service --time=0 unix:///tmp/podman.sock``` + +2. Add those environment variables: +``` +DOCKER_HOST=unix:///tmp/podman.sock; +TESTCONTAINERS_RYUK_DISABLED=true; +TESTCONTAINERS_CHECKS_DISABLE=true +``` + +## 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 17, 11 or 8 as necessary. +2. 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 + ``` +3. Install the [EnvFile](https://plugins.jetbrains.com/plugin/7861-envfile) Intellij plugin. +5. 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. If you do not have the `./tmp` directory, build the docker image and it will be created. + 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 building an image from your local Neo4j repository + +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..0afea317 100644 --- a/Makefile +++ b/Makefile @@ -1,106 +1,43 @@ -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 $@ - -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 $@ - -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))" \ - $( echo -n $$image >$@ - -tmp/local-context-%/.sentinel: tmp/image-%/.sentinel in/$(call tarball,%,$(NEO4J_VERSION)) -> rm -rf $(@D) -> 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 $@ +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 -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 +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 -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) +# create release images and loadable images +package: package-community package-enterprise +.PHONY: package -in/neo4j-enterprise-$(NEO4J_VERSION)-unix.tar.gz: -> mkdir -p in -> cd in -> $(call fetch_tarball,enterprise) +package-community: tmp/.image-id-community tmp/.image-id-neo4j-admin-community out/community/.sentinel out/neo4j-admin-community/.sentinel +> mkdir -p out +> docker tag $$(cat $<) neo4j:$(NEO4JVERSION) +> docker tag $$(cat tmp/.image-id-neo4j-admin-community) neo4j/neo4j-admin:$(NEO4JVERSION) +> docker save neo4j:$(NEO4JVERSION) > out/neo4j-community-$(NEO4JVERSION)-docker-loadable.tar +> docker save neo4j/neo4j-admin:$(NEO4JVERSION) > out/neo4j-admin-community-$(NEO4JVERSION)-docker-loadable.tar -clean: -> rm -rf tmp -> rm -rf out -.PHONY: clean +package-enterprise: tmp/.image-id-enterprise tmp/.image-id-neo4j-admin-enterprise out/enterprise/.sentinel out/neo4j-admin-enterprise/.sentinel +> mkdir -p out +> docker tag $$(cat $<) neo4j:$(NEO4JVERSION)-enterprise +> docker tag $$(cat tmp/.image-id-neo4j-admin-enterprise) neo4j/neo4j-admin:$(NEO4JVERSION)-enterprise +> docker save neo4j:$(NEO4JVERSION)-enterprise > out/neo4j-enterprise-$(NEO4JVERSION)-docker-loadable.tar +> docker save neo4j/neo4j-admin:$(NEO4JVERSION)-enterprise > out/neo4j-admin-enterprise-$(NEO4JVERSION)-docker-loadable.tar 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..31c875e0 --- /dev/null +++ b/docker-image-src/3.5/Dockerfile @@ -0,0 +1,46 @@ +FROM debian:bullseye-slim +ENV JAVA_HOME=/opt/java/openjdk +COPY --from=eclipse-temurin:8 $JAVA_HOME $JAVA_HOME +ENV PATH="${JAVA_HOME}/bin:${PATH}" \ + 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 --system neo4j && adduser --system --no-create-home --home "${NEO4J_HOME}" --ingroup neo4j neo4j + +COPY ./local-package/* /startup/ + +RUN apt update \ + && apt install -y curl gosu jq tini wget \ + && curl --fail --silent --show-error --location --remote-name ${NEO4J_URI} \ + && echo "${NEO4J_SHA256} ${NEO4J_TARBALL}" | sha256sum -c --strict --quiet \ + && tar --extract --file ${NEO4J_TARBALL} --directory /var/lib \ + && mv /var/lib/neo4j-* "${NEO4J_HOME}" \ + && rm ${NEO4J_TARBALL} \ + && mv "${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 \ + && ln -s /startup/docker-entrypoint.sh /docker-entrypoint.sh \ + && apt-get -y purge --auto-remove curl \ + && rm -rf /var/lib/apt/lists/* + + +ENV PATH "${NEO4J_HOME}"/bin:$PATH + +WORKDIR "${NEO4J_HOME}" + +VOLUME /data /logs + +EXPOSE 7474 7473 7687 + +ENTRYPOINT ["tini", "-g", "--", "/startup/docker-entrypoint.sh"] +CMD ["neo4j"] diff --git a/docker-image-src/3.5/docker-entrypoint.sh b/docker-image-src/3.5/docker-entrypoint.sh new file mode 100755 index 00000000..37bd659b --- /dev/null +++ b/docker-image-src/3.5/docker-entrypoint.sh @@ -0,0 +1,534 @@ +#!/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 && ! 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 + else + if ! is_writable "${mountFolder}"; then + #if [[ ! -w "${mountFolder}" ]] && [[ "$(stat -c %U ${mountFolder})" != "neo4j" ]]; then + echo >&2 "Consider unsetting SECURE_FILE_PERMISSIONS environment variable, to enable docker to write to ${mountFolder}." + 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" /startup/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 -L/startup --raw-output "import \"semver\" as lib; [ .[] | select(.neo4j|lib::semver(\"${_neo4j_version}\")) ] | min_by(.neo4j) | .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}'" + 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[]" /startup/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. 2022. 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 [ "${NEO4J_EDITION}" == "enterprise" ]; then + if secure_mode_enabled; then + check_mounted_folder_readable "/metrics" + fi + : ${NEO4J_dbms_directories_metrics:="/metrics"} + fi +fi + +if [ -d /logs ]; then + check_mounted_folder_writable_with_chown "/logs" + : ${NEO4J_dbms_directories_logs:="/logs"} +fi + +if [ -d /data ]; then + check_mounted_folder_writable_with_chown "/data" + if [ -d /data/databases ]; then + check_mounted_folder_writable_with_chown "/data/databases" + fi + if [ -d /data/dbms ]; then + check_mounted_folder_writable_with_chown "/data/dbms" + fi +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 [ ! -d "/conf" ]; then + echo >&2 "You must mount a folder to /conf so that the configuration file(s) can be dumped to there." + exit 1 + fi + check_mounted_folder_writable_with_chown "/conf" + cp --recursive "${NEO4J_HOME}"/conf/* /conf + echo "Config Dumped" + exit 0 +fi + +# 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..f242b232 --- /dev/null +++ b/docker-image-src/3.5/neo4j-admin/Dockerfile @@ -0,0 +1,45 @@ +FROM debian:bullseye-slim +ENV JAVA_HOME=/opt/java/openjdk +COPY --from=eclipse-temurin:8 $JAVA_HOME $JAVA_HOME +ENV PATH="${JAVA_HOME}/bin:${PATH}" \ + 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/* /startup/ + +RUN apt update \ + && apt install -y curl procps \ + && 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} \ + && rm ${NEO4J_HOME}/bin/neo4j \ + && mv "${NEO4J_HOME}"/data /data \ + && chown -R neo4j:neo4j /data \ + && chmod -R 777 /data \ + && mkdir -p /backups \ + && chown -R neo4j:neo4j /backups \ + && chmod -R 777 /backups \ + && 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 /backups +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..71562226 --- /dev/null +++ b/docker-image-src/3.5/neo4j-admin/docker-entrypoint.sh @@ -0,0 +1,122 @@ +#!/bin/bash -eu + +function running_as_root +{ + test "$(id -u)" = "0" +} + +function is_writable +{ + ${exec_cmd} 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 [ $(${exec_cmd} 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="runuser -p -u neo4j -g 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. 2022. 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 /backups ]; then + check_mounted_folder_writable_with_chown "/backups" +fi + +# ==== MAKE SURE NEO4J CANNOT BE RUN FROM THIS CONTAINER ==== + +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..d5d611a0 --- /dev/null +++ b/docker-image-src/4.0/Dockerfile @@ -0,0 +1,46 @@ +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 wget gosu jq 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 ["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..1ed5adaa --- /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 procps \ + && 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 /backups \ + && chown -R neo4j:neo4j /backups \ + && chmod -R 777 /backups \ + && 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 /backups +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..42e2f892 --- /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 /backups ]; then + check_mounted_folder_writable_with_chown "/backups" +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..7787a853 --- /dev/null +++ b/docker-image-src/4.1/Dockerfile @@ -0,0 +1,44 @@ +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/* /startup/ + +RUN apt update \ + && apt install -y curl wget gosu jq 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 \ + && ln -s /startup/docker-entrypoint.sh /docker-entrypoint.sh \ + && apt-get -y purge --auto-remove curl \ + && rm -rf /var/lib/apt/lists/* + + +ENV PATH "${NEO4J_HOME}"/bin:$PATH + +WORKDIR "${NEO4J_HOME}" + +VOLUME /data /logs + +EXPOSE 7474 7473 7687 + +ENTRYPOINT ["tini", "-g", "--", "/startup/docker-entrypoint.sh"] +CMD ["neo4j"] diff --git a/docker-image-src/4.1/docker-entrypoint.sh b/docker-image-src/4.1/docker-entrypoint.sh new file mode 100755 index 00000000..3bffd4c2 --- /dev/null +++ b/docker-image-src/4.1/docker-entrypoint.sh @@ -0,0 +1,511 @@ +#!/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 && ! 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 + else + if ! is_writable "${mountFolder}"; then + #if [[ ! -w "${mountFolder}" ]] && [[ "$(stat -c %U ${mountFolder})" != "neo4j" ]]; then + echo >&2 "Consider unsetting SECURE_FILE_PERMISSIONS environment variable, to enable docker to write to ${mountFolder}." + 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" /startup/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 -L/startup --raw-output "import \"semver\" as lib; [ .[] | select(.neo4j|lib::semver(\"${_neo4j_version}\")) ] | min_by(.neo4j) | .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}'" + 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[]" /startup/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 + # metrics is enterprise only + if [ "${NEO4J_EDITION}" == "enterprise" ]; + then + check_mounted_folder_writable_with_chown "/metrics" + : ${NEO4J_dbms_directories_metrics:="/metrics"} + fi +fi + +if [ -d /logs ]; then + check_mounted_folder_writable_with_chown "/logs" + : ${NEO4J_dbms_directories_logs:="/logs"} +fi + +if [ -d /data ]; then + check_mounted_folder_writable_with_chown "/data" + if [ -d /data/databases ]; then + check_mounted_folder_writable_with_chown "/data/databases" + fi + if [ -d /data/dbms ]; then + check_mounted_folder_writable_with_chown "/data/dbms" + fi + if [ -d /data/transactions ]; then + check_mounted_folder_writable_with_chown "/data/transactions" + fi +fi + +# ==== 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 [ ! -d "/conf" ]; then + echo >&2 "You must mount a folder to /conf so that the configuration file(s) can be dumped to there." + exit 1 + fi + check_mounted_folder_writable_with_chown "/conf" + cp --recursive "${NEO4J_HOME}"/conf/* /conf + echo "Config Dumped" + exit 0 +fi + +# 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..5f3dd11d --- /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/* /startup/ + +RUN apt update \ + && apt install -y curl gosu procps \ + && 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 /backups \ + && chown -R neo4j:neo4j /backups \ + && chmod -R 777 /backups \ + && 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 /backups +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..42e2f892 --- /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 /backups ]; then + check_mounted_folder_writable_with_chown "/backups" +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..7787a853 --- /dev/null +++ b/docker-image-src/4.2/Dockerfile @@ -0,0 +1,44 @@ +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/* /startup/ + +RUN apt update \ + && apt install -y curl wget gosu jq 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 \ + && ln -s /startup/docker-entrypoint.sh /docker-entrypoint.sh \ + && apt-get -y purge --auto-remove curl \ + && rm -rf /var/lib/apt/lists/* + + +ENV PATH "${NEO4J_HOME}"/bin:$PATH + +WORKDIR "${NEO4J_HOME}" + +VOLUME /data /logs + +EXPOSE 7474 7473 7687 + +ENTRYPOINT ["tini", "-g", "--", "/startup/docker-entrypoint.sh"] +CMD ["neo4j"] diff --git a/docker-image-src/4.2/docker-entrypoint.sh b/docker-image-src/4.2/docker-entrypoint.sh new file mode 100755 index 00000000..712d8341 --- /dev/null +++ b/docker-image-src/4.2/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 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 && ! 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 + else + if ! is_writable "${mountFolder}"; then + #if [[ ! -w "${mountFolder}" ]] && [[ "$(stat -c %U ${mountFolder})" != "neo4j" ]]; then + echo >&2 "Consider unsetting SECURE_FILE_PERMISSIONS environment variable, to enable docker to write to ${mountFolder}." + 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" /startup/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 -L/startup --raw-output "import \"semver\" as lib; [ .[] | select(.neo4j|lib::semver(\"${_neo4j_version}\")) ] | min_by(.neo4j) | .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}'" + 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[]" /startup/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 + # metrics is enterprise only + if [ "${NEO4J_EDITION}" == "enterprise" ]; + then + check_mounted_folder_writable_with_chown "/metrics" + : ${NEO4J_dbms_directories_metrics:="/metrics"} + fi +fi + +if [ -d /logs ]; then + check_mounted_folder_writable_with_chown "/logs" + : ${NEO4J_dbms_directories_logs:="/logs"} +fi + +if [ -d /data ]; then + check_mounted_folder_writable_with_chown "/data" + if [ -d /data/databases ]; then + check_mounted_folder_writable_with_chown "/data/databases" + fi + if [ -d /data/dbms ]; then + check_mounted_folder_writable_with_chown "/data/dbms" + fi + if [ -d /data/transactions ]; then + check_mounted_folder_writable_with_chown "/data/transactions" + fi +fi + +if [ -d /licenses ]; then + check_mounted_folder_readable "/licenses" + : ${NEO4J_dbms_directories_licenses:="/licenses"} +fi + +# ==== SET CONFIGURATIONS ==== + +## == DOCKER SPECIFIC DEFAULT CONFIGURATIONS === +## these should not override *any* configurations set by the user + +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 [ ! -d "/conf" ]; then + echo >&2 "You must mount a folder to /conf so that the configuration file(s) can be dumped to there." + exit 1 + fi + check_mounted_folder_writable_with_chown "/conf" + cp --recursive "${NEO4J_HOME}"/conf/* /conf + echo "Config Dumped" + exit 0 +fi + +# 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..5f3dd11d --- /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/* /startup/ + +RUN apt update \ + && apt install -y curl gosu procps \ + && 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 /backups \ + && chown -R neo4j:neo4j /backups \ + && chmod -R 777 /backups \ + && 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 /backups +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..42e2f892 --- /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 /backups ]; then + check_mounted_folder_writable_with_chown "/backups" +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..94d4bdd6 --- /dev/null +++ b/docker-image-src/4.3/Dockerfile @@ -0,0 +1,46 @@ +FROM debian:bullseye-slim +ENV JAVA_HOME=/opt/java/openjdk +COPY --from=eclipse-temurin:11 $JAVA_HOME $JAVA_HOME +ENV PATH="${JAVA_HOME}/bin:${PATH}" \ + NEO4J_SHA256=%%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/* /startup/ + +RUN apt update \ + && apt install -y curl gosu jq tini wget \ + && curl --fail --silent --show-error --location --remote-name ${NEO4J_URI} \ + && echo "${NEO4J_SHA256} ${NEO4J_TARBALL}" | sha256sum -c --strict --quiet \ + && tar --extract --file ${NEO4J_TARBALL} --directory /var/lib \ + && mv /var/lib/neo4j-* "${NEO4J_HOME}" \ + && rm ${NEO4J_TARBALL} \ + && mv "${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 \ + && ln -s /startup/docker-entrypoint.sh /docker-entrypoint.sh \ + && apt-get -y purge --auto-remove curl \ + && rm -rf /var/lib/apt/lists/* + + +ENV PATH "${NEO4J_HOME}"/bin:$PATH + +WORKDIR "${NEO4J_HOME}" + +VOLUME /data /logs + +EXPOSE 7474 7473 7687 + +ENTRYPOINT ["tini", "-g", "--", "/startup/docker-entrypoint.sh"] +CMD ["neo4j"] diff --git a/docker-image-src/4.3/docker-entrypoint.sh b/docker-image-src/4.3/docker-entrypoint.sh new file mode 100755 index 00000000..bc53ab22 --- /dev/null +++ b/docker-image-src/4.3/docker-entrypoint.sh @@ -0,0 +1,576 @@ +#!/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 && ! 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 + else + if ! is_writable "${mountFolder}"; then + #if [[ ! -w "${mountFolder}" ]] && [[ "$(stat -c %U ${mountFolder})" != "neo4j" ]]; then + echo >&2 "Consider unsetting SECURE_FILE_PERMISSIONS environment variable, to enable docker to write to ${mountFolder}." + 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" /startup/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 -L/startup --raw-output "import \"semver\" as lib; [ .[] | select(.neo4j|lib::semver(\"${_neo4j_version}\")) ] | min_by(.neo4j) | .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}'" + 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[]" /startup/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" /startup/neo4jlabs-plugins.json )" + if [ "${_location}" != "null" -a -n "$(shopt -s nullglob; echo ${_location})" ]; 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. 2022. 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 + # metrics is enterprise only + if [ "${NEO4J_EDITION}" == "enterprise" ]; + then + check_mounted_folder_writable_with_chown "/metrics" + : ${NEO4J_dbms_directories_metrics:="/metrics"} + fi +fi + +if [ -d /logs ]; then + check_mounted_folder_writable_with_chown "/logs" + : ${NEO4J_dbms_directories_logs:="/logs"} +fi + +if [ -d /data ]; then + check_mounted_folder_writable_with_chown "/data" + if [ -d /data/databases ]; then + check_mounted_folder_writable_with_chown "/data/databases" + fi + if [ -d /data/dbms ]; then + check_mounted_folder_writable_with_chown "/data/dbms" + fi + if [ -d /data/transactions ]; then + check_mounted_folder_writable_with_chown "/data/transactions" + fi +fi + +if [ -d /licenses ]; then + check_mounted_folder_readable "/licenses" + : ${NEO4J_dbms_directories_licenses:="/licenses"} +fi + +# ==== SET CONFIGURATIONS ==== + +## == DOCKER SPECIFIC DEFAULT CONFIGURATIONS === +## these should not override *any* configurations set by the user + +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 [ ! -d "/conf" ]; then + echo >&2 "You must mount a folder to /conf so that the configuration file(s) can be dumped to there." + exit 1 + fi + check_mounted_folder_writable_with_chown "/conf" + cp --recursive "${NEO4J_HOME}"/conf/* /conf + echo "Config Dumped" + exit 0 +fi + +# this prints out a command for us to run. +# the command is something like: `java ...[lots of java options]... neo4j.mainClass ...[some neo4j options]...` +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)" + ${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..318883a0 --- /dev/null +++ b/docker-image-src/4.3/neo4j-admin/Dockerfile @@ -0,0 +1,45 @@ +FROM debian:bullseye-slim +ENV JAVA_HOME=/opt/java/openjdk +COPY --from=eclipse-temurin:11 $JAVA_HOME $JAVA_HOME +ENV PATH="${JAVA_HOME}/bin:${PATH}" \ + NEO4J_SHA256=%%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/* /startup/ + +RUN apt update \ + && apt install -y curl procps \ + && 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} \ + && rm ${NEO4J_HOME}/bin/neo4j \ + && mv "${NEO4J_HOME}"/data /data \ + && chown -R neo4j:neo4j /data \ + && chmod -R 777 /data \ + && mkdir -p /backups \ + && chown -R neo4j:neo4j /backups \ + && chmod -R 777 /backups \ + && 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 /backups +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..00132c84 --- /dev/null +++ b/docker-image-src/4.3/neo4j-admin/docker-entrypoint.sh @@ -0,0 +1,125 @@ +#!/bin/bash -eu + +function running_as_root +{ + test "$(id -u)" = "0" +} + +function is_writable +{ + ${exec_cmd} 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 [ $(${exec_cmd} 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="runuser -p -u neo4j -g 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. 2022. 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 /backups ]; then + check_mounted_folder_writable_with_chown "/backups" +fi + +# ==== MAKE SURE NEO4J CANNOT BE RUN FROM THIS CONTAINER ==== + +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..94d4bdd6 --- /dev/null +++ b/docker-image-src/4.4/Dockerfile @@ -0,0 +1,46 @@ +FROM debian:bullseye-slim +ENV JAVA_HOME=/opt/java/openjdk +COPY --from=eclipse-temurin:11 $JAVA_HOME $JAVA_HOME +ENV PATH="${JAVA_HOME}/bin:${PATH}" \ + NEO4J_SHA256=%%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/* /startup/ + +RUN apt update \ + && apt install -y curl gosu jq tini wget \ + && curl --fail --silent --show-error --location --remote-name ${NEO4J_URI} \ + && echo "${NEO4J_SHA256} ${NEO4J_TARBALL}" | sha256sum -c --strict --quiet \ + && tar --extract --file ${NEO4J_TARBALL} --directory /var/lib \ + && mv /var/lib/neo4j-* "${NEO4J_HOME}" \ + && rm ${NEO4J_TARBALL} \ + && mv "${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 \ + && ln -s /startup/docker-entrypoint.sh /docker-entrypoint.sh \ + && apt-get -y purge --auto-remove curl \ + && rm -rf /var/lib/apt/lists/* + + +ENV PATH "${NEO4J_HOME}"/bin:$PATH + +WORKDIR "${NEO4J_HOME}" + +VOLUME /data /logs + +EXPOSE 7474 7473 7687 + +ENTRYPOINT ["tini", "-g", "--", "/startup/docker-entrypoint.sh"] +CMD ["neo4j"] diff --git a/docker-image-src/4.4/docker-entrypoint.sh b/docker-image-src/4.4/docker-entrypoint.sh new file mode 100755 index 00000000..99eae9c6 --- /dev/null +++ b/docker-image-src/4.4/docker-entrypoint.sh @@ -0,0 +1,580 @@ +#!/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 && ! 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 + else + if ! is_writable "${mountFolder}"; then + #if [[ ! -w "${mountFolder}" ]] && [[ "$(stat -c %U ${mountFolder})" != "neo4j" ]]; then + echo >&2 "Consider unsetting SECURE_FILE_PERMISSIONS environment variable, to enable docker to write to ${mountFolder}." + 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" /startup/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 -L/startup --raw-output "import \"semver\" as lib; [ .[] | select(.neo4j|lib::semver(\"${_neo4j_version}\")) ] | min_by(.neo4j) | .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}'" + 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[]" /startup/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" /startup/neo4jlabs-plugins.json )" + if [ "${_location}" != "null" -a -n "$(shopt -s nullglob; echo ${_location})" ]; 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}" + 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. 2022. 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 + # metrics is enterprise only + if [ "${NEO4J_EDITION}" == "enterprise" ]; + then + check_mounted_folder_writable_with_chown "/metrics" + : ${NEO4J_dbms_directories_metrics:="/metrics"} + fi +fi + +if [ -d /logs ]; then + check_mounted_folder_writable_with_chown "/logs" + : ${NEO4J_dbms_directories_logs:="/logs"} +fi + +if [ -d /data ]; then + check_mounted_folder_writable_with_chown "/data" + if [ -d /data/databases ]; then + check_mounted_folder_writable_with_chown "/data/databases" + fi + if [ -d /data/dbms ]; then + check_mounted_folder_writable_with_chown "/data/dbms" + fi + if [ -d /data/transactions ]; then + check_mounted_folder_writable_with_chown "/data/transactions" + fi +fi + +if [ -d /licenses ]; then + check_mounted_folder_readable "/licenses" + : ${NEO4J_dbms_directories_licenses:="/licenses"} +fi + +# ==== SET CONFIGURATIONS ==== + +## == DOCKER SPECIFIC DEFAULT CONFIGURATIONS === +## these should not override *any* configurations set by the user + +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 [ ! -d "/conf" ]; then + echo >&2 "You must mount a folder to /conf so that the configuration file(s) can be dumped to there." + exit 1 + fi + check_mounted_folder_writable_with_chown "/conf" + cp --recursive "${NEO4J_HOME}"/conf/* /conf + echo "Config Dumped" + exit 0 +fi + +# this prints out a command for us to run. +# the command is something like: `java ...[lots of java options]... neo4j.mainClass ...[some neo4j options]...` +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 + if [ -f "${NEO4J_HOME}/run/neo4j.pid" ]; then + echo >&2 "Error: It looks like neo4j is already running. If you are sure that neo4j is not running, delete ${NEO4J_HOME}/run/neo4j.pid. This could be caused by not shutting down neo4j gracefully. Consider increasing --time/-t for docker stop." + exit 1 + fi + + # separate declaration and use of get_neo4j_run_cmd so that error codes are correctly surfaced + neo4j_console_cmd="$(get_neo4j_run_cmd)" + ${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..318883a0 --- /dev/null +++ b/docker-image-src/4.4/neo4j-admin/Dockerfile @@ -0,0 +1,45 @@ +FROM debian:bullseye-slim +ENV JAVA_HOME=/opt/java/openjdk +COPY --from=eclipse-temurin:11 $JAVA_HOME $JAVA_HOME +ENV PATH="${JAVA_HOME}/bin:${PATH}" \ + NEO4J_SHA256=%%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/* /startup/ + +RUN apt update \ + && apt install -y curl procps \ + && 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} \ + && rm ${NEO4J_HOME}/bin/neo4j \ + && mv "${NEO4J_HOME}"/data /data \ + && chown -R neo4j:neo4j /data \ + && chmod -R 777 /data \ + && mkdir -p /backups \ + && chown -R neo4j:neo4j /backups \ + && chmod -R 777 /backups \ + && 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 /backups +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..00132c84 --- /dev/null +++ b/docker-image-src/4.4/neo4j-admin/docker-entrypoint.sh @@ -0,0 +1,125 @@ +#!/bin/bash -eu + +function running_as_root +{ + test "$(id -u)" = "0" +} + +function is_writable +{ + ${exec_cmd} 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 [ $(${exec_cmd} 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="runuser -p -u neo4j -g 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. 2022. 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 /backups ]; then + check_mounted_folder_writable_with_chown "/backups" +fi + +# ==== MAKE SURE NEO4J CANNOT BE RUN FROM THIS CONTAINER ==== + +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..e60aabac --- /dev/null +++ b/docker-image-src/5.0/Dockerfile @@ -0,0 +1,45 @@ +FROM debian:bullseye-slim +ENV JAVA_HOME=/opt/java/openjdk +COPY --from=eclipse-temurin:17 $JAVA_HOME $JAVA_HOME +ENV PATH="${JAVA_HOME}/bin:${PATH}" \ + 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/* /startup/ + +RUN apt update \ + && apt install -y curl gosu jq tini wget \ + && curl --fail --silent --show-error --location --remote-name ${NEO4J_URI} \ + && echo "${NEO4J_SHA256} ${NEO4J_TARBALL}" | sha256sum -c --strict --quiet \ + && tar --extract --file ${NEO4J_TARBALL} --directory /var/lib \ + && mv /var/lib/neo4j-* "${NEO4J_HOME}" \ + && rm ${NEO4J_TARBALL} \ + && mv "${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 \ + && apt-get -y purge --auto-remove curl \ + && rm -rf /var/lib/apt/lists/* + + +ENV PATH "${NEO4J_HOME}"/bin:$PATH + +WORKDIR "${NEO4J_HOME}" + +VOLUME /data /logs + +EXPOSE 7474 7473 7687 + +ENTRYPOINT ["tini", "-g", "--", "/startup/docker-entrypoint.sh"] +CMD ["neo4j"] diff --git a/docker-image-src/5.0/docker-entrypoint.sh b/docker-image-src/5.0/docker-entrypoint.sh new file mode 100755 index 00000000..06a9ae0a --- /dev/null +++ b/docker-image-src/5.0/docker-entrypoint.sh @@ -0,0 +1,565 @@ +#!/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 && ! 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 + else + if ! is_writable "${mountFolder}"; then + #if [[ ! -w "${mountFolder}" ]] && [[ "$(stat -c %U ${mountFolder})" != "neo4j" ]]; then + echo >&2 "Consider unsetting SECURE_FILE_PERMISSIONS environment variable, to enable docker to write to ${mountFolder}." + 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" /startup/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 -L/startup --raw-output "import \"semver\" as lib; [ .[] | select(.neo4j|lib::semver(\"${_neo4j_version}\")) ] | min_by(.neo4j) | .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}'" + 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[]" /startup/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 "${NEO4J_PLUGINS}" | jq --raw-output '.[]'); do + local _location="$(jq --raw-output "with_entries( select(.key==\"${plugin_name}\") ) | to_entries[] | .value.location" /startup/neo4jlabs-plugins.json )" + if [ "${_location}" != "null" -a -n "$(shopt -s nullglob; echo ${_location})" ]; 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}" + 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} dbms set-initial-password "${password}" --require-password-change $(expand_commands_optionally) 2>/dev/null || true + else + ${neo4j_admin_cmd} dbms 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. 2022. 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_server_tx__log_rotation_retention__policy env variable to set +# server.tx_log.rotation.retention_policy setting + +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 + +# NEO4JLABS_PLUGINS has been renamed to NEO4J_PLUGINS, but we want the old name to work for now. +if [ -n "${NEO4JLABS_PLUGINS:-}" ]; +then + echo >&2 "NEO4JLABS_PLUGINS has been renamed to NEO4J_PLUGINS in Neo4j 5.0.0. +The old name will still work, but is likely to be deprecated in future releases." + : ${NEO4J_PLUGINS:=${NEO4JLABS_PLUGINS:-}} +fi + +# ==== 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 "${NEO4J_PLUGINS:-}" ]]; then + # We need write permissions + check_mounted_folder_writable_with_chown "/plugins" + fi + check_mounted_folder_readable "/plugins" + : ${NEO4J_server_directories_plugins:="/plugins"} +fi + +if [ -d /import ]; then + check_mounted_folder_readable "/import" + : ${NEO4J_server_directories_import:="/import"} +fi + +if [ -d /metrics ]; then + # metrics is enterprise only + if [ "${NEO4J_EDITION}" == "enterprise" ]; + then + check_mounted_folder_writable_with_chown "/metrics" + : ${NEO4J_server_directories_metrics:="/metrics"} + fi +fi + +if [ -d /logs ]; then + check_mounted_folder_writable_with_chown "/logs" + : ${NEO4J_server_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_server_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 "db.tx_log.rotation.retention_policy" "100M size" "${NEO4J_HOME}" +add_docker_default_to_conf "server.memory.pagecache.size" "512M" "${NEO4J_HOME}" +add_docker_default_to_conf "server.default_listen_address" "0.0.0.0" "${NEO4J_HOME}" +# set enterprise only docker defaults +if [ "${NEO4J_EDITION}" == "enterprise" ]; +then + add_docker_default_to_conf "server.discovery.advertised_address" "$(hostname):5000" "${NEO4J_HOME}" + add_docker_default_to_conf "server.cluster.advertised_address" "$(hostname):6000" "${NEO4J_HOME}" + add_docker_default_to_conf "server.cluster.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:-}" +temp_neo4j_plugins="${NEO4J_PLUGINS:-}" +# 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 NEO4J_PLUGINS +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}" +export NEO4J_PLUGINS="${temp_neo4j_plugins}" +unset temp_neo4j_home temp_neo4j_plugins + +# ==== SET PASSWORD AND PLUGINS ==== + +set_initial_password "${temp_neo4j_auth}" + + +if [[ ! -z "${NEO4J_PLUGINS:-}" ]]; then + # NEO4J_PLUGINS should be a json array of plugins like '["graph-algorithms", "apoc", "streams", "graphql"]' + install_neo4j_labs_plugins +fi + +# ==== INVOKE NEO4J STARTUP ==== + +[ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT} + +if [ "${cmd}" == "dump-config" ]; then + if [ ! -d "/conf" ]; then + echo >&2 "You must mount a folder to /conf so that the configuration file(s) can be dumped to there." + exit 1 + fi + check_mounted_folder_writable_with_chown "/conf" + cp --recursive "${NEO4J_HOME}"/conf/* /conf + echo "Config Dumped" + exit 0 +fi + +# this prints out a command for us to run. +# the command is something like: `java ...[lots of java options]... neo4j.mainClass ...[some neo4j options]...` +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)" + ${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..f651f166 --- /dev/null +++ b/docker-image-src/5.0/neo4j-admin/Dockerfile @@ -0,0 +1,45 @@ +FROM debian:bullseye-slim +ENV JAVA_HOME=/opt/java/openjdk +COPY --from=eclipse-temurin:17 $JAVA_HOME $JAVA_HOME +ENV PATH="${JAVA_HOME}/bin:${PATH}" \ + 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/* /startup/ + +RUN apt update \ + && apt install -y curl procps \ + && 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} \ + && rm ${NEO4J_HOME}/bin/neo4j \ + && mv "${NEO4J_HOME}"/data /data \ + && chown -R neo4j:neo4j /data \ + && chmod -R 777 /data \ + && mkdir -p /backups \ + && chown -R neo4j:neo4j /backups \ + && chmod -R 777 /backups \ + && 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 /backups +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..00132c84 --- /dev/null +++ b/docker-image-src/5.0/neo4j-admin/docker-entrypoint.sh @@ -0,0 +1,125 @@ +#!/bin/bash -eu + +function running_as_root +{ + test "$(id -u)" = "0" +} + +function is_writable +{ + ${exec_cmd} 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 [ $(${exec_cmd} 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="runuser -p -u neo4j -g 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. 2022. 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 /backups ]; then + check_mounted_folder_writable_with_chown "/backups" +fi + +# ==== MAKE SURE NEO4J CANNOT BE RUN FROM THIS CONTAINER ==== + +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/common/neo4jlabs-plugins.json b/docker-image-src/common/neo4jlabs-plugins.json new file mode 100644 index 00000000..76e78802 --- /dev/null +++ b/docker-image-src/common/neo4jlabs-plugins.json @@ -0,0 +1,59 @@ +{ + "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": { + "location": "/var/lib/neo4j/products/bloom-plugin-*.jar", + "versions": "https://bloom-plugins.s3.eu-west-2.amazonaws.com/versions.json", + "properties": { + "dbms.unmanaged_extension_classes": "com.neo4j.bloom.server=/browser/bloom", + "dbms.security.procedures.unrestricted": "bloom.*", + "neo4j.bloom.license_file": "/licenses/bloom.license" + } + }, + "streams": { + "versions": "https://neo4j-contrib.github.io/neo4j-streams/versions.json", + "properties": {} + }, + "graphql": { + "versions": "https://neo4j-graphql.github.io/neo4j-graphql/versions.json", + "properties": { + "dbms.unmanaged_extension_classes": "org.neo4j.graphql=/graphql", + "dbms.security.procedures.unrestricted": "graphql.*" + } + }, + "graph-algorithms": { + "versions": "https://neo4j-contrib.github.io/neo4j-graph-algorithms/versions.json", + "properties": { + "dbms.security.procedures.unrestricted": "algo.*" + } + }, + "graph-data-science": { + "versions": "https://graphdatascience.ninja/versions.json", + "location": "/var/lib/neo4j/products/neo4j-graph-data-science-*.jar", + "properties": { + "dbms.security.procedures.unrestricted": "gds.*" + } + }, + "n10s": { + "versions": "https://neo4j-labs.github.io/neosemantics/versions.json", + "properties": { + "dbms.security.procedures.unrestricted":"semantics.*" + } + }, + "_testing": { + "versions": "http://host.testcontainers.internal:3000/versions.json", + "properties": { + "dbms.security.procedures.unrestricted": "com.neo4j.docker.neo4jserver.plugins.*" + } + } +} diff --git a/docker-image-src/common/semver.jq b/docker-image-src/common/semver.jq new file mode 100755 index 00000000..2d8c144a --- /dev/null +++ b/docker-image-src/common/semver.jq @@ -0,0 +1,22 @@ +def _semver_obj2obj($req): + if . == $req then true + elif .major != $req.major then false + elif .minor != $req.minor and .minor != "x" and .minor != "*" then false + elif .patch != $req.patch and .patch != "x" and .patch != "*" then false + elif $req.minor == null and ( .minor == "x" or .minor == "*" ) then false + elif $req.patch == null and ( .patch == "x" or .patch == "*" ) then false + elif $req.major == null and $req.minor == null and $req.patch == null then false + else true end; + +def _ver2obj: + if type == "object" then . + elif type == "string" and test("(?[0-9]+)(\\.(?[0-9x*]+))?(\\.?(?[0-9x*]+))?") then capture("(?[0-9]+)(\\.(?[0-9x*]+))?(\\.?(?[0-9x*]+))?") + elif type == "string" and . == "" then {major: null, minor:null, patch:null} + elif type == "number" then {minor:floor,patch:(.-floor)} + else {major: .} end; + +# Returns true if input version spec semantically matches the requested version +def semver($req): + if $req == null or $req == "" then false + elif . == $req then true + else _ver2obj|_semver_obj2obj($req|_ver2obj) end; diff --git a/make-arm64.mk b/make-arm64.mk new file mode 100644 index 00000000..cb00de6c --- /dev/null +++ b/make-arm64.mk @@ -0,0 +1,19 @@ +include make-common.mk + +NEO4J_BASE_IMAGE?="openjdk:11-jdk-slim" +TAG ?= neo4j + +package-arm-experimental: TAG:=neo4j/neo4j-arm64-experimental +package-arm-experimental: tag-arm +> mkdir -p out +> docker save neo4j/neo4j-arm64-experimental:$(NEO4JVERSION) > out/neo4j-community-$(NEO4JVERSION)-arm64-docker-loadable.tar +> docker save neo4j/neo4j-arm64-experimental:$(NEO4JVERSION)-enterprise > out/neo4j-enterprise-$(NEO4JVERSION)-arm64-docker-loadable.tar +.PHONY: package-arm-experimental + +tag-arm: build +> docker tag $$(cat tmp/.image-id-community) $(TAG):$(NEO4JVERSION) +> docker tag $$(cat tmp/.image-id-enterprise) $(TAG):$(NEO4JVERSION)-enterprise +.PHONY: tag-arm + + + diff --git a/make-common.mk b/make-common.mk new file mode 100644 index 00000000..c1bddcb9 --- /dev/null +++ b/make-common.mk @@ -0,0 +1,142 @@ +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 ## + +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=${*}" >> ${@} + +# copy the releaseable version of the image to the output folder. +out/%/.sentinel: tmp/image-%/.sentinel +> mkdir -p $(@D) +> cp -r $( touch $@ + +# 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:///startup/$(call tarball,$*,$(NEO4JVERSION))" \ + $( echo -n $$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:///startup/$(call tarball,$*,$(NEO4JVERSION))" \ + $( echo -n $$image >$@ + +## local build context ## + +# 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)) +> rm -rf $(@D) +> mkdir -p $(@D) +> cp -r $( cp $(filter %.tar.gz,$^) $(@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 $@ + +## create Dockerfiles ## + +# 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)) +> mkdir -p $(@D)/local-package +> cp docker-image-src/common/* $(@D)/local-package +> cp $(filter %.sh,$^) $(@D)/local-package +> 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 +> 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 $@ + +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/pom.xml b/pom.xml new file mode 100644 index 00000000..7486a583 --- /dev/null +++ b/pom.xml @@ -0,0 +1,116 @@ + + + 4.0.0 + + com.neo4j + docker-neo4j-tests + 1.0-SNAPSHOT + jar + + + ${env.NEO4JVERSION} + + 1.8 + 1.17.1 + + + + + + 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 + ${testcontainers.version} + test + + + org.testcontainers + testcontainers + ${testcontainers.version} + test + + + org.neo4j.driver + neo4j-java-driver + 4.3.6 + test + + + + com.google.code.gson + gson + 2.8.9 + 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..5264cf6d --- /dev/null +++ b/src/test/java/com/neo4j/docker/neo4jadmin/TestBackupRestore.java @@ -0,0 +1,149 @@ +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.StartupDetector; +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( Neo4jVersion.NEO4J_VERSION_500 ), + "These tests only apply to neo4j-admin images of 5.0 and greater"); + 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 ) ); + StartupDetector.makeContainerWaitForNeo4jReady(container, password, 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 + { + final 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 + String neoDBAddress = neo4j.getHost()+":"+neo4j.getMappedPort( 6362 ); + GenericContainer adminBackup = createAdminContainer( asDefaultUser ) + .withNetworkMode( "host" ) + .waitingFor( new LogMessageWaitStrategy().withRegEx( "^Backup complete successful.*" ) ) + .withCommand( "neo4j-admin", "database", "backup", "--database=neo4j", "--backup-dir=/backups", "--from="+neoDBAddress); + + Path backupDir = HostFileSystemOperations.createTempFolderAndMountAsVolume( + adminBackup, "backup-", "/backups", 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", "database", "restore", "--database=neo4j", "--from=/backups/neo4j", "--force"); + HostFileSystemOperations.mountHostFolderAsVolume( adminRestore, backupDir, "/backups" ); + 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/TestBackupRestore44.java b/src/test/java/com/neo4j/docker/neo4jadmin/TestBackupRestore44.java new file mode 100644 index 00000000..1941476b --- /dev/null +++ b/src/test/java/com/neo4j/docker/neo4jadmin/TestBackupRestore44.java @@ -0,0 +1,153 @@ +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 TestBackupRestore44 +{ + // with authentication + // with non-default user + private static final Logger log = LoggerFactory.getLogger( TestBackupRestore44.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.NEO4J_VERSION.isOlderThan( Neo4jVersion.NEO4J_VERSION_500 ), + "These Neo4j admin tests are only for 4.4"); + 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 + { + final 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 + String neoDBAddress = neo4j.getHost()+":"+neo4j.getMappedPort( 6362 ); + GenericContainer adminBackup = createAdminContainer( asDefaultUser ) + .withNetworkMode( "host" ) + .waitingFor( new LogMessageWaitStrategy().withRegEx( "^Backup complete successful.*" ) ) + .withCommand( "neo4j-admin", "backup", "--database=neo4j", "--backup-dir=/backups", "--from="+neoDBAddress); + + Path backupDir = HostFileSystemOperations.createTempFolderAndMountAsVolume( + adminBackup, "backup-", "/backups", 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=/backups/neo4j", "--force"); + HostFileSystemOperations.mountHostFolderAsVolume( adminRestore, backupDir, "/backups" ); + 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..adb20d2a --- /dev/null +++ b/src/test/java/com/neo4j/docker/neo4jadmin/TestDumpLoad.java @@ -0,0 +1,159 @@ +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.StartupDetector; +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( Neo4jVersion.NEO4J_VERSION_500 ), + "These tests only apply to neo4j-admin images of 5.0 and greater"); + } + + 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 ) ) + // 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 )); + StartupDetector.makeContainerWaitForNeo4jReady(container, password, 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" ) + .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-", "/backups", testOutputFolder ); + admin.withCommand( "neo4j-admin", "database", "dump", "--database=neo4j", "--to=/backups/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, "/backups" ); + admin.withCommand( "neo4j-admin", "database", "load", "--database=neo4j", "--from=/backups/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/neo4jadmin/TestDumpLoad44.java b/src/test/java/com/neo4j/docker/neo4jadmin/TestDumpLoad44.java new file mode 100644 index 00000000..f64dc290 --- /dev/null +++ b/src/test/java/com/neo4j/docker/neo4jadmin/TestDumpLoad44.java @@ -0,0 +1,165 @@ +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 TestDumpLoad44 +{ + private static Logger log = LoggerFactory.getLogger( TestDumpLoad44.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.NEO4J_VERSION.isOlderThan( Neo4jVersion.NEO4J_VERSION_500 ), + "These Neo4j admin tests are only for 4.4"); + } + + 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) + { + log.info( "issuing container stop command" ); + container.getDockerClient().stopContainerCmd( container.getContainerId() ).exec(); + log.info( "Container stopped" ); + } + + 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-", "/backups", testOutputFolder ); + admin.withCommand( "neo4j-admin", "dump", "--database=neo4j", "--to=/backups/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, "/backups" ); + admin.withCommand( "neo4j-admin", "load", "--database=neo4j", "--from=/backups/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..2ac00e2d --- /dev/null +++ b/src/test/java/com/neo4j/docker/neo4jserver/TestBasic.java @@ -0,0 +1,156 @@ +package com.neo4j.docker.neo4jserver; + +import com.github.dockerjava.api.command.CreateContainerCmd; +import com.neo4j.docker.utils.DatabaseIO; +import com.neo4j.docker.utils.Neo4jVersion; +import com.neo4j.docker.utils.StartupDetector; +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.Container; +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.time.Duration; +import java.util.function.Consumer; + +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, 7687 ) + .withLogConsumer( new Slf4jLogConsumer( log ) ); + return container; + } + + @Test + void testListensOn7474() + { + try(GenericContainer container = createBasicContainer()) + { + StartupDetector.makeContainerWaitForNeo4jReady( container, "none" ); + container.start(); + Assertions.assertTrue( container.isRunning() ); + } + } + + @Test + void testNoUnexpectedErrors() throws Exception + { + try(GenericContainer container = createBasicContainer()) + { + StartupDetector.makeContainerWaitForNeo4jReady( container, "none" ); + 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()) + { + StartupDetector.makeContainerWaitForNeo4jReady( container, "none" ); + 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()) + { + StartupDetector.makeContainerWaitForNeo4jReady( container, "none" ); + container.setWorkingDirectory( "/tmp" ); + Assertions.assertDoesNotThrow( () -> container.start(), + "Could not start neo4j from workdir other than NEO4J_HOME" ); + } + } + + @ParameterizedTest(name = "ShutsDownCorrectly_{0}") + @ValueSource(strings = {"SIGTERM", "SIGINT"}) + void verifyShutsDownCleanly(String signal) throws Exception + { + try(GenericContainer container = createBasicContainer()) + { + StartupDetector.makeContainerWaitForNeo4jReady(container, "none"); + // sets sigterm as the stop container signal + container.withCreateContainerCmdModifier((Consumer) cmd -> + cmd.withStopSignal( signal ) + .withStopTimeout( 60 )); + container.start(); + DatabaseIO dbio = new DatabaseIO( container ); + dbio.putInitialDataIntoContainer( "neo4j", "none" ); + log.info( "issuing container stop command " + signal ); + container.getDockerClient().stopContainerCmd( container.getContainerId() ).exec(); + String stdout = container.getLogs(); + Assertions.assertTrue( stdout.contains( "Neo4j Server shutdown initiated by request" ), + "clean shutdown not initiated by " + signal ); + Assertions.assertTrue( stdout.contains( "Stopped." ), + "clean shutdown not initiated by " + signal ); + } + } +} 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/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..52aadbad --- /dev/null +++ b/src/test/java/com/neo4j/docker/neo4jserver/TestMounting.java @@ -0,0 +1,348 @@ +package com.neo4j.docker.neo4jserver; + +import static com.neo4j.docker.utils.StartupDetector.makeContainerWaitForNeo4jReady; + +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.SetContainerUser; +import com.neo4j.docker.utils.TestSettings; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Random; +import java.util.function.Consumer; +import java.util.stream.Stream; +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.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; + + +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 asCurrentUser=false, secureflag=true 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" ); + makeContainerWaitForNeo4jReady( container, "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 ") ); + } + + + @ParameterizedTest(name = "as current user={0}") + @ValueSource(booleans = {true, false}) + void canDumpConfig(boolean asCurrentUser) throws Exception + { + File confFile; + Path confMount; + String assertMsg; + String mountPrefix; + if(asCurrentUser) + { + assertMsg = "Conf file was not successfully dumped when running container as current user"; + mountPrefix = "candumpconf-user-"; + } + else + { + assertMsg = "Conf file was not successfully dumped when running container as root"; + mountPrefix = "candumpconf-root-"; + } + + try(GenericContainer container = setupBasicContainer(asCurrentUser, false)) + { + //Mount /conf + confMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, mountPrefix,"/conf" ); + confFile = confMount.resolve( "neo4j.conf" ).toFile(); + + //Start the container + container.setWaitStrategy( + Wait.forLogMessage( ".*Config Dumped.*", 1 ) + .withStartupTimeout( Duration.ofSeconds( 30 ) ) ); + container.setStartupCheckStrategy( new OneShotStartupCheckStrategy() ); + container.setCommand( "dump-config" ); + container.start(); + } + + // verify conf file was written + Assertions.assertTrue( confFile.exists(), assertMsg ); + // verify conf folder does not have new owner if not running as root + if(asCurrentUser) + { + int fileUID = (Integer) Files.getAttribute( confFile.toPath(), "unix:uid" ); + int expectedUID = Integer.parseInt( SetContainerUser.getNonRootUserString().split( ":" )[0] ); + Assertions.assertEquals( expectedUID, fileUID, "Owner of dumped conf file is not the currently running user" ); + } + } + + @Test + void canDumpConfig_errorsWithoutConfMount() throws Exception + { + try(GenericContainer container = setupBasicContainer( false, false )) + { + container.setWaitStrategy( + Wait.forLogMessage( ".*Config Dumped.*", 1 ) + .withStartupTimeout( Duration.ofSeconds( 30 ) ) ); + container.setStartupCheckStrategy( new OneShotStartupCheckStrategy() ); + container.setCommand( "dump-config" ); + Assertions.assertThrows( ContainerLaunchException.class, + ()->container.start(), + "Did not error when dump config requested without mounted /conf folder"); + String stderr = container.getLogs( OutputFrame.OutputType.STDERR); + Assertions.assertTrue( stderr.endsWith( "You must mount a folder to /conf so that the configuration file(s) can be dumped to there.\n" ) ); + } + } + + @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 ) ) ); + 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 ) ) ); + 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 )) + { + HostFileSystemOperations.createTempFolderAndMountAsVolume( container, "conf", "/conf", testOutputFolder ); + 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"), + 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" ); + } + } + + @Test + void shouldReownSubfilesToNeo4j() throws Exception { + Assumptions.assumeTrue( + TestSettings.NEO4J_VERSION.isAtLeastVersion(new Neo4jVersion(4, 0, 0)), + "User checks not valid before 4.0"); + Path logMount; + Path debugLog; + + try (GenericContainer container = setupBasicContainer(false, false)) { + logMount = HostFileSystemOperations.createTempFolderAndMountAsVolume(container, "subfileownership-", "/logs"); + debugLog = logMount.resolve("debug.log"); + // put file in logMount + Files.write(debugLog, "some log words".getBytes()); + // make neo4j own the conf folder but NOT the neo4j.conf + HostFileSystemOperations.setFileOwnerToNeo4j( logMount ); + HostFileSystemOperations.setFileOwnerToCurrentUser( debugLog ); + container.start(); + // if debug.log doesn't get re-owned, neo4j will not start and this test will fail here + } + } +} 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..d1c0bc41 --- /dev/null +++ b/src/test/java/com/neo4j/docker/neo4jserver/TestPasswords.java @@ -0,0 +1,204 @@ +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.StartupDetector; +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 +{ + public static final String AUTH = "none"; + 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", AUTH); + StartupDetector.makeContainerWaitForNeo4jReady(container, AUTH, Duration.ofSeconds( 90 )); + 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" ); + StartupDetector.makeContainerWaitForDatabaseReady(container, "neo4j", "neo4j", "neo4j", Duration.ofSeconds(90)); + 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 ); + StartupDetector.makeContainerWaitForNeo4jReady(firstContainer, 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" ); + StartupDetector.makeContainerWaitForNeo4jReady(secondContainer, 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 ); + StartupDetector.makeContainerWaitForNeo4jReady(firstContainer, 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" ); + StartupDetector.makeContainerWaitForDatabaseReady(container, user, intialPass, "neo4j", + Duration.ofSeconds(30)); + 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/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/configurations/Configuration.java b/src/test/java/com/neo4j/docker/neo4jserver/configurations/Configuration.java new file mode 100644 index 00000000..4ce19d76 --- /dev/null +++ b/src/test/java/com/neo4j/docker/neo4jserver/configurations/Configuration.java @@ -0,0 +1,98 @@ +package com.neo4j.docker.neo4jserver.configurations; + + +import com.neo4j.docker.utils.Neo4jVersion; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.EnumMap; +import java.util.Map; + +enum Setting{ + CLUSTER_DISCOVERY_ADDRESS, + CLUSTER_RAFT_ADDRESS, + CLUSTER_TRANSACTION_ADDRESS, + DEFAULT_LISTEN_ADDRESS, + DIRECTORIES_DATA, + DIRECTORIES_LOGS, + DIRECTORIES_METRICS, + JVM_ADDITIONAL, + LOGS_GC_ROTATION_KEEPNUMBER, + MEMORY_HEAP_INITIALSIZE, + MEMORY_HEAP_MAXSIZE, + MEMORY_PAGECACHE_SIZE, + SECURITY_PROCEDURES_UNRESTRICTED, + TXLOG_RETENTION_POLICY + } + +public class Configuration +{ + private static Map CONFIGURATIONS_5X = new EnumMap( Setting.class ) {{ + put( Setting.CLUSTER_DISCOVERY_ADDRESS, new Configuration("server.discovery.advertised_address")); + put( Setting.CLUSTER_RAFT_ADDRESS, new Configuration("server.cluster.raft.advertised_address")); + put( Setting.CLUSTER_TRANSACTION_ADDRESS, new Configuration("server.cluster.advertised_address")); + put( Setting.DEFAULT_LISTEN_ADDRESS, new Configuration("server.default_listen_address")); + put( Setting.DIRECTORIES_DATA, new Configuration("server.directories.data")); + put( Setting.DIRECTORIES_LOGS, new Configuration("server.directories.logs")); + put( Setting.DIRECTORIES_METRICS, new Configuration("server.directories.metrics")); + put( Setting.JVM_ADDITIONAL, new Configuration("server.jvm.additional")); + put( Setting.LOGS_GC_ROTATION_KEEPNUMBER, new Configuration( "server.logs.gc.rotation.keep_number")); + put( Setting.MEMORY_HEAP_INITIALSIZE, new Configuration("server.memory.heap.initial_size")); + put( Setting.MEMORY_HEAP_MAXSIZE, new Configuration( "server.memory.heap.max_size")); + put( Setting.MEMORY_PAGECACHE_SIZE, new Configuration("server.memory.pagecache.size")); + put( Setting.SECURITY_PROCEDURES_UNRESTRICTED, new Configuration("dbms.security.procedures.unrestricted")); + put( Setting.TXLOG_RETENTION_POLICY, new Configuration("db.tx_log.rotation.retention_policy")); + }}; + + private static Map CONFIGURATIONS_4X = new EnumMap( Setting.class ) {{ + put( Setting.CLUSTER_DISCOVERY_ADDRESS, new Configuration("causal_clustering.discovery_advertised_address")); + put( Setting.CLUSTER_RAFT_ADDRESS, new Configuration("causal_clustering.raft_advertised_address")); + put( Setting.CLUSTER_TRANSACTION_ADDRESS, new Configuration("causal_clustering.transaction_advertised_address")); + put( Setting.DEFAULT_LISTEN_ADDRESS, new Configuration("dbms.default_listen_address")); + put( Setting.DIRECTORIES_DATA, new Configuration("dbms.directories.data")); + put( Setting.DIRECTORIES_LOGS, new Configuration("dbms.directories.logs")); + put( Setting.DIRECTORIES_METRICS, new Configuration("dbms.directories.metrics")); + put( Setting.JVM_ADDITIONAL, new Configuration("dbms.jvm.additional")); + put( Setting.LOGS_GC_ROTATION_KEEPNUMBER, new Configuration( "dbms.logs.gc.rotation.keep_number")); + put( Setting.MEMORY_HEAP_INITIALSIZE, new Configuration("dbms.memory.heap.initial_size")); + put( Setting.MEMORY_HEAP_MAXSIZE, new Configuration("dbms.memory.heap.max_size")); + put( Setting.MEMORY_PAGECACHE_SIZE, new Configuration("dbms.memory.pagecache.size")); + put( Setting.SECURITY_PROCEDURES_UNRESTRICTED, new Configuration("dbms.security.procedures.unrestricted")); + put( Setting.TXLOG_RETENTION_POLICY, new Configuration("dbms.tx_log.rotation.retention_policy")); + }}; + + public static Map getConfigurationNameMap( Neo4jVersion version ) + { + switch ( version.major ) + { + case 3: + EnumMap out = new EnumMap( CONFIGURATIONS_4X ); + out.put( Setting.DEFAULT_LISTEN_ADDRESS, new Configuration( "dbms.connectors.default_listen_address" ) ); + return out; + case 4: + return CONFIGURATIONS_4X; + default: + return CONFIGURATIONS_5X; + } + } + + public static Path getConfigurationResourcesFolder( Neo4jVersion version ) + { + if(version.isOlderThan( Neo4jVersion.NEO4J_VERSION_500 )) + { + return Paths.get( "src", "test", "resources", "confs", "before50"); + } + else return Paths.get("src", "test", "resources", "confs"); + } + + public String name; + public String envName; + + private Configuration( String name ) + { + this.name = name; + this.envName = "NEO4J_" + name.replace( '_', '-' ) + .replace( '.', '_') + .replace( "-", "__" ); + } +} diff --git a/src/test/java/com/neo4j/docker/neo4jserver/configurations/TestConfSettings.java b/src/test/java/com/neo4j/docker/neo4jserver/configurations/TestConfSettings.java new file mode 100644 index 00000000..4952c245 --- /dev/null +++ b/src/test/java/com/neo4j/docker/neo4jserver/configurations/TestConfSettings.java @@ -0,0 +1,575 @@ +package com.neo4j.docker.neo4jserver.configurations; + +import static com.neo4j.docker.utils.StartupDetector.makeContainerWaitForNeo4jReady; + +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.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.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 final String AUTH = "none"; + private static Logger log = LoggerFactory.getLogger(TestConfSettings.class); + private static Path confFolder; + private static Map confNames; + + @BeforeAll + static void getVersionSpecificConfigurationSettings() + { + confFolder = Configuration.getConfigurationResourcesFolder( TestSettings.NEO4J_VERSION ); + confNames = Configuration.getConfigurationNameMap( TestSettings.NEO4J_VERSION ); + } + + private GenericContainer createContainer() + { + return new GenericContainer(TestSettings.IMAGE_ID) + .withEnv("NEO4J_AUTH", AUTH) + .withEnv("NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes") + .withExposedPorts(7474, 7687) + .withLogConsumer(new Slf4jLogConsumer(log)); + } + + private GenericContainer makeContainerDumpConfig(GenericContainer container) + { + container.setWaitStrategy( Wait.forLogMessage( ".*Config Dumped.*", 1 ) + .withStartupTimeout( Duration.ofSeconds( 30 ) ) ); + container.setCommand("dump-config"); + // 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() ); + SetContainerUser.nonRootUser( container ); + return container; + } + + 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, Configuration setting, String value, boolean shouldBeFound ) throws IOException + { + assertConfigurationPresentInDebugLog( debugLog, setting, new String[]{value}, shouldBeFound ); + } + + private void assertConfigurationPresentInDebugLog( Path debugLog, Configuration 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.name )).findFirst().orElse( "" ); + lines.close(); + if(shouldBeFound) + { + Assertions.assertTrue( !actualSetting.isEmpty(), setting.name+" was never set" ); + Assertions.assertTrue( Arrays.stream( eitherOfValues ).anyMatch( actualSetting::contains ), + setting.name +" is set to the wrong value. Expected either of: "+ + Arrays.toString( eitherOfValues ) +" Actual: " + actualSetting ); + } + else + { + Assertions.assertTrue( actualSetting.isEmpty(),setting.name+" 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(confNames.get( Setting.MEMORY_PAGECACHE_SIZE ).envName, "1000m") + .withEnv(confNames.get( Setting.MEMORY_HEAP_INITIALSIZE ).envName, "2000m") + .withEnv(confNames.get( Setting.MEMORY_HEAP_MAXSIZE ).envName, "3000m") + .withEnv( confNames.get( Setting.DIRECTORIES_LOGS ).envName, "/notdefaultlogs" ) + .withEnv( confNames.get( Setting.DIRECTORIES_DATA ).envName, "/notdefaultdata" ) ) + { + Path confMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "overriddenbyenv-conf-", + "/conf" ); + conf = confMount.resolve( "neo4j.conf" ).toFile(); + makeContainerDumpConfig( 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( confNames.get( Setting.MEMORY_PAGECACHE_SIZE ).name ), + "pagecache size not overridden" ); + Assertions.assertEquals( "1000m", + configurations.get( confNames.get( Setting.MEMORY_PAGECACHE_SIZE ).name ), + "pagecache size not overridden" ); + + Assertions.assertTrue( configurations.containsKey( confNames.get( Setting.MEMORY_HEAP_INITIALSIZE ).name ), + "initial heap size not overridden" ); + Assertions.assertEquals( "2000m", + configurations.get( confNames.get( Setting.MEMORY_HEAP_INITIALSIZE ).name ), + "initial heap size not overridden" ); + + Assertions.assertTrue( configurations.containsKey( confNames.get( Setting.MEMORY_HEAP_MAXSIZE ).name ), + "maximum heap size not overridden" ); + Assertions.assertEquals( "3000m", + configurations.get( confNames.get( Setting.MEMORY_HEAP_MAXSIZE ).name ), + "maximum heap size not overridden" ); + + Assertions.assertTrue( configurations.containsKey( confNames.get( Setting.DIRECTORIES_LOGS ).name ), + "log folder not overridden" ); + Assertions.assertEquals( "/notdefaultlogs", + configurations.get(confNames.get( Setting.DIRECTORIES_LOGS ).name), + "log directory not overridden" ); + Assertions.assertTrue( configurations.containsKey( confNames.get( Setting.DIRECTORIES_DATA ).name ), "data folder not overridden" ); + Assertions.assertEquals( "/notdefaultdata", + configurations.get(confNames.get( Setting.DIRECTORIES_DATA ).name), + "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 = confFolder.resolve( "ReadConf.conf" ); + Files.copy( confFile, confMount.resolve( "neo4j.conf" ) ); + //Start the container + makeContainerWaitForNeo4jReady( container, AUTH ); + container.start(); + } + + //Check if the container reads the conf file + assertConfigurationPresentInDebugLog( debugLog, confNames.get( Setting.MEMORY_HEAP_MAXSIZE ), + "512", true ); + } + + @Test + void testDefaultsConfigsAreSet() throws Exception + { + try(GenericContainer container = createContainer()) + { + //Mount /logs + Path logMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "default-settings-logs-", + "/logs" ); + SetContainerUser.nonRootUser( container ); + //Start the container + makeContainerWaitForNeo4jReady( container, AUTH ); + container.start(); + DatabaseIO dbio = new DatabaseIO( container ); + + dbio.verifyConfigurationSetting("neo4j", "none", confNames.get( Setting.DEFAULT_LISTEN_ADDRESS).name, "0.0.0.0"); + assertConfigurationPresentInDebugLog(logMount.resolve("debug.log"), confNames.get( Setting.DEFAULT_LISTEN_ADDRESS), "0.0.0.0", true); + + // test enterprise only default configurations are set + if (TestSettings.EDITION == TestSettings.Edition.ENTERPRISE) { + String expectedDiscoveryAddress = container.getContainerId().substring(0, 12) + ":5000"; + String expectedTxAddress = container.getContainerId().substring(0, 12) + ":6000"; + String expectedRaftAddress = container.getContainerId().substring(0, 12) + ":7000"; + dbio.verifyConfigurationSetting("neo4j", "none", confNames.get( Setting.CLUSTER_DISCOVERY_ADDRESS).name, expectedDiscoveryAddress); + assertConfigurationPresentInDebugLog(logMount.resolve("debug.log"), confNames.get( Setting.CLUSTER_DISCOVERY_ADDRESS), expectedDiscoveryAddress,true); + dbio.verifyConfigurationSetting("neo4j", "none", confNames.get( Setting.CLUSTER_TRANSACTION_ADDRESS).name, expectedTxAddress); + assertConfigurationPresentInDebugLog(logMount.resolve("debug.log"), confNames.get( Setting.CLUSTER_TRANSACTION_ADDRESS), expectedTxAddress,true); + dbio.verifyConfigurationSetting("neo4j", "none", confNames.get( Setting.CLUSTER_RAFT_ADDRESS).name, expectedRaftAddress); + assertConfigurationPresentInDebugLog(logMount.resolve("debug.log"), confNames.get( Setting.CLUSTER_RAFT_ADDRESS), expectedRaftAddress,true); + + dbio.verifyConfigurationSetting("neo4j", "none", confNames.get( Setting.TXLOG_RETENTION_POLICY).name, "100M size"); + assertConfigurationPresentInDebugLog(logMount.resolve("debug.log"), confNames.get( Setting.TXLOG_RETENTION_POLICY), "100M size", 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 = confFolder.resolve( "ConfsReplaced.conf" ); + Files.copy( confFile, confMount.resolve( "neo4j.conf" ) ); + makeContainerDumpConfig( container ); + //Start the container + container.start(); + } + //Read the config file to check if the config is set correctly + Map configurations = parseConfFile( conf ); + Assertions.assertTrue( configurations.containsKey( confNames.get( Setting.MEMORY_PAGECACHE_SIZE ).name ), + "conf settings not set correctly by docker-entrypoint" ); + Assertions.assertEquals( "512M", + configurations.get(confNames.get( Setting.MEMORY_PAGECACHE_SIZE ).name), + "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 ConfsNotOverridden.conf file + Path confFile = confFolder.resolve( "ConfsNotOverridden.conf" ); + Files.copy( confFile, confMount.resolve( "neo4j.conf" ) ); + makeContainerDumpConfig( container ); + container.start(); + } + + //Read the config file to check if the config is not overriden + Map configurations = parseConfFile(conf); + Assertions.assertTrue(configurations.containsKey(confNames.get( Setting.MEMORY_PAGECACHE_SIZE).name), + "conf settings not set correctly by docker-entrypoint"); + Assertions.assertEquals("1024M", + configurations.get(confNames.get( Setting.MEMORY_PAGECACHE_SIZE).name), + "docker-entrypoint has overridden custom setting set from user's conf"); + } + + @Test + void testEnvVarsOverride() throws Exception + { + Path debugLog; + try(GenericContainer container = createContainer().withEnv(confNames.get(Setting.MEMORY_PAGECACHE_SIZE).envName, "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 = confFolder.resolve("EnvVarsOverride.conf"); + Files.copy( confFile, confMount.resolve( "neo4j.conf" ) ); + //Start the container + makeContainerWaitForNeo4jReady( container, AUTH ); + container.start(); + } + + assertConfigurationPresentInDebugLog(debugLog, confNames.get(Setting.MEMORY_PAGECACHE_SIZE), + new String[]{"512m", "512.00MiB"}, 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 settings already set + Path confFile = confFolder.resolve( "EnterpriseOnlyNotOverwritten.conf" ); + Files.copy( confFile, confMount.resolve( "neo4j.conf" ) ); + + //Start the container + SetContainerUser.nonRootUser( container ); + makeContainerWaitForNeo4jReady( container, AUTH ); + container.start(); + //Read debug.log to check that cluster confs are set successfully + assertConfigurationPresentInDebugLog( logMount.resolve( "debug.log" ), + confNames.get( Setting.CLUSTER_TRANSACTION_ADDRESS ), + "localhost:6060", true ); + } + } + + @Test + void testMountingMetricsFolderShouldNotSetConfInCommunity() throws Exception + { + Assumptions.assumeTrue( TestSettings.EDITION == TestSettings.Edition.COMMUNITY, + "Test only valid with community edition"); + + try ( GenericContainer container = createContainer() ) + { + Path testOutputFolder = HostFileSystemOperations.createTempFolder( "metrics-mounting-" ); + HostFileSystemOperations.createTempFolderAndMountAsVolume( container, + "metrics-", + "/metrics", + testOutputFolder ); + Path confMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( container, + "conf-", + "/conf", + testOutputFolder ); + makeContainerDumpConfig( container ); + container.start(); + + File conf = confMount.resolve( "neo4j.conf" ).toFile(); + Map configurations = parseConfFile(conf); + Assertions.assertFalse(configurations.containsKey(confNames.get( Setting.DIRECTORIES_METRICS ).name), + "should not be setting any metrics configurations in community edition"); + } + } + + @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(confNames.get(Setting.MEMORY_PAGECACHE_SIZE).envName, "512m")) + { + //Mount /logs + Path logMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "enterprisesettingsnotincommunity-logs-", + "/logs" ); + debugLog = logMount.resolve( "debug.log" ); + SetContainerUser.nonRootUser( container ); + //Start the container + makeContainerWaitForNeo4jReady( container, AUTH ); + container.start(); + } + + //Read debug.log to check that cluster confs are not present + assertConfigurationPresentInDebugLog( debugLog, confNames.get(Setting.CLUSTER_TRANSACTION_ADDRESS), "*", false ); + } + + @Test + void testJvmAdditionalNotOverridden() throws Exception + { + Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_400), + "test not applicable in versions older than 4.0." ); + Path logMount; + String expectedJvmAdditional = "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005"; + + try(GenericContainer container = createContainer()) + { + 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 JvmAdditionalNotOverridden.conf file + Path confFile = confFolder.resolve( "JvmAdditionalNotOverridden.conf" ); + Files.copy( confFile, confMount.resolve( "neo4j.conf" ) ); + //Start the container + makeContainerWaitForNeo4jReady( container, AUTH ); + container.start(); + // verify setting correctly loaded into neo4j + DatabaseIO dbio = new DatabaseIO( container ); + dbio.verifyConfigurationSetting( "neo4j", "none", + confNames.get( Setting.JVM_ADDITIONAL ).name, expectedJvmAdditional); + } + + assertConfigurationPresentInDebugLog( logMount.resolve( "debug.log"), + confNames.get( Setting.JVM_ADDITIONAL ), + expectedJvmAdditional, + true ); + } + + @Test + void testDollarInConfigEscapedProperly_conf() throws Exception + { + Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 4,3,0 ) ), + "test not applicable in versions before 4.3." ); + String expectedJvmAdditional = "-Djavax.net.ssl.trustStorePassword=beepbeep$boop1boop2"; + Path logMount; + try(GenericContainer container = createContainer()) + { + Path testOutputFolder = HostFileSystemOperations.createTempFolder( "jvmdollarinconf-" ); + //Mount /conf + Path confMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "conf-", + "/conf", testOutputFolder); + logMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "logs-", + "/logs", + testOutputFolder); + SetContainerUser.nonRootUser( container ); + //copy test conf file + Path confFile = confFolder.resolve("JvmAdditionalWithDollar.conf" ); + Files.copy( confFile, confMount.resolve( "neo4j.conf" ) ); + //Start the container + makeContainerWaitForNeo4jReady( container, AUTH ); + container.start(); + // verify setting correctly loaded into neo4j + DatabaseIO dbio = new DatabaseIO( container ); + dbio.verifyConfigurationSetting( "neo4j", "none", + confNames.get( Setting.JVM_ADDITIONAL ).name, expectedJvmAdditional); + } + + assertConfigurationPresentInDebugLog( logMount.resolve( "debug.log"), + confNames.get( Setting.JVM_ADDITIONAL ), + expectedJvmAdditional, + true ); + } + + @Test + void testDollarInConfigEscapedProperly_env() throws Exception + { + Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 4,3,0 ) ), + "test not applicable in versions before 4.3." ); + Path logMount; + String expectedJvmAdditional = "-Djavax.net.ssl.trustStorePassword=bleepblorp$bleep1blorp4"; + try(GenericContainer container = createContainer()) + { + logMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "confdollarlogs-", + "/logs"); + SetContainerUser.nonRootUser( container ); + container.withEnv( confNames.get( Setting.JVM_ADDITIONAL ).envName, expectedJvmAdditional); + //Start the container + makeContainerWaitForNeo4jReady( container, AUTH ); + container.start(); + // verify setting correctly loaded into neo4j + DatabaseIO dbio = new DatabaseIO( container ); + dbio.verifyConfigurationSetting( "neo4j", "none", + confNames.get( Setting.JVM_ADDITIONAL ).name, expectedJvmAdditional); + } + + assertConfigurationPresentInDebugLog( logMount.resolve( "debug.log"), + confNames.get( Setting.JVM_ADDITIONAL ), + expectedJvmAdditional, + 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(confNames.get(Setting.SECURITY_PROCEDURES_UNRESTRICTED).envName, "*")) + { + confMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "shellexpansionavoided-conf-", + "/conf" ); + makeContainerDumpConfig( container ); + container.start(); + } + File conf = confMount.resolve( "neo4j.conf" ).toFile(); + Map configurations = parseConfFile(conf); + Assertions.assertTrue(configurations.containsKey(confNames.get( Setting.SECURITY_PROCEDURES_UNRESTRICTED).name), + "configuration not set from env var"); + Assertions.assertEquals("*", + configurations.get(confNames.get( Setting.SECURITY_PROCEDURES_UNRESTRICTED).name), + "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/configurations/TestExtendedConf.java b/src/test/java/com/neo4j/docker/neo4jserver/configurations/TestExtendedConf.java new file mode 100644 index 00000000..4be8b0b9 --- /dev/null +++ b/src/test/java/com/neo4j/docker/neo4jserver/configurations/TestExtendedConf.java @@ -0,0 +1,225 @@ +package com.neo4j.docker.neo4jserver.configurations; + +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.Map; +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 ); + private static Path testConfsFolder; + private static Configuration logRotationConfig; + + @BeforeAll + static void ensureFeaturePresent() + { + Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isNewerThan( new Neo4jVersion( 4,2,0 ) ), + "Extended configuration feature not available before 4.2" ); + } + + @BeforeAll + static void createVersionSpecificConfigurationSettings() { + testConfsFolder = Configuration.getConfigurationResourcesFolder( TestSettings.NEO4J_VERSION ); + logRotationConfig = Configuration.getConfigurationNameMap( TestSettings.NEO4J_VERSION ) + .get( Setting.LOGS_GC_ROTATION_KEEPNUMBER ); + } + + 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 = testConfsFolder.resolve( "ExtendedConf.conf" ); + Files.copy( confFile, confFolder.resolve( "neo4j.conf" ) ); + HostFileSystemOperations.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 = testConfsFolder.resolve( "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 '" + logRotationConfig.name + "'" ) ); + // 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 = testConfsFolder.resolve( "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(logRotationConfig.name + "=20")).findFirst(); + lines.close(); + Assertions.assertTrue( isMatch.isPresent(), logRotationConfig.name+" 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 ); + } +} 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..f2e88480 --- /dev/null +++ b/src/test/java/com/neo4j/docker/neo4jserver/plugins/ExampleNeo4jPlugin.java @@ -0,0 +1,59 @@ +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; + } + } +// @ServiceProvider +// public static class ExampleConfigurationSetting implements SettingsDeclaration +// { +// public static final String CONF_NAME = "com.neo4j.docker.neo4jserver.plugins.loaded_verison"; +// +// @Description("Unique setting to identify which semver field was matched") +// public static final Setting loadedVersionValue = SettingImpl.newBuilder( +// CONF_NAME, +// SettingValueParsers.STRING, +// "unset" +// ).build(); +// } + + @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/neo4jserver/plugins/Neo4jPluginEnv.java b/src/test/java/com/neo4j/docker/neo4jserver/plugins/Neo4jPluginEnv.java new file mode 100644 index 00000000..f7b94f2b --- /dev/null +++ b/src/test/java/com/neo4j/docker/neo4jserver/plugins/Neo4jPluginEnv.java @@ -0,0 +1,24 @@ +package com.neo4j.docker.neo4jserver.plugins; + +import com.neo4j.docker.utils.Neo4jVersion; +import com.neo4j.docker.utils.TestSettings; + +public class Neo4jPluginEnv +{ + public static final String PLUGIN_ENV_4X = "NEO4JLABS_PLUGINS"; + public static final String PLUGIN_ENV_5X = "NEO4J_PLUGINS"; + + public static String get( ) + { + return get(TestSettings.NEO4J_VERSION); + } + + public static String get( Neo4jVersion version ) + { + if( version.isOlderThan( Neo4jVersion.NEO4J_VERSION_500 ) ) + { + return PLUGIN_ENV_4X; + } + else return PLUGIN_ENV_5X; + } +} diff --git a/src/test/java/com/neo4j/docker/neo4jserver/plugins/TestBundledPluginInstallation.java b/src/test/java/com/neo4j/docker/neo4jserver/plugins/TestBundledPluginInstallation.java new file mode 100644 index 00000000..fdb68e66 --- /dev/null +++ b/src/test/java/com/neo4j/docker/neo4jserver/plugins/TestBundledPluginInstallation.java @@ -0,0 +1,183 @@ +package com.neo4j.docker.neo4jserver.plugins; + +import com.neo4j.docker.utils.HostFileSystemOperations; +import com.neo4j.docker.utils.Neo4jVersion; +import com.neo4j.docker.utils.TestSettings; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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.wait.strategy.Wait; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +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 ); + + + static Stream bundledPluginsArgs() { + return Stream.of( + // plugin name key, version it's bundled since, is enterprise only + Arguments.arguments("apoc-core", new Neo4jVersion(4, 1, 0), false), + Arguments.arguments( "graph-data-science", new Neo4jVersion( 4,4,0 ), true ), + Arguments.arguments( "bloom", new Neo4jVersion( 4,4,0 ), true ) + ); + } + + private GenericContainer createContainerWithBundledPlugin(String pluginName) + { + GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID ); + + container.withEnv( "NEO4J_AUTH", "none" ) + .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" ) + .withEnv( Neo4jPluginEnv.get(), "[\"" +pluginName+ "\"]" ) + .withExposedPorts( DEFAULT_BROWSER_PORT, DEFAULT_BOLT_PORT ) + .withLogConsumer( new Slf4jLogConsumer( log ) ) + .waitingFor( Wait.forHttp( "/" ) + .forPort( DEFAULT_BROWSER_PORT ) + .forStatusCode( 200 ) + .withStartupTimeout( Duration.ofSeconds( 30 ) ) ); + return container; + } + + @ParameterizedTest(name = "testBundledPlugin_{0}") + @MethodSource("bundledPluginsArgs") + public void testBundledPlugin(String pluginName, Neo4jVersion bundledSince, boolean isEnterpriseOnly) throws Exception + { + Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( bundledSince ), + String.format("plugin %s was not bundled in Neo4j %s", pluginName, bundledSince.toString())); + if(isEnterpriseOnly) + { + Assumptions.assumeTrue( TestSettings.EDITION == TestSettings.Edition.ENTERPRISE, + String.format("plugin %s is enterprise only", pluginName)); + } + + GenericContainer container = null; + Path pluginsMount = null; + try + { + container = createContainerWithBundledPlugin( pluginName ); + pluginsMount = HostFileSystemOperations + .createTempFolderAndMountAsVolume( container, + "bundled-"+pluginName+"-plugin-", + "/plugins" ); + container.start(); + } + catch(ContainerLaunchException e) + { + // we don't want this test to depend on the plugins actually working (that's outside the scope of + // the docker tests), so we have to be robust to the container failing to start. + log.error( String.format("The bundled %s plugin caused Neo4j to fail to start.", pluginName) ); + } + finally + { + // verify the plugins were loaded. + // This is done in the finally block because after stopping the container, the stdout cannot be retrieved. + if (pluginsMount != null) + { + List plugins = Files.list(pluginsMount).map( fname -> fname.getFileName().toString() ) + .filter( fname -> fname.endsWith( ".jar" ) ) + .collect(Collectors.toList()); + Assertions.assertTrue(plugins.size() == 1, "more than one plugin was loaded" ); + Assertions.assertTrue( plugins.get( 0 ).contains( pluginName ) ); + // Verify from container logs, that the plugins were loaded locally rather than downloaded. + String logs = container.getLogs( OutputFrame.OutputType.STDOUT); + String errlogs = container.getLogs( OutputFrame.OutputType.STDERR); + Assertions.assertTrue( + Stream.of(logs.split( "\n" )) + .anyMatch( line -> line.matches( "Installing Plugin '" + pluginName + "' from /var/lib/neo4j/.*" ) ), + "Plugin was not installed from neo4j home"); +// Assertions.assertFalse( +// Stream.of(errlogs.split( "\n" )) +// .anyMatch( line -> line.matches( "Failed to read config .+: Unrecognized setting\\..*" ) ), +// "An invalid configuration setting was set"); + } + if(container !=null) + { + container.stop(); + } + else + { + Assertions.fail("Test failed before container could even be initialised"); + } + } + } + + @ParameterizedTest(name = "testBundledPlugin_downloadsIfNotAvailableLocally_{0}") + @MethodSource("bundledPluginsArgs") + public void testBundledPlugin_downloadsIfNotAvailableLocally + (String pluginName, Neo4jVersion bundledSince, boolean isEnterpriseOnly) throws Exception + { + Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( bundledSince ), + String.format("plugin %s was not bundled in Neo4j %s", pluginName, bundledSince.toString())); + Assumptions.assumeTrue( isEnterpriseOnly, "Test only applies to enterprise only bundled plugins tested against community edition" ); + Assumptions.assumeTrue( TestSettings.EDITION == TestSettings.Edition.COMMUNITY, + "Test only applies to enterprise only bundled plugins tested against community edition" ); + + + GenericContainer container = null; + Path pluginsMount = null; + try + { + container = createContainerWithBundledPlugin( pluginName ); + pluginsMount = HostFileSystemOperations + .createTempFolderAndMountAsVolume( container, + "bundled-"+pluginName+"-plugin-unavailable-", + "/plugins" ); + container.start(); + } + catch(ContainerLaunchException e) + { + // we don't want this test to depend on the plugins actually working (that's outside the scope of + // the docker tests), so we have to be robust to the container failing to start. + log.error( String.format("The %s plugin caused Neo4j to fail to start.", pluginName) ); + } + finally + { + // verify the plugins were loaded. + // This is done in the finally block because after stopping the container, the stdout cannot be retrieved. + if (pluginsMount != null) + { + List plugins = Files.list(pluginsMount).map( fname -> fname.getFileName().toString() ) + .filter( fname -> fname.endsWith( ".jar" ) ) + .collect(Collectors.toList()); + Assertions.assertTrue(plugins.size() == 1, "more than one plugin was loaded" ); + Assertions.assertTrue( plugins.get( 0 ).contains( pluginName ) ); + // Verify from container logs, that the plugins were loaded locally rather than downloaded. + String logs = container.getLogs( OutputFrame.OutputType.STDOUT); + String errlogs = container.getLogs( OutputFrame.OutputType.STDERR); + Assertions.assertTrue( + Stream.of(logs.split( "\n" )) + .anyMatch( line -> line.matches( "Fetching versions.json for Plugin '" + pluginName + "' from http[s]?://.*" ) ), + "Plugin was not installed from cloud"); +// Assertions.assertFalse( +// Stream.of(errlogs.split( "\n" )) +// .anyMatch( line -> line.matches( "Failed to read config .+: Unrecognized setting\\..*" ) ), +// "An invalid configuration setting was set"); + } + if(container !=null) + { + container.stop(); + } + else + { + Assertions.fail("Test failed before container could even be initialised"); + } + } + } +} diff --git a/src/test/java/com/neo4j/docker/neo4jserver/plugins/TestPluginInstallation.java b/src/test/java/com/neo4j/docker/neo4jserver/plugins/TestPluginInstallation.java new file mode 100644 index 00000000..f5fef062 --- /dev/null +++ b/src/test/java/com/neo4j/docker/neo4jserver/plugins/TestPluginInstallation.java @@ -0,0 +1,319 @@ +package com.neo4j.docker.neo4jserver.plugins; + +import com.github.dockerjava.api.command.CreateContainerCmd; +import com.google.gson.Gson; +import com.neo4j.docker.utils.*; +import java.time.Duration; +import org.junit.Rule; +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.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; +import org.junit.jupiter.migrationsupport.rules.EnableRuleMigrationSupport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.Testcontainers; +import org.testcontainers.containers.Container; +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.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import org.neo4j.driver.Record; + +import static com.neo4j.docker.utils.TestSettings.NEO4J_VERSION; + +@EnableRuleMigrationSupport +public class TestPluginInstallation +{ + private static final String DB_USER = "neo4j"; + private static final String DB_PASSWORD = "quality"; + private static final String PLUGIN_JAR = "myPlugin.jar"; + + private static final Logger log = LoggerFactory.getLogger( TestPluginInstallation.class ); + + @Rule + public HttpServerRule httpServer = new HttpServerRule(); + + @BeforeAll + public static void ensureNotARMArchitecture() + { + // These tests make use of a TestContainers feature that means that if you serve something locally on the + // host machine, it is accessible from a container at the address http://host.testcontainers.internal + // This feature does not seem to work on ARM64 machines. I've created a bug report here 27/01/2022: + // https://github.com/testcontainers/testcontainers-java/issues/4956 + // + // For now, we skip these tests on ARM until there is a fix or workaround. + Assumptions.assumeTrue( System.getProperty("os.arch").equals( "amd64" ), + "Plugin tests can only run on amd64 machines at the moment" ); + } + + private GenericContainer createContainerWithTestingPlugin() + { + Testcontainers.exposeHostPorts( httpServer.PORT ); + GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID ); + + container.withEnv( "NEO4J_AUTH", DB_USER+"/"+ DB_PASSWORD) + .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" ) + .withEnv( Neo4jPluginEnv.get(), "[\"_testing\"]" ) + .withExposedPorts( 7474, 7687 ) + .withLogConsumer( new Slf4jLogConsumer( log ) ); + StartupDetector.makeContainerWaitForDatabaseReady(container, DB_USER, DB_PASSWORD, "neo4j", + Duration.ofSeconds(60)); + SetContainerUser.nonRootUser( container ); + return container; + } + + private File createTestVersionsJson(Path destinationFolder, String... versions) throws Exception + { + List entryList = Arrays.stream( versions ) + .map( VersionsJsonEntry::new ) + .collect( Collectors.toList() ); + Gson jsonBuilder = new Gson(); + String jsonStr = jsonBuilder.toJson( entryList ); + + File outputJsonFile = destinationFolder.resolve( "versions.json" ).toFile(); + Files.write( jsonStr, outputJsonFile, StandardCharsets.UTF_8 ); + return outputJsonFile; + } + + private void setupTestPlugin( Path pluginsDir, File versionsJson ) throws Exception + { + File myPluginJar = pluginsDir.resolve( PLUGIN_JAR ).toFile(); + new JarBuilder().createJarFor( myPluginJar, ExampleNeo4jPlugin.class, ExampleNeo4jPlugin.PrimitiveOutput.class ); + + httpServer.registerHandler( versionsJson.getName(), new HostFileHttpHandler( versionsJson, "application/json" ) ); + httpServer.registerHandler( PLUGIN_JAR, new HostFileHttpHandler( myPluginJar, "application/java-archive" ) ); + } + + private void verifyTestPluginLoaded(DatabaseIO db) + { + // when we check the list of installed procedures... + String listProceduresCypherQuery = NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 4, 3, 0 ) ) ? + "SHOW PROCEDURES YIELD name, signature RETURN name, signature" : + "CALL dbms.procedures() YIELD name, signature RETURN name, signature"; + List procedures = db.runCypherQuery( DB_USER, DB_PASSWORD, listProceduresCypherQuery ); + // Then the procedure from the test plugin should be listed + Assertions.assertTrue( procedures.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 + List pluginResponse = db.runCypherQuery(DB_USER, DB_PASSWORD, + "CALL com.neo4j.docker.neo4jserver.plugins.defaultValues" ); + + // Then we get the response we expect + Assertions.assertEquals(1, pluginResponse.size(), "Our procedure should only return a single result"); + Record record = pluginResponse.get(0); + + String message = "Result from calling our procedure doesnt match our expectations"; + Assertions.assertEquals( "a string", record.get( "string" ).asString(), message ); + Assertions.assertEquals( 42L, record.get( "integer" ).asInt(), message ); + Assertions.assertEquals( 3.14d, record.get( "aFloat" ).asDouble(), 0.000001, message ); + Assertions.assertEquals( true, record.get( "aBoolean" ).asBoolean(), message ); + } + + @Test + @DisabledIfEnvironmentVariable(named = "NEO4J_DOCKER_TESTS_TestPluginInstallation", matches = "ignore") + public void testPlugin() throws Exception + { + Path pluginsDir = HostFileSystemOperations.createTempFolder( "plugin-" ); + File versionsJson = createTestVersionsJson( pluginsDir, NEO4J_VERSION.toString() ); + setupTestPlugin( pluginsDir, versionsJson ); + try(GenericContainer container = createContainerWithTestingPlugin()) + { + container.start(); + DatabaseIO db = new DatabaseIO(container); + verifyTestPluginLoaded(db); + } + } + + @Test + @DisabledIfEnvironmentVariable(named = "NEO4J_DOCKER_TESTS_TestPluginInstallation", matches = "ignore") + public void testPlugin_50BackwardsCompatibility() throws Exception + { + Assumptions.assumeTrue( NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_500 ), + "NEO4JLABS_PLUGIN backwards compatibility does not need checking"); + Path pluginsDir = HostFileSystemOperations.createTempFolder( "plugin-backcompat-" ); + File versionsJson = createTestVersionsJson( pluginsDir, NEO4J_VERSION.toString() ); + setupTestPlugin( pluginsDir, versionsJson ); + try(GenericContainer container = createContainerWithTestingPlugin()) + { + container.withEnv( Neo4jPluginEnv.PLUGIN_ENV_5X, "" ); + container.withEnv( Neo4jPluginEnv.PLUGIN_ENV_4X, "[\"_testing\"]" ); + container.start(); + DatabaseIO db = new DatabaseIO(container); + verifyTestPluginLoaded(db); + } + } + + @Test + @DisabledIfEnvironmentVariable(named = "NEO4J_DOCKER_TESTS_TestPluginInstallation", matches = "ignore") + public void testPluginConfigurationDoesNotOverrideUserSetValues() throws Exception + { + Path pluginsDir = HostFileSystemOperations.createTempFolder( "plugin-noOverride-" ); + File versionsJson = createTestVersionsJson( pluginsDir, NEO4J_VERSION.toString() ); + setupTestPlugin( pluginsDir, versionsJson ); + try(GenericContainer container = createContainerWithTestingPlugin()) + { + // When we set a config value explicitly + 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 + // Check that the config remains as set by our env var and is not overridden by the plugin defaults + DatabaseIO db = new DatabaseIO(container); + verifyTestPluginLoaded(db); + db.verifyConfigurationSetting( DB_USER, DB_PASSWORD, + "dbms.security.procedures.unrestricted", + "foo", + "neo4j config should not be overridden by plugin"); + } + } + + @Test + void testSemanticVersioningPlugin_catchesMatchWithX() throws Exception + { + Path pluginsDir = HostFileSystemOperations.createTempFolder( "plugin-semverMatchesX-" ); + File versionsJson = createTestVersionsJson( pluginsDir, NEO4J_VERSION.getBranch()+".x"); + setupTestPlugin( pluginsDir, versionsJson ); + try(GenericContainer container = createContainerWithTestingPlugin()) + { + container.start(); + DatabaseIO db = new DatabaseIO(container); + verifyTestPluginLoaded(db); + } + } + + @Test + void testSemanticVersioningPlugin_catchesMatchWithStar() throws Exception + { + Path pluginsDir = HostFileSystemOperations.createTempFolder( "plugin-semverMatchesStar-" ); + File versionsJson = createTestVersionsJson( pluginsDir, NEO4J_VERSION.getBranch()+".*"); + setupTestPlugin( pluginsDir, versionsJson ); + try(GenericContainer container = createContainerWithTestingPlugin()) + { + container.start(); + DatabaseIO db = new DatabaseIO(container); + verifyTestPluginLoaded(db); + } + } + + @Test + @DisabledIfEnvironmentVariable(named = "NEO4J_DOCKER_TESTS_TestPluginInstallation", matches = "ignore") + public void testPlugin_originalEntrypointLocation() throws Exception + { + Assumptions.assumeFalse( NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_500 ), + "/docker-entrypoint.sh is permanently moved from 5.0 onwards"); + Path pluginsDir = HostFileSystemOperations.createTempFolder( "plugin-oldEntrypoint-" ); + File versionsJson = createTestVersionsJson( pluginsDir, NEO4J_VERSION.getBranch()+".x" ); + setupTestPlugin( pluginsDir, versionsJson ); + try(GenericContainer container = createContainerWithTestingPlugin()) + { + container.withCreateContainerCmdModifier( + (Consumer) cmd -> cmd.withEntrypoint( "/docker-entrypoint.sh", "neo4j" ) ); + container.start(); + DatabaseIO db = new DatabaseIO(container); + verifyTestPluginLoaded(db); + } + } + + + + @Test + void testSemanticVersioningLogic() throws Exception + { + String major = Integer.toString(NEO4J_VERSION.major); + String minor = Integer.toString(NEO4J_VERSION.minor); + + // testing common neo4j name variants + List neo4jVersions = new ArrayList() {{ + add(NEO4J_VERSION.toString()); + add(NEO4J_VERSION.toString()+"-drop01.1"); + add(NEO4J_VERSION.toString()+"-drop01"); + add(NEO4J_VERSION.toString()+"-beta04"); + }}; + + List matchingCases = new ArrayList() {{ + add( NEO4J_VERSION.toString() ); + add( major+'.'+minor+".x" ); + add( major+'.'+minor+".*" ); + }}; + + List nonMatchingCases = new ArrayList() {{ + add( (NEO4J_VERSION.major+1)+'.'+minor+".x" ); + add( (NEO4J_VERSION.major-1)+'.'+minor+".x" ); + add( major+'.'+(NEO4J_VERSION.minor+1)+".x" ); + add( major+'.'+(NEO4J_VERSION.minor-1)+".x" ); + add( (NEO4J_VERSION.major+1)+'.'+minor+".*" ); + add( (NEO4J_VERSION.major-1)+'.'+minor+".*" ); + add( major+'.'+(NEO4J_VERSION.minor+1)+".*" ); + add( major+'.'+(NEO4J_VERSION.minor-1)+".*" ); + }}; + + // Asserting every test case means that if there's a failure, all further tests won't run. + // Instead we're running all tests and saving any failed cases for reporting at the end of the test. + List failedTests = new ArrayList(); + + + try(GenericContainer container = createContainerWithTestingPlugin()) + { + container.withEnv( Neo4jPluginEnv.get(), "" ); // don't need the _testing plugin for this + container.start(); + + String semverQuery = "echo \"{\\\"neo4j\\\":\\\"%s\\\"}\" | " + + "jq -L/startup --raw-output \"import \\\"semver\\\" as lib; " + + ".neo4j | lib::semver(\\\"%s\\\")\""; + for(String neoVer : neo4jVersions) + { + for(String ver : matchingCases) + { + Container.ExecResult out = container.execInContainer( "sh", "-c", String.format( semverQuery, ver, neoVer) ); + if(! out.getStdout().trim().equals( "true" ) ) + { + failedTests.add( String.format( "%s should match %s but did not", ver, neoVer) ); + } + } + for(String ver : nonMatchingCases) + { + Container.ExecResult out = container.execInContainer( "sh", "-c", String.format( semverQuery, ver, neoVer) ); + if(! out.getStdout().trim().equals( "false" ) ) + { + failedTests.add( String.format( "%s should NOT match %s but did", ver, neoVer) ); + } + } + } + if(failedTests.size() > 0) + { + Assertions.fail(failedTests.stream().collect( Collectors.joining("\n"))); + } + } + + } + + private class VersionsJsonEntry + { + String neo4j; + String jar; + String _testing; + + VersionsJsonEntry(String neo4j) + { + this.neo4j = neo4j; + this._testing = "SNAPSHOT"; + this.jar = "http://host.testcontainers.internal:3000/"+PLUGIN_JAR; + } + } +} 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..b990d892 --- /dev/null +++ b/src/test/java/com/neo4j/docker/utils/DatabaseIO.java @@ -0,0 +1,159 @@ +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 = "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 verifyConfigurationSetting(String user, String password, String confName, String expectedValue) + { + verifyConfigurationSetting(user, password, confName, expectedValue, ""); + } + + public void verifyConfigurationSetting(String user, String password, String confName, String expectedValue, String extraFailureMsg) + { + List confRecord = runCypherQuery( user, password, + "CALL dbms.listConfig() YIELD name, value " + + "WHERE name='" + confName + "' " + + "RETURN value" ); + Assertions.assertEquals(1, confRecord.size(), "Configuration "+confName+" was not set." ); + Assertions.assertEquals(expectedValue, confRecord.get( 0 ).get( 0 ).asString(), + String.format("Expected %s to be %s but it was %s.%s", + confName, expectedValue, confRecord.get( 0 ).get( 0 ).asString(), extraFailureMsg)); + } + + 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..543531ae --- /dev/null +++ b/src/test/java/com/neo4j/docker/utils/HostFileSystemOperations.java @@ -0,0 +1,106 @@ +package com.neo4j.docker.utils; + +import org.junit.jupiter.api.Assumptions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.GenericContainer; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Random; +import java.util.stream.Collectors; + +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; + } + + public static void setFileOwnerToCurrentUser(Path file) throws Exception + { + setFileOwnerTo( file, SetContainerUser.getNonRootUserString() ); + } + + public static void setFileOwnerToNeo4j(Path file) throws Exception + { + setFileOwnerTo( file, "7474:7474" ); + } + + private static void setFileOwnerTo(Path file, String userAndGroup) throws Exception + { + ProcessBuilder pb = new ProcessBuilder( "chown", userAndGroup, 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/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..b41f789f --- /dev/null +++ b/src/test/java/com/neo4j/docker/utils/Neo4jVersion.java @@ -0,0 +1,108 @@ +package com.neo4j.docker.utils; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +public class Neo4jVersion +{ + public static final Neo4jVersion NEO4J_VERSION_400 = new Neo4jVersion(4,0,0); + public static final Neo4jVersion NEO4J_VERSION_500 = new Neo4jVersion(5,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]+)(?