diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c6865ae..9b1d29f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## Next Version +## Version 0.9.2 + +#### Bugfixes +* support read-only container root filesystem by writing generated runtime config files to `/tmp/redis-commander/config` (or `RUNTIME_CONFIG_DIR`) when app config directory is not writable +* validate write access to the container config directory using a real write probe, so startup correctly falls back to a writable runtime config directory on read-only filesystems + +#### Enhancements +* add an optional redis username field to the web ui form for creating a new connection and persist it in the local config + ## Version 0.9.1 #### Bugfixes * fix bug with docker variables used by REPLACE_CONFIG_ENV containing some special characters diff --git a/README.md b/README.md index d18ce38f..0aef9811 100644 --- a/README.md +++ b/README.md @@ -224,6 +224,7 @@ SENTINEL_TLS_KEY_FILE SENTINEL_TLS_SERVER_NAME SENTINELS K8S_SIGTERM +RUNTIME_CONFIG_DIR CLUSTERS IS_CLUSTER CLUSTER_NO_TLS_VALIDATION @@ -235,6 +236,10 @@ The `K8S_SIGTERM` variable (default "0") can be set to "1" to work around kubern to allow pod replacement with zero downtime. More information on how kubernetes handles termination of old pods and the setup of new ones can be found within the thread [https://github.com/kubernetes/contrib/issues/1140#issuecomment-290836405] +When running with a read-only root filesystem, redis-commander uses a writable runtime config directory. +By default this is `/tmp/redis-commander/config` and can be changed with `RUNTIME_CONFIG_DIR`. +Ensure `/tmp` (or the directory set via `RUNTIME_CONFIG_DIR`) is mounted writable (for example as tmpfs/emptyDir). + Hosts can be optionally specified with a comma separated string by setting the `REDIS_HOSTS` environment variable. After running the container, `redis-commander` will be available at [localhost:8081](http://localhost:8081). diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 37bd9ef7..f59d2d3d 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -8,7 +8,8 @@ umask 0027 # see https://github.com/lorenwest/node-config/wiki/Configuration-Files # this file only contains the connections to load, nothing else # to overwrite something else just place additional files beside this one inside config folder (e.g. local.json) -CONFIG_FILE=${HOME}/config/local-${NODE_ENV}.json +DEFAULT_CONFIG_DIR=${HOME}/config +RUNTIME_CONFIG_DIR=${RUNTIME_CONFIG_DIR:-/tmp/redis-commander/config} # set default instance for node config ("docker") but allow overwriting via docker env vars NODE_APP_INSTANCE=${NODE_APP_INSTANCE:-docker} @@ -22,6 +23,64 @@ K8S_SIGTERM=${K8S_SIGTERM:-0} GRACE_PERIOD=6 NODE=$(command -v node) +# copy json config files from source to destination if destination does not already have them +copyConfigFiles() { + source_dir="$1" + target_dir="$2" + + if [ ! -d "${source_dir}" ]; then + return + fi + + for json_conf in "${source_dir}"/*.json; do + [ -e "${json_conf}" ] || continue + target_conf="${target_dir}/$(basename "${json_conf}")" + if [ ! -e "${target_conf}" ]; then + cp "${json_conf}" "${target_conf}" + fi + done +} + + +# resolve a writable config directory for runtime. +# if read-only root filesystem is used this falls back to /tmp. +resolveRuntimeConfigDir() { + # node-config supports a list here; writing needs one deterministic path. + source_dir=${NODE_CONFIG_DIR:-${DEFAULT_CONFIG_DIR}} + source_dir="$(echo "${source_dir}" | cut -d: -f1)" + target_dir="${source_dir}" + + # Use a real write probe rather than [ -w ] because permission bits can + # show writable even when the underlying filesystem is read-only. + _probe="${target_dir}/.write-probe-$$" + if ! touch "${_probe}" 2>/dev/null; then + target_dir="${RUNTIME_CONFIG_DIR}" + else + rm -f "${_probe}" + fi + + if [ ! -d "${target_dir}" ]; then + if ! mkdir -p "${target_dir}"; then + echo "ERROR: Failed to create writable config directory '${target_dir}'." >> /dev/stderr + echo "Hint: Mount '/tmp' as writable tmpfs when using read-only root filesystem." >> /dev/stderr + exit 1 + fi + fi + + if [ "${target_dir}" != "${source_dir}" ]; then + copyConfigFiles "${source_dir}" "${target_dir}" + fi + if [ "${target_dir}" != "${DEFAULT_CONFIG_DIR}" ] && [ "${source_dir}" != "${DEFAULT_CONFIG_DIR}" ]; then + copyConfigFiles "${DEFAULT_CONFIG_DIR}" "${target_dir}" + fi + + export NODE_CONFIG_DIR="${target_dir}" + echo "Using redis-commander config directory '${NODE_CONFIG_DIR}'." +} + +resolveRuntimeConfigDir +CONFIG_FILE=${NODE_CONFIG_DIR}/local-${NODE_ENV}.json + # this function checks all arguments given and outputs them. All parameter pairs where key is ending with "password" # are replaced with string "" instead of real password (e.g. "--redis-password XYZ" => "--redis-password ") safe_print_args() { @@ -351,8 +410,9 @@ if [ -n "$REPLACE_CONFIG_ENV" ]; then echo "Going to replace this env vars inside config files: $env_vars_replace" for env_var in ${env_vars_replace}; do - for json_conf in config/*.json; do - if [ "$json_conf" != "config/custom-environment-variables.json" ]; then + for json_conf in "${NODE_CONFIG_DIR}"/*.json; do + [ -e "$json_conf" ] || continue + if [ "$json_conf" != "${NODE_CONFIG_DIR}/custom-environment-variables.json" ]; then if grep -q "$env_var" "$json_conf"; then jq --arg var_name "$env_var" --arg new_value "$(printenv "$env_var")" -f "$(dirname "$0")/replace-var-filter.jq" "$json_conf" | sponge "$json_conf" fi @@ -363,7 +423,8 @@ fi # all other env vars are evaluated by node-config module ... # syntax check of all config files to help detecting invalid ones early -for i in config/*.json; do +for i in "${NODE_CONFIG_DIR}"/*.json; do + [ -e "${i}" ] || continue if ! jq empty "${i}"; then echo "ERROR: config file ${i} has invalid json syntax" >> /dev/stderr exit 1 @@ -390,4 +451,3 @@ else echo "node ./bin/redis-commander $(safe_print_args "$@")" exec "$NODE" ./bin/redis-commander "$@" fi - diff --git a/docs/connections.md b/docs/connections.md index 3beec49b..d884c28b 100644 --- a/docs/connections.md +++ b/docs/connections.md @@ -464,6 +464,11 @@ The file must be mounted read-write as all changes to the connection configurati servers via web-UI) will be saved inside this file too to persist changed across docker container restarts. +If the container runs with a read-only root filesystem, redis-commander uses a writable runtime +config directory under `/tmp/redis-commander/config` (or path from `RUNTIME_CONFIG_DIR`). +In this mode, connection changes are ephemeral unless that runtime directory is backed by a writable +persistent volume. + If this file needs to be read-only mount it as `local.json` inside the container, e.g.: ```yaml services: diff --git a/k8s/helm-chart/redis-commander/README.md b/k8s/helm-chart/redis-commander/README.md index d0af7573..cd5c7285 100644 --- a/k8s/helm-chart/redis-commander/README.md +++ b/k8s/helm-chart/redis-commander/README.md @@ -55,7 +55,7 @@ helm -n myspace install redis-web-ui ./k8s/helm-chart/redis-commander --set redi | redis.username | string | `""` | Specifies redis username - supported since Redis 6.0 with ACL support. | | replicaCount | int | `1` | Number of replicas to create for deployment, should be 1 | | resources | object | `{}` | We usually recommend not to specify default resources and to leave this as a conscious choice for the user. This also increases chances charts run on environments with little resources, such as Minikube. If you do want to specify resources, uncomment the following lines, adjust them as necessary, and remove the curly braces after 'resources:'. | -| securityContext | object | `{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"readOnlyRootFilesystem":false,"runAsNonRoot":true}` | Configuration of the linux security context for the docker image. This restricts the rights of the running docker image as far as possible. "readOnlyRootFilesystem" must be set to false to auto-generate a config file with multiple redis hosts or sentinel hosts | +| securityContext | object | `{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"readOnlyRootFilesystem":false,"runAsNonRoot":true}` | Configuration of the linux security context for the docker image. This restricts the rights of the running docker image as far as possible. If `readOnlyRootFilesystem` is set to `true`, mount a writable volume/tmpfs at `/tmp` so runtime config files can be written under `/tmp/redis-commander/config`. | | service.annotations | object | `{}` | Add additional annotations for the service spec Example: 'my.custom.annotation: value' | | service.port | int | `80` | External port where service is available | | service.type | string | `"ClusterIP"` | Type of k8s service to export | diff --git a/k8s/helm-chart/redis-commander/values.yaml b/k8s/helm-chart/redis-commander/values.yaml index 3c534108..c2433ea5 100644 --- a/k8s/helm-chart/redis-commander/values.yaml +++ b/k8s/helm-chart/redis-commander/values.yaml @@ -70,8 +70,8 @@ podSecurityContext: {} # -- Configuration of the linux security context for the docker image. This restricts the # rights of the running docker image as far as possible. # -# "readOnlyRootFilesystem" must be set to false to auto-generate a config file with multiple redis hosts or -# sentinel hosts +# If "readOnlyRootFilesystem" is true, mount a writable volume/tmpfs at /tmp so redis-commander can +# create runtime config files (for example from REDIS_HOSTS) under /tmp/redis-commander/config. securityContext: runAsNonRoot: true readOnlyRootFilesystem: false diff --git a/lib/connections.js b/lib/connections.js index 8bf26811..10a062a3 100644 --- a/lib/connections.js +++ b/lib/connections.js @@ -308,8 +308,10 @@ class ConnectionWrapper { // redisConnection.options.port); const extraROCmds = require('config').get('redis.extraAllowedReadOnlyCommands'); redisConnection.options.commandList = { - all: p[0].value.map((item) => (item[0].toLowerCase())), - ro: p[0].value.filter((item) => (item[2].indexOf('readonly') >= 0 || extraROCmds.indexOf(item[0]) >= 0)) + all: p[0].value.filter(item => item && Array.isArray(item) && item.length > 0) + .map((item) => (item[0].toLowerCase())), + ro: p[0].value.filter((item) => item && Array.isArray(item) && item.length > 2 && + (item[2].indexOf('readonly') >= 0 || extraROCmds.indexOf(item[0]) >= 0)) .map((item) => (item[0].toLowerCase())) }; } diff --git a/lib/routes/home.js b/lib/routes/home.js index 75da7bf0..378a080a 100644 --- a/lib/routes/home.js +++ b/lib/routes/home.js @@ -40,6 +40,7 @@ module.exports = function() { let newConnection = { label: body.label, port: body.port, + username: body.username || null, password: body.password, dbIndex: body.dbIndex }; diff --git a/package-lock.json b/package-lock.json index 6fcbde00..5b9e3d64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "redis-commander", - "version": "0.9.1", + "version": "0.9.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "redis-commander", - "version": "0.9.1", + "version": "0.9.2", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index dff9fd9e..929f4b88 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "Stefan Seide Add comma-separated list of nodes with "host-1:port,host-2 ..."
+ +