Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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).
Expand Down
70 changes: 65 additions & 5 deletions docker/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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 "<set>" instead of real password (e.g. "--redis-password XYZ" => "--redis-password <set>")
safe_print_args() {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -390,4 +451,3 @@ else
echo "node ./bin/redis-commander $(safe_print_args "$@")"
exec "$NODE" ./bin/redis-commander "$@"
fi

5 changes: 5 additions & 0 deletions docs/connections.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion k8s/helm-chart/redis-commander/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
4 changes: 2 additions & 2 deletions k8s/helm-chart/redis-commander/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions lib/connections.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
};
}
Expand Down
1 change: 1 addition & 0 deletions lib/routes/home.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"Stefan Seide <account-github@seide.st"
],
"name": "redis-commander",
"version": "0.9.1",
"version": "0.9.2",
"description": "Redis web-based management tool written in node.js",
"license": "MIT",
"repository": {
Expand Down
Loading