docker-entrypoint.sh 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. #!/bin/bash -eu
  2. cmd="$1"
  3. function running_as_root
  4. {
  5. test "$(id -u)" = "0"
  6. }
  7. function secure_mode_enabled
  8. {
  9. test "${SECURE_FILE_PERMISSIONS:=no}" = "yes"
  10. }
  11. function containsElement
  12. {
  13. local e match="$1"
  14. shift
  15. for e; do [[ "$e" == "$match" ]] && return 0; done
  16. return 1
  17. }
  18. function is_readable
  19. {
  20. # this code is fairly ugly but works no matter who this script is running as.
  21. # It would be nice if the writability tests could use this logic somehow.
  22. local _file=${1}
  23. perm=$(stat -c %a "${_file}")
  24. # everyone permission
  25. if [[ ${perm:2:1} -ge 4 ]]; then
  26. return 0
  27. fi
  28. # owner permissions
  29. if [[ ${perm:0:1} -ge 4 ]]; then
  30. if [[ "$(stat -c %U ${_file})" = "${userid}" ]] || [[ "$(stat -c %u ${_file})" = "${userid}" ]]; then
  31. return 0
  32. fi
  33. fi
  34. # group permissions
  35. if [[ ${perm:1:1} -ge 4 ]]; then
  36. if containsElement "$(stat -c %g ${_file})" "${groups[@]}" || containsElement "$(stat -c %G ${_file})" "${groups[@]}" ; then
  37. return 0
  38. fi
  39. fi
  40. return 1
  41. }
  42. function is_writable
  43. {
  44. # It would be nice if this and the is_readable function could combine somehow
  45. local _file=${1}
  46. perm=$(stat -c %a "${_file}")
  47. # everyone permission
  48. if containsElement ${perm:2:1} 2 3 6 7; then
  49. return 0
  50. fi
  51. # owner permissions
  52. if containsElement ${perm:0:1} 2 3 6 7; then
  53. if [[ "$(stat -c %U ${_file})" = "${userid}" ]] || [[ "$(stat -c %u ${_file})" = "${userid}" ]]; then
  54. return 0
  55. fi
  56. fi
  57. # group permissions
  58. if containsElement ${perm:1:1} 2 3 6 7; then
  59. if containsElement "$(stat -c %g ${_file})" "${groups[@]}" || containsElement "$(stat -c %G ${_file})" "${groups[@]}" ; then
  60. return 0
  61. fi
  62. fi
  63. return 1
  64. }
  65. function print_permissions_advice_and_fail
  66. {
  67. _directory=${1}
  68. echo >&2 "
  69. 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.
  70. Hints to solve the issue:
  71. 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.
  72. 2) Pass the folder owner's user ID and group ID to docker run, so that docker runs as that user.
  73. If the folder is owned by the current user, this can be done by adding this flag to your docker run command:
  74. --user=\$(id -u):\$(id -g)
  75. "
  76. exit 1
  77. }
  78. function check_mounted_folder_readable
  79. {
  80. local _directory=${1}
  81. if ! is_readable "${_directory}"; then
  82. print_permissions_advice_and_fail "${_directory}"
  83. fi
  84. }
  85. function check_mounted_folder_with_chown
  86. {
  87. # The /data and /log directory are a bit different because they are very likely to be mounted by the user but not
  88. # necessarily writable.
  89. # This depends on whether a user ID is passed to the container and which folders are mounted.
  90. #
  91. # No user ID passed to container:
  92. # 1) No folders are mounted.
  93. # The /data and /log folder are owned by neo4j by default, so should be writable already.
  94. # 2) Both /log and /data are mounted.
  95. # This means on start up, /data and /logs are owned by an unknown user and we should chown them to neo4j for
  96. # backwards compatibility.
  97. #
  98. # User ID passed to container:
  99. # 1) Both /data and /logs are mounted
  100. # The /data and /logs folders are owned by an unknown user but we *should* have rw permission to them.
  101. # That should be verified and error (helpfully) if not.
  102. # 2) User mounts /data or /logs *but not both*
  103. # The unmounted folder is still owned by neo4j, which should already be writable. The mounted folder should
  104. # have rw permissions through user id. This should be verified.
  105. # 3) No folders are mounted.
  106. # The /data and /log folder are owned by neo4j by default, and these are already writable by the user.
  107. # (This is a very unlikely use case).
  108. local mountFolder=${1}
  109. if running_as_root; then
  110. if ! is_writable "${mountFolder}" && ! secure_mode_enabled; then
  111. # warn that we're about to chown the folder and then chown it
  112. echo "Warning: Folder mounted to \"${mountFolder}\" is not writable from inside container. Changing folder owner to ${userid}."
  113. chown -R "${userid}":"${groupid}" "${mountFolder}"
  114. fi
  115. else
  116. if [[ ! -w "${mountFolder}" ]] && [[ "$(stat -c %U ${mountFolder})" != "neo4j" ]]; then
  117. print_permissions_advice_and_fail "${mountFolder}"
  118. fi
  119. fi
  120. }
  121. function load_plugin_from_github
  122. {
  123. # Load a plugin at runtime. The provided github repository must have a versions.json on the master branch with the
  124. # correct format.
  125. local _plugin_name="${1}" #e.g. apoc, graph-algorithms, graph-ql
  126. local _plugins_dir="${NEO4J_HOME}/plugins"
  127. if [ -d /plugins ]; then
  128. local _plugins_dir="/plugins"
  129. fi
  130. local _versions_json_url="$(jq --raw-output "with_entries( select(.key==\"${_plugin_name}\") ) | to_entries[] | .value.versions" /neo4jlabs-plugins.json )"
  131. # Using the same name for the plugin irrespective of version ensures we don't end up with different versions of the same plugin
  132. local _destination="${_plugins_dir}/${_plugin_name}.jar"
  133. local _neo4j_version="$(neo4j --version | cut -d' ' -f2)"
  134. # 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
  135. echo "Fetching versions.json for Plugin '${_plugin_name}' from ${_versions_json_url}"
  136. local _versions_json="$(wget -q --timeout 300 --tries 30 -O - "${_versions_json_url}")"
  137. local _plugin_jar_url="$(echo "${_versions_json}" | jq --raw-output ".[] | select(.neo4j==\"${_neo4j_version}\") | .jar")"
  138. if [[ -z "${_plugin_jar_url}" ]]; then
  139. echo >&2 "Error: No jar URL found for version '${_neo4j_version}' in versions.json from '${_versions_json_url}'"
  140. echo >&2 "${_versions_json}"
  141. exit 1
  142. fi
  143. echo "Installing Plugin '${_plugin_name}' from ${_plugin_jar_url} to ${_destination} "
  144. wget -q --timeout 300 --tries 30 --output-document="${_destination}" "${_plugin_jar_url}"
  145. if ! is_readable "${_destination}"; then
  146. echo >&2 "Plugin at '${_destination}' is not readable"
  147. exit 1
  148. fi
  149. }
  150. function apply_plugin_default_configuration
  151. {
  152. # Set the correct Load a plugin at runtime. The provided github repository must have a versions.json on the master branch with the
  153. # correct format.
  154. local _plugin_name="${1}" #e.g. apoc, graph-algorithms, graph-ql
  155. local _reference_conf="${2}" # used to determine if we can override properties
  156. local _neo4j_conf="${NEO4J_HOME}/conf/neo4j.conf"
  157. local _property _value
  158. echo "Applying default values for plugin ${_plugin_name} to neo4j.conf"
  159. for _entry in $(jq --compact-output --raw-output "with_entries( select(.key==\"${_plugin_name}\") ) | to_entries[] | .value.properties | to_entries[]" /neo4jlabs-plugins.json); do
  160. _property="$(jq --raw-output '.key' <<< "${_entry}")"
  161. _value="$(jq --raw-output '.value' <<< "${_entry}")"
  162. # the first grep strips out comments
  163. if grep -o "^[^#]*" "${_reference_conf}" | grep -q --fixed-strings "${_property}=" ; then
  164. # property is already set in the user provided config. In this case we don't override what has been set explicitly by the user.
  165. echo "Skipping ${_property} for plugin ${_plugin_name} because it is already set"
  166. else
  167. if grep -o "^[^#]*" "${_neo4j_conf}" | grep -q --fixed-strings "${_property}=" ; then
  168. sed --in-place "s/${_property}=/&${_value},/" "${_neo4j_conf}"
  169. else
  170. echo "${_property}=${_value}" >> "${_neo4j_conf}"
  171. fi
  172. fi
  173. done
  174. }
  175. function install_neo4j_labs_plugins
  176. {
  177. # 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
  178. local _old_config="$(mktemp)"
  179. cp "${NEO4J_HOME}"/conf/neo4j.conf "${_old_config}"
  180. for plugin_name in $(echo "${NEO4JLABS_PLUGINS}" | jq --raw-output '.[]'); do
  181. load_plugin_from_github "${plugin_name}"
  182. apply_plugin_default_configuration "${plugin_name}" "${_old_config}"
  183. done
  184. rm "${_old_config}"
  185. }
  186. # If we're running as root, then run as the neo4j user. Otherwise
  187. # docker is running with --user and we simply use that user. Note
  188. # that su-exec, despite its name, does not replicate the functionality
  189. # of exec, so we need to use both
  190. if running_as_root; then
  191. userid="neo4j"
  192. groupid="neo4j"
  193. groups=($(id -G neo4j))
  194. exec_cmd="exec gosu neo4j:neo4j"
  195. else
  196. userid="$(id -u)"
  197. groupid="$(id -g)"
  198. groups=($(id -G))
  199. exec_cmd="exec"
  200. fi
  201. readonly userid
  202. readonly groupid
  203. readonly groups
  204. readonly exec_cmd
  205. # Need to chown the home directory - but a user might have mounted a
  206. # volume here (notably a conf volume). So take care not to chown
  207. # volumes (stuff not owned by neo4j)
  208. if running_as_root; then
  209. # Non-recursive chown for the base directory
  210. chown "${userid}":"${groupid}" "${NEO4J_HOME}"
  211. chmod 700 "${NEO4J_HOME}"
  212. find "${NEO4J_HOME}" -mindepth 1 -maxdepth 1 -user root -type d -exec chown -R ${userid}:${groupid} {} \;
  213. find "${NEO4J_HOME}" -mindepth 1 -maxdepth 1 -type d -exec chmod -R 700 {} \;
  214. fi
  215. # Only prompt for license agreement if command contains "neo4j" in it
  216. if [[ "${cmd}" == *"neo4j"* ]]; then
  217. if [ "${NEO4J_EDITION}" == "enterprise" ]; then
  218. if [ "${NEO4J_ACCEPT_LICENSE_AGREEMENT:=no}" != "yes" ]; then
  219. echo >&2 "
  220. In order to use Neo4j Enterprise Edition you must accept the license agreement.
  221. (c) Neo4j Sweden AB. 2019. All Rights Reserved.
  222. Use of this Software without a proper commercial license with Neo4j,
  223. Inc. or its affiliates is prohibited.
  224. Email inquiries can be directed to: licensing@neo4j.com
  225. More information is also available at: https://neo4j.com/licensing/
  226. To accept the license agreement set the environment variable
  227. NEO4J_ACCEPT_LICENSE_AGREEMENT=yes
  228. To do this you can use the following docker argument:
  229. --env=NEO4J_ACCEPT_LICENSE_AGREEMENT=yes
  230. "
  231. exit 1
  232. fi
  233. fi
  234. fi
  235. # Env variable naming convention:
  236. # - prefix NEO4J_
  237. # - double underscore char '__' instead of single underscore '_' char in the setting name
  238. # - underscore char '_' instead of dot '.' char in the setting name
  239. # Example:
  240. # NEO4J_dbms_tx__log_rotation_retention__policy env variable to set
  241. # dbms.tx_log.rotation.retention_policy setting
  242. # Backward compatibility - map old hardcoded env variables into new naming convention (if they aren't set already)
  243. # Set some to default values if unset
  244. : ${NEO4J_dbms_tx__log_rotation_retention__policy:=${NEO4J_dbms_txLog_rotation_retentionPolicy:-"100M size"}}
  245. : ${NEO4J_wrapper_java_additional:=${NEO4J_UDC_SOURCE:-"-Dneo4j.ext.udc.source=docker"}}
  246. : ${NEO4J_dbms_unmanaged__extension__classes:=${NEO4J_dbms_unmanagedExtensionClasses:-}}
  247. : ${NEO4J_dbms_allow__format__migration:=${NEO4J_dbms_allowFormatMigration:-}}
  248. : ${NEO4J_dbms_connectors_default__advertised__address:=${NEO4J_dbms_connectors_defaultAdvertisedAddress:-}}
  249. if [ "${NEO4J_EDITION}" == "enterprise" ];
  250. then
  251. : ${NEO4J_causal__clustering_expected__core__cluster__size:=${NEO4J_causalClustering_expectedCoreClusterSize:-}}
  252. : ${NEO4J_causal__clustering_initial__discovery__members:=${NEO4J_causalClustering_initialDiscoveryMembers:-}}
  253. : ${NEO4J_causal__clustering_discovery__advertised__address:=${NEO4J_causalClustering_discoveryAdvertisedAddress:-"$(hostname):5000"}}
  254. : ${NEO4J_causal__clustering_transaction__advertised__address:=${NEO4J_causalClustering_transactionAdvertisedAddress:-"$(hostname):6000"}}
  255. : ${NEO4J_causal__clustering_raft__advertised__address:=${NEO4J_causalClustering_raftAdvertisedAddress:-"$(hostname):7000"}}
  256. # Custom settings for dockerized neo4j
  257. : ${NEO4J_causal__clustering_discovery__advertised__address:=$(hostname):5000}
  258. : ${NEO4J_causal__clustering_transaction__advertised__address:=$(hostname):6000}
  259. : ${NEO4J_causal__clustering_raft__advertised__address:=$(hostname):7000}
  260. fi
  261. # unset old hardcoded unsupported env variables
  262. unset NEO4J_dbms_txLog_rotation_retentionPolicy NEO4J_UDC_SOURCE \
  263. NEO4J_dbms_unmanagedExtensionClasses NEO4J_dbms_allowFormatMigration \
  264. NEO4J_dbms_connectors_defaultAdvertisedAddress NEO4J_ha_serverId \
  265. NEO4J_ha_initialHosts NEO4J_causalClustering_expectedCoreClusterSize \
  266. NEO4J_causalClustering_initialDiscoveryMembers \
  267. NEO4J_causalClustering_discoveryListenAddress \
  268. NEO4J_causalClustering_discoveryAdvertisedAddress \
  269. NEO4J_causalClustering_transactionListenAddress \
  270. NEO4J_causalClustering_transactionAdvertisedAddress \
  271. NEO4J_causalClustering_raftListenAddress \
  272. NEO4J_causalClustering_raftAdvertisedAddress
  273. if [ -d /conf ]; then
  274. if secure_mode_enabled; then
  275. check_mounted_folder_readable "/conf"
  276. fi
  277. find /conf -type f -exec cp {} "${NEO4J_HOME}"/conf \;
  278. fi
  279. if [ -d /ssl ]; then
  280. if secure_mode_enabled; then
  281. check_mounted_folder_readable "/ssl"
  282. fi
  283. : ${NEO4J_dbms_directories_certificates:="/ssl"}
  284. fi
  285. if [ -d /plugins ]; then
  286. if secure_mode_enabled; then
  287. if [[ ! -z "${NEO4JLABS_PLUGINS:-}" ]]; then
  288. # We need write permissions
  289. check_mounted_folder_with_chown "/plugins"
  290. fi
  291. check_mounted_folder_readable "/plugins"
  292. fi
  293. : ${NEO4J_dbms_directories_plugins:="/plugins"}
  294. fi
  295. if [ -d /import ]; then
  296. if secure_mode_enabled; then
  297. check_mounted_folder_readable "/import"
  298. fi
  299. : ${NEO4J_dbms_directories_import:="/import"}
  300. fi
  301. if [ -d /metrics ]; then
  302. if secure_mode_enabled; then
  303. check_mounted_folder_readable "/metrics"
  304. fi
  305. : ${NEO4J_dbms_directories_metrics:="/metrics"}
  306. fi
  307. if [ -d /logs ]; then
  308. check_mounted_folder_with_chown "/logs"
  309. : ${NEO4J_dbms_directories_logs:="/logs"}
  310. fi
  311. if [ -d /data ]; then
  312. check_mounted_folder_with_chown "/data"
  313. if [ -d /data/databases ]; then
  314. check_mounted_folder_with_chown "/data/databases"
  315. fi
  316. if [ -d /data/dbms ]; then
  317. check_mounted_folder_with_chown "/data/dbms"
  318. fi
  319. fi
  320. # set the neo4j initial password only if you run the database server
  321. if [ "${cmd}" == "neo4j" ]; then
  322. if [ "${NEO4J_AUTH:-}" == "none" ]; then
  323. NEO4J_dbms_security_auth__enabled=false
  324. elif [[ "${NEO4J_AUTH:-}" == neo4j/* ]]; then
  325. password="${NEO4J_AUTH#neo4j/}"
  326. if [ "${password}" == "neo4j" ]; then
  327. echo >&2 "Invalid value for password. It cannot be 'neo4j', which is the default."
  328. exit 1
  329. fi
  330. if running_as_root; then
  331. # 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
  332. # creating the folder first will avoid that
  333. mkdir -p /data/dbms
  334. chown "${userid}":"${groupid}" /data/dbms
  335. fi
  336. # Will exit with error if users already exist (and print a message explaining that)
  337. # we probably don't want the message though, since it throws an error message on restarting the container.
  338. neo4j-admin set-initial-password "${password}" 2>/dev/null || true
  339. elif [ -n "${NEO4J_AUTH:-}" ]; then
  340. echo >&2 "Invalid value for NEO4J_AUTH: '${NEO4J_AUTH}'"
  341. exit 1
  342. fi
  343. fi
  344. declare -A COMMUNITY
  345. declare -A ENTERPRISE
  346. COMMUNITY=(
  347. [dbms.tx_log.rotation.retention_policy]="100M size"
  348. [dbms.memory.pagecache.size]="512M"
  349. [dbms.connectors.default_listen_address]="0.0.0.0"
  350. [dbms.connector.https.listen_address]="0.0.0.0:7473"
  351. [dbms.connector.http.listen_address]="0.0.0.0:7474"
  352. [dbms.connector.bolt.listen_address]="0.0.0.0:7687"
  353. )
  354. ENTERPRISE=(
  355. [causal_clustering.transaction_listen_address]="0.0.0.0:6000"
  356. [causal_clustering.raft_listen_address]="0.0.0.0:7000"
  357. [causal_clustering.discovery_listen_address]="0.0.0.0:5000"
  358. )
  359. for conf in ${!COMMUNITY[@]} ; do
  360. if ! grep -q "^$conf" "${NEO4J_HOME}"/conf/neo4j.conf
  361. then
  362. echo -e "\n"$conf=${COMMUNITY[$conf]} >> "${NEO4J_HOME}"/conf/neo4j.conf
  363. fi
  364. done
  365. for conf in ${!ENTERPRISE[@]} ; do
  366. if [ "${NEO4J_EDITION}" == "enterprise" ];
  367. then
  368. if ! grep -q "^$conf" "${NEO4J_HOME}"/conf/neo4j.conf
  369. then
  370. echo -e "\n"$conf=${ENTERPRISE[$conf]} >> "${NEO4J_HOME}"/conf/neo4j.conf
  371. fi
  372. fi
  373. done
  374. #The udc.source=tarball should be replaced by udc.source=docker in both dbms.jvm.additional and wrapper.java.additional
  375. #Using sed to replace only this part will allow the custom configs to be added after, separated by a ,.
  376. if grep -q "udc.source=tarball" "${NEO4J_HOME}"/conf/neo4j.conf; then
  377. sed -i -e 's/udc.source=tarball/udc.source=docker/g' "${NEO4J_HOME}"/conf/neo4j.conf
  378. fi
  379. #The udc.source should always be set to docker by default and we have to allow also custom configs to be added after that.
  380. #In this case, this piece of code helps to add the default value and a , to support custom configs after.
  381. if ! grep -q "dbms.jvm.additional=-Dunsupported.dbms.udc.source=docker" "${NEO4J_HOME}"/conf/neo4j.conf; then
  382. sed -i -e 's/dbms.jvm.additional=/dbms.jvm.additional=-Dunsupported.dbms.udc.source=docker,/g' "${NEO4J_HOME}"/conf/neo4j.conf
  383. fi
  384. # list env variables with prefix NEO4J_ and create settings from them
  385. unset NEO4J_AUTH NEO4J_SHA256 NEO4J_TARBALL
  386. for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do
  387. setting=$(echo ${i} | sed 's|^NEO4J_||' | sed 's|_|.|g' | sed 's|\.\.|_|g')
  388. value=$(echo ${!i})
  389. # 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)
  390. if [[ -n ${value} ]]; then
  391. if [[ ! "${setting}" =~ ^[0-9]+.*$ ]]; then
  392. if grep -q -F "${setting}=" "${NEO4J_HOME}"/conf/neo4j.conf; then
  393. # Remove any lines containing the setting already
  394. sed --in-place "/^${setting}=.*/d" "${NEO4J_HOME}"/conf/neo4j.conf
  395. fi
  396. # Then always append setting to file
  397. echo "${setting}=${value}" >> "${NEO4J_HOME}"/conf/neo4j.conf
  398. else
  399. echo >&2 "WARNING: ${setting} not written to conf file because settings that start with a number are not permitted"
  400. fi
  401. fi
  402. done
  403. if [[ ! -z "${NEO4JLABS_PLUGINS:-}" ]]; then
  404. # NEO4JLABS_PLUGINS should be a json array of plugins like '["graph-algorithms", "apoc", "streams", "graphql"]'
  405. install_neo4j_labs_plugins
  406. fi
  407. [ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT}
  408. if [ "${cmd}" == "dump-config" ]; then
  409. if ! is_writable "/conf"; then
  410. print_permissions_advice_and_fail "/conf"
  411. fi
  412. cp --recursive "${NEO4J_HOME}"/conf/* /conf
  413. echo "Config Dumped"
  414. exit 0
  415. fi
  416. # Use su-exec to drop privileges to neo4j user
  417. # Note that su-exec, despite its name, does not replicate the
  418. # functionality of exec, so we need to use both
  419. if [ "${cmd}" == "neo4j" ]; then
  420. ${exec_cmd} neo4j console
  421. else
  422. ${exec_cmd} "$@"
  423. fi