diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..ce3e6c90 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,24 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: jennyowen, bfeshti + +--- + +## Guidelines + +Please note that GitHub issues are only meant for bug reports/feature requests. +If you have questions on how to use Neo4j, please ask on [Neo4j Community](https://community.neo4j.com/) or [StackOverflow](http://stackoverflow.com/questions/tagged/neo4j) instead of creating an issue here. + +To help us understand your issue, please specify important details, primarily: + +- **Steps to reproduce**. *Not including reproduction steps will mean your bug will take considerably longer to investigate and fix. Please don't skip this*. +- Expected behaviour +- Actual behaviour +- Neo4j image tag being used, eg `neo4j:latest`, `neo4j:enterprise-3.5` etc +- The output of the `docker version` command +- Operating system: (for example Windows 95/Ubuntu 16.04) + +Additionally, include (as appropriate) log-files, stacktraces, and other debug output. diff --git a/.gitignore b/.gitignore index 3cad29d3..66248f8d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,10 @@ /out/ /in/ /devenv.local +/local-mounts/ +/target/ + +# simlinks to dev scripts in https://github.com/neo-technology/teamcity-witchcraft +download_tool.py +docker_tests_get_installers.py +__pycache__/ diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 673f3da4..0d0e9cb2 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -8,26 +8,119 @@ 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 +``` + + +## If the Neo4j Version is not Publicly Available + +The make script cannot automatically download unreleased source files, so you need to manually download them before building the images. + +1. Assuming you cloned this repository to `$NEO4J_DOCKER_ROOT`, +download the community and enterprise unix tar.gz files and copy them to `$NEO4J_DOCKER_ROOT/in`. +1. Run the make script setting `NEO4JVERSION` to the version number in the files downloaded into the `in/` folder. + +For example: + +```bash +$ cd $NEO4J_DOCKER_ROOT +$ ls $NEO4J_DOCKER_ROOT/in + neo4j-community-4.0.0-alpha05-unix.tar.gz neo4j-enterprise-4.0.0-alpha05-unix.tar.gz + +$ NEO4JVERSION=4.0.0-alpha05 make clean build +``` + + +# Running the Tests + +The tests are written in java, and require Maven plus jdk 11 for Neo4j version 4.0 onwards or jdk 8 for earlier Neo4j versions. + +The tests require some information about the image before they can test it. +These can be passed as an environment variable or a command line parameter when invoking maven: + + +| Env Variable | Maven parameter | Description | +|-----------------|-----------------|------------------------------------------------------------| +| `NEO4JVERSION` | `-Dversion` | the Neo4j version of the image | +| `NEO4J_IMAGE` | `-Dimage` | the tag of the image to test | +| `NEO4J_EDITION` | `-Dedition` | Either `community` or `enterprise` depending on the image. | + + + +## Using Maven +The Makefile can run the entire test suite. +1. Make sure `java --version` is java 11 or java 8 as necessary. +2. `NEO4JVERSION= make test` This is a make target that will run these commands: +```bash +mvn test -Dimage=$(cat tmp/.image-id-community) -Dedition=community -Dversion=${NEO4JVERSION} +mvn test -Dimage=$(cat tmp/.image-id-enterprise) -Dedition=enterprise -Dversion=${NEO4JVERSION} +``` + +## In Intellij + +1. Make sure the project SDK is java 11 or java 8 as necessary. +1. Edit the [pom.xml file](../master/pom.xml) to replace `${env.NEO4JVERSION}` with the `NEO4JVERSION` you used to build the image. +*(Yes this is terrible, and we need to think of an alternative to this)*. + + For example: + ```xml + ${env.NEO4JVERSION} + ``` + becomes + ```xml + 4.0.0-alpha05 + ``` +1. Install the [EnvFile](https://plugins.jetbrains.com/plugin/7861-envfile) Intellij plugin. +2. Under Run Configurations edit the Template JUnit configuration: + 1. Select the "EnvFile" tab + 2. Make sure "Enable EnvFile" is checked. + 3. Click the `+` then click to add a `.env` file. + 4. In the file selection box select `./tmp/devenv-enterprise.env` or `./tmp/devenv-community.env` depending on which one you want to test. + 5. Rebuilding the Neo4j image will regenerate the `.env` files, so you don't need to worry about keeping the environment up to date. + + +### If the Neo4j Version is not Publicly Available + +1. Clone the Neo4j github repository and checkout the branch you want. +2. Make sure `java --version` returns java 11 if you're building Neo4j 4.0+, or java 8 if building an earlier branch. +1. Run `mvn install` plus whatever maven build flags you like. This should install the latest neo4j jars into the maven cache. +1. Follow instructions for [running tests in Intellij](#in-intellij), +use the `NEO4JVERSION` that is in the pom file of your Neo4j repository clone. + +### cannot find symbol `com.sun.security.auth.module.UnixSystem` -1. create a docker-machine VM -1. export the docker-machine environment for docker +This can happen if you switch from java 8 to java 11 and then try to rebuild the tests in Intellij. -## All platforms +Check that the `java.version` property in the [pom.xml file](../master/pom.xml) is set to 11 instead of 1.8. +DO NOT commit this set to 11 (yes this is a terrible solution). -1. `. devenv` -1. `make` diff --git a/Makefile b/Makefile index 554ee5e2..ffb40828 100644 --- a/Makefile +++ b/Makefile @@ -10,92 +10,110 @@ ifeq ($(origin .RECIPEPREFIX), undefined) endif .RECIPEPREFIX = > -ifndef NEO4J_VERSION - $(error NEO4J_VERSION is not set) +ifndef NEO4JVERSION + $(error NEO4JVERSION is not set) endif 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/') +dist_site := https://dist.neo4j.org +series := $(shell echo "$(NEO4JVERSION)" | sed -E 's/^([0-9]+\.[0-9]+)\..*/\1/') -all: out/enterprise/.sentinel out/community/.sentinel +# 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?="" + +all: test .PHONY: all -test: test-community test-enterprise +test: test-enterprise test-community .PHONY: test -out/%/.sentinel: tmp/image-%/.sentinel tmp/.tests-pass-% +test-enterprise: tmp/.image-id-enterprise +> mvn test -Dimage=$$(cat $<) -Dedition=enterprise -Dversion=$(NEO4JVERSION) -Dtest=$(TESTS) +.PHONY: test-enterprise + +test-community: tmp/.image-id-community +> mvn test -Dimage=$$(cat $<) -Dedition=community -Dversion=$(NEO4JVERSION) -Dtest=$(TESTS) +.PHONY: test-community + +# just build the images, don't test or package +build: tmp/.image-id-community tmp/.image-id-enterprise +.PHONY: build + +# create release images and loadable images +package: package-community package-enterprise +.PHONY: package + +package-community: tmp/.image-id-community out/community/.sentinel +> mkdir -p out +> docker tag $$(cat $<) neo4j:$(NEO4JVERSION) +> docker save neo4j:$(NEO4JVERSION) > out/neo4j-community-$(NEO4JVERSION)-docker-loadable.tar + +package-enterprise: tmp/.image-id-enterprise out/enterprise/.sentinel +> mkdir -p out +> docker tag $$(cat $<) neo4j:$(NEO4JVERSION)-enterprise +> docker save neo4j:$(NEO4JVERSION)-enterprise > out/neo4j-enterprise-$(NEO4JVERSION)-docker-loadable.tar + +out/%/.sentinel: tmp/image-%/.sentinel > 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 $@ +# building the image tmp/.image-id-%: tmp/local-context-%/.sentinel > mkdir -p $(@D) > image=test/$$RANDOM > docker build --tag=$$image \ - --build-arg="NEO4J_URI=file:///tmp/$(call tarball,$*,$(NEO4J_VERSION))" \ + --build-arg="NEO4J_URI=file:///tmp/$(call tarball,$*,$(NEO4JVERSION))" \ $( echo -n $$image >$@ +> echo "NEO4JVERSION=$(NEO4JVERSION)" > tmp/devenv-${*}.env +> echo "NEO4J_IMAGE=$$image" >> tmp/devenv-${*}.env +> echo "NEO4J_EDITION=${*}" >> tmp/devenv-${*}.env -tmp/local-context-%/.sentinel: tmp/image-%/.sentinel in/$(call tarball,%,$(NEO4J_VERSION)) +tmp/neo4jlabs-plugins.json: ./neo4jlabs-plugins.json +> mkdir -p $(@D) +> cp $< $@ + +tmp/local-context-%/.sentinel: tmp/image-%/.sentinel in/$(call tarball,%,$(NEO4JVERSION)) tmp/neo4jlabs-plugins.json > rm -rf $(@D) > mkdir -p $(@D) > cp -r $( cp $(filter %.tar.gz,$^) $(@D)/local-package +> cp $(filter %.json,$^) $(@D)/local-package > touch $@ -tmp/image-%/.sentinel: src/$(series)/Dockerfile src/$(series)/docker-entrypoint.sh \ - in/$(call tarball,%,$(NEO4J_VERSION)) +tmp/image-%/.sentinel: docker-image-src/$(series)/Dockerfile docker-image-src/$(series)/docker-entrypoint.sh \ + in/$(call tarball,%,$(NEO4JVERSION)) tmp/neo4jlabs-plugins.json > mkdir -p $(@D) > cp $(filter %/docker-entrypoint.sh,$^) $(@D)/docker-entrypoint.sh > sha=$$(shasum --algorithm=256 $(filter %.tar.gz,$^) | cut -d' ' -f1) > <$(filter %/Dockerfile,$^) sed \ -e "s|%%NEO4J_SHA%%|$${sha}|" \ - -e "s|%%NEO4J_TARBALL%%|$(call tarball,$*,$(NEO4J_VERSION))|" \ + -e "s|%%NEO4J_TARBALL%%|$(call tarball,$*,$(NEO4JVERSION))|" \ -e "s|%%NEO4J_EDITION%%|$*|" \ -e "s|%%NEO4J_DIST_SITE%%|$(dist_site)|" \ >$(@D)/Dockerfile > mkdir -p $(@D)/local-package +> cp $(filter %.json,$^) $(@D)/local-package > touch $(@D)/local-package/.sentinel > touch $@ -run = trapping-sigint \ - docker run --publish 7474:7474 --publish 7687:7687 \ - --env=NEO4J_ACCEPT_LICENSE_AGREEMENT=yes \ - --env=NEO4J_AUTH=neo4j/foo --rm $$(cat $1) -build-enterprise: tmp/.image-id-enterprise -> @echo "Neo4j $(NEO4J_VERSION)-enterprise available as: $$(cat $<)" -build-community: tmp/.image-id-community -> @echo "Neo4j $(NEO4J_VERSION)-community available as: $$(cat $<)" -run-enterprise: tmp/.image-id-enterprise -> $(call run,$<) -run-community: tmp/.image-id-community -> $(call run,$<) -test-enterprise: tmp/.tests-pass-enterprise -test-community: tmp/.tests-pass-community -.PHONY: run-enterprise run-community build-enterprise build-community test-enterprise test-community - fetch_tarball = curl --fail --silent --show-error --location --remote-name \ - $(dist_site)/$(call tarball,$(1),$(NEO4J_VERSION)) + $(dist_site)/$(call tarball,$(1),$(NEO4JVERSION)) -cache: in/neo4j-%-$(NEO4J_VERSION)-unix.tar.gz +cache: in/neo4j-%-$(NEO4JVERSION)-unix.tar.gz .PHONY: cache -in/neo4j-community-$(NEO4J_VERSION)-unix.tar.gz: +in/neo4j-community-$(NEO4JVERSION)-unix.tar.gz: > mkdir -p in > cd in > $(call fetch_tarball,community) -in/neo4j-enterprise-$(NEO4J_VERSION)-unix.tar.gz: +in/neo4j-enterprise-$(NEO4JVERSION)-unix.tar.gz: > mkdir -p in > cd in > $(call fetch_tarball,enterprise) diff --git a/README.md b/README.md index 2c8d37fc..69a5fc7c 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,41 @@ *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 2.3 container like this: +You can start a Neo4j container like this: ``` docker run \ - --publish=7474:7474 \ + --publish=7474:7474 --publish=7687:7687 \ --volume=$HOME/neo4j/data:/data \ - neo4j:2.3 + --volume=$HOME/neo4j/logs:/logs \ + neo4j:latest ``` -## Neo4j 3.0 - -Documentation for the Neo4j 3.0 image can be found [here](http://neo4j.com/docs/operations-manual/current/deployment/single-instance/docker/). - -You can start a Neo4j 3.0 container like this: +To start a Neo4j Enterprise Edition container, you can run: ``` docker run \ --publish=7474:7474 --publish=7687:7687 \ + --env=NEO4J_ACCEPT_LICENSE_AGREEMENT=yes \ --volume=$HOME/neo4j/data:/data \ - neo4j:3.0 + --volume=$HOME/neo4j/logs:/logs \ + neo4j:enterprise ``` +Mounting the `/data` and `/logs` folder is optional, +but it means that data can persist between closing and reopening Neo4j containers. + + +# 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..ba9c6244 --- /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. 2019. 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..da29fb4a --- /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. 2019. All Rights Reserved. +Use of this Software without a proper commercial license with Neo4j, +Inc. or its affiliates is prohibited. + +Email inquiries can be directed to: licensing@neo4j.com + +More information is also available at: https://neo4j.com/licensing/ + + +To accept the license agreement set the environment variable +NEO4J_ACCEPT_LICENSE_AGREEMENT=yes + +To do this you can use the following docker argument: + + --env=NEO4J_ACCEPT_LICENSE_AGREEMENT=yes +" + exit 1 + fi + fi +fi + +# Env variable naming convention: +# - prefix NEO4J_ +# - double underscore char '__' instead of single underscore '_' char in the setting name +# - underscore char '_' instead of dot '.' char in the setting name +# Example: +# NEO4J_dbms_tx__log_rotation_retention__policy env variable to set +# dbms.tx_log.rotation.retention_policy setting + +# Backward compatibility - map old hardcoded env variables into new naming convention (if they aren't set already) +# Set some to default values if unset +: ${NEO4J_dbms_tx__log_rotation_retention__policy:=${NEO4J_dbms_txLog_rotation_retentionPolicy:-"100M size"}} +: ${NEO4J_wrapper_java_additional:=${NEO4J_UDC_SOURCE:-"-Dneo4j.ext.udc.source=docker"}} +: ${NEO4J_dbms_unmanaged__extension__classes:=${NEO4J_dbms_unmanagedExtensionClasses:-}} +: ${NEO4J_dbms_allow__format__migration:=${NEO4J_dbms_allowFormatMigration:-}} +: ${NEO4J_dbms_connectors_default__advertised__address:=${NEO4J_dbms_connectors_defaultAdvertisedAddress:-}} +: ${NEO4J_ha_server__id:=${NEO4J_ha_serverId:-}} +: ${NEO4J_ha_initial__hosts:=${NEO4J_ha_initialHosts:-}} + +if [ "${NEO4J_EDITION}" == "enterprise" ]; + then + : ${NEO4J_causal__clustering_expected__core__cluster__size:=${NEO4J_causalClustering_expectedCoreClusterSize:-}} + : ${NEO4J_causal__clustering_initial__discovery__members:=${NEO4J_causalClustering_initialDiscoveryMembers:-}} + : ${NEO4J_causal__clustering_discovery__advertised__address:=${NEO4J_causalClustering_discoveryAdvertisedAddress:-"$(hostname):5000"}} + : ${NEO4J_causal__clustering_transaction__advertised__address:=${NEO4J_causalClustering_transactionAdvertisedAddress:-"$(hostname):6000"}} + : ${NEO4J_causal__clustering_raft__advertised__address:=${NEO4J_causalClustering_raftAdvertisedAddress:-"$(hostname):7000"}} + # Custom settings for dockerized neo4j + : ${NEO4J_ha_host_coordination:=$(hostname):5001} + : ${NEO4J_ha_host_data:=$(hostname):6001} + : ${NEO4J_causal__clustering_discovery__advertised__address:=$(hostname):5000} + : ${NEO4J_causal__clustering_transaction__advertised__address:=$(hostname):6000} + : ${NEO4J_causal__clustering_raft__advertised__address:=$(hostname):7000} +fi + +# unset old hardcoded unsupported env variables +unset NEO4J_dbms_txLog_rotation_retentionPolicy NEO4J_UDC_SOURCE \ + NEO4J_dbms_unmanagedExtensionClasses NEO4J_dbms_allowFormatMigration \ + NEO4J_dbms_connectors_defaultAdvertisedAddress NEO4J_ha_serverId \ + NEO4J_ha_initialHosts NEO4J_causalClustering_expectedCoreClusterSize \ + NEO4J_causalClustering_initialDiscoveryMembers \ + NEO4J_causalClustering_discoveryListenAddress \ + NEO4J_causalClustering_discoveryAdvertisedAddress \ + NEO4J_causalClustering_transactionListenAddress \ + NEO4J_causalClustering_transactionAdvertisedAddress \ + NEO4J_causalClustering_raftListenAddress \ + NEO4J_causalClustering_raftAdvertisedAddress + +if [ -d /conf ]; then + if secure_mode_enabled; then + check_mounted_folder_readable "/conf" + fi + find /conf -type f -exec cp {} "${NEO4J_HOME}"/conf \; +fi + +if [ -d /ssl ]; then + if secure_mode_enabled; then + check_mounted_folder_readable "/ssl" + fi + : ${NEO4J_dbms_directories_certificates:="/ssl"} +fi + +if [ -d /plugins ]; then + if secure_mode_enabled; then + if [[ ! -z "${NEO4JLABS_PLUGINS:-}" ]]; then + # We need write permissions + check_mounted_folder_with_chown "/plugins" + fi + check_mounted_folder_readable "/plugins" + fi + : ${NEO4J_dbms_directories_plugins:="/plugins"} +fi + +if [ -d /import ]; then + if secure_mode_enabled; then + check_mounted_folder_readable "/import" + fi + : ${NEO4J_dbms_directories_import:="/import"} +fi + +if [ -d /metrics ]; then + if secure_mode_enabled; then + check_mounted_folder_readable "/metrics" + fi + : ${NEO4J_dbms_directories_metrics:="/metrics"} +fi + +if [ -d /logs ]; then + check_mounted_folder_with_chown "/logs" + : ${NEO4J_dbms_directories_logs:="/logs"} + if [ -d /data/databases ]; then + check_mounted_folder_with_chown "/data/databases" + fi + if [ -d /data/dbms ]; then + check_mounted_folder_with_chown "/data/dbms" + fi +fi + +if [ -d /data ]; then + check_mounted_folder_with_chown "/data" +fi + + +# set the neo4j initial password only if you run the database server +if [ "${cmd}" == "neo4j" ]; then + if [ "${NEO4J_AUTH:-}" == "none" ]; then + NEO4J_dbms_security_auth__enabled=false + elif [[ "${NEO4J_AUTH:-}" == neo4j/* ]]; then + password="${NEO4J_AUTH#neo4j/}" + if [ "${password}" == "neo4j" ]; then + echo >&2 "Invalid value for password. It cannot be 'neo4j', which is the default." + exit 1 + fi + + if running_as_root; then + # running set-initial-password as root will create subfolders to /data as root, causing startup fail when neo4j can't read or write the /data/dbms folder + # creating the folder first will avoid that + mkdir -p /data/dbms + chown "${userid}":"${groupid}" /data/dbms + fi + # Will exit with error if users already exist (and print a message explaining that) + # we probably don't want the message though, since it throws an error message on restarting the container. + neo4j-admin set-initial-password "${password}" 2>/dev/null || true + elif [ -n "${NEO4J_AUTH:-}" ]; then + echo >&2 "Invalid value for NEO4J_AUTH: '${NEO4J_AUTH}'" + exit 1 + fi +fi + +declare -A COMMUNITY +declare -A ENTERPRISE + +COMMUNITY=( + [dbms.tx_log.rotation.retention_policy]="100M size" + [dbms.memory.pagecache.size]="512M" + [dbms.connectors.default_listen_address]="0.0.0.0" + [dbms.connector.https.listen_address]="0.0.0.0:7473" + [dbms.connector.http.listen_address]="0.0.0.0:7474" + [dbms.connector.bolt.listen_address]="0.0.0.0:7687" +) + +ENTERPRISE=( +) + +for conf in ${!COMMUNITY[@]} ; do + if ! grep -q "^$conf" "${NEO4J_HOME}"/conf/neo4j.conf + then + echo -e "\n"$conf=${COMMUNITY[$conf]} >> "${NEO4J_HOME}"/conf/neo4j.conf + fi +done + +for conf in ${!ENTERPRISE[@]} ; do + if [ "${NEO4J_EDITION}" == "enterprise" ]; + then + if ! grep -q "^$conf" "${NEO4J_HOME}"/conf/neo4j.conf + then + echo -e "\n"$conf=${ENTERPRISE[$conf]} >> "${NEO4J_HOME}"/conf/neo4j.conf + fi + fi +done + +#The udc.source=tarball should be replaced by udc.source=docker in both dbms.jvm.additional and wrapper.java.additional +#Using sed to replace only this part will allow the custom configs to be added after, separated by a ,. +if grep -q "udc.source=tarball" "${NEO4J_HOME}"/conf/neo4j.conf; then + sed -i -e 's/udc.source=tarball/udc.source=docker/g' "${NEO4J_HOME}"/conf/neo4j.conf +fi +#The udc.source should always be set to docker by default and we have to allow also custom configs to be added after that. +#In this case, this piece of code helps to add the default value and a , to support custom configs after. +if ! grep -q "dbms.jvm.additional=-Dunsupported.dbms.udc.source=docker" "${NEO4J_HOME}"/conf/neo4j.conf; then + sed -i -e 's/dbms.jvm.additional=/dbms.jvm.additional=-Dunsupported.dbms.udc.source=docker,/g' "${NEO4J_HOME}"/conf/neo4j.conf +fi + +# list env variables with prefix NEO4J_ and create settings from them +unset NEO4J_AUTH NEO4J_SHA256 NEO4J_TARBALL +for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do + setting=$(echo ${i} | sed 's|^NEO4J_||' | sed 's|_|.|g' | sed 's|\.\.|_|g') + value=$(echo ${!i}) + # Don't allow settings with no value or settings that start with a number (neo4j converts settings to env variables and you cannot have an env variable that starts with a number) + if [[ -n ${value} ]]; then + if [[ ! "${setting}" =~ ^[0-9]+.*$ ]]; then + if grep -q -F "${setting}=" "${NEO4J_HOME}"/conf/neo4j.conf; then + # Remove any lines containing the setting already + sed --in-place "/^${setting}=.*/d" "${NEO4J_HOME}"/conf/neo4j.conf + fi + # Then always append setting to file + echo "${setting}=${value}" >> "${NEO4J_HOME}"/conf/neo4j.conf + else + echo >&2 "WARNING: ${setting} not written to conf file because settings that start with a number are not permitted" + fi + fi +done + +if [[ ! -z "${NEO4JLABS_PLUGINS:-}" ]]; then + # NEO4JLABS_PLUGINS should be a json array of plugins like '["graph-algorithms", "apoc", "streams", "graphql"]' + install_neo4j_labs_plugins +fi + +[ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT} + +if [ "${cmd}" == "dump-config" ]; then + if ! is_writable "/conf"; then + print_permissions_advice_and_fail "/conf" + fi + cp --recursive "${NEO4J_HOME}"/conf/* /conf + echo "Config Dumped" + exit 0 +fi + +# Use su-exec to drop privileges to neo4j user +# Note that su-exec, despite its name, does not replicate the +# functionality of exec, so we need to use both +if [ "${cmd}" == "neo4j" ]; then + ${exec_cmd} neo4j console +else + ${exec_cmd} "$@" +fi diff --git a/docker-image-src/3.5/Dockerfile b/docker-image-src/3.5/Dockerfile new file mode 100644 index 00000000..51c0a635 --- /dev/null +++ b/docker-image-src/3.5/Dockerfile @@ -0,0 +1,51 @@ +FROM openjdk:8-jre-slim + +ENV NEO4J_SHA256=%%NEO4J_SHA%% \ + NEO4J_TARBALL=%%NEO4J_TARBALL%% \ + NEO4J_EDITION=%%NEO4J_EDITION%% \ + NEO4J_HOME="/var/lib/neo4j" \ + TINI_VERSION="v0.18.0" \ + TINI_SHA256="12d20136605531b09a2c2dac02ccee85e1b874eb322ef6baf7561cd93f93c855" +ARG NEO4J_URI=%%NEO4J_DIST_SITE%%/%%NEO4J_TARBALL%% + +RUN addgroup --system neo4j && adduser --system --no-create-home --home "${NEO4J_HOME}" --ingroup neo4j neo4j + +COPY ./local-package/* /tmp/ + +RUN apt update \ + && apt install -y curl wget gosu jq \ + && curl -L --fail --silent --show-error "https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini" > /sbin/tini \ + && echo "${TINI_SHA256} /sbin/tini" | sha256sum -c --strict --quiet \ + && chmod +x /sbin/tini \ + && curl --fail --silent --show-error --location --remote-name ${NEO4J_URI} \ + && echo "${NEO4J_SHA256} ${NEO4J_TARBALL}" | sha256sum -c --strict --quiet \ + && tar --extract --file ${NEO4J_TARBALL} --directory /var/lib \ + && mv /var/lib/neo4j-* "${NEO4J_HOME}" \ + && rm ${NEO4J_TARBALL} \ + && mv "${NEO4J_HOME}"/data /data \ + && mv "${NEO4J_HOME}"/logs /logs \ + && chown -R neo4j:neo4j /data \ + && chmod -R 777 /data \ + && chown -R neo4j:neo4j /logs \ + && chmod -R 777 /logs \ + && chown -R neo4j:neo4j "${NEO4J_HOME}" \ + && chmod -R 777 "${NEO4J_HOME}" \ + && ln -s /data "${NEO4J_HOME}"/data \ + && ln -s /logs "${NEO4J_HOME}"/logs \ + && mv /tmp/neo4jlabs-plugins.json /neo4jlabs-plugins.json \ + && rm -rf /tmp/* \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get -y purge --auto-remove curl + +ENV PATH "${NEO4J_HOME}"/bin:$PATH + +WORKDIR "${NEO4J_HOME}" + +VOLUME /data /logs + +COPY docker-entrypoint.sh /docker-entrypoint.sh + +EXPOSE 7474 7473 7687 + +ENTRYPOINT ["/sbin/tini", "-g", "--", "/docker-entrypoint.sh"] +CMD ["neo4j"] diff --git a/docker-image-src/3.5/docker-entrypoint.sh b/docker-image-src/3.5/docker-entrypoint.sh new file mode 100755 index 00000000..4eab0712 --- /dev/null +++ b/docker-image-src/3.5/docker-entrypoint.sh @@ -0,0 +1,479 @@ +#!/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. 2019. 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:-}} + +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_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"} +fi + +if [ -d /data ]; then + check_mounted_folder_with_chown "/data" + 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 + + +# 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 \ No newline at end of file diff --git a/docker-image-src/4.0/Dockerfile b/docker-image-src/4.0/Dockerfile new file mode 100644 index 00000000..433c35e8 --- /dev/null +++ b/docker-image-src/4.0/Dockerfile @@ -0,0 +1,51 @@ +FROM openjdk:11-jdk-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 --gid 7474 --system neo4j && adduser --uid 7474 --system --no-create-home --home "${NEO4J_HOME}" --ingroup neo4j neo4j + +COPY ./local-package/* /tmp/ + +RUN apt update \ + && apt install -y curl wget gosu jq \ + && curl -L --fail --silent --show-error "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/4.0/docker-entrypoint.sh b/docker-image-src/4.0/docker-entrypoint.sh new file mode 100755 index 00000000..3919cbf6 --- /dev/null +++ b/docker-image-src/4.0/docker-entrypoint.sh @@ -0,0 +1,473 @@ +#!/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 ! 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. 2019. 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_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:-"$(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_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 + 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 +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:-}" =~ ^([^/]+)\/([^/]+)/?([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 + +declare -A COMMUNITY +declare -A ENTERPRISE + +COMMUNITY=( + [dbms.tx_log.rotation.retention_policy]="100M size" + [dbms.memory.pagecache.size]="512M" + [dbms.default_listen_address]="0.0.0.0" +) + +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 + +# save NEO4J_HOME to a temp variable that doesn't begin with NEO4J_ so it doesn't get added to the conf +temp_neo4j_home="${NEO4J_HOME}" +# 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 + if grep -q -F "${setting}=" "${temp_neo4j_home}"/conf/neo4j.conf; then + # Remove any lines containing the setting already + sed --in-place "/^${setting}=.*/d" "${temp_neo4j_home}"/conf/neo4j.conf + fi + # Then always append setting to file + echo "${setting}=${value}" >> "${temp_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 +export NEO4J_HOME="${temp_neo4j_home}" +unset temp_neo4j_home + + +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/4.1/Dockerfile b/docker-image-src/4.1/Dockerfile new file mode 100644 index 00000000..433c35e8 --- /dev/null +++ b/docker-image-src/4.1/Dockerfile @@ -0,0 +1,51 @@ +FROM openjdk:11-jdk-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 --gid 7474 --system neo4j && adduser --uid 7474 --system --no-create-home --home "${NEO4J_HOME}" --ingroup neo4j neo4j + +COPY ./local-package/* /tmp/ + +RUN apt update \ + && apt install -y curl wget gosu jq \ + && curl -L --fail --silent --show-error "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/4.1/docker-entrypoint.sh b/docker-image-src/4.1/docker-entrypoint.sh new file mode 100755 index 00000000..c5be32f5 --- /dev/null +++ b/docker-image-src/4.1/docker-entrypoint.sh @@ -0,0 +1,478 @@ +#!/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 ! 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}" +} + +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 + 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 +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. 2019. 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_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:-"$(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_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 + 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 +fi + +declare -A COMMUNITY +declare -A ENTERPRISE + +COMMUNITY=( + [dbms.tx_log.rotation.retention_policy]="100M size" + [dbms.memory.pagecache.size]="512M" + [dbms.default_listen_address]="0.0.0.0" +) + +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 + +# 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 + if grep -q -F "${setting}=" "${temp_neo4j_home}"/conf/neo4j.conf; then + # Remove any lines containing the setting already + sed --in-place "/^${setting}=.*/d" "${temp_neo4j_home}"/conf/neo4j.conf + fi + # Then always append setting to file + echo "${setting}=${value}" >> "${temp_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 +export NEO4J_HOME="${temp_neo4j_home}" +unset temp_neo4j_home +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 + +[ -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.2/Dockerfile b/docker-image-src/4.2/Dockerfile new file mode 100644 index 00000000..433c35e8 --- /dev/null +++ b/docker-image-src/4.2/Dockerfile @@ -0,0 +1,51 @@ +FROM openjdk:11-jdk-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 --gid 7474 --system neo4j && adduser --uid 7474 --system --no-create-home --home "${NEO4J_HOME}" --ingroup neo4j neo4j + +COPY ./local-package/* /tmp/ + +RUN apt update \ + && apt install -y curl wget gosu jq \ + && curl -L --fail --silent --show-error "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/4.2/docker-entrypoint.sh b/docker-image-src/4.2/docker-entrypoint.sh new file mode 100755 index 00000000..c5be32f5 --- /dev/null +++ b/docker-image-src/4.2/docker-entrypoint.sh @@ -0,0 +1,478 @@ +#!/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 ! 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}" +} + +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 + 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 +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. 2019. 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_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:-"$(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_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 + 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 +fi + +declare -A COMMUNITY +declare -A ENTERPRISE + +COMMUNITY=( + [dbms.tx_log.rotation.retention_policy]="100M size" + [dbms.memory.pagecache.size]="512M" + [dbms.default_listen_address]="0.0.0.0" +) + +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 + +# 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 + if grep -q -F "${setting}=" "${temp_neo4j_home}"/conf/neo4j.conf; then + # Remove any lines containing the setting already + sed --in-place "/^${setting}=.*/d" "${temp_neo4j_home}"/conf/neo4j.conf + fi + # Then always append setting to file + echo "${setting}=${value}" >> "${temp_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 +export NEO4J_HOME="${temp_neo4j_home}" +unset temp_neo4j_home +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 + +[ -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/neo4jlabs-plugins.json b/neo4jlabs-plugins.json new file mode 100644 index 00000000..be0b4741 --- /dev/null +++ b/neo4jlabs-plugins.json @@ -0,0 +1,43 @@ +{ + "apoc": { + "versions": "https://neo4j-contrib.github.io/neo4j-apoc-procedures/versions.json", + "properties": { + "dbms.security.procedures.unrestricted": "apoc.*" + } + }, + "streams": { + "versions": "https://neo4j-contrib.github.io/neo4j-streams/versions.json", + "properties": {} + }, + "graphql": { + "versions": "https://neo4j-graphql.github.io/neo4j-graphql/versions.json", + "properties": { + "dbms.unmanaged_extension_classes": "org.neo4j.graphql=/graphql", + "dbms.security.procedures.unrestricted": "graphql.*" + } + }, + "graph-algorithms": { + "versions": "https://neo4j-contrib.github.io/neo4j-graph-algorithms/versions.json", + "properties": { + "dbms.security.procedures.unrestricted": "algo.*" + } + }, + "graph-data-science": { + "versions": "https://s3-eu-west-1.amazonaws.com/com.neo4j.graphalgorithms.dist/graph-data-science/versions.json", + "properties": { + "dbms.security.procedures.unrestricted": "gds.*" + } + }, + "n10s": { + "versions": "https://neo4j-labs.github.io/neosemantics/versions.json", + "properties": { + "dbms.security.procedures.unrestricted":"semantics.*" + } + }, + "_testing": { + "versions": "http://host.testcontainers.internal:3000/versions.json", + "properties": { + "dbms.security.procedures.unrestricted": "com.neo4j.docker.plugins.*" + } + } +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..6c606036 --- /dev/null +++ b/pom.xml @@ -0,0 +1,125 @@ + + + 4.0.0 + + com.neo4j + docker-neo4j-tests + 1.0-SNAPSHOT + jar + + + ${env.NEO4JVERSION} + + 1.8 + + + + + + false + + neo4j-community + neo4j-community + https://neo4j.bintray.com/community + + + + + + + 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.30 + + + org.slf4j + slf4j-log4j12 + 1.7.30 + + + org.junit.jupiter + junit-jupiter-api + 5.6.0 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.6.0 + test + + + org.junit.jupiter + junit-jupiter-params + 5.6.0 + test + + + org.junit.jupiter + junit-jupiter-migrationsupport + 5.6.0 + test + + + org.testcontainers + junit-jupiter + 1.12.5 + test + + + org.testcontainers + testcontainers + 1.12.5 + test + + + org.testcontainers + neo4j + 1.12.5 + test + + + org.neo4j.driver + neo4j-java-driver + 4.0.0 + test + + + + \ No newline at end of file diff --git a/src/3.0/docker-entrypoint.sh b/src/3.0/docker-entrypoint.sh deleted file mode 100755 index 722e4133..00000000 --- a/src/3.0/docker-entrypoint.sh +++ /dev/null @@ -1,151 +0,0 @@ -#!/bin/bash -eu - -setting() { - setting="${1}" - value="${2}" - file="${3:-neo4j.conf}" - - if [ ! -f "conf/${file}" ]; then - if [ -f "conf/neo4j.conf" ]; then - file="neo4j.conf" - fi - fi - - if [ -n "${value}" ]; then - if grep --quiet --fixed-strings "${setting}=" conf/"${file}"; then - sed --in-place "s|.*${setting}=.*|${setting}=${value}|" conf/"${file}" - else - echo "${setting}=${value}" >>conf/"${file}" - fi - fi -} - -if [ "$1" == "neo4j" ]; then - - # Env variable naming convention: - # - prefix NEO4J_ - # - double underscore char '__' instead of single underscore '_' char in the setting name - # - underscore char '_' instead of dot '.' char in the setting name - # Example: - # NEO4J_dbms_tx__log_rotation_retention__policy env variable to set - # dbms.tx_log.rotation.retention_policy setting - - # Backward compatibility - map old hardcoded env variables into new naming convention (if they aren't set already) - # Set some to default values if unset - : ${NEO4J_dbms_tx__log_rotation_retention__policy:=${NEO4J_dbms_txLog_rotation_retentionPolicy:-"100M size"}} - : ${NEO4J_wrapper_java_additional:=${NEO4J_UDC_SOURCE:-"-Dneo4j.ext.udc.source=docker"}} - : ${NEO4J_dbms_memory_heap_initial__size:=${NEO4J_dbms_memory_heap_maxSize:-"512"}} - : ${NEO4J_dbms_memory_heap_max__size:=${NEO4J_dbms_memory_heap_maxSize:-"512"}} - : ${NEO4J_dbms_unmanaged__extension__classes:=${NEO4J_dbms_unmanagedExtensionClasses:-}} - : ${NEO4J_dbms_allow__format__migration:=${NEO4J_dbms_allowFormatMigration:-}} - : ${NEO4J_ha_server__id:=${NEO4J_ha_serverId:-}} - : ${NEO4J_ha_initial__hosts:=${NEO4J_ha_initialHosts:-}} - - : ${NEO4J_dbms_connector_http_address:="0.0.0.0:7474"} - : ${NEO4J_dbms_connector_https_address:="0.0.0.0:7473"} - : ${NEO4J_dbms_connector_bolt_address:="0.0.0.0:7687"} - : ${NEO4J_ha_host_coordination:="$(hostname):5001"} - : ${NEO4J_ha_host_data:="$(hostname):6001"} - - # unset old hardcoded unsupported env variables - unset NEO4J_dbms_txLog_rotation_retentionPolicy NEO4J_UDC_SOURCE \ - NEO4J_dbms_memory_heap_maxSize NEO4J_dbms_memory_heap_maxSize \ - NEO4J_dbms_unmanagedExtensionClasses NEO4J_dbms_allowFormatMigration \ - NEO4J_ha_initialHosts - - if [ -d /conf ]; then - find /conf -type f -exec cp {} conf \; - fi - - if [ -d /ssl ]; then - NEO4J_dbms_directories_certificates="/ssl" - fi - - if [ -d /plugins ]; then - NEO4J_dbms_directories_plugins="/plugins" - fi - - if [ -d /logs ]; then - NEO4J_dbms_directories_logs="/logs" - fi - - if [ -d /import ]; then - NEO4J_dbms_directories_import="/import" - fi - - if [ -d /metrics ]; then - NEO4J_dbms_directories_metrics="/metrics" - fi - - if [ "${NEO4J_AUTH:-}" == "none" ]; then - NEO4J_dbms_security_auth__enabled=false - elif [[ "${NEO4J_AUTH:-}" == neo4j/* ]]; then - password="${NEO4J_AUTH#neo4j/}" - if [ "${password}" == "neo4j" ]; then - echo "Invalid value for password. It cannot be 'neo4j', which is the default." - exit 1 - fi - - setting "dbms.connector.http.address" "127.0.0.1:7474" - setting "dbms.connector.https.address" "127.0.0.1:7473" - setting "dbms.connector.bolt.address" "127.0.0.1:7687" - bin/neo4j start || \ - (cat logs/neo4j.log && echo "Neo4j failed to start for password change" && exit 1) - - end="$((SECONDS+100))" - while true; do - http_code="$(curl --silent --write-out %{http_code} --user "neo4j:${password}" --output /dev/null http://localhost:7474/db/data/ || true)" - - if [[ "${http_code}" = "200" ]]; then - break; - fi - - if [[ "${http_code}" = "401" ]]; then - curl --fail --silent --show-error --user neo4j:neo4j \ - --data '{"password": "'"${password}"'"}' \ - --header 'Content-Type: application/json' \ - http://localhost:7474/user/neo4j/password - break; - fi - - if [[ "${SECONDS}" -ge "${end}" ]]; then - (cat logs/neo4j.log && echo "Neo4j failed to start" && exit 1) - fi - - sleep 1 - done - - bin/neo4j stop - elif [ -n "${NEO4J_AUTH:-}" ]; then - echo "Invalid value for NEO4J_AUTH: '${NEO4J_AUTH}'" - exit 1 - fi - - # list env variables with prefix NEO4J_ and create settings from them - unset NEO4J_AUTH NEO4J_SHA256 NEO4J_TARBALL - for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do - setting=$(echo ${i} | sed 's|^NEO4J_||' | sed 's|_|.|g' | sed 's|\.\.|_|g') - value=$(echo ${!i}) - if [[ -n ${value} ]]; then - if grep -q -F "${setting}=" conf/neo4j.conf; then - # Remove any lines containing the setting already - sed --in-place "/${setting}=.*/d" conf/neo4j.conf - fi - # Then always append setting to file - echo "${setting}=${value}" >> conf/neo4j.conf - fi - done - - [ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT} - - exec bin/neo4j console -elif [ "$1" == "dump-config" ]; then - if [ -d /conf ]; then - cp --recursive conf/* /conf - else - echo "You must provide a /conf volume" - exit 1 - fi -else - exec "$@" -fi diff --git a/src/3.1/Dockerfile b/src/3.1/Dockerfile deleted file mode 100644 index 5c7e40cc..00000000 --- a/src/3.1/Dockerfile +++ /dev/null @@ -1,31 +0,0 @@ -FROM openjdk:8-jre-alpine - -RUN apk add --no-cache --quiet \ - bash \ - curl - -ENV NEO4J_SHA256=%%NEO4J_SHA%% \ - NEO4J_TARBALL=%%NEO4J_TARBALL%% -ARG NEO4J_URI=%%NEO4J_DIST_SITE%%/%%NEO4J_TARBALL%% - -COPY ./local-package/* /tmp/ - -RUN curl --fail --silent --show-error --location --remote-name ${NEO4J_URI} \ - && echo "${NEO4J_SHA256} ${NEO4J_TARBALL}" | sha256sum -csw - \ - && tar --extract --file ${NEO4J_TARBALL} --directory /var/lib \ - && mv /var/lib/neo4j-* /var/lib/neo4j \ - && rm ${NEO4J_TARBALL} - -WORKDIR /var/lib/neo4j - -RUN mv data /data \ - && ln -s /data - -VOLUME /data - -COPY docker-entrypoint.sh /docker-entrypoint.sh - -EXPOSE 7474 7473 7687 - -ENTRYPOINT ["/docker-entrypoint.sh"] -CMD ["neo4j"] diff --git a/src/3.1/docker-entrypoint.sh b/src/3.1/docker-entrypoint.sh deleted file mode 100755 index 1fac31e9..00000000 --- a/src/3.1/docker-entrypoint.sh +++ /dev/null @@ -1,120 +0,0 @@ -#!/bin/bash -eu - -if [ "$1" == "neo4j" ]; then - - # Env variable naming convention: - # - prefix NEO4J_ - # - double underscore char '__' instead of single underscore '_' char in the setting name - # - underscore char '_' instead of dot '.' char in the setting name - # Example: - # NEO4J_dbms_tx__log_rotation_retention__policy env variable to set - # dbms.tx_log.rotation.retention_policy setting - - # Backward compatibility - map old hardcoded env variables into new naming convention (if they aren't set already) - # Set some to default values if unset - : ${NEO4J_dbms_tx__log_rotation_retention__policy:=${NEO4J_dbms_txLog_rotation_retentionPolicy:-"100M size"}} - : ${NEO4J_wrapper_java_additional:=${NEO4J_UDC_SOURCE:-"-Dneo4j.ext.udc.source=docker"}} - : ${NEO4J_dbms_memory_heap_initial__size:=${NEO4J_dbms_memory_heap_maxSize:-"512M"}} - : ${NEO4J_dbms_memory_heap_max__size:=${NEO4J_dbms_memory_heap_maxSize:-"512M"}} - : ${NEO4J_dbms_unmanaged__extension__classes:=${NEO4J_dbms_unmanagedExtensionClasses:-}} - : ${NEO4J_dbms_allow__format__migration:=${NEO4J_dbms_allowFormatMigration:-}} - : ${NEO4J_dbms_connectors_default__advertised__address:=${NEO4J_dbms_connectors_defaultAdvertisedAddress:-}} - : ${NEO4J_ha_server__id:=${NEO4J_ha_serverId:-}} - : ${NEO4J_ha_initial__hosts:=${NEO4J_ha_initialHosts:-}} - : ${NEO4J_causal__clustering_expected__core__cluster__size:=${NEO4J_causalClustering_expectedCoreClusterSize:-}} - : ${NEO4J_causal__clustering_initial__discovery__members:=${NEO4J_causalClustering_initialDiscoveryMembers:-}} - : ${NEO4J_causal__clustering_discovery__listen__address:=${NEO4J_causalClustering_discoveryListenAddress:-"0.0.0.0:5000"}} - : ${NEO4J_causal__clustering_discovery__advertised__address:=${NEO4J_causalClustering_discoveryAdvertisedAddress:-"$(hostname):5000"}} - : ${NEO4J_causal__clustering_transaction__listen__address:=${NEO4J_causalClustering_transactionListenAddress:-"0.0.0.0:6000"}} - : ${NEO4J_causal__clustering_transaction__advertised__address:=${NEO4J_causalClustering_transactionAdvertisedAddress:-"$(hostname):6000"}} - : ${NEO4J_causal__clustering_raft__listen__address:=${NEO4J_causalClustering_raftListenAddress:-"0.0.0.0:7000"}} - : ${NEO4J_causal__clustering_raft__advertised__address:=${NEO4J_causalClustering_raftAdvertisedAddress:-"$(hostname):7000"}} - - : ${NEO4J_dbms_connectors_default__listen__address:="0.0.0.0"} - : ${NEO4J_dbms_connector_http_listen__address:="0.0.0.0:7474"} - : ${NEO4J_dbms_connector_https_listen__address:="0.0.0.0:7473"} - : ${NEO4J_dbms_connector_bolt_listen__address:="0.0.0.0:7687"} - : ${NEO4J_ha_host_coordination:="$(hostname):5001"} - : ${NEO4J_ha_host_data:="$(hostname):6001"} - - # unset old hardcoded unsupported env variables - unset NEO4J_dbms_txLog_rotation_retentionPolicy NEO4J_UDC_SOURCE \ - NEO4J_dbms_memory_heap_maxSize NEO4J_dbms_memory_heap_maxSize \ - NEO4J_dbms_unmanagedExtensionClasses NEO4J_dbms_allowFormatMigration \ - NEO4J_dbms_connectors_defaultAdvertisedAddress NEO4J_ha_serverId \ - NEO4J_ha_initialHosts NEO4J_causalClustering_expectedCoreClusterSize \ - NEO4J_causalClustering_initialDiscoveryMembers \ - NEO4J_causalClustering_discoveryListenAddress \ - NEO4J_causalClustering_discoveryAdvertisedAddress \ - NEO4J_causalClustering_transactionListenAddress \ - NEO4J_causalClustering_transactionAdvertisedAddress \ - NEO4J_causalClustering_raftListenAddress \ - NEO4J_causalClustering_raftAdvertisedAddress - - if [ -d /conf ]; then - find /conf -type f -exec cp {} conf \; - fi - - if [ -d /ssl ]; then - NEO4J_dbms_directories_certificates="/ssl" - fi - - if [ -d /plugins ]; then - NEO4J_dbms_directories_plugins="/plugins" - fi - - if [ -d /logs ]; then - NEO4J_dbms_directories_logs="/logs" - fi - - if [ -d /import ]; then - NEO4J_dbms_directories_import="/import" - fi - - if [ -d /metrics ]; then - NEO4J_dbms_directories_metrics="/metrics" - fi - - if [ "${NEO4J_AUTH:-}" == "none" ]; then - NEO4J_dbms_security_auth__enabled=false - elif [[ "${NEO4J_AUTH:-}" == neo4j/* ]]; then - password="${NEO4J_AUTH#neo4j/}" - if [ "${password}" == "neo4j" ]; then - echo "Invalid value for password. It cannot be 'neo4j', which is the default." - exit 1 - fi - # Will exit with error if users already exist (and print a message explaining that) - bin/neo4j-admin set-initial-password "${password}" || true - elif [ -n "${NEO4J_AUTH:-}" ]; then - echo "Invalid value for NEO4J_AUTH: '${NEO4J_AUTH}'" - exit 1 - fi - - # list env variables with prefix NEO4J_ and create settings from them - unset NEO4J_AUTH NEO4J_SHA256 NEO4J_TARBALL - for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do - setting=$(echo ${i} | sed 's|^NEO4J_||' | sed 's|_|.|g' | sed 's|\.\.|_|g') - value=$(echo ${!i}) - if [[ -n ${value} ]]; then - if grep -q -F "${setting}=" conf/neo4j.conf; then - # Remove any lines containing the setting already - sed --in-place "/${setting}=.*/d" conf/neo4j.conf - fi - # Then always append setting to file - echo "${setting}=${value}" >> conf/neo4j.conf - fi - done - - [ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT} - - exec bin/neo4j console -elif [ "$1" == "dump-config" ]; then - if [ -d /conf ]; then - cp --recursive conf/* /conf - else - echo "You must provide a /conf volume" - exit 1 - fi -else - exec "$@" -fi diff --git a/src/3.2/Dockerfile b/src/3.2/Dockerfile deleted file mode 100644 index 6bf83a68..00000000 --- a/src/3.2/Dockerfile +++ /dev/null @@ -1,31 +0,0 @@ -FROM openjdk:8-jre-alpine - -RUN apk add --no-cache --quiet \ - bash \ - curl - -ENV NEO4J_SHA256=%%NEO4J_SHA%% \ - NEO4J_TARBALL=%%NEO4J_TARBALL%% -ARG NEO4J_URI=%%NEO4J_DIST_SITE%%/%%NEO4J_TARBALL%% - -COPY ./local-package/* /tmp/ - -RUN curl --fail --silent --show-error --location --remote-name ${NEO4J_URI} \ - && echo "${NEO4J_SHA256} ${NEO4J_TARBALL}" | sha256sum -csw - \ - && tar --extract --file ${NEO4J_TARBALL} --directory /var/lib \ - && mv /var/lib/neo4j-* /var/lib/neo4j \ - && rm ${NEO4J_TARBALL} \ - && mv /var/lib/neo4j/data /data \ - && ln -s /data /var/lib/neo4j/data \ - && apk del curl - -WORKDIR /var/lib/neo4j - -VOLUME /data - -COPY docker-entrypoint.sh /docker-entrypoint.sh - -EXPOSE 7474 7473 7687 - -ENTRYPOINT ["/docker-entrypoint.sh"] -CMD ["neo4j"] diff --git a/src/3.2/docker-entrypoint.sh b/src/3.2/docker-entrypoint.sh deleted file mode 100755 index 1fac31e9..00000000 --- a/src/3.2/docker-entrypoint.sh +++ /dev/null @@ -1,120 +0,0 @@ -#!/bin/bash -eu - -if [ "$1" == "neo4j" ]; then - - # Env variable naming convention: - # - prefix NEO4J_ - # - double underscore char '__' instead of single underscore '_' char in the setting name - # - underscore char '_' instead of dot '.' char in the setting name - # Example: - # NEO4J_dbms_tx__log_rotation_retention__policy env variable to set - # dbms.tx_log.rotation.retention_policy setting - - # Backward compatibility - map old hardcoded env variables into new naming convention (if they aren't set already) - # Set some to default values if unset - : ${NEO4J_dbms_tx__log_rotation_retention__policy:=${NEO4J_dbms_txLog_rotation_retentionPolicy:-"100M size"}} - : ${NEO4J_wrapper_java_additional:=${NEO4J_UDC_SOURCE:-"-Dneo4j.ext.udc.source=docker"}} - : ${NEO4J_dbms_memory_heap_initial__size:=${NEO4J_dbms_memory_heap_maxSize:-"512M"}} - : ${NEO4J_dbms_memory_heap_max__size:=${NEO4J_dbms_memory_heap_maxSize:-"512M"}} - : ${NEO4J_dbms_unmanaged__extension__classes:=${NEO4J_dbms_unmanagedExtensionClasses:-}} - : ${NEO4J_dbms_allow__format__migration:=${NEO4J_dbms_allowFormatMigration:-}} - : ${NEO4J_dbms_connectors_default__advertised__address:=${NEO4J_dbms_connectors_defaultAdvertisedAddress:-}} - : ${NEO4J_ha_server__id:=${NEO4J_ha_serverId:-}} - : ${NEO4J_ha_initial__hosts:=${NEO4J_ha_initialHosts:-}} - : ${NEO4J_causal__clustering_expected__core__cluster__size:=${NEO4J_causalClustering_expectedCoreClusterSize:-}} - : ${NEO4J_causal__clustering_initial__discovery__members:=${NEO4J_causalClustering_initialDiscoveryMembers:-}} - : ${NEO4J_causal__clustering_discovery__listen__address:=${NEO4J_causalClustering_discoveryListenAddress:-"0.0.0.0:5000"}} - : ${NEO4J_causal__clustering_discovery__advertised__address:=${NEO4J_causalClustering_discoveryAdvertisedAddress:-"$(hostname):5000"}} - : ${NEO4J_causal__clustering_transaction__listen__address:=${NEO4J_causalClustering_transactionListenAddress:-"0.0.0.0:6000"}} - : ${NEO4J_causal__clustering_transaction__advertised__address:=${NEO4J_causalClustering_transactionAdvertisedAddress:-"$(hostname):6000"}} - : ${NEO4J_causal__clustering_raft__listen__address:=${NEO4J_causalClustering_raftListenAddress:-"0.0.0.0:7000"}} - : ${NEO4J_causal__clustering_raft__advertised__address:=${NEO4J_causalClustering_raftAdvertisedAddress:-"$(hostname):7000"}} - - : ${NEO4J_dbms_connectors_default__listen__address:="0.0.0.0"} - : ${NEO4J_dbms_connector_http_listen__address:="0.0.0.0:7474"} - : ${NEO4J_dbms_connector_https_listen__address:="0.0.0.0:7473"} - : ${NEO4J_dbms_connector_bolt_listen__address:="0.0.0.0:7687"} - : ${NEO4J_ha_host_coordination:="$(hostname):5001"} - : ${NEO4J_ha_host_data:="$(hostname):6001"} - - # unset old hardcoded unsupported env variables - unset NEO4J_dbms_txLog_rotation_retentionPolicy NEO4J_UDC_SOURCE \ - NEO4J_dbms_memory_heap_maxSize NEO4J_dbms_memory_heap_maxSize \ - NEO4J_dbms_unmanagedExtensionClasses NEO4J_dbms_allowFormatMigration \ - NEO4J_dbms_connectors_defaultAdvertisedAddress NEO4J_ha_serverId \ - NEO4J_ha_initialHosts NEO4J_causalClustering_expectedCoreClusterSize \ - NEO4J_causalClustering_initialDiscoveryMembers \ - NEO4J_causalClustering_discoveryListenAddress \ - NEO4J_causalClustering_discoveryAdvertisedAddress \ - NEO4J_causalClustering_transactionListenAddress \ - NEO4J_causalClustering_transactionAdvertisedAddress \ - NEO4J_causalClustering_raftListenAddress \ - NEO4J_causalClustering_raftAdvertisedAddress - - if [ -d /conf ]; then - find /conf -type f -exec cp {} conf \; - fi - - if [ -d /ssl ]; then - NEO4J_dbms_directories_certificates="/ssl" - fi - - if [ -d /plugins ]; then - NEO4J_dbms_directories_plugins="/plugins" - fi - - if [ -d /logs ]; then - NEO4J_dbms_directories_logs="/logs" - fi - - if [ -d /import ]; then - NEO4J_dbms_directories_import="/import" - fi - - if [ -d /metrics ]; then - NEO4J_dbms_directories_metrics="/metrics" - fi - - if [ "${NEO4J_AUTH:-}" == "none" ]; then - NEO4J_dbms_security_auth__enabled=false - elif [[ "${NEO4J_AUTH:-}" == neo4j/* ]]; then - password="${NEO4J_AUTH#neo4j/}" - if [ "${password}" == "neo4j" ]; then - echo "Invalid value for password. It cannot be 'neo4j', which is the default." - exit 1 - fi - # Will exit with error if users already exist (and print a message explaining that) - bin/neo4j-admin set-initial-password "${password}" || true - elif [ -n "${NEO4J_AUTH:-}" ]; then - echo "Invalid value for NEO4J_AUTH: '${NEO4J_AUTH}'" - exit 1 - fi - - # list env variables with prefix NEO4J_ and create settings from them - unset NEO4J_AUTH NEO4J_SHA256 NEO4J_TARBALL - for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do - setting=$(echo ${i} | sed 's|^NEO4J_||' | sed 's|_|.|g' | sed 's|\.\.|_|g') - value=$(echo ${!i}) - if [[ -n ${value} ]]; then - if grep -q -F "${setting}=" conf/neo4j.conf; then - # Remove any lines containing the setting already - sed --in-place "/${setting}=.*/d" conf/neo4j.conf - fi - # Then always append setting to file - echo "${setting}=${value}" >> conf/neo4j.conf - fi - done - - [ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT} - - exec bin/neo4j console -elif [ "$1" == "dump-config" ]; then - if [ -d /conf ]; then - cp --recursive conf/* /conf - else - echo "You must provide a /conf volume" - exit 1 - fi -else - exec "$@" -fi diff --git a/src/3.3/Dockerfile b/src/3.3/Dockerfile deleted file mode 100644 index 1524dead..00000000 --- a/src/3.3/Dockerfile +++ /dev/null @@ -1,32 +0,0 @@ -FROM openjdk:8-jre-alpine - -RUN apk add --no-cache --quiet \ - bash \ - curl - -ENV NEO4J_SHA256=%%NEO4J_SHA%% \ - NEO4J_TARBALL=%%NEO4J_TARBALL%% \ - NEO4J_EDITION=%%NEO4J_EDITION%% -ARG NEO4J_URI=%%NEO4J_DIST_SITE%%/%%NEO4J_TARBALL%% - -COPY ./local-package/* /tmp/ - -RUN curl --fail --silent --show-error --location --remote-name ${NEO4J_URI} \ - && echo "${NEO4J_SHA256} ${NEO4J_TARBALL}" | sha256sum -csw - \ - && tar --extract --file ${NEO4J_TARBALL} --directory /var/lib \ - && mv /var/lib/neo4j-* /var/lib/neo4j \ - && rm ${NEO4J_TARBALL} \ - && mv /var/lib/neo4j/data /data \ - && ln -s /data /var/lib/neo4j/data \ - && apk del curl - -WORKDIR /var/lib/neo4j - -VOLUME /data - -COPY docker-entrypoint.sh /docker-entrypoint.sh - -EXPOSE 7474 7473 7687 - -ENTRYPOINT ["/docker-entrypoint.sh"] -CMD ["neo4j"] diff --git a/src/3.3/docker-entrypoint.sh b/src/3.3/docker-entrypoint.sh deleted file mode 100755 index 220844c3..00000000 --- a/src/3.3/docker-entrypoint.sh +++ /dev/null @@ -1,163 +0,0 @@ -#!/bin/bash -eu - -if [ "$1" == "neo4j" ]; then - if [ "$NEO4J_EDITION" == "enterprise" ]; then - if [ "${NEO4J_ACCEPT_LICENSE_AGREEMENT:=no}" != "yes" ]; then - echo " -In order to use Neo4j Enterprise Edition you must accept the license agreement. - -(c) Network Engine for Objects in Lund AB. 2017. All Rights Reserved. -Use of this Software without a proper commercial license with Neo4j, -Inc. or its affiliates is prohibited. - -Email inquiries can be directed to: licensing@neo4j.com - -More information is also available at: https://neo4j.com/licensing/ - - -To accept the license agreemnt set the environment variable -NEO4J_ACCEPT_LICENSE_AGREEMENT=yes - -To do this you can use the following docker argument: - - --env=NEO4J_ACCEPT_LICENSE_AGREEMENT=yes -" - exit 1 - fi - fi - - # Env variable naming convention: - # - prefix NEO4J_ - # - double underscore char '__' instead of single underscore '_' char in the setting name - # - underscore char '_' instead of dot '.' char in the setting name - # Example: - # NEO4J_dbms_tx__log_rotation_retention__policy env variable to set - # dbms.tx_log.rotation.retention_policy setting - - # Backward compatibility - map old hardcoded env variables into new naming convention (if they aren't set already) - # Set some to default values if unset - : ${NEO4J_dbms_tx__log_rotation_retention__policy:=${NEO4J_dbms_txLog_rotation_retentionPolicy:-"100M size"}} - : ${NEO4J_wrapper_java_additional:=${NEO4J_UDC_SOURCE:-"-Dneo4j.ext.udc.source=docker"}} - : ${NEO4J_dbms_memory_heap_initial__size:=${NEO4J_dbms_memory_heap_maxSize:-"512M"}} - : ${NEO4J_dbms_memory_heap_max__size:=${NEO4J_dbms_memory_heap_maxSize:-"512M"}} - : ${NEO4J_dbms_unmanaged__extension__classes:=${NEO4J_dbms_unmanagedExtensionClasses:-}} - : ${NEO4J_dbms_allow__format__migration:=${NEO4J_dbms_allowFormatMigration:-}} - : ${NEO4J_dbms_connectors_default__advertised__address:=${NEO4J_dbms_connectors_defaultAdvertisedAddress:-}} - : ${NEO4J_ha_server__id:=${NEO4J_ha_serverId:-}} - : ${NEO4J_ha_initial__hosts:=${NEO4J_ha_initialHosts:-}} - : ${NEO4J_causal__clustering_expected__core__cluster__size:=${NEO4J_causalClustering_expectedCoreClusterSize:-}} - : ${NEO4J_causal__clustering_initial__discovery__members:=${NEO4J_causalClustering_initialDiscoveryMembers:-}} - : ${NEO4J_causal__clustering_discovery__listen__address:=${NEO4J_causalClustering_discoveryListenAddress:-"0.0.0.0:5000"}} - : ${NEO4J_causal__clustering_discovery__advertised__address:=${NEO4J_causalClustering_discoveryAdvertisedAddress:-"$(hostname):5000"}} - : ${NEO4J_causal__clustering_transaction__listen__address:=${NEO4J_causalClustering_transactionListenAddress:-"0.0.0.0:6000"}} - : ${NEO4J_causal__clustering_transaction__advertised__address:=${NEO4J_causalClustering_transactionAdvertisedAddress:-"$(hostname):6000"}} - : ${NEO4J_causal__clustering_raft__listen__address:=${NEO4J_causalClustering_raftListenAddress:-"0.0.0.0:7000"}} - : ${NEO4J_causal__clustering_raft__advertised__address:=${NEO4J_causalClustering_raftAdvertisedAddress:-"$(hostname):7000"}} - - : ${NEO4J_dbms_connectors_default__listen__address:="0.0.0.0"} - : ${NEO4J_dbms_connector_http_listen__address:="0.0.0.0:7474"} - : ${NEO4J_dbms_connector_https_listen__address:="0.0.0.0:7473"} - : ${NEO4J_dbms_connector_bolt_listen__address:="0.0.0.0:7687"} - : ${NEO4J_ha_host_coordination:="$(hostname):5001"} - : ${NEO4J_ha_host_data:="$(hostname):6001"} - - # unset old hardcoded unsupported env variables - unset NEO4J_dbms_txLog_rotation_retentionPolicy NEO4J_UDC_SOURCE \ - NEO4J_dbms_memory_heap_maxSize NEO4J_dbms_memory_heap_maxSize \ - NEO4J_dbms_unmanagedExtensionClasses NEO4J_dbms_allowFormatMigration \ - NEO4J_dbms_connectors_defaultAdvertisedAddress NEO4J_ha_serverId \ - NEO4J_ha_initialHosts NEO4J_causalClustering_expectedCoreClusterSize \ - NEO4J_causalClustering_initialDiscoveryMembers \ - NEO4J_causalClustering_discoveryListenAddress \ - NEO4J_causalClustering_discoveryAdvertisedAddress \ - NEO4J_causalClustering_transactionListenAddress \ - NEO4J_causalClustering_transactionAdvertisedAddress \ - NEO4J_causalClustering_raftListenAddress \ - NEO4J_causalClustering_raftAdvertisedAddress - - # Custom settings for dockerized neo4j - : ${NEO4J_dbms_tx__log_rotation_retention__policy:=100M size} - : ${NEO4J_dbms_memory_pagecache_size:=512M} - : ${NEO4J_wrapper_java_additional:=-Dneo4j.ext.udc.source=docker} - : ${NEO4J_dbms_memory_heap_initial__size:=512M} - : ${NEO4J_dbms_memory_heap_max__size:=512M} - : ${NEO4J_dbms_connectors_default__listen__address:=0.0.0.0} - : ${NEO4J_dbms_connector_http_listen__address:=0.0.0.0:7474} - : ${NEO4J_dbms_connector_https_listen__address:=0.0.0.0:7473} - : ${NEO4J_dbms_connector_bolt_listen__address:=0.0.0.0:7687} - : ${NEO4J_ha_host_coordination:=$(hostname):5001} - : ${NEO4J_ha_host_data:=$(hostname):6001} - : ${NEO4J_causal__clustering_discovery__listen__address:=0.0.0.0:5000} - : ${NEO4J_causal__clustering_discovery__advertised__address:=$(hostname):5000} - : ${NEO4J_causal__clustering_transaction__listen__address:=0.0.0.0:6000} - : ${NEO4J_causal__clustering_transaction__advertised__address:=$(hostname):6000} - : ${NEO4J_causal__clustering_raft__listen__address:=0.0.0.0:7000} - : ${NEO4J_causal__clustering_raft__advertised__address:=$(hostname):7000} - - if [ -d /conf ]; then - find /conf -type f -exec cp {} conf \; - fi - - if [ -d /ssl ]; then - NEO4J_dbms_directories_certificates="/ssl" - fi - - if [ -d /plugins ]; then - NEO4J_dbms_directories_plugins="/plugins" - fi - - if [ -d /logs ]; then - NEO4J_dbms_directories_logs="/logs" - fi - - if [ -d /import ]; then - NEO4J_dbms_directories_import="/import" - fi - - if [ -d /metrics ]; then - NEO4J_dbms_directories_metrics="/metrics" - fi - - if [ "${NEO4J_AUTH:-}" == "none" ]; then - NEO4J_dbms_security_auth__enabled=false - elif [[ "${NEO4J_AUTH:-}" == neo4j/* ]]; then - password="${NEO4J_AUTH#neo4j/}" - if [ "${password}" == "neo4j" ]; then - echo "Invalid value for password. It cannot be 'neo4j', which is the default." - exit 1 - fi - # Will exit with error if users already exist (and print a message explaining that) - bin/neo4j-admin set-initial-password "${password}" || true - elif [ -n "${NEO4J_AUTH:-}" ]; then - echo "Invalid value for NEO4J_AUTH: '${NEO4J_AUTH}'" - exit 1 - fi - - # list env variables with prefix NEO4J_ and create settings from them - unset NEO4J_AUTH NEO4J_SHA256 NEO4J_TARBALL - for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do - setting=$(echo ${i} | sed 's|^NEO4J_||' | sed 's|_|.|g' | sed 's|\.\.|_|g') - value=$(echo ${!i}) - if [[ -n ${value} ]]; then - if grep -q -F "${setting}=" conf/neo4j.conf; then - # Remove any lines containing the setting already - sed --in-place "/${setting}=.*/d" conf/neo4j.conf - fi - # Then always append setting to file - echo "${setting}=${value}" >> conf/neo4j.conf - fi - done - - [ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT} - - exec bin/neo4j console -elif [ "$1" == "dump-config" ]; then - if [ -d /conf ]; then - cp --recursive conf/* /conf - else - echo "You must provide a /conf volume" - exit 1 - fi -else - exec "$@" -fi diff --git a/src/3.4/Dockerfile b/src/3.4/Dockerfile deleted file mode 100644 index 1524dead..00000000 --- a/src/3.4/Dockerfile +++ /dev/null @@ -1,32 +0,0 @@ -FROM openjdk:8-jre-alpine - -RUN apk add --no-cache --quiet \ - bash \ - curl - -ENV NEO4J_SHA256=%%NEO4J_SHA%% \ - NEO4J_TARBALL=%%NEO4J_TARBALL%% \ - NEO4J_EDITION=%%NEO4J_EDITION%% -ARG NEO4J_URI=%%NEO4J_DIST_SITE%%/%%NEO4J_TARBALL%% - -COPY ./local-package/* /tmp/ - -RUN curl --fail --silent --show-error --location --remote-name ${NEO4J_URI} \ - && echo "${NEO4J_SHA256} ${NEO4J_TARBALL}" | sha256sum -csw - \ - && tar --extract --file ${NEO4J_TARBALL} --directory /var/lib \ - && mv /var/lib/neo4j-* /var/lib/neo4j \ - && rm ${NEO4J_TARBALL} \ - && mv /var/lib/neo4j/data /data \ - && ln -s /data /var/lib/neo4j/data \ - && apk del curl - -WORKDIR /var/lib/neo4j - -VOLUME /data - -COPY docker-entrypoint.sh /docker-entrypoint.sh - -EXPOSE 7474 7473 7687 - -ENTRYPOINT ["/docker-entrypoint.sh"] -CMD ["neo4j"] diff --git a/src/3.4/docker-entrypoint.sh b/src/3.4/docker-entrypoint.sh deleted file mode 100755 index 7e31aa72..00000000 --- a/src/3.4/docker-entrypoint.sh +++ /dev/null @@ -1,155 +0,0 @@ -#!/bin/bash -eu - -if [ "$1" == "neo4j" ]; then - if [ "$NEO4J_EDITION" == "enterprise" ]; then - if [ "${NEO4J_ACCEPT_LICENSE_AGREEMENT:=no}" != "yes" ]; then - echo " -In order to use Neo4j Enterprise Edition you must accept the license agreement. - -(c) Network Engine for Objects in Lund AB. 2017. All Rights Reserved. -Use of this Software without a proper commercial license with Neo4j, -Inc. or its affiliates is prohibited. - -Email inquiries can be directed to: licensing@neo4j.com - -More information is also available at: https://neo4j.com/licensing/ - - -To accept the license agreemnt set the environment variable -NEO4J_ACCEPT_LICENSE_AGREEMENT=yes - -To do this you can use the following docker argument: - - --env=NEO4J_ACCEPT_LICENSE_AGREEMENT=yes -" - exit 1 - fi - fi - - # Env variable naming convention: - # - prefix NEO4J_ - # - double underscore char '__' instead of single underscore '_' char in the setting name - # - underscore char '_' instead of dot '.' char in the setting name - # Example: - # NEO4J_dbms_tx__log_rotation_retention__policy env variable to set - # dbms.tx_log.rotation.retention_policy setting - - # Backward compatibility - map old hardcoded env variables into new naming convention - NEO4J_dbms_tx__log_rotation_retention__policy=${NEO4J_dbms_txLog_rotation_retentionPolicy:-} - NEO4J_wrapper_java_additional=${NEO4J_UDC_SOURCE:-} - NEO4J_dbms_memory_heap_initial__size=${NEO4J_dbms_memory_heap_maxSize:-} - NEO4J_dbms_memory_heap_max__size=${NEO4J_dbms_memory_heap_maxSize:-} - NEO4J_dbms_unmanaged__extension__classes=${NEO4J_dbms_unmanagedExtensionClasses:-} - NEO4J_dbms_allow__format__migration=${NEO4J_dbms_allowFormatMigration:-} - NEO4J_dbms_connectors_default__advertised__address=${NEO4J_dbms_connectors_defaultAdvertisedAddress:-} - NEO4J_ha_server__id=${NEO4J_ha_serverId:-} - NEO4J_ha_initial__hosts=${NEO4J_ha_initialHosts:-} - NEO4J_causal__clustering_expected__core__cluster__size=${NEO4J_causalClustering_expectedCoreClusterSize:-} - NEO4J_causal__clustering_initial__discovery__members=${NEO4J_causalClustering_initialDiscoveryMembers:-} - NEO4J_causal__clustering_discovery__listen__address=${NEO4J_causalClustering_discoveryListenAddress:-} - NEO4J_causal__clustering_discovery__advertised__address=${NEO4J_causalClustering_discoveryAdvertisedAddress:-} - NEO4J_causal__clustering_transaction__listen__address=${NEO4J_causalClustering_transactionListenAddress:-} - NEO4J_causal__clustering_transaction__advertised__address=${NEO4J_causalClustering_transactionAdvertisedAddress:-} - NEO4J_causal__clustering_raft__listen__address=${NEO4J_causalClustering_raftListenAddress:-} - NEO4J_causal__clustering_raft__advertised__address=${NEO4J_causalClustering_raftAdvertisedAddress:-} - - # unset old hardcoded unsupported env variables - unset NEO4J_dbms_txLog_rotation_retentionPolicy NEO4J_UDC_SOURCE \ - NEO4J_dbms_memory_heap_maxSize NEO4J_dbms_memory_heap_maxSize \ - NEO4J_dbms_unmanagedExtensionClasses NEO4J_dbms_allowFormatMigration \ - NEO4J_dbms_connectors_defaultAdvertisedAddress NEO4J_ha_serverId \ - NEO4J_ha_initialHosts NEO4J_causalClustering_expectedCoreClusterSize \ - NEO4J_causalClustering_initialDiscoveryMembers \ - NEO4J_causalClustering_discoveryListenAddress \ - NEO4J_causalClustering_discoveryAdvertisedAddress \ - NEO4J_causalClustering_transactionListenAddress \ - NEO4J_causalClustering_transactionAdvertisedAddress \ - NEO4J_causalClustering_raftListenAddress \ - NEO4J_causalClustering_raftAdvertisedAddress - - # Custom settings for dockerized neo4j - : ${NEO4J_dbms_tx__log_rotation_retention__policy:=100M size} - : ${NEO4J_dbms_memory_pagecache_size:=512M} - : ${NEO4J_wrapper_java_additional:=-Dneo4j.ext.udc.source=docker} - : ${NEO4J_dbms_memory_heap_initial__size:=512M} - : ${NEO4J_dbms_memory_heap_max__size:=512M} - : ${NEO4J_dbms_connectors_default__listen__address:=0.0.0.0} - : ${NEO4J_dbms_connector_http_listen__address:=0.0.0.0:7474} - : ${NEO4J_dbms_connector_https_listen__address:=0.0.0.0:7473} - : ${NEO4J_dbms_connector_bolt_listen__address:=0.0.0.0:7687} - : ${NEO4J_ha_host_coordination:=$(hostname):5001} - : ${NEO4J_ha_host_data:=$(hostname):6001} - : ${NEO4J_causal__clustering_discovery__listen__address:=0.0.0.0:5000} - : ${NEO4J_causal__clustering_discovery__advertised__address:=$(hostname):5000} - : ${NEO4J_causal__clustering_transaction__listen__address:=0.0.0.0:6000} - : ${NEO4J_causal__clustering_transaction__advertised__address:=$(hostname):6000} - : ${NEO4J_causal__clustering_raft__listen__address:=0.0.0.0:7000} - : ${NEO4J_causal__clustering_raft__advertised__address:=$(hostname):7000} - - if [ -d /conf ]; then - find /conf -type f -exec cp {} conf \; - fi - - if [ -d /ssl ]; then - NEO4J_dbms_directories_certificates="/ssl" - fi - - if [ -d /plugins ]; then - NEO4J_dbms_directories_plugins="/plugins" - fi - - if [ -d /logs ]; then - NEO4J_dbms_directories_logs="/logs" - fi - - if [ -d /import ]; then - NEO4J_dbms_directories_import="/import" - fi - - if [ -d /metrics ]; then - NEO4J_dbms_directories_metrics="/metrics" - fi - - if [ "${NEO4J_AUTH:-}" == "none" ]; then - NEO4J_dbms_security_auth__enabled=false - elif [[ "${NEO4J_AUTH:-}" == neo4j/* ]]; then - password="${NEO4J_AUTH#neo4j/}" - if [ "${password}" == "neo4j" ]; then - echo "Invalid value for password. It cannot be 'neo4j', which is the default." - exit 1 - fi - # Will exit with error if users already exist (and print a message explaining that) - bin/neo4j-admin set-initial-password "${password}" || true - elif [ -n "${NEO4J_AUTH:-}" ]; then - echo "Invalid value for NEO4J_AUTH: '${NEO4J_AUTH}'" - exit 1 - fi - - # list env variables with prefix NEO4J_ and create settings from them - unset NEO4J_AUTH NEO4J_SHA256 NEO4J_TARBALL - for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do - setting=$(echo ${i} | sed 's|^NEO4J_||' | sed 's|_|.|g' | sed 's|\.\.|_|g') - value=$(echo ${!i}) - if [[ -n ${value} ]]; then - if grep -q -F "${setting}=" conf/neo4j.conf; then - # Remove any lines containing the setting already - sed --in-place "/${setting}=.*/d" conf/neo4j.conf - fi - # Then always append setting to file - echo "${setting}=${value}" >> conf/neo4j.conf - fi - done - - [ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT} - - exec bin/neo4j console -elif [ "$1" == "dump-config" ]; then - if [ -d /conf ]; then - cp --recursive conf/* /conf - else - echo "You must provide a /conf volume" - exit 1 - fi -else - exec "$@" -fi diff --git a/src/main/resources/log4j.properties b/src/main/resources/log4j.properties new file mode 100644 index 00000000..43fc63ed --- /dev/null +++ b/src/main/resources/log4j.properties @@ -0,0 +1,3 @@ +log4j.rootLogger=INFO, console +log4j.appender.console=org.apache.log4j.ConsoleAppender +log4j.appender.console.layout=org.apache.log4j.PatternLayout \ No newline at end of file diff --git a/src/test/java/com/neo4j/docker/TestBasic.java b/src/test/java/com/neo4j/docker/TestBasic.java new file mode 100644 index 00000000..006cda8a --- /dev/null +++ b/src/test/java/com/neo4j/docker/TestBasic.java @@ -0,0 +1,121 @@ +package com.neo4j.docker; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.Container; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.OutputFrame; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.output.ToStringConsumer; +import org.testcontainers.containers.output.WaitingConsumer; +import com.neo4j.docker.utils.Neo4jVersion; +import com.neo4j.docker.utils.TestSettings; +import org.testcontainers.containers.wait.strategy.Wait; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +public class TestBasic +{ + private static Logger log = LoggerFactory.getLogger( TestBasic.class ); + + private GenericContainer createBasicContainer() + { + GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID ); + container.withEnv( "NEO4J_AUTH", "none" ) + .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" ) + .withExposedPorts( 7474 ) + .withLogConsumer( new Slf4jLogConsumer( log ) ); + return container; + } + + + @Test + void testListensOn7474() + { + try(GenericContainer container = createBasicContainer()) + { + createBasicContainer(); + container.setWaitStrategy( Wait.forHttp( "/" ).forPort( 7474 ).forStatusCode( 200 ) ); + container.start(); + Assertions.assertTrue( container.isRunning() ); + } + } + + @Test + void testNoUnexpectedErrors() throws Exception + { + // version 4.0 still has some annoying warnings that haven't been cleaned up, skip this test for now + + try(GenericContainer container = createBasicContainer()) + { + container.setWaitStrategy( Wait.forHttp( "/" ).forPort( 7474 ).forStatusCode( 200 ) ); + container.start(); + Assertions.assertTrue( container.isRunning() ); + + String stderr = container.getLogs(OutputFrame.OutputType.STDERR); + Assertions.assertEquals( "", stderr, + "Unexpected errors in stderr from container!\n" + + stderr ); + } + } + + @Test + void testLicenseAcceptanceRequired() + { + Assumptions.assumeTrue( TestSettings.EDITION == TestSettings.Edition.ENTERPRISE, + "No license checks for community edition"); + Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3,3,0 ) ), + "No license checks before version 3.3.0"); + + String logsOut; + try(GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID ) + .withLogConsumer( new Slf4jLogConsumer( log ) ) ) + { + container.waitingFor( Wait.forLogMessage( ".*must accept the license.*", 1 ) + .withStartupTimeout( Duration.ofSeconds( 30 ) ) ); + + Assertions.assertDoesNotThrow( () -> container.start(), + "Neo4j did not notify about accepting the license agreement" ); + logsOut = container.getLogs(); + } + // double check the container didn't warn and start neo4j anyway + Assertions.assertTrue( logsOut.contains( "must accept the license" ), + "Neo4j did not notify about accepting the license agreement" ); + Assertions.assertFalse( logsOut.contains( "Remote interface available" ), + "Neo4j was started even though the license was not accepted" ); + } + + @Test + void testCypherShellOnPath() throws Exception + { + String expectedCypherShellPath = "/var/lib/neo4j/bin/cypher-shell"; + try(GenericContainer container = createBasicContainer()) + { + container.setWaitStrategy( Wait.forHttp( "/" ).forPort( 7474 ).forStatusCode( 200 ) ); + container.start(); + + Container.ExecResult whichResult = container.execInContainer( "which", "cypher-shell" ); + Assertions.assertTrue( whichResult.getStdout().contains( expectedCypherShellPath ), + "cypher-shell not on path" ); + } + } + + @Test + void testCanChangeWorkDir() throws Exception + { + try(GenericContainer container = createBasicContainer()) + { + container.setWorkingDirectory( "/tmp" ); + container.setWaitStrategy( Wait.forHttp( "/" ) + .forPort( 7474 ) + .forStatusCode( 200 ) + .withStartupTimeout( Duration.ofSeconds( 60 ) ) ); + Assertions.assertDoesNotThrow( () -> container.start(), + "Could not start neo4j from workdir NEO4J_HOME" ); + } + } +} diff --git a/src/test/java/com/neo4j/docker/TestCausalCluster.java b/src/test/java/com/neo4j/docker/TestCausalCluster.java new file mode 100644 index 00000000..c28e75d9 --- /dev/null +++ b/src/test/java/com/neo4j/docker/TestCausalCluster.java @@ -0,0 +1,115 @@ +package com.neo4j.docker; + +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/TestConfSettings.java b/src/test/java/com/neo4j/docker/TestConfSettings.java new file mode 100644 index 00000000..47ad983e --- /dev/null +++ b/src/test/java/com/neo4j/docker/TestConfSettings.java @@ -0,0 +1,395 @@ +package com.neo4j.docker; + +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.Disabled; +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.wait.strategy.Wait; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Scanner; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +public class TestConfSettings { + private static Logger log = LoggerFactory.getLogger(TestConfSettings.class); + + private GenericContainer createContainer() + { + return new GenericContainer(TestSettings.IMAGE_ID) + .withEnv("NEO4J_AUTH", "none") + .withEnv("NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes") + .withExposedPorts(7474, 7687) + .withLogConsumer(new Slf4jLogConsumer(log)); + } + + private Map parseConfFile(File conf) throws FileNotFoundException + { + Map configurations = new HashMap<>(); + Scanner scanner = new Scanner(conf); + while ( scanner.hasNextLine() ) + { + String[] params = scanner.nextLine().split( "=", 2 ); + if(params.length < 2) + { + continue; + } + log.debug( params[0] + "\t:\t" + params[1] ); + configurations.put( params[0], params[1] ); + } + return configurations; + } + + private boolean isStringPresentInDebugLog( Path debugLog, String matchThis) throws IOException + { + // searches the debug log for the given string, returns true if present + Stream lines = Files.lines(debugLog); + Optional isMatch = lines.filter(s -> s.contains(matchThis)).findFirst(); + lines.close(); + return isMatch.isPresent(); + } + + @Test + void testIgnoreNumericVars() + { + try(GenericContainer container = createContainer()) + { + container.withEnv( "NEO4J_1a", "1" ); + container.start(); + Assertions.assertTrue( container.isRunning() ); + + WaitingConsumer waitingConsumer = new WaitingConsumer(); + container.followOutput( waitingConsumer ); + + Assertions.assertDoesNotThrow( () -> waitingConsumer.waitUntil( frame -> frame.getUtf8String() + .contains( "WARNING: 1a not written to conf file because settings that start with a number are not permitted" ), + 15, TimeUnit.SECONDS ), + "Neo4j did not warn about invalid numeric config variable `Neo4j_1a`" ); + } + } + + @Test + void testEnvVarsOverrideDefaultConfigurations() throws Exception + { + Assumptions.assumeTrue(TestSettings.NEO4J_VERSION.isAtLeastVersion(new Neo4jVersion(3, 0, 0)), + "No neo4j-admin in 2.3: skipping neo4j-admin-conf-override test"); + + File conf; + try(GenericContainer container = createContainer() + .withEnv("NEO4J_dbms_memory_pagecache_size", "1000m") + .withEnv("NEO4J_dbms_memory_heap_initial__size", "2000m") + .withEnv("NEO4J_dbms_memory_heap_max__size", "3000m") + .withEnv( "NEO4J_dbms_directories_logs", "/notdefaultlogs" ) + .withEnv( "NEO4J_dbms_directories_data", "/notdefaultdata" ) + .withCommand("dump-config") ) + { + Path confMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "overriddenbyenv-conf-", + "/conf" ); + conf = confMount.resolve( "neo4j.conf" ).toFile(); + container.setWaitStrategy( + Wait.forLogMessage( ".*Config Dumped.*", 1 ).withStartupTimeout( Duration.ofSeconds( 30 ) ) ); + SetContainerUser.nonRootUser( container ); + container.start(); + } + + // now check the settings we set via env are in the new conf file + Assertions.assertTrue( conf.exists(), "configuration file not written" ); + Assertions.assertTrue( conf.canRead(), "configuration file not readable for some reason?" ); + + Map configurations = parseConfFile( conf ); + Assertions.assertTrue( configurations.containsKey( "dbms.memory.pagecache.size" ), "pagecache size not overridden" ); + Assertions.assertEquals( "1000m", + configurations.get( "dbms.memory.pagecache.size" ), + "pagecache size not overridden" ); + + Assertions.assertTrue( configurations.containsKey( "dbms.memory.heap.initial_size" ), "initial heap size not overridden" ); + Assertions.assertEquals( "2000m", + configurations.get( "dbms.memory.heap.initial_size" ), + "initial heap size not overridden" ); + + Assertions.assertTrue( configurations.containsKey( "dbms.memory.heap.max_size" ), "maximum heap size not overridden" ); + Assertions.assertEquals( "3000m", + configurations.get( "dbms.memory.heap.max_size" ), + "maximum heap size not overridden" ); + + Assertions.assertTrue( configurations.containsKey( "dbms.directories.logs" ), "log folder not overridden" ); + Assertions.assertEquals( "/notdefaultlogs", + configurations.get( "dbms.directories.logs" ), + "log directory not overridden" ); + Assertions.assertTrue( configurations.containsKey( "dbms.directories.data" ), "data folder not overridden" ); + Assertions.assertEquals( "/notdefaultdata", + configurations.get( "dbms.directories.data" ), + "data directory not overridden" ); + } + + @Test + void testReadsTheConfFile() throws Exception + { + Path debugLog; + + try(GenericContainer container = createContainer()) + { + Path testOutputFolder = HostFileSystemOperations.createTempFolder( "confIsRead-" ); + //Mount /conf + Path confMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + testOutputFolder, + "conf-", + "/conf" ); + Path logMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + testOutputFolder, + "logs-", + "/logs" ); + debugLog = logMount.resolve("debug.log"); + SetContainerUser.nonRootUser( container ); + //Create ReadConf.conf file with the custom env variables + Path confFile = Paths.get( "src", "test", "resources", "confs", "ReadConf.conf" ); + Files.copy( confFile, confMount.resolve( "neo4j.conf" ) ); + //Start the container + container.setWaitStrategy( Wait.forHttp( "/" ).forPort( 7474 ).forStatusCode( 200 ) ); + container.start(); + } + + //Check if the container reads the conf file + Assertions.assertTrue( isStringPresentInDebugLog( debugLog, "dbms.memory.heap.max_size=512m" ), + "dbms.memory.heap.max_size was not set correctly"); + } + + @Test + void testCommentedConfigsAreReplacedByDefaultOnes() throws Exception + { + File conf; + try(GenericContainer container = createContainer()) + { + //Mount /conf + Path confMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "replacedbydefault-conf-", + "/conf" ); + conf = confMount.resolve( "neo4j.conf" ).toFile(); + SetContainerUser.nonRootUser( container ); + //Create ConfsReplaced.conf file + Path confFile = Paths.get( "src", "test", "resources", "confs", "ConfsReplaced.conf" ); + Files.copy( confFile, confMount.resolve( "neo4j.conf" ) ); + //Start the container + container.setWaitStrategy( + Wait.forLogMessage( ".*Config Dumped.*", 1 ).withStartupTimeout( Duration.ofSeconds( 30 ) ) ); + container.setCommand( "dump-config" ); + container.start(); + } + //Read the config file to check if the config is set correctly + Map configurations = parseConfFile( conf ); + Assertions.assertTrue( configurations.containsKey( "dbms.memory.pagecache.size" ), + "conf settings not set correctly by docker-entrypoint" ); + Assertions.assertEquals( "512M", + configurations.get( "dbms.memory.pagecache.size" ), + "conf settings not appended correctly by docker-entrypoint" ); + } + + @Test + void testConfigsAreNotOverridenByDockerentrypoint() throws Exception + { + File conf; + try(GenericContainer container = createContainer()) + { + //Mount /conf + Path confMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "notoverriddenbydefault-conf-", + "/conf" ); + conf = confMount.resolve( "neo4j.conf" ).toFile(); + SetContainerUser.nonRootUser( container ); + //Create ConfsNotOverriden.conf file + Path confFile = Paths.get( "src", "test", "resources", "confs", "ConfsNotOverriden.conf" ); + Files.copy( confFile, confMount.resolve( "neo4j.conf" ) ); + //Start the container + container.setWaitStrategy( + Wait.forLogMessage( ".*Config Dumped.*", 1 ) + .withStartupTimeout( Duration.ofSeconds( 30 ) ) ); + container.setCommand( "dump-config" ); + container.start(); + } + + //Read the config file to check if the config is not overriden + Map configurations = parseConfFile(conf); + Assertions.assertTrue(configurations.containsKey("dbms.memory.pagecache.size"), "conf settings not set correctly by docker-entrypoint"); + Assertions.assertEquals("1024M", + configurations.get("dbms.memory.pagecache.size"), + "docker-entrypoint has overriden custom setting set from user's conf"); + } + + @Test + void testEnvVarsOverride() throws Exception + { + Path debugLog; + try(GenericContainer container = createContainer().withEnv("NEO4J_dbms_memory_pagecache_size", "512m")) + { + Path testOutputFolder = HostFileSystemOperations.createTempFolder( "envoverrideworks-" ); + Path confMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + testOutputFolder, + "conf-", + "/conf" ); + Path logMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + testOutputFolder, + "logs-", + "/logs" ); + debugLog = logMount.resolve( "debug.log" ); + SetContainerUser.nonRootUser( container ); + //Create EnvVarsOverride.conf file + Path confFile = Paths.get( "src", "test", "resources", "confs", "EnvVarsOverride.conf" ); + Files.copy( confFile, confMount.resolve( "EnvVarsOverride.conf" ) ); + //Start the container + container.setWaitStrategy( Wait.forHttp( "/" ).forPort( 7474 ).forStatusCode( 200 ) ); + container.start(); + } + + Assertions.assertTrue( isStringPresentInDebugLog( debugLog, "dbms.memory.pagecache.size=512m" ), + "dbms.memory.pagecache.size was not set correctly"); + } + + @Test + void testEnterpriseOnlyDefaultsConfigsAreSet () throws Exception + { + Assumptions.assumeTrue(TestSettings.EDITION == TestSettings.Edition.ENTERPRISE, + "This is testing only ENTERPRISE EDITION configs"); + + try(GenericContainer container = createContainer().withEnv("NEO4J_dbms_memory_pagecache_size", "512m")) + { + //Mount /logs + Path logMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "enterpriseonlysettings-logs-", + "/logs" ); + SetContainerUser.nonRootUser( container ); + //Start the container + container.setWaitStrategy( Wait.forHttp( "/" ).forPort( 7474 ).forStatusCode( 200 ) ); + container.start(); + //Read debug.log to check that causal_clustering confs are set successfully + String expectedTxAddress = container.getContainerId().substring( 0, 12 ) + ":6000"; + + Assertions.assertTrue( isStringPresentInDebugLog(logMount.resolve( "debug.log" ), + "causal_clustering.transaction_advertised_address=" + expectedTxAddress ), + "causal_clustering.transaction_advertised_address was not set correctly" ); + } + } + + // disabled because there are currently no enterprise-only settings + @Disabled + @Test + void testCommunityDoesNotHaveEnterpriseConfigs() throws Exception + { + Assert.fail( "This test should not have run!" ); + Assumptions.assumeTrue(TestSettings.EDITION == TestSettings.Edition.COMMUNITY, + "This is testing only COMMUNITY EDITION configs"); + Path debugLog; + try(GenericContainer container = createContainer().withEnv("NEO4J_dbms_memory_pagecache_size", "512m")) + { + //Mount /logs + Path logMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "enterprisesettingsnotincommunity-logs-", + "/logs" ); + debugLog = logMount.resolve( "debug.log" ); + SetContainerUser.nonRootUser( container ); + //Start the container + container.setWaitStrategy( Wait.forHttp( "/" ).forPort( 7474 ).forStatusCode( 200 ) ); + container.start(); + } + + //Read debug.log to check that causal_clustering confs are not present + Assertions.assertFalse(isStringPresentInDebugLog( debugLog, "causal_clustering.transaction_listen_address" ), + "causal_clustering.transaction_listen_address should not be on the Community debug.log"); + } + + @Test + void testJvmAdditionalNotOverridden() throws Exception + { + Path logMount; + + try(GenericContainer container = createContainer()) + { + Assumptions.assumeFalse( TestSettings.NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_400), "test not applicable in versions newer than 4.0." ); + + Path testOutputFolder = HostFileSystemOperations.createTempFolder( "jvmaddnotoverridden-" ); + //Mount /conf + Path confMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + testOutputFolder, + "conf-", + "/conf" ); + logMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + testOutputFolder, + "logs-", + "/logs" ); + SetContainerUser.nonRootUser( container ); + //Create JvmAdditionalNotOverriden.conf file + Path confFile = Paths.get( "src", "test", "resources", "confs", "JvmAdditionalNotOverriden.conf" ); + Files.copy( confFile, confMount.resolve( "neo4j.conf" ) ); + //Start the container + container.setWaitStrategy( Wait.forHttp( "/" ).forPort( 7474 ).forStatusCode( 200 ) ); + container.start(); + } + + //Read the debug.log to check that dbms.jvm.additional was set correctly + Stream lines = Files.lines(logMount.resolve("debug.log")); + Optional jvmAdditionalMatch = lines.filter(s -> s.contains("dbms.jvm.additional=-Dunsupported.dbms.udc.source=docker,-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005")).findFirst(); + lines.close(); + Assertions.assertTrue(isStringPresentInDebugLog( logMount.resolve("debug.log"), + "dbms.jvm.additional=-Dunsupported.dbms.udc.source=docker,-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005"), + "dbms.jvm.additional is overriden by Docker-entrypoint"); + } + + @Test + void testShellExpansionAvoided() throws Exception + { + Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_400), "test only applicable to 4.0 and beyond." ); + + Path confMount; + try(GenericContainer container = createContainer().withEnv("NEO4J_dbms_security_procedures_unrestricted", "*")) + { + confMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "shellexpansionavoided-conf-", + "/conf" ); + + SetContainerUser.nonRootUser( container ); + //Start the container + container.setWaitStrategy( + Wait.forLogMessage( ".*Config Dumped.*", 1 ) + .withStartupTimeout( Duration.ofSeconds( 30 ) ) ); + container.setCommand( "dump-config" ); + container.start(); + } + File conf = confMount.resolve( "neo4j.conf" ).toFile(); + Map configurations = parseConfFile(conf); + Assertions.assertTrue(configurations.containsKey("dbms.security.procedures.unrestricted"), "configuration not set from env var"); + Assertions.assertEquals("*", + configurations.get("dbms.security.procedures.unrestricted"), + "Configuration value should be *. If it's not docker-entrypoint.sh probably evaluated it as a glob expression."); + } +} \ No newline at end of file diff --git a/src/test/java/com/neo4j/docker/TestHACluster.java b/src/test/java/com/neo4j/docker/TestHACluster.java new file mode 100644 index 00000000..6ccb1577 --- /dev/null +++ b/src/test/java/com/neo4j/docker/TestHACluster.java @@ -0,0 +1,115 @@ +package com.neo4j.docker; + +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/TestMounting.java b/src/test/java/com/neo4j/docker/TestMounting.java new file mode 100644 index 00000000..b8dcef40 --- /dev/null +++ b/src/test/java/com/neo4j/docker/TestMounting.java @@ -0,0 +1,237 @@ +package com.neo4j.docker; + +import com.neo4j.docker.utils.HostFileSystemOperations; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Neo4jContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import com.neo4j.docker.utils.SetContainerUser; +import com.neo4j.docker.utils.Neo4jVersion; +import com.neo4j.docker.utils.TestSettings; +import org.testcontainers.containers.wait.strategy.Wait; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.UserPrincipal; +import java.time.Duration; +import java.util.stream.Stream; + + +public class TestMounting +{ + private static Logger log = LoggerFactory.getLogger( TestMounting.class ); + + static Stream defaultUserFlagSecurePermissionsFlag() + { + // "asUser={0}, secureFlag={1}" + // expected behaviour is that if you set --user flag, your data should be read/writable + // if you don't set --user flag then read/writability should be controlled by the secure file permissions flag + // the asUser=true, secureflag=false combination is tested separately because the container should fail to start. + return Stream.of( + Arguments.arguments( false, false ), + Arguments.arguments( true, false ), + Arguments.arguments( true, true )); + } + + private GenericContainer setupBasicContainer( boolean asCurrentUser, boolean isSecurityFlagSet ) + { + log.info( "Running as user {}, {}", + asCurrentUser?"non-root":"root", + isSecurityFlagSet?"with secure file permissions":"with unsecured file permissions" ); + + Neo4jContainer container = new Neo4jContainer( TestSettings.IMAGE_ID ); + container.withExposedPorts( 7474, 7687 ) + .withLogConsumer( new Slf4jLogConsumer( log ) ) + .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" ) + .withEnv( "NEO4J_AUTH", "none" ); + + if(asCurrentUser) + { + SetContainerUser.nonRootUser( container ); + } + if(isSecurityFlagSet) + { + container.withEnv( "SECURE_FILE_PERMISSIONS", "yes" ); + } + return container; + } + + private void verifySingleFolder( Path folderToCheck, boolean shouldBeWritable ) + { + String folderForDiagnostics = folderToCheck.toAbsolutePath().toString(); + + Assertions.assertTrue( folderToCheck.toFile().exists(), "did not create " + folderForDiagnostics + " folder on host" ); + if( shouldBeWritable ) + { + Assertions.assertTrue( folderToCheck.toFile().canRead(), "cannot read host "+folderForDiagnostics+" folder" ); + Assertions.assertTrue(folderToCheck.toFile().canWrite(), "cannot write to host "+folderForDiagnostics+" folder" ); + } + } + + private void verifyDataFolderContentsArePresentOnHost( Path dataMount, boolean shouldBeWritable ) + { + verifySingleFolder( dataMount.resolve( "dbms" ), shouldBeWritable ); + verifySingleFolder( dataMount.resolve( "databases" ), shouldBeWritable ); + + if(TestSettings.NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_400 )) + { + verifySingleFolder( dataMount.resolve( "transactions" ), shouldBeWritable ); + } + } + + private void verifyLogsFolderContentsArePresentOnHost( Path logsMount, boolean shouldBeWritable ) + { + verifySingleFolder( logsMount, shouldBeWritable ); + Assertions.assertTrue( logsMount.resolve( "debug.log" ).toFile().exists(), + "Neo4j did not write a debug.log file to "+logsMount.toString() ); + Assertions.assertEquals( shouldBeWritable, + logsMount.resolve( "debug.log" ).toFile().canWrite(), + String.format( "The debug.log file should %sbe writable", shouldBeWritable ? "" : "not ") ); + } + + + @Test + void testDumpConfig( ) throws Exception + { + try(GenericContainer container = setupBasicContainer( true, false )) + { + Path confMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "dumpconfig-conf-", + "/conf" ); + container.setWaitStrategy( + Wait.forLogMessage( ".*Config Dumped.*", 1 ) + .withStartupTimeout( Duration.ofSeconds( 30 ) ) ); + container.withCommand( "dump-config" ); + container.start(); + + Path expectedConfDumpFile = confMount.resolve( "neo4j.conf" ); + Assertions.assertTrue( expectedConfDumpFile.toFile().exists(), + "dump-config did not dump the config file to " + confMount.toString() ); + } + } + + + @ParameterizedTest(name = "asUser={0}, secureFlag={1}") + @MethodSource( "defaultUserFlagSecurePermissionsFlag" ) + void testCanMountJustDataFolder(boolean asCurrentUser, boolean isSecurityFlagSet) throws IOException + { + Assumptions.assumeTrue(TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3,1,0 ) ), + "User checks not valid before 3.1" ); + + try(GenericContainer container = setupBasicContainer( asCurrentUser, isSecurityFlagSet )) + { + Path dataMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "canmountjustdata-data-", + "/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-", + "/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, + testOutputFolder, + "data-", + "/data" ); + Path logsMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + testOutputFolder, + "logs-", + "/logs" ); + container.start(); + + verifyDataFolderContentsArePresentOnHost( dataMount, asCurrentUser ); + verifyLogsFolderContentsArePresentOnHost( logsMount, asCurrentUser ); + } + } + + @Test + void testCantWriteIfSecureEnabledAndNoPermissions_data() throws IOException + { + Assumptions.assumeTrue(TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3,1,0 ) ), + "User checks not valid before 3.1" ); + + try(GenericContainer container = setupBasicContainer( false, true )) + { + HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "nopermissioninsecuremode-data-", + "/data" ); + + // currently Neo4j will try to start and fail. It should be fixed to throw an error and not try starting + // container.setWaitStrategy( Wait.forLogMessage( "[fF]older /data is not accessible for user", 1 ).withStartupTimeout( Duration.ofSeconds( 20 ) ) ); + container.setWaitStrategy( Wait.forListeningPort() + .withStartupTimeout( Duration.ofSeconds( 20 ) ) ); + Assertions.assertThrows( org.testcontainers.containers.ContainerLaunchException.class, + () -> container.start(), + "Neo4j should not start in secure mode if data folder is unwritable" ); + } + } + + @Test + void testCantWriteIfSecureEnabledAndNoPermissions_logs() throws IOException + { + Assumptions.assumeTrue(TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3,1,0 ) ), + "User checks not valid before 3.1" ); + + try(GenericContainer container = setupBasicContainer( false, true )) + { + HostFileSystemOperations.createTempFolderAndMountAsVolume( + container, + "nopermissioninsecuremode-logs-", + "/logs" ); + + // currently Neo4j will try to start and fail. It should be fixed to throw an error and not try starting + // container.setWaitStrategy( Wait.forLogMessage( "[fF]older /logs is not accessible for user", 1 ).withStartupTimeout( Duration.ofSeconds( 20 ) ) ); + container.setWaitStrategy( Wait.forListeningPort() + .withStartupTimeout( Duration.ofSeconds( 20 ) ) ); + Assertions.assertThrows( org.testcontainers.containers.ContainerLaunchException.class, + () -> container.start(), + "Neo4j should not start in secure mode if logs folder is unwritable" ); + } + } +} diff --git a/src/test/java/com/neo4j/docker/TestPasswords.java b/src/test/java/com/neo4j/docker/TestPasswords.java new file mode 100644 index 00000000..239fadc2 --- /dev/null +++ b/src/test/java/com/neo4j/docker/TestPasswords.java @@ -0,0 +1,183 @@ +package com.neo4j.docker; + +import com.neo4j.docker.utils.DatabaseIO; +import com.neo4j.docker.utils.HostFileSystemOperations; +import com.neo4j.docker.utils.Neo4jVersion; +import com.neo4j.docker.utils.SetContainerUser; +import com.neo4j.docker.utils.TestSettings; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.output.WaitingConsumer; +import org.testcontainers.containers.wait.strategy.Wait; + +import java.nio.file.Path; +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +public class TestPasswords +{ + private static Logger log = LoggerFactory.getLogger( TestPasswords.class); + + private GenericContainer createContainer( boolean asCurrentUser ) + { + GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID ); + container.withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" ) + .withExposedPorts( 7474, 7687 ) + .withLogConsumer( new Slf4jLogConsumer( log ) ) + .waitingFor( Wait.forHttp( "/" ) + .forPort( 7474 ) + .forStatusCode( 200 ) + .withStartupTimeout( Duration.ofSeconds( 90 ) ) ); + if(asCurrentUser) + { + SetContainerUser.nonRootUser( container ); + } + return container; + } + + + @Test + void testPasswordCantBeNeo4j() throws Exception + { + try(GenericContainer failContainer = new GenericContainer( TestSettings.IMAGE_ID ).withLogConsumer( new Slf4jLogConsumer( log ) )) + { + if ( TestSettings.EDITION == TestSettings.Edition.ENTERPRISE ) + { + failContainer.withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" ); + } + failContainer.withEnv( "NEO4J_AUTH", "neo4j/neo4j" ); + failContainer.start(); + + WaitingConsumer waitingConsumer = new WaitingConsumer(); + failContainer.followOutput( waitingConsumer ); + + Assertions.assertDoesNotThrow( () -> waitingConsumer.waitUntil( + frame -> frame.getUtf8String().contains("Invalid value for password" ), 10, TimeUnit.SECONDS ), + "did not error due to invalid password" ); + } + } + + @Test + void testDefaultPasswordAndPasswordResetIfNoNeo4jAuthSet() + { + try(GenericContainer container = createContainer( true )) + { + log.info( "Starting first container as current user and not specifying NEO4J_AUTH" ); + container.start(); + DatabaseIO db = new DatabaseIO(container); + // try with no password, this should fail because the default password should be applied with no NEO4J_AUTH env variable + Assertions.assertThrows( org.neo4j.driver.exceptions.AuthenticationException.class, + () -> db.putInitialDataIntoContainer( "neo4j", "" ), + "Able to access database with no password, even though NEO4J_AUTH=none was not specified!"); + Assertions.assertThrows( org.neo4j.driver.exceptions.ClientException.class, + () -> db.putInitialDataIntoContainer( "neo4j", "neo4j" ), + "Was not prompted for a new password when using default"); + db.changePassword( "neo4j", "neo4j", "newpassword" ); + db.putInitialDataIntoContainer( "neo4j", "newpassword" ); + } + } + + @ParameterizedTest(name = "as current user={0}") + @ValueSource(booleans = {true, false}) + void testCanSetPassword( boolean asCurrentUser ) throws Exception + { + // create container and mount /data folder so that data can persist between sessions + String password = "some_valid_password"; + Path dataMount; + + try(GenericContainer firstContainer = createContainer( asCurrentUser )) + { + firstContainer.withEnv( "NEO4J_AUTH", "neo4j/"+password ); + dataMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + firstContainer, + "password-defaultuser-data-", + "/data" ); + log.info( String.format( "Starting first container as %s user and setting password", + asCurrentUser? "current" : "default" ) ); + // create a database with stuff in + firstContainer.start(); + DatabaseIO db = new DatabaseIO(firstContainer); + db.putInitialDataIntoContainer( "neo4j", password ); + } + + // with a new container, check the database data. + try(GenericContainer secondContainer = createContainer( asCurrentUser )) + { + secondContainer.withFileSystemBind( dataMount.toString(), "/data", BindMode.READ_WRITE ); + log.info( "starting new container with same /data mount as same user without setting password" ); + secondContainer.start(); + DatabaseIO db = new DatabaseIO(secondContainer); + db.verifyDataInContainer( "neo4j", password ); + } + } + + + @ParameterizedTest(name = "as current user={0}") + @ValueSource(booleans = {true, false}) + void testSettingNeo4jAuthDoesntOverrideExistingPassword( boolean asCurrentUser ) throws Exception + { + String password = "some_valid_password"; + Path dataMount; + + try(GenericContainer firstContainer = createContainer( asCurrentUser )) + { + firstContainer.withEnv( "NEO4J_AUTH", "neo4j/"+password ); + dataMount = HostFileSystemOperations.createTempFolderAndMountAsVolume( + firstContainer, + "password-envoverride-data-", + "/data" ); + + // create a database with stuff in + log.info( String.format( "Starting first container as %s user and setting password", + asCurrentUser? "current" : "default" ) ); + firstContainer.start(); + DatabaseIO db = new DatabaseIO(firstContainer); + db.putInitialDataIntoContainer( "neo4j", password ); + } + + // with a new container, check the database data. + try(GenericContainer secondContainer = createContainer( asCurrentUser )) + { + String wrongPassword = "not_the_password"; + secondContainer.withEnv( "NEO4J_AUTH", "neo4j/"+wrongPassword ); + HostFileSystemOperations.mountHostFolderAsVolume( secondContainer, dataMount, "/data" ); + log.info( "starting new container with same /data mount as same user without setting password" ); + secondContainer.start(); + DatabaseIO db = new DatabaseIO(secondContainer); + db.verifyDataInContainer( "neo4j", password ); + Assertions.assertThrows( org.neo4j.driver.exceptions.AuthenticationException.class, + () -> db.verifyConnectivity( "neo4j", wrongPassword) ); + } + } + + @Test + void testPromptsForPasswordReset() + { + Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3,6,0 ) ), + "Require password reset is only a feature in 3.6 onwards"); + try(GenericContainer container = createContainer( false )) + { + String user = "neo4j"; + String intialPass = "apassword"; + String resetPass = "new_password"; + container.withEnv("NEO4J_AUTH", user+"/"+intialPass+"/true" ); + container.start(); + DatabaseIO db = new DatabaseIO(container); + Assertions.assertThrows( org.neo4j.driver.exceptions.ClientException.class, + () -> db.putInitialDataIntoContainer( user, intialPass ), + "Neo4j did not error because of password reset requirement"); + + db.changePassword( user, intialPass, resetPass ); + db.putInitialDataIntoContainer( user, resetPass ); + db.verifyDataInContainer( user, resetPass ); + } + } +} diff --git a/src/test/java/com/neo4j/docker/TestPluginInstallation.java b/src/test/java/com/neo4j/docker/TestPluginInstallation.java new file mode 100644 index 00000000..56b06d4e --- /dev/null +++ b/src/test/java/com/neo4j/docker/TestPluginInstallation.java @@ -0,0 +1,182 @@ +package com.neo4j.docker; + +import com.neo4j.docker.plugins.ExampleNeo4jPlugin; +import com.neo4j.docker.utils.HostFileHttpHandler; +import com.neo4j.docker.utils.HttpServerRule; +import com.neo4j.docker.plugins.JarBuilder; +import com.neo4j.docker.utils.SetContainerUser; +import com.neo4j.docker.utils.TestSettings; +import org.junit.Rule; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.migrationsupport.rules.EnableRuleMigrationSupport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.Testcontainers; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.shaded.com.google.common.io.Files; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; + +import org.neo4j.driver.AuthTokens; +import org.neo4j.driver.Driver; +import org.neo4j.driver.GraphDatabase; +import org.neo4j.driver.Record; +import org.neo4j.driver.Session; +import org.neo4j.driver.Result; + +import static com.neo4j.docker.utils.TestSettings.NEO4J_VERSION; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@EnableRuleMigrationSupport +public class TestPluginInstallation +{ + private static final int DEFAULT_BROWSER_PORT = 7474; + private static final int DEFAULT_BOLT_PORT = 7687; + + private static final String versions = "versions.json"; + private static final String myPlugin = "myPlugin.jar"; + + private static final Logger log = LoggerFactory.getLogger( TestPluginInstallation.class ); + + @Rule + public HttpServerRule httpServer = new HttpServerRule(); + + private GenericContainer container; + + @BeforeAll + public static void checkVersionIsCompatibleWithTest() + { + // Should work for all versions + } + + private void createContainerWithTestingPlugin() + { + Testcontainers.exposeHostPorts( httpServer.PORT ); + container = new GenericContainer( TestSettings.IMAGE_ID ); + + container.withEnv( "NEO4J_AUTH", "neo4j/neo" ) + .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" ) + .withEnv( "NEO4JLABS_PLUGINS", "[\"_testing\"]" ) + .withExposedPorts( DEFAULT_BROWSER_PORT, DEFAULT_BOLT_PORT ) + .withLogConsumer( new Slf4jLogConsumer( log ) ); + + SetContainerUser.nonRootUser( container ); + } + + @BeforeEach + public void setUp( @TempDir Path pluginsDir ) throws Exception + { + File versionsJson = pluginsDir.resolve( versions ).toFile(); + + Files.write( getResource( "versions.json" ).replace( "$NEO4JVERSION", NEO4J_VERSION.toString() ), versionsJson, StandardCharsets.UTF_8 ); + + File myPluginJar = pluginsDir.resolve( myPlugin ).toFile(); + + new JarBuilder().createJarFor( myPluginJar, ExampleNeo4jPlugin.class, ExampleNeo4jPlugin.PrimitiveOutput.class ); + + httpServer.registerHandler( versions, new HostFileHttpHandler( versionsJson, "application/json" ) ); + httpServer.registerHandler( myPlugin, new HostFileHttpHandler( myPluginJar, "application/java-archive" ) ); + + createContainerWithTestingPlugin(); + container.setWaitStrategy( Wait.forHttp( "/" ).forPort( DEFAULT_BROWSER_PORT ).forStatusCode( 200 ) ); + } + + @Test + @DisabledIfEnvironmentVariable(named = "NEO4J_DOCKER_TESTS_TestPluginInstallation", matches = "ignore") + public void testPlugin() throws Exception + { + // When we start the neo4j docker container + container.start(); + + // Then the plugin is downloaded and placed in the plugins directory + String lsPluginsDir = container.execInContainer( "ls", "/var/lib/neo4j/plugins" ).getStdout(); + // Two options here because it varies depending on whether the plugins dir _only_ contains our file or if it contains multiple files + assertTrue( lsPluginsDir.contains( "\n_testing.jar\n" ) || lsPluginsDir.equals( "_testing.jar\n" ), "Plugin jar file not found in plugins directory" ); + + // When we connect to the database with the plugin + String boltAddress = "bolt://" + container.getContainerIpAddress() + ":" + container.getMappedPort( DEFAULT_BOLT_PORT ); + try ( Driver coreDriver = GraphDatabase.driver( boltAddress, AuthTokens.basic( "neo4j", "neo" ) ) ) + { + Session session = coreDriver.session(); + Result res = session.run( "CALL dbms.procedures() YIELD name, signature RETURN name, signature" ); + + // Then the procedure from the plugin is listed + assertTrue( res.stream().anyMatch( x -> x.get( "name" ).asString().equals( "com.neo4j.docker.plugins.defaultValues" ) ), + "Missing procedure provided by our plugin" ); + + // When we call the procedure from the plugin + res = session.run( "CALL com.neo4j.docker.plugins.defaultValues" ); + + // Then we get the response we expect + Record record = res.single(); + String message = "Result from calling our procedure doesnt match our expectations"; + assertEquals( record.get( "string" ).asString(), "a string", message ); + assertEquals( record.get( "integer" ).asInt(), 42L, message ); + assertEquals( record.get( "aFloat" ).asDouble(), 3.14d, 0.000001, message ); + assertEquals( record.get( "aBoolean" ).asBoolean(), true, message ); + assertFalse( res.hasNext(), "Our procedure should only return a single result" ); + + // Check that the config has been set + res = session.run ( "CALL dbms.listConfig() YIELD name, value WHERE name='dbms.security.procedures.unrestricted' RETURN value" ); + record = res.single(); + assertEquals( record.get( "value" ).asString(), "com.neo4j.docker.plugins.*", "neo4j config not updated for plugin" ); + assertFalse( res.hasNext(), "Config lookup should only return a single result" ); + } + finally + { + container.stop(); + } + } + + @Test + @DisabledIfEnvironmentVariable(named = "NEO4J_DOCKER_TESTS_TestPluginInstallation", matches = "ignore") + public void testPluginConfigurationDoesNotOverrideUserSetValues() throws Exception + { + // When we set a config value explicitly + container = container.withEnv ("NEO4J_dbms_security_procedures_unrestricted", "foo" ); + // When we start the neo4j docker container + container.start(); + + // When we connect to the database with the plugin + String boltAddress = "bolt://" + container.getContainerIpAddress() + ":" + container.getMappedPort( DEFAULT_BOLT_PORT ); + try ( Driver coreDriver = GraphDatabase.driver( boltAddress, AuthTokens.basic( "neo4j", "neo" ) ) ) + { + Session session = coreDriver.session(); + // Check that the config remains as set by our env var and is not overriden by the plugin defaults + Result res = session.run ( "CALL dbms.listConfig() YIELD name, value WHERE name='dbms.security.procedures.unrestricted' RETURN value" ); + Record record = res.single(); + assertEquals( record.get( "value" ).asString(), "foo", "neo4j config should not be overriden by plugin" ); + assertFalse( res.hasNext(), "Config lookup should only return a single result" ); + } + finally + { + container.stop(); + } + } + + private String getResource( String path ) throws IOException + { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream( path ); + ByteArrayOutputStream result = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int length; + while ( (length = inputStream.read( buffer )) != -1 ) + { + result.write( buffer, 0, length ); + } + return result.toString( "UTF-8" ); + } +} diff --git a/src/test/java/com/neo4j/docker/TestUpgrade.java b/src/test/java/com/neo4j/docker/TestUpgrade.java new file mode 100644 index 00000000..4665b050 --- /dev/null +++ b/src/test/java/com/neo4j/docker/TestUpgrade.java @@ -0,0 +1,318 @@ +package com.neo4j.docker; + +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.eclipse.collections.impl.block.factory.Comparators; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.ContainerFetchException; +import org.testcontainers.containers.ContainerLaunchException; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.images.PullPolicy; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.neo4j.driver.Record; + +import static com.neo4j.docker.utils.HostFileSystemOperations.createTempFolderAndMountAsVolume; +import static com.neo4j.docker.utils.HostFileSystemOperations.mountHostFolderAsVolume; + +public class TestUpgrade +{ + private static final Logger log = LoggerFactory.getLogger( TestUpgrade.class ); + private static final List readonlyMounts = Collections.singletonList( "conf" ); + private static final List writableMounts = getWriteableMounts(); + + private final String user = "neo4j"; + private final String password = "quality"; + + private GenericContainer makeContainer( String image ) + { + GenericContainer container = new GenericContainer( image ); + container = container.withEnv( "NEO4J_AUTH", user + "/" + password ) + .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" ) + .withExposedPorts( 7474 ) + .withExposedPorts( 7687 ) + .withLogConsumer( new Slf4jLogConsumer( log ) ) + .waitingFor( Wait.forHttp( "/" ) + .forPort( 7474 ) + .forStatusCode( 200 ) + .withStartupTimeout( Duration.ofSeconds( 120 ) ) ); + + return container; + } + + private Map createAllMounts( GenericContainer container, Path parentFolder ) throws IOException + { + HashMap hostFolders = new HashMap<>( readonlyMounts.size() + writableMounts.size() ); + for ( String mount : readonlyMounts ) + { + hostFolders.put( mount, createTempFolderAndMountAsVolume( container, parentFolder, mount, "/" + mount ) ); + } + for ( String mount : writableMounts ) + { + hostFolders.put( mount, createTempFolderAndMountAsVolume( container, parentFolder, mount, "/" + mount ) ); + } + return hostFolders; + } + + @Test + void canUpgradeFromBeforeFilePermissionFix35() throws Exception + { + Neo4jVersion beforeFix = new Neo4jVersion( 3, 5, 3 ); + String beforeFixImage = (TestSettings.EDITION == TestSettings.Edition.ENTERPRISE) ? "neo4j:3.5.3-enterprise" : "neo4j:3.5.3"; + Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isNewerThan( beforeFix ), "test only applicable to latest 3.5 docker" ); + Assumptions.assumeFalse( TestSettings.NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_400 ), + "test only applicable to latest 3.5 docker" ); + + Path dataMount = HostFileSystemOperations.createTempFolder( "data-upgrade-" ); + log.info( "created folder " + dataMount.toString() + " to test upgrade" ); + + try ( GenericContainer container = makeContainer( beforeFixImage ) ) + { + mountHostFolderAsVolume( container, dataMount, "/data" ); + container.start(); + DatabaseIO db = new DatabaseIO( container ); + db.putInitialDataIntoContainer( user, password ); + } + + try ( GenericContainer container = makeContainer( TestSettings.IMAGE_ID ) ) + { + mountHostFolderAsVolume( container, dataMount, "/data" ); + container.start(); + DatabaseIO db = new DatabaseIO( container ); + db.verifyDataInContainer( user, password ); + } + } + + // TODO: parameterize these tests for different configurations (e.g. running as non-root user) + @Test + void canUpgradeFromSameMinorVersion() throws Exception + { + Neo4jVersion version = TestSettings.NEO4J_VERSION; + + // If this is the very first in a new minor series we don't expect there to be a released version available to test upgrade + Assumptions.assumeTrue( version.patch > 0 ); + + TestUpgradeFromImage( releaseImageName( version.major, version.minor ) ); + } + + @Test + void canUpgradeFromPreviousMinorVersion() throws Exception + { + Neo4jVersion version = TestSettings.NEO4J_VERSION; + + // If this is the very first in a new major series (i.e. a x.0.0 release) then this test isn't expected to work + Assumptions.assumeTrue( version.minor > 0 ); + + try + { + TestUpgradeFromImage( releaseImageName( version.major, version.minor - 1 ) ); + } + catch ( ContainerLaunchException launchException ) + { + if ( causedByContainerNotFound( launchException ) ) + { + // There is a period when we create a new minor branch but the previous minor version has not yet been published. + // during this time it should be safe to test upgrades from the n-2 minor version - provided n >= 2. + Assumptions.assumeTrue( version.minor >= 2 ); + TestUpgradeFromImage( releaseImageName( version.major, version.minor - 2 ) ); + } + else + { + throw launchException; + } + } + } + + private void TestUpgradeFromImage( String fromImageName ) throws IOException, InterruptedException + { + String testMountDirPrefix = String.format( "upgrade-from-%s", fromImageName ).replaceAll( ":", "_" ); + Path testMountDir = HostFileSystemOperations.createTempFolder( testMountDirPrefix ); + Map allMounts; + + // Start and write data using the released image + log.info( "Testing upgrade from {}", fromImageName ); + try ( GenericContainer container = makeContainer( fromImageName ) ) + { + // Make sure we use a reasonably up to date released version + container.withImagePullPolicy( PullPolicy.ageBased( Duration.ofDays( 1 ) ) ); + + // given + allMounts = createAllMounts( container, testMountDir ); + writeNeoConf( allMounts.get( "conf" ), "dbms.memory.pagecache.size=9m" ); + // sleep a tiny bit otherwise some of the files-last-modified checks fail + Thread.sleep( 1 ); + + // when + final long startTime = Instant.now().toEpochMilli(); + container.start(); + DatabaseIO db = new DatabaseIO( container ); + db.putInitialDataIntoContainer( user, password ); + + // then + validateDb( db ); + + // TODO: test plugin and ssl features work after upgrade + + container.stop(); + + // check mounts after container stop to be sure that shutdown doesn't mess with them + validateMounts( allMounts, startTime ); + } + + // Now try with the current image + try ( GenericContainer container = makeContainer( TestSettings.IMAGE_ID ) ) + { + + allMounts.forEach( ( mount, hostFolder ) -> mountHostFolderAsVolume( container, hostFolder, "/" + mount ) ); + + final long startTime = Instant.now().toEpochMilli(); + container.start(); + DatabaseIO db = new DatabaseIO( container ); + + validateDb( db ); + + container.stop(); + + // check mounts after container stop to be sure that shutdown doesn't mess with them + validateMounts( allMounts, startTime ); + } + } + + private void validateMounts( Map allMounts, long startTime ) + { + // check that writes updated the mounted directories + writableMounts.forEach( mount -> assertDirectoryModifiedSince( allMounts.get( mount ), startTime ) ); + // check that we didn't write anything to read only directories + readonlyMounts.forEach( mount -> assertDirectoryNotModifiedSince( allMounts.get( mount ), startTime ) ); + } + + private void writeNeoConf( Path confMount, String... confValues ) throws IOException + { + try ( Writer confFile = new FileWriter( confMount.resolve( "neo4j.conf" ).toFile() ) ) + { + for ( String confString : confValues ) + { + confFile.write( confString ); + } + } + } + + private void validateDb( DatabaseIO db ) + { + // check that test data is present + db.verifyDataInContainer( user, password ); + + // check the config + validateConfig( db ); + + // check that writes work + db.createAndDeleteNode( user, password ); + } + + private void validateConfig( DatabaseIO db ) + { + String cypher = "CALL dbms.listConfig() YIELD name, value WHERE name='dbms.memory.pagecache.size' RETURN value"; + List configValue = db.runCypherProcedure( user, password, cypher ); + Assertions.assertEquals( "9m", configValue.get( 0 ).get( "value" ).asString() ); + } + + /** + * Checks that the {@code mountDirectory} contains at lease one file that has been modified after the {@code startTimestamp} + * + * @param mountDirectory path to local directory mounted into the docker container. + * @param startTimestamp timestamp (milliseconds since epoch) to check for modifications since. + */ + private static void assertDirectoryModifiedSince( Path mountDirectory, long startTimestamp ) + { + log.info( "Checking {} for files modified since {}", mountDirectory, startTimestamp ); + File lastModifiedFile = getLastModifiedFile( mountDirectory ); + Assertions.assertTrue( lastModifiedFile.lastModified() > startTimestamp ); + } + + /** + * Checks that the {@code mountDirectory} contains at lease one file that has been modified after the {@code startTimestamp} + * + * @param mountDirectory path to local directory mounted into the docker container. + * @param startTimestamp timestamp (milliseconds since epoch) to check for modifications since. + */ + private static void assertDirectoryNotModifiedSince( Path mountDirectory, long startTimestamp ) + { + log.info( "Checking {} does not contain files modified since {}", mountDirectory, startTimestamp ); + File lastModifiedFile = getLastModifiedFile( mountDirectory ); + Assertions.assertTrue( lastModifiedFile.lastModified() < startTimestamp ); + } + + @NotNull + private static File getLastModifiedFile( Path mountDirectory ) + { + File dir = mountDirectory.toFile(); + Assertions.assertTrue( dir.isDirectory() ); + + try + { + Optional lastModified = Files.walk( dir.toPath() ) + .filter( Files::isRegularFile ) + .map( Path::toFile ) + .max( Comparators.byLongFunction( File::lastModified ) ); + Assertions.assertTrue( lastModified.isPresent() ); + return lastModified.get(); + } + catch ( IOException e ) + { + // convert to RuntimeException so we can use this in lambdas + throw new RuntimeException( e ); + } + } + + private static boolean causedByContainerNotFound( ContainerLaunchException launchException ) + { + Throwable cause = launchException.getCause(); + return cause != null && + cause instanceof ContainerFetchException && + cause.getCause() instanceof com.github.dockerjava.api.exception.NotFoundException; + } + + private static String releaseImageName( int major, int minor ) + { + return String.format( "neo4j:%d.%d%s", major, minor, + (TestSettings.EDITION == TestSettings.Edition.ENTERPRISE) ? "-enterprise" : "" ); + } + + private static List getWriteableMounts() + { + switch ( TestSettings.EDITION ) + { + case COMMUNITY: + return Arrays.asList( "data", "logs" ); + case ENTERPRISE: + // /metrics doesn't get chowned in 3.x so doesn't always work + return TestSettings.NEO4J_VERSION.major < 4 ? Arrays.asList( "data", "logs" ) : Arrays.asList( "data", "logs", "metrics" ); + default: + Assertions.fail( "Unknown Edition: " + TestSettings.EDITION ); + return Collections.emptyList(); + } + } +} diff --git a/src/test/java/com/neo4j/docker/plugins/ExampleNeo4jPlugin.java b/src/test/java/com/neo4j/docker/plugins/ExampleNeo4jPlugin.java new file mode 100644 index 00000000..73ceb53d --- /dev/null +++ b/src/test/java/com/neo4j/docker/plugins/ExampleNeo4jPlugin.java @@ -0,0 +1,47 @@ +package com.neo4j.docker.plugins; + +import java.util.stream.Stream; + +import org.neo4j.graphdb.GraphDatabaseService; +import org.neo4j.logging.Log; +import org.neo4j.procedure.Context; +import org.neo4j.procedure.Name; +import org.neo4j.procedure.Procedure; + +/* +This class is a basic Neo4J plugin that defines a procedure which can be called via Cypher. + */ +public class ExampleNeo4jPlugin +{ + // Output data class containing primitive types + public static class PrimitiveOutput + { + public String string; + public long integer; + public double aFloat; + public boolean aBoolean; + + public PrimitiveOutput( String string, long integer, double aFloat, boolean aBoolean ) + { + this.string = string; + this.integer = integer; + this.aFloat = aFloat; + this.aBoolean = aBoolean; + } + } + + @Context + public GraphDatabaseService db; + + @Context + public Log log; + + // A Neo4j procedure that always returns fixed values + @Procedure + public Stream defaultValues( @Name( value = "string", defaultValue = "a string" ) String string, + @Name( value = "integer", defaultValue = "42" ) long integer, @Name( value = "float", defaultValue = "3.14" ) double aFloat, + @Name( value = "boolean", defaultValue = "true" ) boolean aBoolean ) + { + return Stream.of( new PrimitiveOutput( string, integer, aFloat, aBoolean ) ); + } +} \ No newline at end of file diff --git a/src/test/java/com/neo4j/docker/plugins/JarBuilder.java b/src/test/java/com/neo4j/docker/plugins/JarBuilder.java new file mode 100644 index 00000000..86d9938c --- /dev/null +++ b/src/test/java/com/neo4j/docker/plugins/JarBuilder.java @@ -0,0 +1,45 @@ +package com.neo4j.docker.plugins; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.jar.JarOutputStream; +import java.util.zip.ZipEntry; + +/** + * Utility to create jar files containing classes from the current classpath. + */ +public class JarBuilder +{ + public URL createJarFor( File f, Class... classesToInclude ) throws IOException + { + try ( FileOutputStream fout = new FileOutputStream( f ); JarOutputStream jarOut = new JarOutputStream( fout ) ) + { + for ( Class target : classesToInclude ) + { + String fileName = target.getName().replace( ".", "/" ) + ".class"; + jarOut.putNextEntry( new ZipEntry( fileName ) ); + jarOut.write( classCompiledBytes( fileName ) ); + jarOut.closeEntry(); + } + } + return f.toURI().toURL(); + } + + private byte[] classCompiledBytes( String fileName ) throws IOException + { + try ( InputStream in = getClass().getClassLoader().getResourceAsStream( fileName ) ) + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + while ( in.available() > 0 ) + { + out.write( in.read() ); + } + + return out.toByteArray(); + } + } +} diff --git a/src/test/java/com/neo4j/docker/utils/DatabaseIO.java b/src/test/java/com/neo4j/docker/utils/DatabaseIO.java new file mode 100644 index 00000000..49ac58e3 --- /dev/null +++ b/src/test/java/com/neo4j/docker/utils/DatabaseIO.java @@ -0,0 +1,113 @@ +package com.neo4j.docker.utils; + +import org.junit.jupiter.api.Assertions; +import org.testcontainers.containers.GenericContainer; + +import java.util.List; + +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.Result; +import org.neo4j.driver.Session; +import org.neo4j.driver.SessionConfig; + +public class DatabaseIO +{ + private static Config TEST_DRIVER_CONFIG = Config.builder().withoutEncryption().build(); + + private GenericContainer container; + private String boltUri; + + public DatabaseIO( GenericContainer container ) + { + this.container = container; + this.boltUri = getBoltURIFromContainer( container ); + } + + public static String getBoltURIFromContainer( GenericContainer container ) + { + return "bolt://" + container.getContainerIpAddress() + ":" + container.getMappedPort( 7687 ); + } + + public void createAndDeleteNode( String user, String password ) + { + + try ( + Driver driver = GraphDatabase.driver( boltUri, AuthTokens.basic( user, password ), TEST_DRIVER_CONFIG ); + Session session = driver.session() + ) + { + Result rs = session.run( "CREATE (node:createAndDeleteNode {val:1}) RETURN node.val" ); + Assertions.assertEquals( 1, rs.single().get( 0 ).asInt(), "did not receive expected result from cypher CREATE query" ); + rs = session.run( "MATCH (node:createAndDeleteNode {val:1}) WITH node LIMIT 2 DETACH DELETE node" ); + Assertions.assertEquals( 1, rs.consume().counters().nodesDeleted(), "did not receive expected result from cypher DELETE query" ); + } + } + + public void putInitialDataIntoContainer( String user, String password ) + { + try ( + Driver driver = GraphDatabase.driver( boltUri, AuthTokens.basic( user, password ), TEST_DRIVER_CONFIG ); + 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" ); + } + } + + public void verifyDataInContainer( String user, String password ) + { + try ( + Driver driver = GraphDatabase.driver( boltUri, AuthTokens.basic( user, password ), TEST_DRIVER_CONFIG ); + 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" ); + } + } + + public void changePassword( String user, String oldPassword, String newPassword ) + { + if ( TestSettings.NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_400 ) ) + { + try ( + Driver driver = GraphDatabase.driver( boltUri, AuthTokens.basic( user, oldPassword ), TEST_DRIVER_CONFIG ); + Session session = driver.session( SessionConfig.forDatabase( "system" ) ) + ) + { + Result rs = session.run( "ALTER CURRENT USER SET PASSWORD FROM '" + oldPassword + "' TO '" + newPassword + "'" ); + } + } + else + { + runCypherProcedure( user, oldPassword, "CALL dbms.changePassword('" + newPassword + "')" ); + } + } + + public List runCypherProcedure( String user, String password, String cypher ) + { + try ( + Driver driver = GraphDatabase.driver( boltUri, AuthTokens.basic( user, password ), TEST_DRIVER_CONFIG ); + Session session = driver.session() + ) + { + return session.run( cypher ).list(); + } + } + + public void verifyConnectivity( String user, String password ) + { + try ( Driver driver = GraphDatabase.driver( + getBoltURIFromContainer( container ), + AuthTokens.basic( user, password ), + TEST_DRIVER_CONFIG ) ) + { + driver.verifyConnectivity(); + } + } +} 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..6d59638d --- /dev/null +++ b/src/test/java/com/neo4j/docker/utils/HostFileSystemOperations.java @@ -0,0 +1,68 @@ +package com.neo4j.docker.utils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.GenericContainer; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Random; + +public class HostFileSystemOperations +{ + private static Logger log = LoggerFactory.getLogger( HostFileSystemOperations.class); + private static Random rng = new Random( ); + + public static Path createTempFolderAndMountAsVolume( GenericContainer container, String hostFolderNamePrefix, + String containerMountPoint ) throws IOException + { + return createTempFolderAndMountAsVolume( container, TestSettings.TEST_TMP_FOLDER, hostFolderNamePrefix, containerMountPoint ); + } + + public static Path createTempFolderAndMountAsVolume( GenericContainer container, Path parentFolder, + String hostFolderNamePrefix, String containerMountPoint ) 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 + { + String randomStr = String.format( "%04d", rng.nextInt(10000 ) ); // random 4 digit number + Path hostFolder = TestSettings.TEST_TMP_FOLDER.resolve( folderNamePrefix + randomStr); + try + { + Files.createDirectories( hostFolder ); + } + catch ( IOException e ) + { + log.error( "could not create directory: " + hostFolder.toAbsolutePath().toString() ); + e.printStackTrace(); + throw e; + } + + return hostFolder; + } +} diff --git a/src/test/java/com/neo4j/docker/utils/HttpServerRule.java b/src/test/java/com/neo4j/docker/utils/HttpServerRule.java new file mode 100644 index 00000000..146fc850 --- /dev/null +++ b/src/test/java/com/neo4j/docker/utils/HttpServerRule.java @@ -0,0 +1,42 @@ +package com.neo4j.docker.utils; + +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import org.junit.rules.ExternalResource; + +import java.net.InetSocketAddress; + +/** + * Runs a HTTP Server with to allow integration testing + */ +public class HttpServerRule extends ExternalResource +{ + public final int PORT = 3000; + private HttpServer server; + + @Override + protected void before() throws Throwable + { + server = HttpServer.create( new InetSocketAddress( PORT ), 0 ); + server.setExecutor( null ); // creates a default executor + server.start(); + } + + @Override + protected void after() + { + if ( server != null ) + { + server.stop( 0 ); // doesn't wait all current exchange handlers complete + } + } + + // Register a handler to provide desired behaviour on a specific uri path + public void registerHandler( String uriToHandle, HttpHandler httpHandler ) + { + if (!uriToHandle.startsWith( "/" )){ + uriToHandle = '/' + uriToHandle; + } + server.createContext( uriToHandle, httpHandler ); + } +} \ No newline at end of file diff --git a/src/test/java/com/neo4j/docker/utils/Neo4jVersion.java b/src/test/java/com/neo4j/docker/utils/Neo4jVersion.java new file mode 100644 index 00000000..6a2244f7 --- /dev/null +++ b/src/test/java/com/neo4j/docker/utils/Neo4jVersion.java @@ -0,0 +1,96 @@ +package com.neo4j.docker.utils; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +public class Neo4jVersion +{ + //public static final Neo4jVersion EXPECTED_NEO4J_VERSION = Neo4jVersion.fromVersionString( System.getenv( "NEO4J_VERSION" ) ); + //public static final Neo4jVersion LATEST_2X_VERSION = new Neo4jVersion(2,3,12); + //public static final Neo4jVersion LATEST_32_VERSION = new Neo4jVersion(3,2,14); + public static final Neo4jVersion NEO4J_VERSION_400 = new Neo4jVersion(4,0,0); + + public final int major; + public final int minor; + public final int patch; + public final String label; + + public static Neo4jVersion fromVersionString(String version) + { + // Could be one of the forms: + // A.B.C, A.B.C-alphaDD, A.B.C-betaDD, A.B.C-rcDD + // (?\d)\.(?\d)\.(?[\d]+)(?