diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7500c9a..98325ce 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -38,6 +38,9 @@ jobs: - name: helm unittest run: make test-unit + - name: config-seed script against sample files + run: make test-config-seed + config-keys: runs-on: ubuntu-24.04 steps: diff --git a/Makefile b/Makefile index 5e0d3b5..4105e0a 100644 --- a/Makefile +++ b/Makefile @@ -2,12 +2,13 @@ HELM ?= helm CHART_DIR := charts/arcadedb HELM_UNITTEST_VERSION := 0.5.2 -.PHONY: help lint test-unit test-config-keys test-integration test plugin-install +.PHONY: help lint test-unit test-config-seed test-config-keys test-integration test plugin-install help: @echo "Targets:" @echo " make lint Run helm lint" @echo " make test-unit Run helm-unittest suites" + @echo " make test-config-seed Run the config-seed init container script against sample files" @echo " make test-config-keys Check rendered -Darcadedb.* keys against the server image (needs docker)" @echo " make test-integration Run kind-based integration tests" @echo " make test All of the above" @@ -27,10 +28,13 @@ plugin-install: test-unit: plugin-install $(HELM) unittest $(CHART_DIR) +test-config-seed: + bash ci/test-config-seed.sh + test-config-keys: bash ci/check-config-keys.sh test-integration: bash ci/integration-test.sh -test: lint test-unit test-config-keys test-integration +test: lint test-unit test-config-seed test-config-keys test-integration diff --git a/charts/arcadedb/README.md b/charts/arcadedb/README.md index 380f533..f34688b 100644 --- a/charts/arcadedb/README.md +++ b/charts/arcadedb/README.md @@ -100,7 +100,8 @@ Enable plugins by adding a plugin entry under `arcadedb.plugins`. | `arcadedb.plugins.bolt.enabled` | Enable Bolt protocol support | not set | | `arcadedb.plugins.bolt.port` | Bolt protocol port | `7687` when enabled | | `arcadedb.plugins.gremlin.enabled` | Enable Gremlin protocol support | not set | -| `arcadedb.plugins.gremlin.port` | Gremlin protocol port; only `8182` is accepted (see below) | `8182` when enabled | +| `arcadedb.plugins.gremlin.port` | Gremlin protocol port; only `8182` unless `writePortToConfig` or `portManagedExternally` is set (see below) | `8182` when enabled | +| `arcadedb.plugins.gremlin.writePortToConfig` | Have `config-seed` write `port` into `gremlin-server.yaml` on every start (see below) | `false` | | `arcadedb.plugins.gremlin.portManagedExternally` | Accept a `port` other than `8182` because your own `gremlin-server.yaml` sets it (see below) | `false` | | `arcadedb.plugins.postgres.enabled` | Enable PostgreSQL protocol support | not set | | `arcadedb.plugins.postgres.port` | PostgreSQL protocol port | `5432` when enabled | @@ -115,14 +116,34 @@ Enable plugins by adding a plugin entry under `arcadedb.plugins`. | `arcadedb.plugins..class` | Custom plugin implementation class | required | The Gremlin plugin takes its port from `gremlin-server.yaml` (the image's sets none, so TinkerPop's default -`8182`), not from a `-D` setting, so the chart cannot move it. `arcadedb.plugins.gremlin.port` only sets the port -the Services and the NetworkPolicy expose, and any value other than `8182` fails the render instead of pointing -them at a port nothing listens on. +`8182`), not from a `-D` setting, so the chart cannot pass it on the command line. By default +`arcadedb.plugins.gremlin.port` only sets the port the Services and the NetworkPolicy expose, and any value other +than `8182` fails the render instead of pointing them at a port nothing listens on. -To run Gremlin on another port, set `port:` in the `gremlin-server.yaml` on the config volume (the `config-seed` -init container copies the image's file there only when it is absent, so an edited file is kept), then set -`arcadedb.plugins.gremlin.port` to the same value and `arcadedb.plugins.gremlin.portManagedExternally: true`. -The chart then exposes that port without checking it; keeping the two in step is up to you: +To run Gremlin on another port, set `arcadedb.plugins.gremlin.writePortToConfig: true` with the port you want: + +```yaml +arcadedb: + plugins: + gremlin: + enabled: true + port: 8183 + writePortToConfig: true +``` + +The `config-seed` init container then sets the top-level `port:` key of `gremlin-server.yaml` on the config volume +to that value on every pod start (adding the key when the file has none), and leaves every other line as it is. The +server, the Services and the NetworkPolicy all follow the one value, and a later `helm upgrade` that changes it +restarts the pods with the new port. It is opt-in because it edits a file `config-seed` otherwise never touches once it +exists. It needs `initContainers.configSeed.enabled` and a volume holding `arcadedb.configDirectory` (both the +default), and it fails the render when `gremlin-server.yaml` is mounted on its own from another volume (for example +a ConfigMap `subPath`), since the server would read that file instead. Turning it off later leaves the last `port:` +written in the file: set `port` back to `8182` there, or keep the value with `portManagedExternally` below. + +If you manage `gremlin-server.yaml` yourself instead, set `port:` in it (the `config-seed` init container copies the +image's file there only when it is absent, so an edited file is kept), then set `arcadedb.plugins.gremlin.port` to +the same value and `arcadedb.plugins.gremlin.portManagedExternally: true`. The chart then exposes that port without +checking it; keeping the two in step is up to you: ```yaml arcadedb: @@ -569,7 +590,8 @@ The chart adds a `config-seed` init container that runs the server image itself tag, so the defaults match the server version), mounts the volume holding the config directory at `/config-seed` so the image's own `config/` stays visible, and copies each of those files that the config directory does not hold yet. A file that already exists is never overwritten, so changes made at runtime or by -hand survive restarts. The logging files are not seeded: the chart ships +hand survive restarts. The one exception is opt-in: with `arcadedb.plugins.gremlin.writePortToConfig` it sets the +top-level `port:` of `gremlin-server.yaml` on every start (see [arcadedb.plugins](#arcadedbplugins)). The logging files are not seeded: the chart ships `arcadedb-log.properties` from a ConfigMap (see `arcadedb.logging`). The container uses `securityContext`, so it runs as the ArcadeDB user, and it runs after `config-chown` when that one is enabled. diff --git a/charts/arcadedb/files/config-seed.sh b/charts/arcadedb/files/config-seed.sh new file mode 100644 index 0000000..08db97a --- /dev/null +++ b/charts/arcadedb/files/config-seed.sh @@ -0,0 +1,39 @@ +set -e +src="$1"; dst="$2"; shift 2 +mkdir -p "$dst" +for f in "$@"; do + if [ -e "$dst/$f" ]; then + echo "config-seed: keeping existing $f" + elif [ -f "$src/$f" ]; then + cat "$src/$f" > "$dst/.$f.seed" + mv "$dst/.$f.seed" "$dst/$f" + echo "config-seed: seeded $f" + else + echo "config-seed: $f is not in the image, skipped" + fi +done +# GREMLIN_PORT (arcadedb.plugins.gremlin.writePortToConfig, #58): the Gremlin +# plugin reads its port only from gremlin-server.yaml, so set the top-level +# port: key there on every start. Every other line is kept as it is. +if [ -n "${GREMLIN_PORT:-}" ]; then + yaml="$dst/gremlin-server.yaml" + tmp="$dst/.gremlin-server.yaml.port" + found="" + : > "$tmp" + if [ -f "$yaml" ]; then + while IFS= read -r line || [ -n "$line" ]; do + case "$line" in + port:*|"port :"*|\"port\":*|\'port\':*) + [ -n "$found" ] || printf 'port: %s\n' "$GREMLIN_PORT" >> "$tmp" + found=1 + ;; + *) + printf '%s\n' "$line" >> "$tmp" + ;; + esac + done < "$yaml" + fi + [ -n "$found" ] || printf 'port: %s\n' "$GREMLIN_PORT" >> "$tmp" + mv "$tmp" "$yaml" + echo "config-seed: set port: $GREMLIN_PORT in gremlin-server.yaml" +fi diff --git a/charts/arcadedb/templates/_helpers.tpl b/charts/arcadedb/templates/_helpers.tpl index 81965e0..24ba3a8 100644 --- a/charts/arcadedb/templates/_helpers.tpl +++ b/charts/arcadedb/templates/_helpers.tpl @@ -170,13 +170,22 @@ parameters, service and networkPolicy helpers build on this output. gremlin-server.yaml on the config volume sets that port (#52): the value is then only what the Services and the NetworkPolicy expose, and the chart still passes no port to the server. + writePortToConfig: true has config-seed set port: in gremlin-server.yaml + on every start (#58), so the chart moves the port itself; the two + settings are alternatives and may not be combined. */}} - {{- if eq (toString $config.portManagedExternally) "true" }} + {{- $external := eq (toString $config.portManagedExternally) "true" }} + {{- $writePort := eq (toString $config.writePortToConfig) "true" }} + {{- if and $external $writePort }} + {{- fail "arcadedb.plugins.gremlin.portManagedExternally and arcadedb.plugins.gremlin.writePortToConfig are both true: either your own gremlin-server.yaml sets the port, or the chart writes it there. Set only one." -}} + {{- end }} + {{- if or $external $writePort }} {{- if or (lt (int $port) 1) (gt (int $port) 65535) }} {{- fail (printf "arcadedb.plugins.gremlin.port is %v, which is not a TCP port (1-65535)." $port) -}} {{- end }} + {{- if $writePort }}{{- $_ := include "arcadedb.gremlin.configPort" $ }}{{- end }} {{- else if ne (int $port) 8182 }} - {{- fail (printf "arcadedb.plugins.gremlin.port is %v, but the Gremlin plugin listens on 8182: it reads its port from gremlin-server.yaml and the server has no setting the chart can pass to change it (#47). Leave arcadedb.plugins.gremlin.port unset or set it to 8182." $port) -}} + {{- fail (printf "arcadedb.plugins.gremlin.port is %v, but the Gremlin plugin listens on 8182: it reads its port from gremlin-server.yaml and the server has no setting the chart can pass to change it (#47). Leave arcadedb.plugins.gremlin.port unset or set it to 8182, or set arcadedb.plugins.gremlin.writePortToConfig: true to have the chart write it into gremlin-server.yaml (#58)." $port) -}} {{- end }} {{- else if eq $plugin "postgres" }} {{- $port = default 5432 $config.port }} @@ -717,6 +726,35 @@ the chart ships arcadedb-log.properties from a ConfigMap (#38). {{- list "server-groups.json" "gremlin-server.yaml" "gremlin-server.properties" "gremlin-server.groovy" | toJson -}} {{- end -}} +{{/* +The Gremlin port config-seed writes into gremlin-server.yaml (#58), or nothing. +Set when arcadedb.plugins.gremlin is enabled with writePortToConfig: true: the +plugin reads its port only from that file, so config-seed sets its top-level +port: key on every start. Fails when config-seed cannot reach the file the +server reads: config-seed disabled, no volumeMounts entry holding +configDirectory, or a volumeMounts entry mounted on gremlin-server.yaml itself +(config-seed would edit the copy under it, which the server never sees). +*/}} +{{- define "arcadedb.gremlin.configPort" -}} +{{- $config := (.Values.arcadedb.plugins | default dict).gremlin | default dict -}} +{{- if and $config.enabled (eq (toString $config.writePortToConfig) "true") -}} +{{- $seed := (.Values.initContainers | default dict).configSeed | default dict -}} +{{- if not (or (not (hasKey $seed "enabled")) $seed.enabled) -}} +{{- fail "arcadedb.plugins.gremlin.writePortToConfig needs the config-seed init container, which writes the port into gremlin-server.yaml: set initContainers.configSeed.enabled=true, or set the port in your own gremlin-server.yaml with arcadedb.plugins.gremlin.portManagedExternally: true." -}} +{{- end -}} +{{- if not (include "arcadedb.configSeed.mount" . | fromJson) -}} +{{- fail (printf "arcadedb.plugins.gremlin.writePortToConfig needs a volumeMounts entry that holds arcadedb.configDirectory (%s): without one, the gremlin-server.yaml config-seed writes would not outlive the init container." .Values.arcadedb.configDirectory) -}} +{{- end -}} +{{- $file := printf "%s/gremlin-server.yaml" (trimSuffix "/" .Values.arcadedb.configDirectory) -}} +{{- range .Values.volumeMounts -}} +{{- if eq (trimSuffix "/" (toString .mountPath)) $file -}} +{{- fail (printf "arcadedb.plugins.gremlin.writePortToConfig cannot write the port: volumeMounts entry '%s' is mounted on %s, so the server reads that file, not the one config-seed edits. Set port: in it and use arcadedb.plugins.gremlin.portManagedExternally: true instead." .name $file) -}} +{{- end -}} +{{- end -}} +{{- int (default 8182 $config.port) -}} +{{- end -}} +{{- end -}} + {{/* The volumeMounts entry that holds arcadedb.configDirectory: the deepest mountPath that is the directory or one of its parents. Returns JSON {} when none does. @@ -762,7 +800,10 @@ The config-seed init container. It runs the image named by arcadedb.configSeed.image with the volume that holds configDirectory mounted at /config-seed instead of over the image's config/, and copies each file the directory does not hold yet. Existing files, including ones the server or an -operator changed, are never overwritten. +operator changed, are never overwritten; the one exception is the top-level +port: of gremlin-server.yaml, set on every start when arcadedb.gremlin.configPort +returns a port (#58). The script is files/config-seed.sh (ci/test-config-seed.sh +runs it against sample files). Skipped when disabled, and when no volumeMounts entry holds configDirectory (nothing would outlive the init container). A values file without the key (helm upgrade --reuse-values) keeps it on. @@ -786,20 +827,12 @@ A values file without the key (helm upgrade --reuse-values) keeps it on. - sh - -c - | - set -e - src="$1"; dst="$2"; shift 2 - mkdir -p "$dst" - for f in "$@"; do - if [ -e "$dst/$f" ]; then - echo "config-seed: keeping existing $f" - elif [ -f "$src/$f" ]; then - cat "$src/$f" > "$dst/.$f.seed" - mv "$dst/.$f.seed" "$dst/$f" - echo "config-seed: seeded $f" - else - echo "config-seed: $f is not in the image, skipped" - fi - done +{{ .Files.Get "files/config-seed.sh" | trim | indent 6 }} + {{- with include "arcadedb.gremlin.configPort" . }} + env: + - name: GREMLIN_PORT + value: {{ . | quote }} + {{- end }} args: - config-seed - {{ $source | quote }} diff --git a/charts/arcadedb/tests/gremlin_port_external_test.yaml b/charts/arcadedb/tests/gremlin_port_external_test.yaml index ad821fe..a5af2a1 100644 --- a/charts/arcadedb/tests/gremlin_port_external_test.yaml +++ b/charts/arcadedb/tests/gremlin_port_external_test.yaml @@ -103,7 +103,7 @@ tests: arcadedb.plugins.gremlin.portManagedExternally: false asserts: - failedTemplate: - errorMessage: "arcadedb.plugins.gremlin.port is 9999, but the Gremlin plugin listens on 8182: it reads its port from gremlin-server.yaml and the server has no setting the chart can pass to change it (#47). Leave arcadedb.plugins.gremlin.port unset or set it to 8182." + errorMessage: "arcadedb.plugins.gremlin.port is 9999, but the Gremlin plugin listens on 8182: it reads its port from gremlin-server.yaml and the server has no setting the chart can pass to change it (#47). Leave arcadedb.plugins.gremlin.port unset or set it to 8182, or set arcadedb.plugins.gremlin.writePortToConfig: true to have the chart write it into gremlin-server.yaml (#58)." - it: refuses a port that is not a valid TCP port even when managed externally template: statefulset.yaml diff --git a/charts/arcadedb/tests/gremlin_port_test.yaml b/charts/arcadedb/tests/gremlin_port_test.yaml index 14866ae..f5d3895 100644 --- a/charts/arcadedb/tests/gremlin_port_test.yaml +++ b/charts/arcadedb/tests/gremlin_port_test.yaml @@ -68,7 +68,7 @@ tests: arcadedb.plugins.gremlin.port: 9999 asserts: - failedTemplate: - errorMessage: "arcadedb.plugins.gremlin.port is 9999, but the Gremlin plugin listens on 8182: it reads its port from gremlin-server.yaml and the server has no setting the chart can pass to change it (#47). Leave arcadedb.plugins.gremlin.port unset or set it to 8182." + errorMessage: "arcadedb.plugins.gremlin.port is 9999, but the Gremlin plugin listens on 8182: it reads its port from gremlin-server.yaml and the server has no setting the chart can pass to change it (#47). Leave arcadedb.plugins.gremlin.port unset or set it to 8182, or set arcadedb.plugins.gremlin.writePortToConfig: true to have the chart write it into gremlin-server.yaml (#58)." - it: refuses a port other than 8182 with the NetworkPolicy and external Service enabled template: statefulset.yaml @@ -79,7 +79,7 @@ tests: arcadedb.plugins.gremlin.port: 9999 asserts: - failedTemplate: - errorMessage: "arcadedb.plugins.gremlin.port is 9999, but the Gremlin plugin listens on 8182: it reads its port from gremlin-server.yaml and the server has no setting the chart can pass to change it (#47). Leave arcadedb.plugins.gremlin.port unset or set it to 8182." + errorMessage: "arcadedb.plugins.gremlin.port is 9999, but the Gremlin plugin listens on 8182: it reads its port from gremlin-server.yaml and the server has no setting the chart can pass to change it (#47). Leave arcadedb.plugins.gremlin.port unset or set it to 8182, or set arcadedb.plugins.gremlin.writePortToConfig: true to have the chart write it into gremlin-server.yaml (#58)." - it: a port given as a string is compared by value template: statefulset.yaml @@ -88,7 +88,7 @@ tests: arcadedb.plugins.gremlin.port: "7777" asserts: - failedTemplate: - errorMessage: "arcadedb.plugins.gremlin.port is 7777, but the Gremlin plugin listens on 8182: it reads its port from gremlin-server.yaml and the server has no setting the chart can pass to change it (#47). Leave arcadedb.plugins.gremlin.port unset or set it to 8182." + errorMessage: "arcadedb.plugins.gremlin.port is 7777, but the Gremlin plugin listens on 8182: it reads its port from gremlin-server.yaml and the server has no setting the chart can pass to change it (#47). Leave arcadedb.plugins.gremlin.port unset or set it to 8182, or set arcadedb.plugins.gremlin.writePortToConfig: true to have the chart write it into gremlin-server.yaml (#58)." - it: a disabled gremlin entry with another port renders template: statefulset.yaml diff --git a/charts/arcadedb/tests/gremlin_port_write_test.yaml b/charts/arcadedb/tests/gremlin_port_write_test.yaml new file mode 100644 index 0000000..1e46f24 --- /dev/null +++ b/charts/arcadedb/tests/gremlin_port_write_test.yaml @@ -0,0 +1,241 @@ +suite: Gremlin port written into gremlin-server.yaml by the chart (#58) +templates: + - statefulset.yaml + - service.yaml + - networkpolicy.yaml +release: + name: test + namespace: default +# The Gremlin plugin reads its port only from gremlin-server.yaml (#47), and the +# server has no setting the chart can pass instead. With +# arcadedb.plugins.gremlin.writePortToConfig: true the config-seed init container +# sets the top-level port: key of gremlin-server.yaml on the config volume to +# arcadedb.plugins.gremlin.port on every start (GREMLIN_PORT), so the server, the +# Services and the NetworkPolicy all follow that one value. +# ci/test-config-seed.sh runs the script itself against sample files. +tests: + - it: hands the port to config-seed when writePortToConfig is set + template: statefulset.yaml + set: + arcadedb.plugins.gremlin.enabled: true + arcadedb.plugins.gremlin.port: 9999 + arcadedb.plugins.gremlin.writePortToConfig: true + asserts: + - notFailedTemplate: {} + - equal: + path: spec.template.spec.initContainers[0].name + value: config-seed + - equal: + path: spec.template.spec.initContainers[0].env + value: + - name: GREMLIN_PORT + value: "9999" + - matchRegex: + path: spec.template.spec.initContainers[0].command[2] + pattern: 'if \[ -n "\$\{GREMLIN_PORT:-\}" \]' + - notContains: + path: spec.template.spec.containers[0].command + content: "-Darcadedb.gremlin.port=9999" + + - it: writes 8182 when the port is left unset + template: statefulset.yaml + set: + arcadedb.plugins.gremlin.enabled: true + arcadedb.plugins.gremlin.writePortToConfig: true + asserts: + - equal: + path: spec.template.spec.initContainers[0].env + value: + - name: GREMLIN_PORT + value: "8182" + + - it: accepts a string port + template: statefulset.yaml + set: + arcadedb.plugins.gremlin.enabled: true + arcadedb.plugins.gremlin.port: "7777" + arcadedb.plugins.gremlin.writePortToConfig: true + asserts: + - equal: + path: spec.template.spec.initContainers[0].env + value: + - name: GREMLIN_PORT + value: "7777" + + - it: the headless Service exposes the written port + template: service.yaml + documentIndex: 1 + set: + arcadedb.plugins.gremlin.enabled: true + arcadedb.plugins.gremlin.port: 9999 + arcadedb.plugins.gremlin.writePortToConfig: true + asserts: + - contains: + path: spec.ports + content: { port: 9999, targetPort: 9999, protocol: TCP, name: gremlin-port } + - notContains: + path: spec.ports + content: { port: 8182, targetPort: 8182, protocol: TCP, name: gremlin-port } + + - it: the external Service exposes the written port + template: service.yaml + documentIndex: 2 + set: + service.external.enabled: true + arcadedb.plugins.gremlin.enabled: true + arcadedb.plugins.gremlin.port: 9999 + arcadedb.plugins.gremlin.writePortToConfig: true + asserts: + - equal: { path: metadata.name, value: test-arcadedb-external } + - contains: + path: spec.ports + content: { port: 9999, targetPort: 9999, protocol: TCP, name: gremlin-port } + + - it: the NetworkPolicy opens the written port + template: networkpolicy.yaml + documentIndex: 0 + set: + networkPolicy.enabled: true + arcadedb.plugins.gremlin.enabled: true + arcadedb.plugins.gremlin.port: 9999 + arcadedb.plugins.gremlin.writePortToConfig: true + asserts: + - contains: + path: spec.ingress[0].ports + content: { port: 9999, protocol: TCP } + - notContains: + path: spec.ingress[0].ports + content: { port: 8182, protocol: TCP } + + - it: passes no GREMLIN_PORT by default, leaving gremlin-server.yaml to the operator + template: statefulset.yaml + set: + arcadedb.plugins.gremlin.enabled: true + asserts: + - notExists: + path: spec.template.spec.initContainers[0].env + + - it: passes no GREMLIN_PORT when the port is managed externally + template: statefulset.yaml + set: + arcadedb.plugins.gremlin.enabled: true + arcadedb.plugins.gremlin.port: 9999 + arcadedb.plugins.gremlin.portManagedExternally: true + asserts: + - notExists: + path: spec.template.spec.initContainers[0].env + + - it: passes no GREMLIN_PORT when the gremlin plugin is disabled + template: statefulset.yaml + set: + arcadedb.plugins.gremlin.enabled: false + arcadedb.plugins.gremlin.port: 9999 + arcadedb.plugins.gremlin.writePortToConfig: true + asserts: + - notFailedTemplate: {} + - notExists: + path: spec.template.spec.initContainers[0].env + + - it: refuses writePortToConfig together with portManagedExternally + template: statefulset.yaml + set: + arcadedb.plugins.gremlin.enabled: true + arcadedb.plugins.gremlin.port: 9999 + arcadedb.plugins.gremlin.writePortToConfig: true + arcadedb.plugins.gremlin.portManagedExternally: true + asserts: + - failedTemplate: + errorMessage: "arcadedb.plugins.gremlin.portManagedExternally and arcadedb.plugins.gremlin.writePortToConfig are both true: either your own gremlin-server.yaml sets the port, or the chart writes it there. Set only one." + + - it: refuses a port that is not a valid TCP port + template: statefulset.yaml + set: + arcadedb.plugins.gremlin.enabled: true + arcadedb.plugins.gremlin.port: 70000 + arcadedb.plugins.gremlin.writePortToConfig: true + asserts: + - failedTemplate: + errorMessage: "arcadedb.plugins.gremlin.port is 70000, which is not a TCP port (1-65535)." + + # Every path where config-seed cannot reach the file the server reads fails + # the render: a port the chart exposes but never writes would point the + # Services at nothing. The check runs in _arcadedb.plugin.ports, so (as for + # #47) the failure surfaces at the StatefulSet, which every release renders. + - it: refuses writePortToConfig when config-seed is disabled + template: statefulset.yaml + set: + initContainers.configSeed.enabled: false + arcadedb.plugins.gremlin.enabled: true + arcadedb.plugins.gremlin.port: 9999 + arcadedb.plugins.gremlin.writePortToConfig: true + asserts: + - failedTemplate: + errorMessage: "arcadedb.plugins.gremlin.writePortToConfig needs the config-seed init container, which writes the port into gremlin-server.yaml: set initContainers.configSeed.enabled=true, or set the port in your own gremlin-server.yaml with arcadedb.plugins.gremlin.portManagedExternally: true." + + - it: keeps working when the configSeed key is missing (helm upgrade --reuse-values) + template: statefulset.yaml + set: + initContainers.configSeed: null + arcadedb.plugins.gremlin.enabled: true + arcadedb.plugins.gremlin.port: 9999 + arcadedb.plugins.gremlin.writePortToConfig: true + asserts: + - equal: + path: spec.template.spec.initContainers[0].env + value: + - name: GREMLIN_PORT + value: "9999" + + # Under the default readOnlyRootFilesystem an unmounted config directory + # already fails the render (arcadedb.configDirectory.validate); with it off, + # the server could write there, but config-seed's edit would be lost. + - it: refuses writePortToConfig when no volume holds the config directory + template: statefulset.yaml + set: + securityContext.readOnlyRootFilesystem: false + volumeMounts: + - name: arcadedb-data + mountPath: /home/arcadedb/databases + arcadedb.plugins.gremlin.enabled: true + arcadedb.plugins.gremlin.port: 9999 + arcadedb.plugins.gremlin.writePortToConfig: true + asserts: + - failedTemplate: + errorMessage: "arcadedb.plugins.gremlin.writePortToConfig needs a volumeMounts entry that holds arcadedb.configDirectory (/home/arcadedb/config): without one, the gremlin-server.yaml config-seed writes would not outlive the init container." + + - it: refuses writePortToConfig when gremlin-server.yaml itself is mounted from another volume + template: statefulset.yaml + set: + volumeMounts: + - name: arcadedb-config + mountPath: /home/arcadedb/config + - name: my-gremlin + mountPath: /home/arcadedb/config/gremlin-server.yaml + subPath: gremlin-server.yaml + volumes: + - name: my-gremlin + configMap: + name: my-gremlin + arcadedb.plugins.gremlin.enabled: true + arcadedb.plugins.gremlin.port: 9999 + arcadedb.plugins.gremlin.writePortToConfig: true + asserts: + - failedTemplate: + errorMessage: "arcadedb.plugins.gremlin.writePortToConfig cannot write the port: volumeMounts entry 'my-gremlin' is mounted on /home/arcadedb/config/gremlin-server.yaml, so the server reads that file, not the one config-seed edits. Set port: in it and use arcadedb.plugins.gremlin.portManagedExternally: true instead." + + - it: writes into a relocated config directory below a parent mount + template: statefulset.yaml + set: + arcadedb.configDirectory: /home/arcadedb/config/custom + arcadedb.plugins.gremlin.enabled: true + arcadedb.plugins.gremlin.port: 9999 + arcadedb.plugins.gremlin.writePortToConfig: true + asserts: + - contains: + path: spec.template.spec.initContainers[0].args + content: /config-seed/custom + - equal: + path: spec.template.spec.initContainers[0].env + value: + - name: GREMLIN_PORT + value: "9999" diff --git a/charts/arcadedb/values.yaml b/charts/arcadedb/values.yaml index 7715343..039e662 100644 --- a/charts/arcadedb/values.yaml +++ b/charts/arcadedb/values.yaml @@ -145,12 +145,18 @@ arcadedb: # port: 7687 # gremlin: # enabled: true - # # Must be 8182 (any other value fails the render): the Gremlin plugin reads - # # its port from gremlin-server.yaml, not from a -D setting the chart can pass (#47). + # # Must be 8182 (any other value fails the render) unless one of the two + # # settings below is true: the Gremlin plugin reads its port from + # # gremlin-server.yaml, not from a -D setting the chart can pass (#47). # port: 8182 + # # Set to true to have the chart move the port: the config-seed init container + # # sets the top-level `port:` of gremlin-server.yaml on the config volume to + # # `port` above on every start, keeping every other line (#58). It edits a file + # # you may have changed, so it is off by default; it needs config-seed enabled. + # writePortToConfig: false # # Set to true when your own gremlin-server.yaml on the config volume sets `port:`; # # `port` above must then match it, and is only what the Services and the - # # NetworkPolicy expose (#52). The chart still does not change the server's port. + # # NetworkPolicy expose (#52). Cannot be combined with writePortToConfig. # portManagedExternally: false # postgres: # enabled: true diff --git a/ci/test-config-seed.sh b/ci/test-config-seed.sh new file mode 100755 index 0000000..efa1d55 --- /dev/null +++ b/ci/test-config-seed.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash +# Runs the config-seed init container script (charts/arcadedb/files/config-seed.sh) +# against sample config directories. The helm-unittest suites only see the +# rendered text; this checks what the script does to the files: copy-if-absent +# seeding (#39) and the gremlin-server.yaml port rewrite (GREMLIN_PORT, #58). +# +# Runs the script with every POSIX shell found among sh, dash and busybox sh. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +SCRIPT="$ROOT/charts/arcadedb/files/config-seed.sh" +FILES=(server-groups.json gremlin-server.yaml gremlin-server.properties gremlin-server.groovy) + +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +SHELLS=() +for s in sh dash; do + command -v "$s" >/dev/null 2>&1 && SHELLS+=("$s") +done +command -v busybox >/dev/null 2>&1 && SHELLS+=("busybox sh") + +FAILURES=0 +fail() { echo "FAIL [$SH] $*"; FAILURES=$((FAILURES + 1)); } + +IMAGE_YAML='host: 0.0.0.0 +graphs: + graph: config/gremlin-server.properties +scriptEngines: + gremlin-lang: {} +serializers: + - className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1 + config: + port: 1234' + +# new_case : fresh image dir (src) holding the image's files, empty volume (dst). +new_case() { + CASE="$WORK/$SH_ID/$1" + SRC="$CASE/src"; DST="$CASE/dst" + mkdir -p "$SRC" "$DST" + printf '%s\n' "$IMAGE_YAML" > "$SRC/gremlin-server.yaml" + printf '{}\n' > "$SRC/server-groups.json" + printf 'gremlin.graph=x\n' > "$SRC/gremlin-server.properties" + printf '// groovy\n' > "$SRC/gremlin-server.groovy" +} + +# seed [GREMLIN_PORT]: run the script the way the init container does. +seed() { + if [ $# -gt 0 ]; then + GREMLIN_PORT="$1" $SH -c "$(cat "$SCRIPT")" config-seed "$SRC" "$DST" "${FILES[@]}" >"$CASE/out.log" 2>&1 + else + env -u GREMLIN_PORT $SH -c "$(cat "$SCRIPT")" config-seed "$SRC" "$DST" "${FILES[@]}" >"$CASE/out.log" 2>&1 + fi +} + +top_ports() { grep -E '^("|'"'"')?port("|'"'"')? *:' "$DST/gremlin-server.yaml" || true; } + +for SH in "${SHELLS[@]}"; do + SH_ID="${SH// /-}" + + # 1. Default (no GREMLIN_PORT): files seeded byte for byte, no port written. + new_case default + seed || fail "default: script exited non-zero: $(cat "$CASE/out.log")" + for f in "${FILES[@]}"; do + cmp -s "$SRC/$f" "$DST/$f" || fail "default: $f not seeded verbatim" + done + [ -z "$(top_ports)" ] || fail "default: a top-level port was written: $(top_ports)" + + # 2. Fresh volume with GREMLIN_PORT: seeded, then port appended; nested port untouched. + new_case fresh + seed 9999 || fail "fresh: script exited non-zero: $(cat "$CASE/out.log")" + [ "$(top_ports)" = "port: 9999" ] || fail "fresh: top-level port is '$(top_ports)'" + grep -qx ' port: 1234' "$DST/gremlin-server.yaml" || fail "fresh: nested port: 1234 changed" + [ "$(grep -v '^port: 9999$' "$DST/gremlin-server.yaml")" = "$IMAGE_YAML" ] \ + || fail "fresh: lines other than port: changed" + grep -q 'config-seed: set port: 9999 in gremlin-server.yaml' "$CASE/out.log" || fail "fresh: no log line" + ls -A "$DST" | grep -q '^\.' && fail "fresh: temp file left behind: $(ls -A "$DST")" + + # 3. Every start rewrites: a second run with a new port replaces, never duplicates. + seed 9999 + cp "$DST/gremlin-server.yaml" "$CASE/after-first" + seed 9999 + cmp -s "$CASE/after-first" "$DST/gremlin-server.yaml" || fail "rerun: same port is not idempotent" + seed 8183 + [ "$(top_ports)" = "port: 8183" ] || fail "rerun: port not replaced: '$(top_ports)'" + + # 4. Operator-edited file with its own port: replaced in place, rest kept. + new_case edited + printf 'host: 0.0.0.0\nport: 8182\n# operator note\nthreadPoolWorker: 4\n' > "$DST/gremlin-server.yaml" + seed 7777 + [ "$(cat "$DST/gremlin-server.yaml")" = "$(printf 'host: 0.0.0.0\nport: 7777\n# operator note\nthreadPoolWorker: 4')" ] \ + || fail "edited: unexpected result: $(cat "$DST/gremlin-server.yaml")" + grep -q 'config-seed: keeping existing gremlin-server.yaml' "$CASE/out.log" || fail "edited: file was re-seeded" + + # 5. Without GREMLIN_PORT an operator-edited file is left byte for byte. + new_case untouched + printf 'host: 0.0.0.0\nport: 8190\n' > "$DST/gremlin-server.yaml" + cp "$DST/gremlin-server.yaml" "$CASE/before" + seed + cmp -s "$CASE/before" "$DST/gremlin-server.yaml" || fail "untouched: file changed without GREMLIN_PORT" + + # 6. Duplicate, quoted and spaced top-level keys collapse into one port line; no trailing newline kept. + new_case variants + printf 'host: 0.0.0.0\n"port": 1\nport : 2\n'"'"'port'"'"': 3\nlast: line' > "$DST/gremlin-server.yaml" + seed 9000 + [ "$(cat "$DST/gremlin-server.yaml")" = "$(printf 'host: 0.0.0.0\nport: 9000\nlast: line')" ] \ + || fail "variants: unexpected result: $(cat "$DST/gremlin-server.yaml")" + + # 7. No gremlin-server.yaml in the image or on the volume: one holding the port is created. + new_case absent + rm "$SRC/gremlin-server.yaml" + seed 9001 + [ "$(cat "$DST/gremlin-server.yaml")" = "port: 9001" ] \ + || fail "absent: unexpected result: $(cat "$DST/gremlin-server.yaml" 2>&1)" + + # 8. A key that only starts with "port" is not the port. + new_case prefix + printf 'portable: yes\nports: [1]\n' > "$DST/gremlin-server.yaml" + seed 9002 + [ "$(cat "$DST/gremlin-server.yaml")" = "$(printf 'portable: yes\nports: [1]\nport: 9002')" ] \ + || fail "prefix: unexpected result: $(cat "$DST/gremlin-server.yaml")" + + echo "config-seed script checked with: $SH" +done + +if [ "$FAILURES" -ne 0 ]; then + echo "$FAILURES config-seed check(s) failed" + exit 1 +fi +echo "All config-seed checks passed (${#SHELLS[@]} shell(s))."