diff --git a/.github/scripts/check-docker-images.sh b/.github/scripts/check-docker-images.sh index 23b99147d4..785b8bdc3d 100644 --- a/.github/scripts/check-docker-images.sh +++ b/.github/scripts/check-docker-images.sh @@ -30,6 +30,33 @@ for pair in \ HC=$(docker image inspect --format '{{json .Config.Healthcheck}}' "$IMAGE_ID") [[ "$HC" != null ]] || { echo "Missing HEALTHCHECK: $dockerfile"; exit 1; } + case "$dockerfile" in + hugegraph-pd/Dockerfile) + EXPECTED_VOLUME=/hugegraph-pd/pd_data + ;; + hugegraph-store/Dockerfile) + EXPECTED_VOLUME=/hugegraph-store/storage + ;; + hugegraph-server/Dockerfile) + EXPECTED_VOLUME=/hugegraph-server/rocksdb-data + ;; + hugegraph-server/Dockerfile-hstore) + EXPECTED_VOLUME= + ;; + esac + ACTUAL_VOLUMES=$(docker image inspect --format \ + '{{range $path, $_ := .Config.Volumes}}{{$path}}{{"\n"}}{{end}}' \ + "$IMAGE_ID" | sed '/^$/d' | sort) + [[ "$ACTUAL_VOLUMES" == "$EXPECTED_VOLUME" ]] || { + echo "ERROR: $dockerfile declares volumes [$ACTUAL_VOLUMES]; expected [$EXPECTED_VOLUME]" + exit 1 + } + if [[ "$dockerfile" != hugegraph-server/Dockerfile-hstore ]]; then + expected_provider=rocksdb + if [[ "${RUNTIME_VARIANT:-standard}" == topling ]]; then expected_provider=topling; fi + docker image inspect --format '{{range .Config.Env}}{{println .}}{{end}}' "$IMAGE_ID" | + grep -qx "TOPLINGDB_ROCKSDB_PROVIDER=$expected_provider" + fi # The images must run as the fixed non-root user and own the directories # the start scripts and default volumes write to. case "$dockerfile" in diff --git a/.github/scripts/ci-policy.py b/.github/scripts/ci-policy.py index d3e75dad2e..cc7cb024dc 100644 --- a/.github/scripts/ci-policy.py +++ b/.github/scripts/ci-policy.py @@ -101,6 +101,7 @@ TRAVIS = "hugegraph-server/hugegraph-dist/src/assembly/travis/" # Contract tests follow their executing suites, without runtime consumer fan-out. CONTRACT_SUITES = { + "docker/test-topling-images.sh": {"pd"}, TRAVIS + "test-check-java-runtime-logs.sh": {"struct"}, TRAVIS + "check-java-runtime-logs.sh": {"server", "commons", "struct", "pd", "store", "hstore", "cluster", "macos", "macos_full", "hbase", "riscv64"}, diff --git a/.github/scripts/docker-deployment.py b/.github/scripts/docker-deployment.py old mode 100644 new mode 100755 index 12b253ec2f..e220b5b4de --- a/.github/scripts/docker-deployment.py +++ b/.github/scripts/docker-deployment.py @@ -20,15 +20,22 @@ import json import os from pathlib import Path +import re import subprocess import sys +import tempfile +import time import urllib.error +import urllib.parse import urllib.request import xml.etree.ElementTree as ET ADMIN_PASSWORD = "ci-compose-password" PD_PASSWORD = "ci-compose-pd-secret" +GRAPH_CONFIG_LINK = "../rocksdb-data/.hugegraph-config/graphs" +PERSISTED_GRAPH = "ci_persisted" +PERSISTED_VERTEX = "retained-vertex" def command(*args): @@ -41,8 +48,11 @@ def verify_image(container, image, expected): raise RuntimeError(f"Container {container} uses {actual}; expected {image} ({expected})") -def response(url, credentials=None): - request = urllib.request.Request(url) +def response(url, credentials=None, *, method=None, payload=None): + data = json.dumps(payload).encode() if payload is not None else None + request = urllib.request.Request(url, data=data, method=method) + if data is not None: + request.add_header("Content-Type", "application/json") if credentials: encoded = base64.b64encode(credentials.encode()).decode() request.add_header("Authorization", "Basic " + encoded) @@ -53,8 +63,8 @@ def response(url, credentials=None): return error.code, error.read().decode() -def expect_response(url, expected=200, credentials=None): - actual, body = response(url, credentials) +def expect_response(url, expected=200, credentials=None, **request_options): + actual, body = response(url, credentials, **request_options) if actual != expected: raise RuntimeError(f"{url}: HTTP {actual}, expected {expected}") return body @@ -124,6 +134,140 @@ def verify_graph(run_id): check=True, timeout=960, env=environment) +def create_persisted_graph(): + url = f"http://localhost:8080/graphspaces/DEFAULT/graphs/{PERSISTED_GRAPH}" + credentials = f"admin:{ADMIN_PASSWORD}" + expect_response(url, 201, credentials, method="POST", payload={ + "backend": "rocksdb", + "rocksdb.data_path": "/hugegraph-server/rocksdb-data/ci-persisted/data", + "rocksdb.wal_path": "/hugegraph-server/rocksdb-data/ci-persisted/wal", + }) + expect_response(url + "/schema/propertykeys", 202, credentials, method="POST", + payload={"name": "name", "data_type": "TEXT", "cardinality": "SINGLE"}) + expect_response(url + "/schema/vertexlabels", 201, credentials, method="POST", + payload={"name": "retained", "id_strategy": "CUSTOMIZE_STRING", + "properties": ["name"]}) + expect_response(url + "/graph/vertices", 201, credentials, method="POST", + payload={"id": PERSISTED_VERTEX, "label": "retained", + "properties": {"name": "survives-recreate"}}) + verify_persisted_graph() + + +def verify_persisted_graph(): + url = f"http://localhost:8080/graphspaces/DEFAULT/graphs/{PERSISTED_GRAPH}" + credentials = f"admin:{ADMIN_PASSWORD}" + vertex_id = urllib.parse.quote(json.dumps(PERSISTED_VERTEX), safe="") + vertex = json.loads(expect_response(url + "/graph/vertices/" + vertex_id, + credentials=credentials)) + if vertex.get("properties", {}).get("name") != "survives-recreate": + raise RuntimeError("API-created graph data did not survive container replacement") + + +def verify_standalone_recreate(compose, container, image, expected): + # GraphManager writes dynamic definitions through conf/graphs. The image + # redirects that path into the existing data volume; prove a real Compose + # replacement keeps the path and does not merely pass a static Dockerfile check. + link = command("docker", "exec", container, "readlink", "/hugegraph-server/conf/graphs") + if link != GRAPH_CONFIG_LINK: + raise RuntimeError(f"Graph configuration does not use the data volume: {link}") + create_persisted_graph() + subprocess.run(compose + ["up", "-d", "--pull", "never", "--force-recreate", + "--no-deps", "--wait", "--wait-timeout", "600", "server"], + check=True, timeout=660) + replacement = command(*compose, "ps", "-q", "server") + if not replacement or "\n" in replacement or replacement == container: + raise RuntimeError("Compose did not replace the standalone Server container") + verify_image(replacement, image, expected) + health = command("docker", "inspect", "--format", "{{.State.Health.Status}}", replacement) + if health != "healthy": + raise RuntimeError(f"Replacement Server is {health}") + verify_server() + verify_persisted_graph() + + +def verify_readonly_graphs(image): + """Start actual images with directory and individual-file read-only binds.""" + environment = json.loads(command("docker", "image", "inspect", "--format", + "{{json .Config.Env}}", image)) + selected = dict(item.split("=", 1) for item in environment if "=" in item)[ + "TOPLINGDB_ROCKSDB_PROVIDER"] + if selected not in {"rocksdb", "topling"}: + raise RuntimeError(f"Unexpected image provider: {selected}") + default = command("docker", "run", "--rm", "--pull", "never", "--entrypoint", "cat", + image, "/hugegraph-server/conf/graphs.default/hugegraph.properties") + default = re.sub(r"(?m)^[ \t]*rocksdb\.provider\s*[:=].*$", "", default) + "\n" + with tempfile.TemporaryDirectory(prefix="hg-readonly-") as temporary: + root = Path(temporary) + for mount_kind in ("directory", "file"): + for configured in (None, "rocksdb", "topling"): + case = f"{mount_kind}-{configured or 'default'}" + graphs = root / case + graphs.mkdir() + # The private temp parent stays 0700. Give the image UID baseline + # access to these non-secret fixtures so only the read-only bind + # denies writes, independently of the runner's umask/identity. + graphs.chmod(0o777) + config = graphs / "hugegraph.properties" + config.write_text(default + (f"rocksdb.provider={configured}\n" if configured else "")) + config.chmod(0o666) + original = config.read_bytes() + source = graphs if mount_kind == "directory" else config + destination = "/hugegraph-server/conf/graphs" + if mount_kind == "file": + destination += "/hugegraph.properties" + name = root.name + "-" + case + should_start = (configured or "rocksdb") == selected + try: + container = command("docker", "run", "--detach", "--pull", "never", "--name", name, + "--publish", "127.0.0.1::8080", "--mount", + f"type=bind,src={source},dst={destination},readonly", image) + port = (command("docker", "port", container, "8080/tcp").rsplit(":", 1)[1] + if should_start else None) + deadline = time.monotonic() + 180 + while True: + state = json.loads(command("docker", "inspect", "--format", + "{{json .State}}", container)) + if not state["Running"]: + break + if should_start: + try: + if response(f"http://127.0.0.1:{port}/graphspaces/DEFAULT/graphs")[0] == 200: + break + except (urllib.error.URLError, ConnectionError, TimeoutError): + pass + if time.monotonic() >= deadline: + raise RuntimeError(f"Read-only graph case timed out: {case}") + time.sleep(1) + if should_start: + if not state["Running"]: + raise RuntimeError(f"Matching read-only graph did not start: {case}") + probe = subprocess.run(["docker", "exec", container, "sh", "-c", + "printf probe >> /hugegraph-server/conf/graphs/hugegraph.properties"], + capture_output=True, text=True, timeout=30) + if probe.returncode == 0 or "Read-only file system" not in probe.stderr: + raise RuntimeError(f"Graph configuration mount is not read-only: {case}") + subprocess.run(["docker", "stop", "--time", "60", container], check=True, timeout=90) + stopped = json.loads(command("docker", "inspect", "--format", + "{{json .State}}", container)) + if stopped["OOMKilled"] or stopped["ExitCode"] not in {0, 143}: + raise RuntimeError(f"Read-only graph shutdown failed: {case}: {stopped}") + else: + logs = command("docker", "logs", container) + expected = f"cannot change rocksdb.provider from '{configured or 'rocksdb'}'" + if state["ExitCode"] == 0 or expected not in logs or \ + f"'{selected}' in ./conf/graphs/hugegraph.properties" not in logs or \ + "Initializing HugeGraph Store" in logs: + raise RuntimeError(f"Mismatched read-only graph did not fail clearly: {case}: {logs}") + if config.read_bytes() != original: + raise RuntimeError(f"Read-only graph configuration changed: {case}") + print(f"PASS: {selected} {case} read-only graph configuration", flush=True) + except BaseException: + subprocess.run(["docker", "logs", name], check=False, timeout=30) + raise + finally: + subprocess.run(["docker", "rm", "--force", "--volumes", name], check=True, timeout=120) + + def smoke(tag, topology, images): root = Path(__file__).resolve().parents[2] project = f"hg-pr-{tag}-{Path(topology).stem}" @@ -132,6 +276,7 @@ def smoke(tag, topology, images): expected = {service: command("docker", "image", "inspect", "--format", "{{.Id}}", f"{repository}:{tag}") for service, repository in images.items()} failed = False + containers = {} try: subprocess.run(compose + ["up", "-d", "--pull", "never", "--wait", "--wait-timeout", "600"] + list(images), check=True, timeout=660) @@ -139,6 +284,7 @@ def smoke(tag, topology, images): container = command(*compose, "ps", "-q", service) if not container or "\n" in container: raise RuntimeError(f"Expected exactly one {service} container") + containers[service] = container verify_image(container, f"{repository}:{tag}", expected[service]) health = command("docker", "inspect", "--format", "{{.State.Health.Status}}", container) if health != "healthy": @@ -147,6 +293,9 @@ def smoke(tag, topology, images): if "pd" in images: verify_storage() verify_graph(project.replace("-", "_")) + if topology == "docker-compose.yml": + verify_standalone_recreate(compose, containers["server"], + f"{images['server']}:{tag}", expected["server"]) except BaseException: failed = True for args in (["ps"], ["logs", "--no-color", "--tail", "200"]): @@ -172,6 +321,7 @@ def main(tag): HUGEGRAPH_AUTH_TOKEN_SECRET="0123456789abcdef" * 4, HG_PD_AUTH_SECRET_KEY=PD_PASSWORD) smoke(tag, "docker-compose.yml", {"server": "hugegraph/hugegraph"}) + verify_readonly_graphs(f"hugegraph/hugegraph:{tag}") smoke(tag, "docker-compose-hstore.yml", {"pd": "hugegraph/pd", "store": "hugegraph/store", "server": "hugegraph/server"}) diff --git a/.github/scripts/test_ci_policy.py b/.github/scripts/test_ci_policy.py index 5c1d84f2a7..77c11e3e67 100644 --- a/.github/scripts/test_ci_policy.py +++ b/.github/scripts/test_ci_policy.py @@ -406,6 +406,14 @@ def test_contract_scripts_reach_their_actual_inner_jobs_and_steps(self): condition = re.search(r"^ if: (.+)$", step, re.M).group(1) self.assertTrue(eval(condition.replace("inputs.backend", repr(backend)), {"__builtins__": {}})) + + def test_topling_image_contract_selects_its_pd_executor(self): + plan = self.plan_for(["docker/test-topling-images.sh"]) + self.assertEqual({"pd"}, set(plan["selected"])) + self.assertEqual({"pd_store"}, self.coordinator_jobs(plan)) + workflow = (Path(__file__).resolve().parents[1] / "workflows/pd-store-ci.yml").read_text() + pd = workflow.split("\n pd:\n", 1)[1].split("\n store:\n", 1)[0] + self.assertIn("bash docker/test-topling-images.sh", pd) mixed = self.plan_for([policy.TRAVIS + "test-stop-hugegraph-store.sh", "hugegraph-pd/src/A.java"]) self.assertTrue({"struct", "pd", "store", "hstore", "cluster"}.issubset(mixed["selected"])) checker = self.plan_for([policy.TRAVIS + "check-java-runtime-logs.sh"]) diff --git a/.github/scripts/test_docker_deployment.py b/.github/scripts/test_docker_deployment.py old mode 100644 new mode 100755 index 7eeca7ff8e..3c9a36976e --- a/.github/scripts/test_docker_deployment.py +++ b/.github/scripts/test_docker_deployment.py @@ -20,6 +20,7 @@ import json import os from pathlib import Path +import stat import unittest from unittest.mock import patch @@ -68,6 +69,22 @@ def test_startup_timeout_preserves_failure_and_cleans_compose(self): deployment.smoke("ci-1-1", "docker-compose.yml", {"server": "hugegraph/hugegraph"}) self.assertEqual(run.call_args_list[-1].args[0][-3:], ["down", "-v", "--remove-orphans"]) + def test_standalone_recreates_after_graph_checks(self): + with patch.object(deployment, "command", + side_effect=["sha256:server", "server", "sha256:server", "healthy"]), \ + patch.object(deployment.subprocess, "run") as run, \ + patch.object(deployment, "verify_server") as server, \ + patch.object(deployment, "verify_graph") as graph, \ + patch.object(deployment, "verify_standalone_recreate") as recreate: + deployment.smoke("ci-1-1", "docker-compose.yml", {"server": "hugegraph/hugegraph"}) + args = recreate.call_args.args + self.assertEqual(args[1:], ("server", "hugegraph/hugegraph:ci-1-1", "sha256:server")) + self.assertEqual(args[0][:4], ["docker", "compose", "-p", + "hg-pr-ci-1-1-docker-compose"]) + server.assert_called_once_with() + graph.assert_called_once_with("hg_pr_ci_1_1_docker_compose") + self.assertEqual(run.call_args_list[-1].args[0][-3:], ["down", "-v", "--remove-orphans"]) + def test_hstore_starts_only_pr_services_and_cleans_after_checks(self): images = {"pd": "hugegraph/pd", "store": "hugegraph/store", "server": "hugegraph/server"} identities = ["sha256:pd", "sha256:store", "sha256:server"] @@ -87,6 +104,74 @@ def test_hstore_starts_only_pr_services_and_cleans_after_checks(self): self.assertEqual(run.call_args_list[-1].args[0][-3:], ["down", "-v", "--remove-orphans"]) +class ReadOnlyGraphFixtureTest(unittest.TestCase): + def test_fixture_is_usable_by_image_uid_before_readonly_mount_under_restrictive_umask(self): + inspected = [] + + def command(*args): + if args[:3] == ("docker", "image", "inspect"): + return json.dumps(["TOPLINGDB_ROCKSDB_PROVIDER=rocksdb"]) + if "--entrypoint" in args: + return "backend=rocksdb\n" + mount = args[args.index("--mount") + 1] + source = Path(dict(option.split("=", 1) for option in mount.split(",") if "=" in option)["src"]) + directory_mode = stat.S_IMODE(source.stat().st_mode) + file_mode = stat.S_IMODE((source / "hugegraph.properties").stat().st_mode) + self.assertEqual(directory_mode & (stat.S_IROTH | stat.S_IWOTH | stat.S_IXOTH), + stat.S_IROTH | stat.S_IWOTH | stat.S_IXOTH) + self.assertEqual(file_mode & (stat.S_IROTH | stat.S_IWOTH), stat.S_IROTH | stat.S_IWOTH) + inspected.append(source) + raise RuntimeError("fixture inspected before Docker startup") + + previous_umask = os.umask(0o077) + try: + with patch.object(deployment, "command", side_effect=command), \ + patch.object(deployment.subprocess, "run"): + with self.assertRaisesRegex(RuntimeError, "fixture inspected"): + deployment.verify_readonly_graphs("hugegraph/hugegraph:fixture") + self.assertEqual(len(inspected), 1) + self.assertFalse(inspected[0].exists()) + finally: + os.umask(previous_umask) + + +class GraphDefinitionPersistenceTest(unittest.TestCase): + def test_force_recreate_keeps_graph_config_in_data_volume(self): + compose = ["docker", "compose", "-p", "fixture", "-f", "/repo/docker/docker-compose.yml"] + with patch.object(deployment, "command", + side_effect=[deployment.GRAPH_CONFIG_LINK, "replacement", "healthy"]) as command, \ + patch.object(deployment.subprocess, "run") as run, \ + patch.object(deployment, "verify_image") as image, \ + patch.object(deployment, "verify_server") as server, \ + patch.object(deployment, "create_persisted_graph") as create, \ + patch.object(deployment, "verify_persisted_graph") as verify: + deployment.verify_standalone_recreate( + compose, "original", "hugegraph/hugegraph:ci-1-1", "sha256:pr") + + recreate = run.call_args.args[0] + self.assertEqual(recreate[:len(compose)], compose) + for option in ("--force-recreate", "--no-deps", "--wait"): + self.assertIn(option, recreate) + self.assertEqual(recreate[-1], "server") + image.assert_called_once_with("replacement", "hugegraph/hugegraph:ci-1-1", "sha256:pr") + server.assert_called_once_with() + create.assert_called_once_with() + verify.assert_called_once_with() + self.assertEqual(command.call_args_list[0].args, + ("docker", "exec", "original", "readlink", + "/hugegraph-server/conf/graphs")) + + def test_rejects_missing_graph_after_recreate(self): + with patch.object(deployment, "response", return_value=(404, "missing graph")): + with self.assertRaisesRegex(RuntimeError, "expected 200"): + deployment.verify_persisted_graph() + + def test_rejects_missing_vertex_even_when_graph_exists(self): + with patch.object(deployment, "response", return_value=(200, '{"properties":{}}')): + with self.assertRaisesRegex(RuntimeError, "did not survive"): + deployment.verify_persisted_graph() + + class GraphSmokeTest(unittest.TestCase): def test_reuses_graph_write_read_and_gremlin_checks_with_auth(self): with patch.dict(os.environ, {"PATH": "/existing/tools"}), \ diff --git a/.github/workflows/docker-build-ci.yml b/.github/workflows/docker-build-ci.yml index 5ed8fd5acc..d5d393445e 100644 --- a/.github/workflows/docker-build-ci.yml +++ b/.github/workflows/docker-build-ci.yml @@ -30,9 +30,14 @@ jobs: docker-build: runs-on: ubuntu-24.04 timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + runtime: [standard, topling] env: - IMAGE_TAG: ci-${{ github.run_id }}-${{ github.run_attempt }} + IMAGE_TAG: ci-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.runtime }} SOURCE_REVISION: ${{ github.sha }} + RUNTIME_VARIANT: ${{ matrix.runtime }} steps: - name: Checkout id: checkout @@ -89,7 +94,7 @@ jobs: set -euo pipefail docker buildx bake --file docker/bake.hcl --print > /tmp/hugegraph-bake.json - jq -e ' + jq -e --arg runtime "$RUNTIME_VARIANT" ' .group.default.targets == [ "build-cache", "pd", @@ -98,7 +103,8 @@ jobs: "store" ] and ([.target[] | .platforms] | - all(. == ["linux/amd64", "linux/arm64"])) and + all(. == (if $runtime == "topling" then ["linux/amd64"] + else ["linux/amd64", "linux/arm64"] end))) and .target["build-cache"].target == "build" and .target["build-cache"].output[0].type == "cacheonly" and ([.target | to_entries[] | diff --git a/.github/workflows/pd-store-ci.yml b/.github/workflows/pd-store-ci.yml index e4dd1809ea..aab6c3cb2e 100644 --- a/.github/workflows/pd-store-ci.yml +++ b/.github/workflows/pd-store-ci.yml @@ -189,6 +189,7 @@ jobs: run: | bash $TRAVIS_DIR/test-topling-selection.sh bash hugegraph-store/hg-store-dist/src/assembly/travis/test-store-preloads.sh + bash docker/test-topling-images.sh - name: Run PD docker entrypoint secret override tests run: | diff --git a/docker/README.md b/docker/README.md index ee5e39f36f..e61301d206 100644 --- a/docker/README.md +++ b/docker/README.md @@ -272,6 +272,172 @@ The Hubble `latest` image is expected to work with HugeGraph Server 1.7 and Serv +### ToplingDB images + +Select the ToplingDB variant with the existing image-version parameter. The same +Compose files, credentials, ports and lifecycle commands apply: + +```bash +export HUGEGRAPH_VERSION=latest-topling +# Standalone Server with embedded ToplingDB +# First deployment +docker compose -p hugegraph-topling-standalone -f docker-compose.yml up -d --wait + +# Or PD + Store using ToplingDB, with an HStore Server +# Generate conf/hubble/hstore.local.properties as described above first. +docker compose -p hugegraph-topling-hstore -f docker-compose-hstore.yml up -d --wait +``` + +Use the same project name on subsequent `ps`, `stop`, `down` and `up` commands. +Persist `HUGEGRAPH_VERSION=latest-topling` in that deployment's `.env` if preferred. +A published version uses `-topling`; the unsuffixed tag keeps RocksDB. +Topling images support **Linux amd64 on a Haswell-compatible CPU**, including +AVX2 and BMI2, as required by the current JNI build. All HugeGraph images in a deployment set +use the same source revision. HStore Server keeps `backend=hstore` and delegates +storage to PD/Store; it does not load a local Topling JNI runtime. Hubble keeps its +independent image tag. + +The images include the verified JNI runtime and set both launcher and business +provider configuration. No JAR download or `LD_PRELOAD` setting is needed at startup. +The default image uses the community JNI with SkipList memtables and BlockBasedTable +SSTs; it includes no proprietary trial modules. Its native profile starts with a +64 MiB block cache and three 16 MiB write buffers per column family. Native memory +still scales with databases and column families; these settings do not cap total +process memory. Mount a tuned `conf/toplingdb.yaml` or select an external file with +`TOPLINGDB_EASY_MIGRATE_CONF` when sizing a production deployment. CSPP and other +optional modules require your own compatible JNI artifact and native profile. +The unpacked distribution's full profile is unchanged. The default community +profile uses standard BlockBasedTable SSTs and supports opening compatible RocksDB +data after a normal shutdown. The publication check seeds data using standard +images from the same source commit, switches to Topling on the same volumes, and +checks schema, indexes, graph operations and restart persistence. See +[Topling runtime preparation and validation](../docs/storage-lifecycle.md#storage-provider-selection-and-toplingdb) +for the native configuration and tested compatibility boundary. + +Standalone images retain graph definitions in +`rocksdb-data/.hugegraph-config/graphs`, reached through the `conf/graphs` symlink. +The supplied `server-data` volume therefore keeps API-created graph definitions +as well as database files through container replacement. If an older nonempty +volume or a bind mount lacks this directory, the entrypoint initializes it once +from the image's `conf/graphs.default`. Existing definitions and database files +are preserved; explicitly mounted `conf/graphs` directories take precedence. + +An explicitly mounted graph directory or file may be read-only when its +`rocksdb.provider` already matches the selected image. An absent property means +`rocksdb`, so a Topling configuration must explicitly select `topling`. Matching +files are left unchanged. If the provider differs, startup reports the graph +file and required change; supply a matching configuration or use a writable +directory mount before switching images. A single-file bind mount cannot be +replaced by the entrypoint's in-place editor, even when mounted writable. +Other environment-driven configuration changes, authentication setup, and API +graph creation/deletion still require writable configuration where they apply. + +For the first upgrade from an older image, preserve **custom graph definitions +and settings** that existed only in its installation directory. Default seeding +does not recover those files from the database. Stop the old Server and copy its +definitions while that container still exists: + +```bash +docker compose -f docker-compose.yml stop server +docker compose -f docker-compose.yml cp server:/hugegraph-server/conf/graphs ./graphs-backup +cp -a ./graphs-backup ./graph-config +``` + +Keep `graphs-backup` untouched. Save a writable configuration mount for the working +copy as `docker-compose.graph-config.yml`, and retain it on every subsequent start: + +```yaml +services: + server: + volumes: + - ./graph-config:/hugegraph-server/conf/graphs +``` + +Apply this override alongside the normal Compose file when starting the new +image. Verify all expected definitions and their data/WAL paths before replacement. +Custom `graphs` directories need equivalent backup and mounts. This one-time step +preserves older configuration files that are outside the data volume; it does not +migrate old anonymous volumes. Subsequent API-created definitions persist in the +selected configuration directory, either the default data-volume location above +or this explicit mount. + +Then stop incoming work, stop the services normally, and +select the matching Topling images. Keep the same Compose project, credentials and +data mounts. For standalone, from this directory: + +```bash +docker compose -f docker-compose.yml stop +HUGEGRAPH_VERSION='-topling' \ + docker compose -f docker-compose.yml up -d --wait +``` + +If you use the working configuration mount above, retain +`-f docker-compose.graph-config.yml` after the base Compose file on subsequent +commands. Deployments using the default data-volume configuration need no override. + +Use `docker-compose-hstore.yml` for the corresponding distributed deployment. +The named data volumes are reattached automatically; no export/import or new +project is needed for the compatible database files. To return to standard +RocksDB, use the same stop/up sequence and matching unsuffixed tag. Standard +images explicitly select `rocksdb`, so the entrypoint resets the provider in all +persisted or writable mounted `backend=rocksdb` graph definitions. Topling images +select `topling` in the same way. Other graph settings and `backend=hstore` +definitions remain unchanged. Retain any configuration override used above. +Prefer versioned tags or verified digests: Topling +`latest-topling` is published on demand and can lag standard `latest`. Changing +storage providers does not bypass HugeGraph or RocksDB version compatibility. +Back up the data and qualify representative existing data before production use. + +The images declare only database directories as Docker volumes, so replacing an +image also replaces its binaries, configuration defaults and JNI. The supplied +Compose files mount these database directories separately and standalone graph +definitions live in that data volume. Older custom definitions require the +one-time configuration preservation described above. If an older +custom deployment kept data only in an anonymous whole-application volume, first +identify and preserve that volume's database directories; they are not copied +automatically into a new data volume. Custom data/WAL paths must remain mounted. + +Source builds use the community JNI release and trusted SHA-256 pinned in +[`topling-runtime.env`](topling-runtime.env). The image build downloads and verifies +that artifact once; selecting the variant is the only required build input. +For a local HStore source build, use the existing development overlay: + +```bash +export RUNTIME_VARIANT=topling +docker compose -p hugegraph-topling-dev \ + -f docker-compose-hstore.yml -f docker-compose.dev.yml up -d --build --wait +``` + +The overlay uses the usual local `:dev` tags. Build it on Linux amd64. For direct Docker and +Bake builds, use `--build-arg RUNTIME_VARIANT=topling` or `RUNTIME_VARIANT=topling` +respectively; the same pinned JNI defaults apply. + +To use a different compatible JNI release, optionally override both +`TOPLING_JNI_URL` and `TOPLING_JNI_SHA256`. Compose and Bake accept them as environment +variables; direct Docker builds accept the same names as build arguments: + +```bash +export TOPLING_JNI_URL='https://your-artifact-host/immutable/rocksdbjni.jar' +export TOPLING_JNI_SHA256='' +``` + +For a verified local JNI JAR, put it in a directory as `rocksdbjni.jar` and supply +that directory as a named build context instead of a download URL: + +```bash +docker buildx build --platform linux/amd64 -f ../hugegraph-server/Dockerfile \ + --build-arg RUNTIME_VARIANT=topling \ + --build-arg TOPLING_JNI_SHA256='' \ + --build-context topling-jni=/absolute/path/to/jni-directory \ + -t hugegraph/hugegraph:local-topling --load .. +``` + +Run this example from the `docker` directory. Bake accepts the same directory +with `--set '*.contexts.topling-jni=/absolute/path/to/jni-directory'`. The checksum +and normal native capability probe still apply. Networks with a private CA can additionally supply +`--secret id=proxy_ca,src=/absolute/path/ca.pem`; the CA is mounted only while +Maven, package installation and artifact download run. + ### Data persistence Each topology creates its own normal Compose network and named volumes. No network or volume needs to be created in advance. @@ -281,6 +447,11 @@ Each topology creates its own normal Compose network and named volumes. No netwo Standalone stores RocksDB data at `/hugegraph-server/rocksdb-data`. The HStore topologies keep PD and Store data in topology-local volumes. Hubble uses `jdbc:h2:file:/hubble/data/hubble;DB_CLOSE_ON_EXIT=FALSE` and stores uploaded files under `/hubble/data/upload-files`. +Standalone API-created graph definitions are separate files under `conf/graphs`. +Before any first replacement of an older root-volume image, including a standard +to standard upgrade, follow the [configuration backup and mount steps](#toplingdb-images). +The data volume alone does not preserve or reload those definitions. + `docker compose down` keeps named-volume data. `docker compose down -v` intentionally deletes it. @@ -371,7 +542,22 @@ Both Server images (`hugegraph/hugegraph` and `hugegraph/server`) write the Huge `/hugegraph-server/logs` holds `hugegraph-server.log`, JVM crash logs (`hs_err_pid__.log`) and out-of-memory heap dumps (`heapdump__/java_pid.hprof`), where `` is the container's local time as `YYYYMMDD-HHMMSS`. Each start gets its own `heapdump_*` directory, so the Server and the JVMs it starts, such as computer jobs, write separate dumps. HotSpot names each dump and crash log after the JVM's PID, so a JVM that reuses the PID of an earlier one from the same start cannot write its dump, and its crash log replaces the earlier one's. The host name (on Kubernetes, the pod name, or `spec.hostname` when that is set) and a `-1`, `-2`, ... counter keep names from clashing; the counter also separates pods that share a host name. If the directory cannot be created, for example on a full volume, the Server still starts and dumps go to `logs/` itself as `java_pid.hprof`. That is best effort: a full volume may have no room for the dump either, and a restarted container that reuses a PID cannot write over an earlier dump with the same name. If the logs path contains a double quote or `%`, the launcher warns and heap dumps go to `logs/` itself, with no per-launch directory and no crash log default; the `JAVA_TOOL_OPTIONS` opt-out below still applies. A path that contains both quote characters gets no heap dump default at all. -Both images declare `VOLUME /hugegraph-server`, so `docker restart` and a Compose recreate keep the files in that anonymous volume. `docker compose down` leaves the volume behind but the next `up` does not attach it, so the files become hard to reach and keep using disk. A later `docker compose down -v` does not remove that orphaned volume either, since it only removes anonymous volumes of the containers it removes; note the volume name with `docker inspect` before `down` and delete it with `docker volume rm `, or stop with `docker compose down -v` in the first place. Avoid `docker volume prune` for this: it deletes every unused anonymous volume on the Docker host, including other projects' data. Mount a named volume or a bind mount at `/hugegraph-server/logs` to keep the files reachable. Kubernetes starts a new container on every restart and does not turn the image's `VOLUME` into a pod volume, so mount an `emptyDir` or a PersistentVolumeClaim at `/hugegraph-server/logs`; for a PersistentVolumeClaim, set `fsGroup: 10001` in the Server pod security context so the non-root user can write it. Give each pod its own volume, or a per-pod directory on a shared PVC: crash files get distinct names, but `hugegraph-server.log`, the audit log and the slow-query log have fixed names, and pods sharing them would roll them over for each other. For a per-pod directory, pass the pod name in through the Downward API and use it in `subPathExpr` (the Helm chart sets neither): +The images persist database directories rather than the application installation. A +`docker restart` keeps logs in the existing container; replacing the container requires +a named volume or bind mount at `/hugegraph-server/logs` to retain them. Older images +declared `VOLUME /hugegraph-server`, which could also retain old binaries and JNI during +an image change. Do not mount an old installation directory over a replacement image; +keep database and any custom configuration mounts separate. An old anonymous root volume +can remain after its container is removed. Inspect its contents before removing that +specific volume; it may contain logs or data from a deployment without explicit data +mounts. Kubernetes starts a new container on every restart, so mount an `emptyDir` or a +PersistentVolumeClaim at `/hugegraph-server/logs` to preserve logs across container +restarts. For a PersistentVolumeClaim, set `fsGroup: 10001` in the Server pod +security context so the non-root user can write it. Give each pod its own volume, or a per-pod directory on a shared PVC: crash +files get distinct names, but `hugegraph-server.log`, the audit log and the slow-query +log have fixed names, and pods sharing them would roll them over for each other. For a +per-pod directory, pass the pod name in through the Downward API and use it in +`subPathExpr` (the Helm chart sets neither): ```yaml env: diff --git a/docker/bake.hcl b/docker/bake.hcl index 2fc1cf4626..346f2fd323 100644 --- a/docker/bake.hcl +++ b/docker/bake.hcl @@ -31,6 +31,27 @@ variable "SOURCE_REVISION" { default = "local" } +variable "SOURCE_URL" { + default = "https://github.com/apache/hugegraph" +} + +variable "RUNTIME_VARIANT" { + default = "standard" + + validation { + condition = RUNTIME_VARIANT == "standard" || RUNTIME_VARIANT == "topling" + error_message = "RUNTIME_VARIANT must be standard or topling" + } +} + +variable "TOPLING_JNI_URL" { + default = null +} + +variable "TOPLING_JNI_SHA256" { + default = null +} + variable "IMAGE_TAG" { default = "local" } @@ -50,8 +71,13 @@ target "_common" { MAVEN_PROJECTS = MAVEN_PROJECTS RUNTIME_DEPS_EPOCH = RUNTIME_DEPS_EPOCH SOURCE_REVISION = SOURCE_REVISION + SOURCE_REPOSITORY = SOURCE_URL + RUNTIME_VARIANT = RUNTIME_VARIANT + TOPLING_JNI_URL = TOPLING_JNI_URL + TOPLING_JNI_SHA256 = TOPLING_JNI_SHA256 } - platforms = [ + # The published Topling JNI currently contains Linux x86_64 native code only. + platforms = RUNTIME_VARIANT == "topling" ? ["linux/amd64"] : [ "linux/amd64", "linux/arm64", ] diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 4da9c32e19..6d33219fc1 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -22,6 +22,10 @@ services: build: context: .. dockerfile: hugegraph-pd/Dockerfile + args: + RUNTIME_VARIANT: ${RUNTIME_VARIANT:-standard} + TOPLING_JNI_URL: ${TOPLING_JNI_URL:-} + TOPLING_JNI_SHA256: ${TOPLING_JNI_SHA256:-} healthcheck: start_period: 20s @@ -31,6 +35,10 @@ services: build: context: .. dockerfile: hugegraph-store/Dockerfile + args: + RUNTIME_VARIANT: ${RUNTIME_VARIANT:-standard} + TOPLING_JNI_URL: ${TOPLING_JNI_URL:-} + TOPLING_JNI_SHA256: ${TOPLING_JNI_SHA256:-} healthcheck: start_period: 30s diff --git a/docker/prepare-topling-image.sh b/docker/prepare-topling-image.sh new file mode 100755 index 0000000000..f3bf83fe61 --- /dev/null +++ b/docker/prepare-topling-image.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Called from the stopped component directory during an image build only. +set -euo pipefail + +if [[ -z "${TOPLING_JNI_URL:-}" || -z "${TOPLING_JNI_SHA256:-}" ]] && + [[ -f /tmp/topling-runtime.env ]]; then + requested_url="${TOPLING_JNI_URL:-}" + requested_sha="${TOPLING_JNI_SHA256:-}" + # Checked-in, reviewed artifact defaults; explicit build inputs win. + source /tmp/topling-runtime.env + TOPLING_JNI_URL="${requested_url:-${TOPLING_JNI_URL:-}}" + TOPLING_JNI_SHA256="${requested_sha:-${TOPLING_JNI_SHA256:-}}" +fi + +[[ "${TOPLING_JNI_SHA256:-}" =~ ^[[:xdigit:]]{64}$ ]] || { + echo 'Error: Topling builds require a trusted TOPLING_JNI_SHA256' >&2 + exit 1 +} +export TOPLING_JNI_SHA256 + +if [[ -r /run/topling-jni/rocksdbjni.jar ]]; then + export TOPLING_JNI_JAR=/run/topling-jni/rocksdbjni.jar +else + [[ "${TOPLING_JNI_URL:-}" == https://* ]] || { + echo 'Error: Topling builds require an HTTPS TOPLING_JNI_URL or a topling-jni build context containing rocksdbjni.jar' >&2 + exit 1 + } + download_dir=$(mktemp -d) + trap 'rm -rf "$download_dir"' EXIT + curl --fail --location --retry 3 --proto '=https' --proto-redir '=https' \ + --output "$download_dir/rocksdbjni.jar" "$TOPLING_JNI_URL" + export TOPLING_JNI_JAR="$download_dir/rocksdbjni.jar" +fi +# The shared preparer verifies the hash, ELF/dependencies, and real EasyMigrate +# behavior before installing a component-local runtime and checksum receipt. +bash bin/prepare-topling.sh + +# Put the optional runtime's notices next to the installed JNI instead of in +# the generic Maven/source distribution, which does not contain this artifact. +license_stage=topling/.license-stage +rm -rf "$license_stage" topling/licenses +mkdir -p "$license_stage" +unzip -qq "$TOPLING_JNI_JAR" 'META-INF/licenses/*' -d "$license_stage" +[[ -d "$license_stage/META-INF/licenses" && + -s "$license_stage/META-INF/licenses/manifest.sha256" ]] || { + echo 'Error: Topling JNI is missing embedded license materials' >&2 + exit 1 +} +mv "$license_stage/META-INF/licenses" topling/licenses +rm -rf "$license_stage" +cat > topling/LICENSE <<'EOF' +ToplingDB JNI community runtime license materials +================================================= + +The complete license texts distributed with this optional JNI runtime are in +this directory's licenses/ subdirectory. The immutable rocksdbjni.jar retains +the same META-INF/licenses/ collection and checksum manifest. + +RocksDB core uses its Apache License 2.0 option. Public Topling components and +bundled dependencies retain their respective Apache-2.0, BSD, MIT and Boost +licenses. See NOTICE for principal attributions and +META-INF/topling-build-info.txt inside rocksdbjni.jar for source revisions and +build configuration. +EOF +cat > topling/NOTICE <<'EOF' +ToplingDB JNI community runtime + +This optional runtime includes software developed by Topling, Inc., together +with code from RocksDB, LevelDB and bundled public dependencies. + +Copyright (c) 2011-present, Facebook, Inc. All rights reserved. +Copyright (c) 2011 The LevelDB Authors. All rights reserved. +Copyright (c) 2021, Topling Inc. All rights reserved. +Copyright (c) 2020, Bytedance Inc. All rights reserved. + +Original component notices and license texts are retained in licenses/ beside +this file and in META-INF/licenses/ inside rocksdbjni.jar. Source revisions and +the selected community components are recorded in +META-INF/topling-build-info.txt inside rocksdbjni.jar. +EOF +chmod 0644 topling/LICENSE topling/NOTICE +find topling/licenses -type f -exec chmod 0644 {} + + +# Community images need no proprietary factory registration. Keep the full +# distribution profile unchanged and install the image's smaller native profile. +cp "$(dirname "${BASH_SOURCE[0]}")/toplingdb.yaml" conf/toplingdb.yaml diff --git a/docker/test-compose.sh b/docker/test-compose.sh index 54418d413a..e3fb826aa3 100644 --- a/docker/test-compose.sh +++ b/docker/test-compose.sh @@ -40,10 +40,13 @@ RENDER_DIR="" # baseline render shows the Compose default whatever the developer exported; # render_with_timeout swaps in a value. Kept as one array rather than a second # copy of the environment below, so a variable added there reaches both renders. -STARTUP_TIMEOUT_ENV=(-u HG_SERVER_STARTUP_TIMEOUT_S) +STARTUP_TIMEOUT_ENV=() +BUILD_RUNTIME_ENV=() compose_auth() { - env "${STARTUP_TIMEOUT_ENV[@]}" \ + env -u HG_SERVER_STARTUP_TIMEOUT_S -u RUNTIME_VARIANT \ + -u TOPLING_JNI_URL -u TOPLING_JNI_SHA256 \ + "${STARTUP_TIMEOUT_ENV[@]}" "${BUILD_RUNTIME_ENV[@]}" \ HUGEGRAPH_VERSION="${VERSION}" \ HUBBLE_IMAGE="${RENDER_HUBBLE_IMAGE}" \ HUGEGRAPH_ADMIN_PASSWORD="${PASSWORD}" \ @@ -385,6 +388,45 @@ hubble_password_helper_check() { "${DOCKER_DIR}/set-hubble-pd-password.sh" hstore "${PD_SECRET}" >/dev/null } +# A published runtime is selected with the existing version parameter. Every +# other setting, including volumes and HStore topology, must render identically. +assert_topling_variants() { + local topology file + for topology in standalone hstore ha; do + case "${topology}" in + standalone) file=docker-compose.yml ;; + hstore) file=docker-compose-hstore.yml ;; + ha) file=docker-compose-3pd-3store-3server.yml ;; + esac + ( + VERSION=ci-version-topling + render "${RENDER_DIR}/${topology}-topling.json" -f "${DOCKER_DIR}/${file}" + ) + jq -e --slurpfile standard "${RENDER_DIR}/${topology}.json" ' + (.services |= with_entries( + if .key != "hubble" then .value.image |= sub("-topling$"; "") + else . end)) | + {services, volumes, networks, name} == + ($standard[0] | {services, volumes, networks, name}) + ' "${RENDER_DIR}/${topology}-topling.json" >/dev/null + done + ( + BUILD_RUNTIME_ENV=(RUNTIME_VARIANT=topling TOPLING_JNI_URL=https://example.invalid/jni.jar + TOPLING_JNI_SHA256=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef) + render "${RENDER_DIR}/dev-topling.json" \ + -f "${DOCKER_DIR}/docker-compose-hstore.yml" \ + -f "${DOCKER_DIR}/docker-compose.dev.yml" + ) + jq -e ' + .services.pd.build.args.RUNTIME_VARIANT == "topling" and + .services.store.build.args.RUNTIME_VARIANT == "topling" and + .services.pd.build.args.TOPLING_JNI_SHA256 == + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" and + .services.server.build.args.RUNTIME_VARIANT == null and + .services.server.environment.HG_SERVER_BACKEND == "hstore" + ' "${RENDER_DIR}/dev-topling.json" >/dev/null +} + run_render() { RENDER_DIR="$(mktemp -d)" trap cleanup EXIT INT TERM @@ -407,6 +449,8 @@ run_render() { assert_dev_override "${RENDER_DIR}/dev.json" \ "${RENDER_DIR}/override.json" + assert_topling_variants + # An absent host value renders the entrypoint default in every topology and # a host value overrides it, so the container budget can be lowered to meet # the healthcheck budget without editing the Compose files. Raising it past diff --git a/docker/test-topling-images.sh b/docker/test-topling-images.sh new file mode 100755 index 0000000000..5badc80ec0 --- /dev/null +++ b/docker/test-topling-images.sh @@ -0,0 +1,215 @@ +#!/usr/bin/env bash +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +set -euo pipefail +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +python3 - "$repo" <<'PY' +import json +import os +from pathlib import Path +import re +import shutil +import subprocess +import sys +import tempfile +import zipfile + +repo = Path(sys.argv[1]) + +# Lock the source/build contracts that do not require a Docker daemon. +bake = (repo / "docker/bake.hcl").read_text() +assert 'condition = RUNTIME_VARIANT == "standard" || RUNTIME_VARIANT == "topling"' in bake +assert 'error_message = "RUNTIME_VARIANT must be standard or topling"' in bake +standalone = (repo / "hugegraph-server/Dockerfile").read_text() +assert "mv ./conf/graphs ./rocksdb-data/.hugegraph-config/graphs" in standalone +assert "ln -s ../rocksdb-data/.hugegraph-config/graphs ./conf/graphs" in standalone +assert "cp -a ./conf/graphs ./conf/graphs.default" in standalone +assert "VOLUME /hugegraph-server/rocksdb-data" in standalone +for generic in (repo / "install-dist/release-docs/LICENSE", + repo / "install-dist/release-docs/NOTICE"): + assert "ToplingDB JNI" not in generic.read_text(), generic +assert not (repo / "install-dist/release-docs/licenses/LICENSE-toplingdb-jni-8.10.2.txt").exists() +inventory = (repo / "install-dist/scripts/dependency/optional-image-dependencies.txt").read_text() +for entry in ("installed_license_directory=topling/licenses/", + "installed_license_summary=topling/LICENSE", + "installed_notice=topling/NOTICE"): + assert entry in inventory, entry + +# Execute the actual RUN chain with failures injected after payload creation. +# Ownership adjustment must never hide a failed capability/license preparation. +for dockerfile in ("hugegraph-server/Dockerfile", "hugegraph-pd/Dockerfile", "hugegraph-store/Dockerfile"): + commands = re.findall(r"(?m)^RUN (.*(?:\n[ \t].*)*)", (repo / dockerfile).read_text()) + preparation = next(command for command in commands if "bash /tmp/prepare-topling-image.sh" in command) + preparation = re.sub(r"--mount=\S+\s*", "", preparation.replace("\\\n", " ")) + stubs = ('bash() { echo PREPARE; return "$PREPARE_STATUS"; }; ' + 'chown() { echo OWNERSHIP; return "$CHOWN_STATUS"; }; ' + 'rm() { echo CLEANUP; }; ') + for variant, prepare_status, chown_status, expected in ( + ("topling", 17, 0, 17), ("topling", 0, 23, 23), + ("topling", 0, 0, 0), ("standard", 17, 23, 0)): + environment = dict(os.environ, RUNTIME_VARIANT=variant, HG_UID="10001", HG_GID="10001", + PREPARE_STATUS=str(prepare_status), CHOWN_STATUS=str(chown_status)) + result = subprocess.run(["sh", "-c", stubs + preparation], env=environment, + capture_output=True, text=True) + assert result.returncode == expected, (dockerfile, variant, result.returncode, result.stdout) + assert ("CLEANUP" in result.stdout) == (expected == 0), result.stdout + assert ("OWNERSHIP" in result.stdout) == (variant == "topling" and prepare_status == 0) + print(f"PASS: {dockerfile} preserves preparation/ownership failures before non-root runtime") + +docker = shutil.which("docker") +if docker is not None: + buildx = subprocess.run([docker, "buildx", "version"], capture_output=True, text=True) + if buildx.returncode == 0: + invalid = subprocess.run( + [docker, "buildx", "bake", "-f", str(repo / "docker/bake.hcl"), "--print", "pd"], + env={**os.environ, "RUNTIME_VARIANT": "toplingdb"}, + capture_output=True, text=True) + assert invalid.returncode != 0, "Bake accepted an invalid runtime variant" + assert "RUNTIME_VARIANT must be standard or topling" in invalid.stderr + invalid.stdout + else: + print("SKIP: Docker buildx is unavailable; Bake validation checked statically") +else: + print("SKIP: Docker CLI is unavailable; Bake validation checked statically") + +required = { + "pd": { + "HG_PD_GRPC_HOST": "pd", + "HG_PD_RAFT_ADDRESS": "pd:8610", + "HG_PD_RAFT_PEERS_LIST": "pd:8610", + "HG_PD_INITIAL_STORE_LIST": "store:8500", + "HG_PD_AUTH_SECRET_KEY": "test-secret", + }, + "store": { + "HG_STORE_PD_ADDRESS": "pd:8686", + "HG_STORE_GRPC_HOST": "store", + "HG_STORE_RAFT_ADDRESS": "store:8510", + }, +} +with tempfile.TemporaryDirectory(prefix="topling-entrypoints-") as temporary: + for component, settings in required.items(): + directory = Path(temporary) / component + (directory / "bin").mkdir(parents=True) + entrypoint = repo / f"hugegraph-{component}/hg-{component}-dist/docker/docker-entrypoint.sh" + shutil.copy(entrypoint, directory / "entrypoint.sh") + launcher = directory / f"bin/start-hugegraph-{component}.sh" + launcher.write_text('#!/bin/bash\nprintf \'%s\' "$SPRING_APPLICATION_JSON" > spring.json\n') + launcher.chmod(0o755) + environment = dict(os.environ, **settings) + environment.pop("TOPLINGDB_ROCKSDB_PROVIDER", None) + for provider in (None, "rocksdb", "topling"): + if provider is not None: + environment["TOPLINGDB_ROCKSDB_PROVIDER"] = provider + result = subprocess.run(["bash", "entrypoint.sh"], cwd=directory, env=environment, + capture_output=True, text=True) + assert result.returncode == 0, result.stderr + document = json.loads((directory / "spring.json").read_text()) + if provider is None: + assert "rocksdb" not in document, "default must preserve mounted provider" + assert "rocksdb.provider=" in result.stdout + assert "rocksdb.provider=rocksdb" not in result.stdout + else: + assert document["rocksdb"]["provider"] == provider, document + assert f"rocksdb.provider={provider}" in result.stdout + (directory / "spring.json").unlink() + environment["TOPLINGDB_ROCKSDB_PROVIDER"] = 'topling", "unexpected":true' + result = subprocess.run(["bash", "entrypoint.sh"], cwd=directory, env=environment, + capture_output=True, text=True) + assert result.returncode != 0 and not (directory / "spring.json").exists() + print(f"PASS: {component} provider selection, default preservation and invalid input") + + environment = dict(os.environ) + environment.pop("TOPLING_JNI_SHA256", None) + environment.pop("TOPLING_JNI_URL", None) + helper = repo / "docker/prepare-topling-image.sh" + for values in ({}, {"TOPLING_JNI_SHA256": "not-a-sha"}, + {"TOPLING_JNI_SHA256": "a" * 64, "TOPLING_JNI_URL": "http://example.invalid/jni.jar"}): + result = subprocess.run(["bash", str(helper)], cwd=temporary, env=dict(environment, **values), + capture_output=True, text=True) + assert result.returncode != 0, "unverified artifact accepted" + assert "Error: Topling builds require" in result.stderr, result.stderr + print("PASS: image preparation rejects missing/untrusted artifact inputs") + + # Exercise the real image helper through preparation, notice extraction and + # profile installation, with only native preparation stubbed. + fixture_jar = Path(temporary) / "rocksdbjni.jar" + with zipfile.ZipFile(fixture_jar, "w") as jar: + jar.writestr("META-INF/licenses/LICENSE.Apache", "Apache fixture\n") + jar.writestr("META-INF/licenses/LICENSE.leveldb", "BSD fixture\n") + jar.writestr("META-INF/licenses/manifest.sha256", "fixture manifest\n") + jar.writestr("META-INF/topling-build-info.txt", "fixture build\n") + + distribution = Path(temporary) / "distribution" + (distribution / "bin").mkdir(parents=True) + (distribution / "conf").mkdir() + (distribution / "tools").mkdir() + original_profile = repo / "hugegraph-server/hugegraph-dist/src/assembly/static/conf/toplingdb.yaml" + original_bytes = original_profile.read_bytes() + shutil.copy(original_profile, distribution / "conf/toplingdb.yaml") + preparer = distribution / "bin/prepare-topling.sh" + preparer.write_text("#!/bin/bash\n" + "test -r \"$TOPLING_JNI_JAR\" && test \"${#TOPLING_JNI_SHA256}\" -eq 64\n" + "mkdir -p topling\n" + "cp \"$TOPLING_JNI_JAR\" topling/rocksdbjni.jar\n") + downloader = distribution / "tools/curl" + downloader.write_text("#!/bin/bash\n" + "while (( $# )); do\n" + " if [[ $1 == --output ]]; then cp \"$FIXTURE_JAR\" \"$2\"; exit 0; fi\n" + " shift\n" + "done\n" + "exit 1\n") + downloader.chmod(0o755) + environment.update(TOPLING_JNI_SHA256="a" * 64, + TOPLING_JNI_URL="https://example.invalid/immutable.jar", + FIXTURE_JAR=str(fixture_jar)) + environment["PATH"] = str(distribution / "tools") + os.pathsep + environment["PATH"] + result = subprocess.run(["bash", str(helper)], cwd=distribution, env=environment, + capture_output=True, text=True) + assert result.returncode == 0, result.stderr + community_profile = repo / "docker/toplingdb.yaml" + assert (distribution / "conf/toplingdb.yaml").read_bytes() == community_profile.read_bytes() + assert original_profile.read_bytes() == original_bytes, "distribution source profile changed" + assert (distribution / "topling/LICENSE").is_file() + assert (distribution / "topling/NOTICE").is_file() + assert (distribution / "topling/licenses/LICENSE.Apache").read_text() == "Apache fixture\n" + assert (distribution / "topling/licenses/LICENSE.leveldb").read_text() == "BSD fixture\n" + assert (distribution / "topling/licenses/manifest.sha256").is_file() + assert "ToplingDB JNI community runtime" in (distribution / "topling/NOTICE").read_text() + + profile = community_profile.read_text() + # This profile contains only indentation-based mappings. Reject duplicate + # paths instead of silently letting the YAML reader discard an earlier key. + seen = set() + parents = [] + for line in profile.splitlines(): + match = re.match(r"^( *)([A-Za-z_][\w]*):", line) + if match is None: + continue + indent, key = len(match[1]), match[2] + while parents and parents[-1][0] >= indent: + parents.pop() + path = tuple(parent[1] for parent in parents) + (key,) + assert path not in seen, "duplicate YAML key: " + ".".join(path) + seen.add(path) + parents.append((indent, key)) + classes = set(re.findall(r"^\s+class:\s*(\S+)$", profile, re.MULTILINE)) + assert classes == {"LRUCache", "SkipList", "BlockBasedTable"}, classes + assert "CSPP" not in profile and "ToplingZip" not in profile + for setting in ("capacity: 64M", "write_buffer_size: 16M", "max_write_buffer_number: 3", + "memtable_as_log_index: false", "auto_start_http: false"): + assert setting in profile, setting + print("PASS: community image installs notices, small native profile and preserves full distribution profile") +PY diff --git a/docker/topling-runtime.env b/docker/topling-runtime.env new file mode 100644 index 0000000000..fab1bcaff3 --- /dev/null +++ b/docker/topling-runtime.env @@ -0,0 +1,21 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# Community JNI: no private ToplingZip builder, CSPP MemTable or dcompact. +# Native probe and provenance: hugegraph/toplingdb Actions run 39969281764. +TOPLING_JNI_URL=https://github.com/hugegraph/toplingdb/releases/download/jni-2916f6895c62e535a03985d8e9b0879e64304715/rocksdbjni-8.10.2-linux64.jar +TOPLING_JNI_SHA256=c9066873d5eb38585517c0c4065565897aab96629585731b6fb36d64b81efef2 diff --git a/docker/toplingdb.yaml b/docker/toplingdb.yaml new file mode 100644 index 0000000000..5c0ee4226e --- /dev/null +++ b/docker/toplingdb.yaml @@ -0,0 +1,53 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Community image defaults. Memory grows with databases and column families; +# these are individual native budgets, not a process memory limit. +http: + auto_start_http: false +Cache: + lru_cache: + class: LRUCache + params: + capacity: 64M +MemTableRepFactory: + skiplist: + class: SkipList + params: + lookahead: 0 +TableFactory: + block_based: + class: BlockBasedTable + params: + block_cache: "${lru_cache}" +CFOptions: + default: + memtable_factory: "${skiplist}" + table_factory: block_based + write_buffer_size: 16M + max_write_buffer_number: 3 + merge_operator: uint64add +DBOptions: + log: + create_if_missing: true + create_missing_column_families: true + default: + create_if_missing: true + create_missing_column_families: false + max_background_jobs: 2 + # Java WriteBatch does not carry Topling's mmap WAL references. + memtable_as_log_index: false diff --git a/docs/storage-lifecycle.md b/docs/storage-lifecycle.md index ddf7731a0a..52c81f029b 100644 --- a/docs/storage-lifecycle.md +++ b/docs/storage-lifecycle.md @@ -495,6 +495,70 @@ For a distributed graph, Server uses `backend=hstore`; PD and Store own its pers storage. A standalone Server keeps `backend=rocksdb`. The launcher selection and the business provider must agree. A mismatch fails before opening that component's database. +### Docker, Compose and Helm + +Prepared image variants reuse the normal deployment mechanisms. Select +`hugegraph/hugegraph:latest-topling` for a standalone Server, or the matching +`hugegraph/pd`, `hugegraph/store` and `hugegraph/server` tags for distributed +HStore. Published versions use `-topling`. Topling images support Linux +amd64 and include the verified community JNI runtime; startup does not download +dependencies. Source image builds select it with `RUNTIME_VARIANT=topling` and use +the immutable release URL and trusted SHA-256 in +[`docker/topling-runtime.env`](../docker/topling-runtime.env). Custom JNI URL/checksum +build inputs are optional overrides. The unpacked-distribution preparation below +still requires an explicit JAR and its trusted checksum. + +Compose uses its existing `HUGEGRAPH_VERSION=latest-topling` parameter. Helm uses +`--set global.runtimeVariant=topling` to select matching images and amd64 scheduling. +The HStore Server keeps `backend=hstore` and uses the Topling-enabled PD and Store; +it does not load a local Topling runtime. Explicit `TOPLINGDB_ROCKSDB_PROVIDER` +selection stays above retained PD-managed Store configuration without rewriting +that PD configuration. Newly created local RocksDB graphs inherit the selected +image provider only when the request omits `rocksdb.provider`; explicit graph +values and HStore backends remain unchanged. See the [Compose guide](../docker/README.md#toplingdb-images) +and [Helm guide](../helm/hugegraph/README.md#toplingdb-runtime) for complete commands, +source builds, custom registries and immutable image digests. + +Validate a new deployment first, then qualify a normal stop and image switch using +existing RocksDB data. With the default community profile, Topling uses standard +BlockBasedTable SSTs; a compatible existing database can be opened directly, without +graph API export/import. Preserve the Compose project or Helm release, credentials, +data/WAL paths and named volumes/PVCs. The chart permits a runtime change while +retaining its storage and Raft membership protections. The images keep binaries and +JNI outside Docker data volumes so a Compose image change loads the selected runtime. + +The image publication gate checks standard-to-Topling compatibility with standard +images built from the same HugeGraph source and the pinned community JNI (currently +RocksDB API 8.10.2). It checks real native-library selection, persisted data, schema, +indexes, continued writes and normal restart. This does not establish compatibility +between arbitrary HugeGraph/RocksDB releases or custom native table formats. Use a +matching version pair, retain a backup and qualify representative data. Topling +Latest is manual and may lag standard Latest; formal Release publishes both variants +from one Apache HugeGraph commit. + +For an older custom Docker deployment with data stored only in an anonymous volume +covering the entire installation, preserve its database directories explicitly before +changing to data-only mounts. Standalone images now link `conf/graphs` to +`rocksdb-data/.hugegraph-config/graphs`, so the supplied Compose data volume keeps +API-created graph definitions across replacement. When an old nonempty volume or +bind mount lacks that directory, the entrypoint seeds it from `conf/graphs.default` +without replacing existing definitions or changing database files. Explicit +configuration mounts keep precedence. Custom definitions and settings that existed +only in the older installation still need one-time backup and a writable working +configuration mount before the first replacement; database files cannot recreate +those missing definitions. See the linked Compose guide for that upgrade path. + +Both image variants explicitly select their provider. The entrypoint synchronizes +`rocksdb.provider` in all persisted or writable mounted `backend=rocksdb` graph +files to that selection, including when returning to the unsuffixed standard +image. Files whose provider already matches are not written; an absent property +defaults to `rocksdb`. Matching read-only graph mounts can start, while a required +provider change fails with the graph path and current/selected providers. Use a +writable directory or supply a matching configuration before switching; see the +[Compose guide](../docker/README.md) for configuration-mount constraints. +Other settings and `backend=hstore` definitions are preserved. PD/Store +apply the selected provider through their runtime configuration overrides. + ### Standalone Server setup ![Switch RocksDB to ToplingDB](images/switch-rocksdb-to-toplingdb.png) @@ -656,9 +720,12 @@ Stop incoming work and use the component's normal stop script. PD drains its sch metadata work, joins Raft and closes the metadata database and native options. Do not use a forced kill as evidence of normal resource cleanup. -To return to standard RocksDB, stop the component, unset `TOPLINGDB_ROCKSDB_PROVIDER` -and set its business provider back to `rocksdb`. Use the corresponding standard-provider -data directory; do not test a provider switch by reusing the other provider's database. +To return to standard RocksDB, stop the component normally, unset +`TOPLINGDB_ROCKSDB_PROVIDER` and set its business provider back to `rocksdb`. Keep +the same qualified data and WAL paths when switching the default BlockBasedTable +profile. Native profiles that produce custom SST formats need separate compatibility +validation before returning to a standard runtime. For containers, select the +matching unsuffixed image instead; it configures the launcher and provider together. ## Implementation navigation diff --git a/helm/hugegraph/README.md b/helm/hugegraph/README.md index e771e09c9e..5aceff1e8f 100644 --- a/helm/hugegraph/README.md +++ b/helm/hugegraph/README.md @@ -97,6 +97,60 @@ Store's wait init container and Helm test reuse the Store and Server images, which already contain `curl`; no separate curl image is needed by default. See the [complete registry example](examples/values-internal-registry.yaml). +### ToplingDB runtime + +Switch an existing release to ToplingDB with one value while keeping its values +and persistent volumes: + +```bash +helm upgrade hugegraph ./helm/hugegraph -n hugegraph --reuse-values \ + --set global.runtimeVariant=topling --wait --timeout 15m +``` + +Use matching standard/Topling images of the same HugeGraph version. Development +`latest` and `latest-topling` tags are published independently and may refer to +different source revisions; select a matching build before changing the runtime. +For a new installation, use the same runtime parameter: + +```bash +helm install hugegraph-topling ./helm/hugegraph -n hugegraph-topling \ + --create-namespace --set global.runtimeVariant=topling --wait --timeout 15m +``` + +This selects the matching `-topling` image tags for PD, Store and HStore Server, +including the Store wait container and Helm test image. PD and Store own the +ToplingDB databases; Server continues to use `backend=hstore`. Hubble keeps its +independent image. Linux amd64 scheduling is added to these HugeGraph workloads +and the test hook. Contradictory OS/architecture node selectors are rejected; +other scheduling constraints are preserved. The deployment images already include +the verified community JNI; no artifact URL or checksum is needed in Helm values. +For source image builds, `RUNTIME_VARIANT=topling` uses the release pinned in +[`docker/topling-runtime.env`](../../docker/topling-runtime.env). + +The current JNI requires a Haswell-compatible x86-64 CPU, including AVX2 and BMI2. +The architecture selector does not inspect CPU extensions; use a compatible node pool. + +A component tag such as `1.8.0` becomes `1.8.0-topling`; tags already ending in +`-topling` are kept. Custom repositories and `global.imageRegistry` work as usual. +In `global.runtimeVariant=topling`, clear `pd.image.digest`, `store.image.digest` +and `server.image.digest`; the chart rejects these overrides because a digest +would bypass the derived `-topling` tag. Use matching version tags for this runtime. +Variant-specific immutable digest fields are not currently supported. In standard +mode an explicit digest still takes precedence over the tag. Explicit helper image +overrides remain exact. + +The default `global.runtimeVariant=standard` keeps RocksDB. The verified stock +RocksDB 8.10.2 and pinned community ToplingDB 8.10.2 use compatible data: the +upgrade retains PD/Store data and Raft volumes, schema, indexes and graph records. +Keep the release name, namespace, data paths, storage settings and credentials; +no graph export or new release is needed. Allow normal Pod shutdown during the +maintenance window. The same verified pair also supports returning to RocksDB: +repeat the upgrade with `global.runtimeVariant=standard`, keeping the same PVCs. +Use base image tags without the `-topling` suffix, or update explicit digests to +the matching RocksDB images. See [storage runtime upgrades](docs/operations.md#storage-runtime-upgrades) +for rollout behavior and the [storage runtime guide](../../docs/storage-lifecycle.md#storage-provider-selection-and-toplingdb) +for the verified native versions, profile and compatibility coverage. + ### Storage Use a named dynamic StorageClass when the cluster has no suitable default: @@ -166,7 +220,7 @@ Use `--set mode=strict` or `-f helm/hugegraph/values-strict.yaml`. **Explicit component value > global value > mode default.** An explicit `false` is preserved; null component values inherit. The global settings are -`persistence.enabled` and `global.imageRegistry`. No mode requires HTTPS. +`persistence.enabled`, `global.imageRegistry` and `global.runtimeVariant`. No mode requires HTTPS. | Entry | Access | When to use it | |---|---|---| @@ -204,6 +258,7 @@ with `-f`; see [values.yaml](values.yaml) for all fields and their comments. | `pd.service.type` | PD client entry type | `ClusterIP` | | `*.service.nodePort` | Fixed Server/Hubble port; omitted/0 allocates automatically | unset | | `global.imageRegistry` | Prefix image repositories without an explicit registry | `""` | +| `global.runtimeVariant` | `standard` RocksDB or Linux amd64 `topling` deployment images | `standard` | | `*.image.repository` / `tag` / `digest` | Component image; digest takes priority | see values | | `persistence.enabled` | Global persistent storage | `true` | | `pd.storage.enabled` / `store.storage.enabled` / `hubble.persistence.enabled` | Explicit component storage override | `null` (inherits) | diff --git a/helm/hugegraph/docs/operations.md b/helm/hugegraph/docs/operations.md index b12fc95873..0e8ff8ab7e 100644 --- a/helm/hugegraph/docs/operations.md +++ b/helm/hugegraph/docs/operations.md @@ -211,6 +211,37 @@ Server replicas can be changed routinely; CPU-utilization HPA requires positive CPU requests. Store growth still needs volume and shard planning. Use chart updates for Store scale changes so Hubble's endpoint list is updated too. +## Storage runtime upgrades + +For the verified stock RocksDB 8.10.2 and pinned community ToplingDB 8.10.2, +change `global.runtimeVariant` with the ordinary +[Helm upgrade command](../README.md#toplingdb-runtime). The existing PD and Store +StatefulSet names, data paths and PVCs stay the same, including Store Raft data. +The verified pair supports both `standard` to `topling` and a return to `standard` +after Topling writes, using the same volumes. +Use matching HugeGraph image versions and retain the installed credentials and +storage values; runtime selection does not require graph export or initialization. +Development `latest` tags may have different source revisions because they are +published independently. + +The default `RollingUpdate` strategy terminates each Pod normally before its +replacement starts with the same ordinal and PVC. `podManagementPolicy: Parallel` +controls creation and scaling, not parallel rolling replacement within a +StatefulSet. PD and Store have separate controllers, so plan the change in a +maintenance window; this does not promise uninterrupted requests. Keep enough +termination grace for database shutdown and do not force-delete the old Pods. +The existing [Store rollout and recovery checks](#rolling-store-updates-and-recovery) +still apply. + +The compatibility scope is the pinned community profile, which uses SkipList and +BlockBasedTable. Other JNI versions and custom SST formats need their own +compatibility evidence. For `global.runtimeVariant=topling`, clear the PD, Store +and Server `image.digest` values and use matching `-topling` version tags; the +chart rejects those digest overrides instead of silently running the wrong +runtime. Standard mode retains exact digest support. See the +[storage runtime guide](../../../docs/storage-lifecycle.md#storage-provider-selection-and-toplingdb) +for the tested native versions and data coverage. + ## Rolling Store updates and recovery Use `store.updateStrategy.type=OnDelete` for a controlled rollout. Replace one diff --git a/helm/hugegraph/templates/_helpers.tpl b/helm/hugegraph/templates/_helpers.tpl index b59a3fb9c2..40c1f668ae 100644 --- a/helm/hugegraph/templates/_helpers.tpl +++ b/helm/hugegraph/templates/_helpers.tpl @@ -45,6 +45,27 @@ {{- $_ := set $component $key $storage -}} {{- if or (not (hasKey $storage "enabled")) (eq (get $storage "enabled" | toString) "") -}}{{- $_ := set $storage "enabled" $persistent -}}{{- end -}} {{- end -}} +{{/* The runtime is an image variant. Keep business/JNI configuration in the image. */}} +{{- $variant := get (.Values.global | default dict) "runtimeVariant" | default "standard" -}} +{{- if not (has $variant (list "standard" "topling")) -}} +{{- fail "global.runtimeVariant must be standard or topling" -}} +{{- end -}} +{{- if eq $variant "topling" -}} +{{- range $comp := list "pd" "store" "server" -}} +{{- $component := get $.Values $comp -}} +{{- $image := $component.image -}} +{{- $tag := $image.tag | default $.Chart.AppVersion -}} +{{- if not (hasSuffix "-topling" $tag) -}}{{- $_ := set $image "tag" (printf "%s-topling" $tag) -}}{{- end -}} +{{- $selector := get $component "nodeSelector" | default dict -}} +{{- range $key, $required := dict "kubernetes.io/arch" "amd64" "kubernetes.io/os" "linux" -}} +{{- if and (hasKey $selector $key) (ne (get $selector $key) $required) -}} +{{- fail (printf "%s.nodeSelector.%s must be %s for global.runtimeVariant=topling" $comp $key $required) -}} +{{- end -}} +{{- $_ := set $selector $key $required -}} +{{- end -}} +{{- $_ := set $component "nodeSelector" $selector -}} +{{- end -}} +{{- end -}} {{- $registry := trimSuffix "/" (get (.Values.global | default dict) "imageRegistry" | default "") -}} {{- if contains "://" $registry -}}{{- fail "global.imageRegistry must be a registry host/path without http:// or https://" -}}{{- end -}} {{- if $registry -}} diff --git a/helm/hugegraph/templates/pd-statefulset.yaml b/helm/hugegraph/templates/pd-statefulset.yaml index d1603e319d..b72f3df928 100644 --- a/helm/hugegraph/templates/pd-statefulset.yaml +++ b/helm/hugegraph/templates/pd-statefulset.yaml @@ -20,6 +20,8 @@ apiVersion: apps/v1 kind: StatefulSet metadata: name: {{ include "hugegraph.pd.name" . }} + annotations: + hugegraph.apache.org/rocksdb-runtime: {{ get (.Values.global | default dict) "runtimeVariant" | default "standard" | quote }} labels: {{- include "hugegraph.labels" . | nindent 4 }} app.kubernetes.io/component: pd diff --git a/helm/hugegraph/templates/runtime-variant-validation.yaml b/helm/hugegraph/templates/runtime-variant-validation.yaml new file mode 100644 index 0000000000..ca88ce4325 --- /dev/null +++ b/helm/hugegraph/templates/runtime-variant-validation.yaml @@ -0,0 +1,26 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +{{- include "hugegraph.resolveValues" . -}} +{{- if eq (get (.Values.global | default dict) "runtimeVariant" | default "standard") "topling" -}} +{{- range $component := list "pd" "store" "server" -}} +{{- $image := get (get $.Values $component) "image" | default dict -}} +{{- if ne (trim (get $image "digest" | default "")) "" -}} +{{- fail (printf "%s.image.digest cannot be used with global.runtimeVariant=topling: digest overrides the selected -topling tag. Clear the digest and use a matching version tag; variant-specific immutable digests require an explicit chart model" $component) -}} +{{- end -}} +{{- end -}} +{{- end -}} diff --git a/helm/hugegraph/templates/store-statefulset.yaml b/helm/hugegraph/templates/store-statefulset.yaml index 75ea6f5ba3..7693b53415 100644 --- a/helm/hugegraph/templates/store-statefulset.yaml +++ b/helm/hugegraph/templates/store-statefulset.yaml @@ -20,6 +20,8 @@ apiVersion: apps/v1 kind: StatefulSet metadata: name: {{ include "hugegraph.store.name" . }} + annotations: + hugegraph.apache.org/rocksdb-runtime: {{ get (.Values.global | default dict) "runtimeVariant" | default "standard" | quote }} labels: {{- include "hugegraph.labels" . | nindent 4 }} app.kubernetes.io/component: store diff --git a/helm/hugegraph/templates/tests/test-connection.yaml b/helm/hugegraph/templates/tests/test-connection.yaml index 020c484f6a..be800d0915 100644 --- a/helm/hugegraph/templates/tests/test-connection.yaml +++ b/helm/hugegraph/templates/tests/test-connection.yaml @@ -45,6 +45,11 @@ metadata: # run replaces it, so both forms exit 0 and a failed Pod keeps its logs. "helm.sh/hook-delete-policy": before-hook-creation spec: + {{- if eq (get (.Values.global | default dict) "runtimeVariant" | default "standard") "topling" }} + nodeSelector: + kubernetes.io/arch: amd64 + kubernetes.io/os: linux + {{- end }} {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 4 }} diff --git a/helm/hugegraph/tests/persistence_upgrade_persistent_test.yaml b/helm/hugegraph/tests/persistence_upgrade_persistent_test.yaml index 378ae5705c..3698581755 100644 --- a/helm/hugegraph/tests/persistence_upgrade_persistent_test.yaml +++ b/helm/hugegraph/tests/persistence_upgrade_persistent_test.yaml @@ -113,3 +113,12 @@ tests: asserts: - isKind: of: Deployment + + - it: accepts changing runtime while keeping persistent storage + release: + upgrade: true + set: + global.runtimeVariant: topling + asserts: + - isKind: + of: Deployment diff --git a/helm/hugegraph/tests/runtime_upgrade_standard_test.yaml b/helm/hugegraph/tests/runtime_upgrade_standard_test.yaml new file mode 100644 index 0000000000..02491e1189 --- /dev/null +++ b/helm/hugegraph/tests/runtime_upgrade_standard_test.yaml @@ -0,0 +1,109 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +suite: Retained RocksDB volumes on Topling upgrades +templates: + - pd-statefulset.yaml + - store-statefulset.yaml +kubernetesProvider: + scheme: + "apps/v1/StatefulSet": + gvr: + group: apps + version: v1 + resource: statefulsets + namespaced: true + objects: + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: RELEASE-NAME-hugegraph-pd + namespace: NAMESPACE + spec: + replicas: "3" + volumeClaimTemplates: + - metadata: + name: pd-data + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: RELEASE-NAME-hugegraph-store + namespace: NAMESPACE + spec: + replicas: "3" + volumeClaimTemplates: + - metadata: + name: store-data +tests: + - it: changes PD to Topling while retaining its existing claim and data mount + template: pd-statefulset.yaml + release: + upgrade: true + set: + global.runtimeVariant: topling + asserts: + - equal: + path: metadata.name + value: RELEASE-NAME-hugegraph-pd + - equal: + path: spec.template.spec.containers[0].image + value: hugegraph/pd:latest-topling + - equal: + path: spec.volumeClaimTemplates[0].metadata.name + value: pd-data + - equal: + path: spec.template.spec.containers[0].volumeMounts[0] + value: + name: pd-data + mountPath: /hugegraph-pd/pd_data + - equal: + path: spec.replicas + value: 3 + - equal: + path: spec.updateStrategy.type + value: RollingUpdate + - equal: + path: spec.template.spec.terminationGracePeriodSeconds + value: 300 + + - it: changes Store to Topling while retaining its existing claim and data mount + template: store-statefulset.yaml + release: + upgrade: true + set: + global.runtimeVariant: topling + asserts: + - equal: + path: metadata.name + value: RELEASE-NAME-hugegraph-store + - equal: + path: spec.template.spec.containers[0].image + value: hugegraph/store:latest-topling + - equal: + path: spec.volumeClaimTemplates[0].metadata.name + value: store-data + - equal: + path: spec.template.spec.containers[0].volumeMounts[0] + value: + name: store-data + mountPath: /hugegraph-store/storage + - equal: + path: spec.replicas + value: 3 + - equal: + path: spec.updateStrategy.type + value: RollingUpdate diff --git a/helm/hugegraph/tests/runtime_upgrade_test.yaml b/helm/hugegraph/tests/runtime_upgrade_test.yaml new file mode 100644 index 0000000000..e92e9efdda --- /dev/null +++ b/helm/hugegraph/tests/runtime_upgrade_test.yaml @@ -0,0 +1,121 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +suite: Retained Topling volumes on runtime upgrades +templates: + - pd-statefulset.yaml + - store-statefulset.yaml +kubernetesProvider: + scheme: + "apps/v1/StatefulSet": + gvr: + group: apps + version: v1 + resource: statefulsets + namespaced: true + objects: + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: RELEASE-NAME-hugegraph-pd + namespace: NAMESPACE + annotations: + hugegraph.apache.org/rocksdb-runtime: topling + spec: + replicas: "3" + volumeClaimTemplates: + - metadata: + name: pd-data + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: RELEASE-NAME-hugegraph-store + namespace: NAMESPACE + annotations: + hugegraph.apache.org/rocksdb-runtime: topling + spec: + replicas: "3" + volumeClaimTemplates: + - metadata: + name: store-data +tests: + - it: returns PD to RocksDB with the same persistent volume and rollout settings + template: pd-statefulset.yaml + release: + upgrade: true + asserts: + - equal: + path: metadata.name + value: RELEASE-NAME-hugegraph-pd + - equal: + path: spec.template.spec.containers[0].image + value: hugegraph/pd:latest + - equal: + path: spec.volumeClaimTemplates[0].metadata.name + value: pd-data + - equal: + path: spec.template.spec.containers[0].volumeMounts[0] + value: + name: pd-data + mountPath: /hugegraph-pd/pd_data + - equal: + path: spec.replicas + value: 3 + - equal: + path: spec.updateStrategy.type + value: RollingUpdate + - equal: + path: spec.template.spec.terminationGracePeriodSeconds + value: 300 + + - it: returns Store to RocksDB with the same persistent volume + template: store-statefulset.yaml + release: + upgrade: true + asserts: + - equal: + path: metadata.name + value: RELEASE-NAME-hugegraph-store + - equal: + path: spec.template.spec.containers[0].image + value: hugegraph/store:latest + - equal: + path: spec.volumeClaimTemplates[0].metadata.name + value: store-data + - equal: + path: spec.template.spec.containers[0].volumeMounts[0] + value: + name: store-data + mountPath: /hugegraph-store/storage + - equal: + path: spec.replicas + value: 3 + - equal: + path: spec.updateStrategy.type + value: RollingUpdate + + - it: accepts an unchanged persistent Topling upgrade + release: + upgrade: true + set: + global.runtimeVariant: topling + asserts: + - isKind: + of: StatefulSet + - equal: + path: metadata.annotations["hugegraph.apache.org/rocksdb-runtime"] + value: topling diff --git a/helm/hugegraph/tests/runtime_variant_digest_test.yaml b/helm/hugegraph/tests/runtime_variant_digest_test.yaml new file mode 100644 index 0000000000..9909f32116 --- /dev/null +++ b/helm/hugegraph/tests/runtime_variant_digest_test.yaml @@ -0,0 +1,44 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +suite: Topling runtime digest selection +templates: + - runtime-variant-validation.yaml +tests: + - it: rejects a digest that would override the selected PD Topling tag + set: + global.runtimeVariant: topling + pd.image.digest: sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + asserts: + - failedTemplate: + errorPattern: "pd.image.digest cannot be used with global.runtimeVariant=topling" + + - it: rejects a digest that would override the selected Store Topling tag + set: + global.runtimeVariant: topling + store.image.digest: sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + asserts: + - failedTemplate: + errorPattern: "store.image.digest cannot be used with global.runtimeVariant=topling" + + - it: rejects a digest that would override the selected Server companion tag + set: + global.runtimeVariant: topling + server.image.digest: sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc + asserts: + - failedTemplate: + errorPattern: "server.image.digest cannot be used with global.runtimeVariant=topling" diff --git a/helm/hugegraph/tests/runtime_variant_test.yaml b/helm/hugegraph/tests/runtime_variant_test.yaml new file mode 100644 index 0000000000..a71f66c172 --- /dev/null +++ b/helm/hugegraph/tests/runtime_variant_test.yaml @@ -0,0 +1,139 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +suite: Topling runtime image selection +templates: + - pd-statefulset.yaml + - store-statefulset.yaml + - server-deployment.yaml + - hubble-deployment.yaml + - tests/test-connection.yaml +tests: + - it: selects the matching PD runtime image and architecture + template: pd-statefulset.yaml + set: + global.runtimeVariant: topling + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: hugegraph/pd:latest-topling + - equal: + path: spec.template.spec.nodeSelector + value: + kubernetes.io/arch: amd64 + kubernetes.io/os: linux + - equal: + path: metadata.annotations["hugegraph.apache.org/rocksdb-runtime"] + value: topling + + - it: reuses the selected Store image for its wait helper + template: store-statefulset.yaml + set: + global.runtimeVariant: topling + store.image.tag: 1.8.0 + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: hugegraph/store:1.8.0-topling + - equal: + path: spec.template.spec.initContainers[0].image + value: hugegraph/store:1.8.0-topling + - equal: + path: spec.template.spec.nodeSelector["kubernetes.io/arch"] + value: amd64 + + - it: selects the matching HStore Server image without changing the backend + template: server-deployment.yaml + set: + global.runtimeVariant: topling + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: hugegraph/server:latest-topling + - equal: + path: spec.template.spec.nodeSelector["kubernetes.io/arch"] + value: amd64 + - contains: + path: spec.template.spec.containers[0].env + content: + name: HG_SERVER_BACKEND + value: hstore + + - it: keeps the independent Hubble image + template: hubble-deployment.yaml + set: + global.runtimeVariant: topling + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: hugegraph/hubble:latest + - notExists: + path: spec.template.spec.nodeSelector + + - it: schedules the inherited Helm test image on Linux amd64 + template: tests/test-connection.yaml + set: + global.runtimeVariant: topling + asserts: + - equal: + path: spec.containers[0].image + value: hugegraph/server:latest-topling + - equal: + path: spec.nodeSelector + value: + kubernetes.io/arch: amd64 + kubernetes.io/os: linux + + - it: keeps exact digest references and registry overrides + template: pd-statefulset.yaml + set: + global.runtimeVariant: topling + global.imageRegistry: registry.example.com + pd.image.digest: sha256:43999a5ccda34883a9e4e458e25cb903ce34adbc66782d9f4a5260d68b2b5a82 + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: registry.example.com/hugegraph/pd@sha256:43999a5ccda34883a9e4e458e25cb903ce34adbc66782d9f4a5260d68b2b5a82 + + - it: keeps a custom repository and an already suffixed tag + template: store-statefulset.yaml + set: + global.runtimeVariant: topling + global.imageRegistry: ignored.example.com + store.image.repository: custom.example.com/team/store + store.image.tag: custom-topling + store.nodeSelector: + workload: graph + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: custom.example.com/team/store:custom-topling + - equal: + path: spec.template.spec.nodeSelector.workload + value: graph + + - it: keeps standard images and unrestricted architecture by default + template: pd-statefulset.yaml + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: hugegraph/pd:latest + - notExists: + path: spec.template.spec.nodeSelector + - equal: + path: metadata.annotations["hugegraph.apache.org/rocksdb-runtime"] + value: standard diff --git a/helm/hugegraph/tests/validate_values_test.yaml b/helm/hugegraph/tests/validate_values_test.yaml index 7f83a4779d..c9db767109 100644 --- a/helm/hugegraph/tests/validate_values_test.yaml +++ b/helm/hugegraph/tests/validate_values_test.yaml @@ -260,3 +260,12 @@ tests: cidr: 10.0.0.0/8 asserts: - notFailedTemplate: {} + + - it: rejects contradictory architecture scheduling + set: + global.runtimeVariant: topling + pd.nodeSelector: + kubernetes.io/arch: arm64 + asserts: + - failedTemplate: + errorPattern: "pd.nodeSelector.kubernetes.io/arch must be amd64" diff --git a/helm/hugegraph/values.schema.json b/helm/hugegraph/values.schema.json index 987e650c16..267f38a558 100644 --- a/helm/hugegraph/values.schema.json +++ b/helm/hugegraph/values.schema.json @@ -34,6 +34,13 @@ "properties": { "imageRegistry": { "type": "string" + }, + "runtimeVariant": { + "type": "string", + "enum": [ + "standard", + "topling" + ] } } }, diff --git a/helm/hugegraph/values.yaml b/helm/hugegraph/values.yaml index 8ca4a2598a..65eea25580 100644 --- a/helm/hugegraph/values.yaml +++ b/helm/hugegraph/values.yaml @@ -23,6 +23,9 @@ mode: easy global: imageRegistry: "" + # standard: normal RocksDB images; topling: matching -topling tags on Linux amd64. + # Matching compatible images reuse existing data/PVCs after normal shutdown. + runtimeVariant: standard persistence: enabled: true diff --git a/hugegraph-pd/Dockerfile b/hugegraph-pd/Dockerfile index 0017899cda..b484765405 100644 --- a/hugegraph-pd/Dockerfile +++ b/hugegraph-pd/Dockerfile @@ -17,6 +17,9 @@ # # Dockerfile for HugeGraph PD +# Select the native runtime without changing the source build or deployment layout. +ARG RUNTIME_VARIANT=standard + # 1st stage: build source code FROM --platform=$BUILDPLATFORM maven:3.9.16-eclipse-temurin-17 AS build @@ -29,13 +32,45 @@ ARG MAVEN_PROJECTS="hugegraph-server/hugegraph-dist,hugegraph-pd/hg-pd-dist,huge ARG SOURCE_REVISION=local RUN --mount=type=cache,id=hugegraph-maven-${SOURCE_REVISION},target=/root/.m2,sharing=locked \ + --mount=type=secret,id=proxy_ca,required=false \ + --mount=type=secret,id=maven_settings,required=false \ + set -e; \ + if [ -f /run/secrets/proxy_ca ]; then \ + cp "$JAVA_HOME/lib/security/cacerts" /tmp/hugegraph-build-cacerts; \ + keytool -importcert -noprompt -alias hugegraph-build-proxy \ + -file /run/secrets/proxy_ca -keystore /tmp/hugegraph-build-cacerts -storepass changeit; \ + export MAVEN_OPTS="${MAVEN_OPTS:-} -Djavax.net.ssl.trustStore=/tmp/hugegraph-build-cacerts"; \ + fi; \ + if [ -f /run/secrets/maven_settings ]; then \ + MAVEN_ARGS="-s /run/secrets/maven_settings ${MAVEN_ARGS:-}"; \ + fi; \ mvn install -pl "$MAVEN_PROJECTS" \ - -am $MAVEN_ARGS -e -B -ntp -Dmaven.test.skip=true -Dmaven.javadoc.skip=true \ - && rm ./hugegraph-server/*.tar.gz ./hugegraph-pd/*.tar.gz ./hugegraph-store/*.tar.gz + -am $MAVEN_ARGS -e -B -ntp -Dmaven.test.skip=true -Dmaven.javadoc.skip=true; \ + rm -f /tmp/hugegraph-build-cacerts; \ + rm ./hugegraph-server/*.tar.gz ./hugegraph-pd/*.tar.gz ./hugegraph-store/*.tar.gz + +# Optional local JNI directory: --build-context topling-jni=/path/to/directory. +# An empty default stage keeps ordinary and URL-based builds self-contained. +FROM scratch AS topling-jni # 2nd stage: runtime env # Note: ZGC (The Z Garbage Collector) is only supported on ARM-Mac with java > 13 -FROM eclipse-temurin:17-jre-jammy +FROM eclipse-temurin:17-jre-jammy AS standard-base +ENV TOPLINGDB_ROCKSDB_PROVIDER=rocksdb + +# The fork's JNI requires glibc 2.38+ and a Java 17 JDK for its capability probe. +FROM eclipse-temurin:17-jdk-noble AS topling-base +ARG TARGETARCH +RUN test "$TARGETARCH" = amd64 || { echo 'Topling images require linux/amd64' >&2; exit 1; } +ENV TOPLINGDB_ROCKSDB_PROVIDER=topling + +FROM ${RUNTIME_VARIANT}-base AS runtime +ARG RUNTIME_VARIANT +ARG SOURCE_REVISION=local +ARG SOURCE_REPOSITORY=https://github.com/apache/hugegraph +LABEL org.opencontainers.image.source="$SOURCE_REPOSITORY" \ + org.opencontainers.image.revision="$SOURCE_REVISION" \ + org.apache.hugegraph.rocksdb-runtime="$RUNTIME_VARIANT" LABEL maintainer="HugeGraph Docker Maintainers " @@ -51,12 +86,19 @@ WORKDIR /hugegraph-pd/ # from hg-pd-dist bin/util.sh. PD runs no port preflight, so unlike the server # images this needs no socket-table tool in its place. ARG RUNTIME_DEPS_EPOCH=1 -RUN apt-get -q update \ - && apt-get -q install -y --no-install-recommends --no-install-suggests \ +RUN --mount=type=secret,id=proxy_ca,required=false \ + native_deps="" \ + && if [ "$RUNTIME_VARIANT" = topling ]; then \ + native_deps="libaio1t64 libstdc++6 libsnappy1v5 libbz2-1.0 liblz4-1 libzstd1 zlib1g unzip"; \ + fi \ + && apt_ca=/etc/ssl/certs/ca-certificates.crt \ + && if [ -f /run/secrets/proxy_ca ]; then apt_ca=/run/secrets/proxy_ca; fi \ + && apt-get -q -o Acquire::https::CaInfo="$apt_ca" update \ + && apt-get -q -o Acquire::https::CaInfo="$apt_ca" install -y --no-install-recommends --no-install-suggests \ dumb-init \ procps \ curl \ - vim \ + vim ${native_deps} \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* @@ -76,10 +118,25 @@ COPY --from=build --chown=${HG_UID}:${HG_GID} /pkg/hugegraph-pd/apache-hugegraph # 3. Init docker script COPY --chown=${HG_UID}:${HG_GID} --chmod=755 hugegraph-pd/hg-pd-dist/docker/docker-entrypoint.sh . +# Download only during an explicit Topling build, verify and prepare with the +# distribution's normal capability probe. No network access is needed at startup. +ARG TOPLING_JNI_URL +ARG TOPLING_JNI_SHA256 +COPY docker/prepare-topling-image.sh docker/topling-runtime.env docker/toplingdb.yaml /tmp/ +RUN --mount=type=secret,id=proxy_ca,required=false \ + --mount=type=bind,from=topling-jni,target=/run/topling-jni \ + if [ "$RUNTIME_VARIANT" = topling ]; then \ + if [ -f /run/secrets/proxy_ca ]; then export CURL_CA_BUNDLE=/run/secrets/proxy_ca; fi; \ + bash /tmp/prepare-topling-image.sh \ + && chown -R "${HG_UID}:${HG_GID}" topling conf/toplingdb.yaml; \ + fi \ + && rm /tmp/prepare-topling-image.sh /tmp/topling-runtime.env /tmp/toplingdb.yaml + USER ${HG_UID}:${HG_GID} EXPOSE 8620 -VOLUME /hugegraph-pd +# Persist data without hiding a replacement image's binaries and native runtime. +VOLUME /hugegraph-pd/pd_data HEALTHCHECK --interval=15s --timeout=10s --start-period=90s --retries=3 \ CMD curl -fsS http://localhost:8620/v1/health >/dev/null diff --git a/hugegraph-pd/hg-pd-dist/docker/docker-entrypoint.sh b/hugegraph-pd/hg-pd-dist/docker/docker-entrypoint.sh index f3bf1437e1..e8df173a78 100755 --- a/hugegraph-pd/hg-pd-dist/docker/docker-entrypoint.sh +++ b/hugegraph-pd/hg-pd-dist/docker/docker-entrypoint.sh @@ -5,7 +5,7 @@ # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at +# the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # @@ -19,6 +19,12 @@ set -euo pipefail log() { echo "[hugegraph-pd-entrypoint] $*"; } +ROCKSDB_PROVIDER="${TOPLINGDB_ROCKSDB_PROVIDER:-rocksdb}" +case "${ROCKSDB_PROVIDER}" in + rocksdb | topling) ;; + *) log "ERROR: TOPLINGDB_ROCKSDB_PROVIDER must be rocksdb or topling"; exit 2 ;; +esac + require_env() { local name="$1" if [[ -z "${!name:-}" ]]; then @@ -125,10 +131,18 @@ AUTH_JSON="\"auth\": { \"secret-key\": \"$(json_escape "${HG_PD_AUTH_SECRET_KEY} # only way to expose another endpoint from this image. MANAGEMENT_JSON="\"management\": { \"endpoints\": { \"web\": { \"exposure\": { \"include\": \"$(json_escape "${HG_PD_ACTUATOR_EXPOSURE}")\" } } } }," +# Preserve mounted business configuration in the ordinary image. An explicitly +# selected runtime (including the Topling image's default) configures both sides. +ROCKSDB_JSON="" +if [[ -n "${TOPLINGDB_ROCKSDB_PROVIDER:-}" ]]; then + ROCKSDB_JSON="\"rocksdb\": { \"provider\": \"${ROCKSDB_PROVIDER}\" }," +fi + SPRING_APPLICATION_JSON="$(cat <" +fi log " grpc.host=${HG_PD_GRPC_HOST}" log " grpc.port=${HG_PD_GRPC_PORT}" log " server.port=${HG_PD_REST_PORT}" diff --git a/hugegraph-server/Dockerfile b/hugegraph-server/Dockerfile index 912ddf8755..f7d8ded3f0 100644 --- a/hugegraph-server/Dockerfile +++ b/hugegraph-server/Dockerfile @@ -17,6 +17,9 @@ # # Dockerfile for HugeGraph Server +# Select the native runtime without changing the source build or deployment layout. +ARG RUNTIME_VARIANT=standard + # 1st stage: build source code FROM --platform=$BUILDPLATFORM maven:3.9.16-eclipse-temurin-17 AS build @@ -29,13 +32,45 @@ ARG MAVEN_PROJECTS="hugegraph-server/hugegraph-dist,hugegraph-pd/hg-pd-dist,huge ARG SOURCE_REVISION=local RUN --mount=type=cache,id=hugegraph-maven-${SOURCE_REVISION},target=/root/.m2,sharing=locked \ + --mount=type=secret,id=proxy_ca,required=false \ + --mount=type=secret,id=maven_settings,required=false \ + set -e; \ + if [ -f /run/secrets/proxy_ca ]; then \ + cp "$JAVA_HOME/lib/security/cacerts" /tmp/hugegraph-build-cacerts; \ + keytool -importcert -noprompt -alias hugegraph-build-proxy \ + -file /run/secrets/proxy_ca -keystore /tmp/hugegraph-build-cacerts -storepass changeit; \ + export MAVEN_OPTS="${MAVEN_OPTS:-} -Djavax.net.ssl.trustStore=/tmp/hugegraph-build-cacerts"; \ + fi; \ + if [ -f /run/secrets/maven_settings ]; then \ + MAVEN_ARGS="-s /run/secrets/maven_settings ${MAVEN_ARGS:-}"; \ + fi; \ mvn install -pl "$MAVEN_PROJECTS" \ - -am $MAVEN_ARGS -e -B -ntp -Dmaven.test.skip=true -Dmaven.javadoc.skip=true \ - && rm ./hugegraph-server/*.tar.gz ./hugegraph-pd/*.tar.gz ./hugegraph-store/*.tar.gz + -am $MAVEN_ARGS -e -B -ntp -Dmaven.test.skip=true -Dmaven.javadoc.skip=true; \ + rm -f /tmp/hugegraph-build-cacerts; \ + rm ./hugegraph-server/*.tar.gz ./hugegraph-pd/*.tar.gz ./hugegraph-store/*.tar.gz + +# Optional local JNI directory: --build-context topling-jni=/path/to/directory. +# An empty default stage keeps ordinary and URL-based builds self-contained. +FROM scratch AS topling-jni # 2nd stage: runtime env # Note: ZGC (The Z Garbage Collector) is only supported on ARM-Mac with java > 13 -FROM eclipse-temurin:17-jdk-jammy +FROM eclipse-temurin:17-jdk-jammy AS standard-base +ENV TOPLINGDB_ROCKSDB_PROVIDER=rocksdb + +# The fork's JNI requires glibc 2.38+ and a Java 17 JDK for its capability probe. +FROM eclipse-temurin:17-jdk-noble AS topling-base +ARG TARGETARCH +RUN test "$TARGETARCH" = amd64 || { echo 'Topling images require linux/amd64' >&2; exit 1; } +ENV TOPLINGDB_ROCKSDB_PROVIDER=topling + +FROM ${RUNTIME_VARIANT}-base AS runtime +ARG RUNTIME_VARIANT +ARG SOURCE_REVISION=local +ARG SOURCE_REPOSITORY=https://github.com/apache/hugegraph +LABEL org.opencontainers.image.source="$SOURCE_REPOSITORY" \ + org.opencontainers.image.revision="$SOURCE_REVISION" \ + org.apache.hugegraph.rocksdb-runtime="$RUNTIME_VARIANT" # Local Arthas attach requires the JDK attach and compiler modules. LABEL maintainer="HugeGraph Docker Maintainers " @@ -53,13 +88,20 @@ WORKDIR /hugegraph-server/ # permanently inconclusive and a duplicate start is no longer caught. It # replaces lsof, which no shipped script calls any more. ARG RUNTIME_DEPS_EPOCH=1 -RUN apt-get -q update \ - && apt-get -q install -y --no-install-recommends --no-install-suggests \ +RUN --mount=type=secret,id=proxy_ca,required=false \ + native_deps="" \ + && if [ "$RUNTIME_VARIANT" = topling ]; then \ + native_deps="libaio1t64 libstdc++6 libsnappy1v5 libbz2-1.0 liblz4-1 libzstd1 zlib1g unzip"; \ + fi \ + && apt_ca=/etc/ssl/certs/ca-certificates.crt \ + && if [ -f /run/secrets/proxy_ca ]; then apt_ca=/run/secrets/proxy_ca; fi \ + && apt-get -q -o Acquire::https::CaInfo="$apt_ca" update \ + && apt-get -q -o Acquire::https::CaInfo="$apt_ca" install -y --no-install-recommends --no-install-suggests \ dumb-init \ procps \ curl \ iproute2 \ - vim \ + vim ${native_deps} \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* @@ -75,16 +117,37 @@ RUN groupadd --system --gid "${HG_GID}" hugegraph \ && install -d -o "${HG_UID}" -g "${HG_GID}" /hugegraph-server /hugegraph-server/rocksdb-data /hugegraph-server/logs COPY --from=build --chown=${HG_UID}:${HG_GID} /pkg/hugegraph-server/apache-hugegraph-server-*/ /hugegraph-server/ -USER ${HG_UID}:${HG_GID} -RUN sed -i "s/^restserver.url.*$/restserver.url=http:\/\/0.0.0.0:8080/g" ./conf/rest-server.properties +RUN sed -i "s/^restserver.url.*$/restserver.url=http:\/\/0.0.0.0:8080/g" ./conf/rest-server.properties \ + && cp -a ./conf/graphs ./conf/graphs.default \ + && install -d -o "${HG_UID}" -g "${HG_GID}" ./rocksdb-data/.hugegraph-config \ + && mv ./conf/graphs ./rocksdb-data/.hugegraph-config/graphs \ + && ln -s ../rocksdb-data/.hugegraph-config/graphs ./conf/graphs # 3. Init docker script COPY --chown=${HG_UID}:${HG_GID} hugegraph-server/hugegraph-dist/docker/scripts/remote-connect.groovy ./scripts COPY --chown=${HG_UID}:${HG_GID} hugegraph-server/hugegraph-dist/docker/scripts/detect-storage.groovy ./scripts COPY --chown=${HG_UID}:${HG_GID} --chmod=755 hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh . +# Download only during an explicit Topling build, verify and prepare with the +# distribution's normal capability probe. No network access is needed at startup. +ARG TOPLING_JNI_URL +ARG TOPLING_JNI_SHA256 +COPY docker/prepare-topling-image.sh docker/topling-runtime.env docker/toplingdb.yaml /tmp/ +RUN --mount=type=secret,id=proxy_ca,required=false \ + --mount=type=bind,from=topling-jni,target=/run/topling-jni \ + if [ "$RUNTIME_VARIANT" = topling ]; then \ + if [ -f /run/secrets/proxy_ca ]; then export CURL_CA_BUNDLE=/run/secrets/proxy_ca; fi; \ + bash /tmp/prepare-topling-image.sh \ + && chown -R "${HG_UID}:${HG_GID}" topling conf/toplingdb.yaml; \ + fi \ + && rm /tmp/prepare-topling-image.sh /tmp/topling-runtime.env /tmp/toplingdb.yaml + +USER ${HG_UID}:${HG_GID} + EXPOSE 8080 -VOLUME /hugegraph-server +# The named data volume seeds and retains graph definitions through conf/graphs, +# while lib/bin and the rest of conf stay image-owned across replacement. +VOLUME /hugegraph-server/rocksdb-data HEALTHCHECK --interval=15s --timeout=10s --start-period=90s --retries=3 \ CMD curl -fsS http://localhost:8080/versions >/dev/null diff --git a/hugegraph-server/Dockerfile-hstore b/hugegraph-server/Dockerfile-hstore index bcf405e42b..3c9b0b07a8 100644 --- a/hugegraph-server/Dockerfile-hstore +++ b/hugegraph-server/Dockerfile-hstore @@ -29,15 +29,39 @@ ARG MAVEN_PROJECTS="hugegraph-server/hugegraph-dist,hugegraph-pd/hg-pd-dist,huge ARG SOURCE_REVISION=local RUN --mount=type=cache,id=hugegraph-maven-${SOURCE_REVISION},target=/root/.m2,sharing=locked \ + --mount=type=secret,id=proxy_ca,required=false \ + --mount=type=secret,id=maven_settings,required=false \ + set -e; \ + if [ -f /run/secrets/proxy_ca ]; then \ + cp "$JAVA_HOME/lib/security/cacerts" /tmp/hugegraph-build-cacerts; \ + keytool -importcert -noprompt -alias hugegraph-build-proxy \ + -file /run/secrets/proxy_ca -keystore /tmp/hugegraph-build-cacerts -storepass changeit; \ + export MAVEN_OPTS="${MAVEN_OPTS:-} -Djavax.net.ssl.trustStore=/tmp/hugegraph-build-cacerts"; \ + fi; \ + if [ -f /run/secrets/maven_settings ]; then \ + MAVEN_ARGS="-s /run/secrets/maven_settings ${MAVEN_ARGS:-}"; \ + fi; \ mvn install -pl "$MAVEN_PROJECTS" \ - -am $MAVEN_ARGS -e -B -ntp -Dmaven.test.skip=true -Dmaven.javadoc.skip=true \ - && rm ./hugegraph-server/*.tar.gz ./hugegraph-pd/*.tar.gz ./hugegraph-store/*.tar.gz + -am $MAVEN_ARGS -e -B -ntp -Dmaven.test.skip=true -Dmaven.javadoc.skip=true; \ + rm -f /tmp/hugegraph-build-cacerts; \ + rm ./hugegraph-server/*.tar.gz ./hugegraph-pd/*.tar.gz ./hugegraph-store/*.tar.gz + +# Optional local JNI directory: --build-context topling-jni=/path/to/directory. +# An empty default stage keeps ordinary and URL-based builds self-contained. +FROM scratch AS topling-jni +# HStore intentionally does not consume this stage and keeps its standard runtime. # 2nd stage: runtime env # Note: ZGC (The Z Garbage Collector) is only supported on ARM-Mac with java > 13 FROM eclipse-temurin:17-jdk-jammy # Local Arthas attach requires the JDK attach and compiler modules. +ARG SOURCE_REVISION=local +ARG SOURCE_REPOSITORY=https://github.com/apache/hugegraph +LABEL org.opencontainers.image.source="$SOURCE_REPOSITORY" \ + org.opencontainers.image.revision="$SOURCE_REVISION" \ + org.apache.hugegraph.rocksdb-runtime="hstore" + LABEL maintainer="HugeGraph Docker Maintainers " # TODO: use g1gc or zgc as default @@ -53,8 +77,13 @@ WORKDIR /hugegraph-server/ # permanently inconclusive and a duplicate start is no longer caught. It # replaces lsof, which no shipped script calls any more. ARG RUNTIME_DEPS_EPOCH=1 -RUN apt-get -q update \ - && apt-get -q install -y --no-install-recommends --no-install-suggests \ +RUN --mount=type=secret,id=proxy_ca,required=false \ + --mount=type=secret,id=maven_settings,required=false \ + set -e; \ + apt_ca=/etc/ssl/certs/ca-certificates.crt; \ + if [ -f /run/secrets/proxy_ca ]; then apt_ca=/run/secrets/proxy_ca; fi; \ + apt-get -q -o Acquire::https::CaInfo="$apt_ca" update \ + && apt-get -q -o Acquire::https::CaInfo="$apt_ca" install -y --no-install-recommends --no-install-suggests \ dumb-init \ procps \ curl \ @@ -88,7 +117,6 @@ COPY --chown=${HG_UID}:${HG_GID} hugegraph-server/hugegraph-dist/docker/scripts/ COPY --chown=${HG_UID}:${HG_GID} --chmod=755 hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh . EXPOSE 8080 -VOLUME /hugegraph-server HEALTHCHECK --interval=15s --timeout=10s --start-period=90s --retries=3 \ CMD curl -fsS http://localhost:8080/versions >/dev/null diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/core/GraphManager.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/core/GraphManager.java index badd9b7295..bc64fcde29 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/core/GraphManager.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/core/GraphManager.java @@ -1474,11 +1474,23 @@ public HugeGraph createGraphLocal(String name, String configText) { PropertiesConfiguration propConfig = ConfigUtil.buildConfig(configText); HugeConfig config = new HugeConfig(propConfig); + applyImageProvider(config, System.getenv("TOPLINGDB_ROCKSDB_PROVIDER")); this.checkOptions(config); return this.createGraphLocal(config, name); } + private static void applyImageProvider(HugeConfig config, String provider) { + if (StringUtils.isEmpty(provider) || + !"rocksdb".equals(config.get(CoreOptions.BACKEND)) || + config.containsKey("rocksdb.provider")) { + return; + } + E.checkArgument("rocksdb".equals(provider) || "topling".equals(provider), + "Invalid TOPLINGDB_ROCKSDB_PROVIDER: %s", provider); + config.setProperty("rocksdb.provider", provider); + } + private HugeGraph createGraphLocal(HugeConfig config, String name) { HugeGraph graph = null; try { diff --git a/hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh b/hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh index fa4f9bcfbd..eddb4c55c1 100755 --- a/hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh +++ b/hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh @@ -561,5 +561,103 @@ for invalid_k8s_flag in 1 0 enabled "t rue" "tr ue"; do "${TEST_HOME}/conf/graphs/hugegraph.properties" done +# A Topling image configures every local graph while retaining remote graphs. +printf 'backend=rocksdb\n' > "${TEST_HOME}/conf/graphs/analytics.properties" +printf 'backend=hstore\nrocksdb.provider=rocksdb\n' > "${TEST_HOME}/conf/graphs/remote.properties" +( + cd "${TEST_HOME}" + HG_SERVER_BACKEND=rocksdb TOPLINGDB_ROCKSDB_PROVIDER=topling bash ./docker-entrypoint.sh +) +for graph in hugegraph analytics; do + grep -qx 'rocksdb.provider=topling' "${TEST_HOME}/conf/graphs/${graph}.properties" +done +grep -qx 'rocksdb.provider=rocksdb' "${TEST_HOME}/conf/graphs/remote.properties" +# A bare launcher with no selection retains the supplied provider. Docker's +# standard image explicitly selects rocksdb, including when returning from Topling. +( + cd "${TEST_HOME}" + env -u TOPLINGDB_ROCKSDB_PROVIDER bash ./docker-entrypoint.sh +) +grep -qx 'rocksdb.provider=topling' "${TEST_HOME}/conf/graphs/analytics.properties" +( + cd "${TEST_HOME}" + TOPLINGDB_ROCKSDB_PROVIDER=rocksdb bash ./docker-entrypoint.sh +) +for graph in hugegraph analytics; do + grep -qx 'rocksdb.provider=rocksdb' "${TEST_HOME}/conf/graphs/${graph}.properties" +done +grep -qx 'backend=hstore' "${TEST_HOME}/conf/graphs/remote.properties" +cp "${TEST_HOME}/conf/graphs/analytics.properties" "${TEST_HOME}/analytics.before-invalid-provider" +if ( + cd "${TEST_HOME}" + TOPLINGDB_ROCKSDB_PROVIDER=invalid bash ./docker-entrypoint.sh +); then + echo "invalid TOPLINGDB_ROCKSDB_PROVIDER unexpectedly succeeded" >&2 + exit 1 +fi +cmp "${TEST_HOME}/analytics.before-invalid-provider" "${TEST_HOME}/conf/graphs/analytics.properties" + +# Matching providers must leave the configuration byte-for-byte unchanged. +# Real read-only directory/file mounts are also exercised by Docker deployment CI. +for provider_case in default rocksdb topling; do + selected_provider="${provider_case}" + [[ "${provider_case}" != default ]] || selected_provider=rocksdb + readonly_graph="${TEST_HOME}/conf/graphs/readonly.properties" + printf 'backend=rocksdb\n# Preserve formatting and unrelated settings\ncustom=true\n' > "${readonly_graph}" + if [[ "${provider_case}" != default ]]; then + printf 'rocksdb.provider : %s \n' "${provider_case}" >> "${readonly_graph}" + fi + cp "${readonly_graph}" "${TEST_HOME}/readonly.before" + chmod 444 "${readonly_graph}" + readonly_identity=$(ls -di "${readonly_graph}") + ( + cd "${TEST_HOME}" + TOPLINGDB_ROCKSDB_PROVIDER="${selected_provider}" bash ./docker-entrypoint.sh + ) + cmp "${TEST_HOME}/readonly.before" "${readonly_graph}" + [[ "$(ls -di "${readonly_graph}")" == "${readonly_identity}" ]] + chmod 644 "${readonly_graph}" + rm "${readonly_graph}" +done + +# Reproduce the image's symlink with an already populated legacy data directory. +# The default copy must bootstrap it, retain data, and never replace later user +# definitions. An explicit conf/graphs directory must also bypass bootstrapping. +mv "${TEST_HOME}/conf/graphs" "${TEST_HOME}/conf/graphs.default" +mkdir -p "${TEST_HOME}/rocksdb-data/data" "${TEST_HOME}/rocksdb-data/wal" +printf 'legacy data\n' > "${TEST_HOME}/rocksdb-data/data/keep" +ln -s ../rocksdb-data/.hugegraph-config/graphs "${TEST_HOME}/conf/graphs" +( + cd "${TEST_HOME}" + env -u HG_SERVER_AUTH_TOKEN_SECRET PASSWORD=entrypoint-test-secret \ + TOPLINGDB_ROCKSDB_PROVIDER=topling bash ./docker-entrypoint.sh +) +grep -qx 'legacy data' "${TEST_HOME}/rocksdb-data/data/keep" +grep -qx 'rocksdb.provider=topling' "${TEST_HOME}/conf/graphs/hugegraph.properties" +printf 'custom=true\n' >> "${TEST_HOME}/conf/graphs/analytics.properties" +cp "${TEST_HOME}/conf/graphs/analytics.properties" "${TEST_HOME}/analytics.persisted" +printf 'unrelated image default\n' > "${TEST_HOME}/conf/graphs.default/analytics.properties" +( + cd "${TEST_HOME}" + TOPLINGDB_ROCKSDB_PROVIDER=topling bash ./docker-entrypoint.sh +) +cmp "${TEST_HOME}/analytics.persisted" "${TEST_HOME}/conf/graphs/analytics.properties" +( + cd "${TEST_HOME}" + TOPLINGDB_ROCKSDB_PROVIDER=rocksdb bash ./docker-entrypoint.sh +) +grep -qx 'custom=true' "${TEST_HOME}/conf/graphs/analytics.properties" +grep -qx 'rocksdb.provider=rocksdb' "${TEST_HOME}/conf/graphs/analytics.properties" +grep -qx 'rocksdb.provider=rocksdb' "${TEST_HOME}/conf/graphs/hugegraph.properties" +[[ -z "$(find "${TEST_HOME}/rocksdb-data/.hugegraph-config" -name '.graphs-init.*' -print -quit)" ]] +unlink "${TEST_HOME}/conf/graphs" +mv "${TEST_HOME}/rocksdb-data/.hugegraph-config/graphs" "${TEST_HOME}/conf/graphs" +( + cd "${TEST_HOME}" + TOPLINGDB_ROCKSDB_PROVIDER=rocksdb bash ./docker-entrypoint.sh +) +[[ ! -e "${TEST_HOME}/rocksdb-data/.hugegraph-config/graphs" ]] +grep -qx 'custom=true' "${TEST_HOME}/conf/graphs/analytics.properties" + echo "PASS: Docker entrypoint configures HStore discovery and authentication" bash "${SCRIPT_DIR}/test/test-docker-entrypoint-process.sh" diff --git a/hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh b/hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh index 4835f45e5e..e80f800fb5 100755 --- a/hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh +++ b/hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh @@ -26,6 +26,12 @@ mkdir -p "${DOCKER_FOLDER}" log() { echo "[hugegraph-server-entrypoint] $*"; } +ROCKSDB_PROVIDER="${TOPLINGDB_ROCKSDB_PROVIDER:-rocksdb}" +case "${ROCKSDB_PROVIDER}" in + rocksdb | topling) ;; + *) log "ERROR: TOPLINGDB_ROCKSDB_PROVIDER must be rocksdb or topling"; exit 2 ;; +esac + source ./bin/util.sh record_jvm_startup_diagnostics run_startup_step() { @@ -68,7 +74,7 @@ set_prop_encoded() { key_re="^[[:space:]]*${esc_key}([[:space:]]*[:=]|[[:space:]]+|[[:space:]]*$)" if grep -qE "${key_re}" "${file}"; then - sed -ri "0,/${key_re}/!{/${key_re}/d;}" "${file}" + sed -ri "0,/${key_re}/!{/${key_re}/d;}" "${file}" || return sed -ri "0,/${key_re}/s~${key_re}.*~${key}=${esc_val}~" "${file}" else printf '%s=%s\n' "$key" "$encoded_val" >> "${file}" @@ -180,6 +186,49 @@ if [[ -n "${PASSWORD:-}" && exit 1 fi +# Same accepted spellings as HG_SERVER_INIT_STORE_ENABLED below, checked +# before the mapping so an invalid value leaves both conf files untouched. +# URLs hold no whitespace, and the Server splits the list on bare commas, so +# "a, b" would otherwise register an escaped space in front of the second URL. +URLS_TO_PD=$(printf '%s' "${HG_SERVER_URLS_TO_PD:-}" | tr -d '[:space:]') +# Only surrounding whitespace is trimmed, so a value such as "t rue" stays invalid. +DEPLOY_IN_K8S="${HG_SERVER_DEPLOY_IN_K8S:-}" +DEPLOY_IN_K8S="${DEPLOY_IN_K8S#"${DEPLOY_IN_K8S%%[![:space:]]*}"}" +DEPLOY_IN_K8S="${DEPLOY_IN_K8S%"${DEPLOY_IN_K8S##*[![:space:]]}"}" +DEPLOY_IN_K8S=$(printf '%s' "${DEPLOY_IN_K8S}" | tr '[:upper:]' '[:lower:]') +case "${DEPLOY_IN_K8S}" in + "" | y | t | yes | on | true | n | f | no | off | false) ;; + *) log "ERROR: invalid HG_SERVER_DEPLOY_IN_K8S '${HG_SERVER_DEPLOY_IN_K8S}'" + exit 1 ;; +esac + +# An old nonempty volume or a bind mount hides the image's seeded graphs. +# Only initialize a missing target of the image-owned link; existing graph +# definitions and explicitly mounted conf/graphs directories remain authoritative. +prepare_graph_directory() { + local graphs=./conf/graphs defaults=./conf/graphs.default + local parent=./rocksdb-data/.hugegraph-config target staging + [[ -L "$graphs" && "$(readlink "$graphs")" == ../rocksdb-data/.hugegraph-config/graphs ]] || return 0 + [[ ! -d "$graphs" ]] || return 0 + target="$parent/graphs" + if [[ -e "$target" || -L "$target" || ! -d "$defaults" ]]; then + log "ERROR: cannot initialize graph definitions at $target from $defaults" + return 1 + fi + mkdir -p "$parent" + staging=$(mktemp -d "$parent/.graphs-init.XXXXXX") + if ! cp -a "$defaults/." "$staging/" || ! mv -T -n "$staging" "$target"; then + rm -rf -- "$staging" + log "ERROR: failed to initialize graph definitions at $target" + return 1 + fi + rm -rf -- "$staging" + [[ -d "$graphs" ]] || { log "ERROR: graph definitions target is not a directory: $target"; return 1; } + log "Initialized graph definitions in the existing data volume" +} +prepare_graph_directory + +# Read persisted authentication only after a missing graph directory is seeded. AUTH_TOKEN_SECRET_ENCODED="" if [[ -n "${PASSWORD:-}" && -z "${HG_SERVER_AUTH_TOKEN_SECRET:-}" ]]; then rest_secret=$(get_prop_encoded "auth.token_secret" "${REST_SERVER_CONF}") @@ -206,28 +255,36 @@ if [[ -n "${PASSWORD:-}" ]]; then AUTH_ADMIN_PASSWORD_ENCODED=$(encode_auth_prop_value "${PASSWORD}") fi -# Same accepted spellings as HG_SERVER_INIT_STORE_ENABLED below, checked -# before the mapping so an invalid value leaves both conf files untouched. -# URLs hold no whitespace, and the Server splits the list on bare commas, so -# "a, b" would otherwise register an escaped space in front of the second URL. -URLS_TO_PD=$(printf '%s' "${HG_SERVER_URLS_TO_PD:-}" | tr -d '[:space:]') -# Only surrounding whitespace is trimmed, so a value such as "t rue" stays invalid. -DEPLOY_IN_K8S="${HG_SERVER_DEPLOY_IN_K8S:-}" -DEPLOY_IN_K8S="${DEPLOY_IN_K8S#"${DEPLOY_IN_K8S%%[![:space:]]*}"}" -DEPLOY_IN_K8S="${DEPLOY_IN_K8S%"${DEPLOY_IN_K8S##*[![:space:]]}"}" -DEPLOY_IN_K8S=$(printf '%s' "${DEPLOY_IN_K8S}" | tr '[:upper:]' '[:lower:]') -case "${DEPLOY_IN_K8S}" in - "" | y | t | yes | on | true | n | f | no | off | false) ;; - *) log "ERROR: invalid HG_SERVER_DEPLOY_IN_K8S '${HG_SERVER_DEPLOY_IN_K8S}'" - exit 1 ;; -esac - # ── Map env → properties file ───────────────────────────────────────── # TODO: drop the Helm chart's (helm/hugegraph) rest-server.properties wrapper once its minimum # Server image has the server.urls_to_pd and server.deploy_in_k8s mappings below. A read-only root # filesystem also needs the conf files on a writable path, since set_prop in this script edits them # in place; the chart refuses readOnlyRootFilesystem until then. [[ -n "${HG_SERVER_BACKEND:-}" ]] && set_prop "backend" "${HG_SERVER_BACKEND}" "${GRAPH_CONF}" +# Apply the image's selected JNI provider to every embedded graph in the normal +# container graphs directory, including mounted additional graph definitions. +# Distributed graphs keep backend=hstore and do not select a local provider. +if [[ -n "${TOPLINGDB_ROCKSDB_PROVIDER:-}" ]]; then + for graph_conf in ./conf/graphs/*.properties; do + [[ -f "${graph_conf}" ]] || continue + if [[ "$(get_prop_encoded backend "${graph_conf}")" == rocksdb ]]; then + current_provider=$(get_prop_encoded rocksdb.provider "${graph_conf}") + current_provider="${current_provider%"${current_provider##*[![:space:]]}"}" + # RocksDBOptions defaults an absent property to rocksdb. Do not + # touch matching configurations, including read-only mounts. + if ! grep -qE '^[[:space:]]*rocksdb[.]provider([[:space:]]*[:=]|[[:space:]]|$)' "${graph_conf}"; then + current_provider=rocksdb + fi + if [[ "${current_provider}" != "${ROCKSDB_PROVIDER}" ]] && + ! set_prop "rocksdb.provider" "${ROCKSDB_PROVIDER}" "${graph_conf}"; then + log "ERROR: cannot change rocksdb.provider from '${current_provider}' to" \ + "'${ROCKSDB_PROVIDER}' in ${graph_conf}; make the graph configuration" \ + "writable or configure the provider to match the selected image" + exit 1 + fi + fi + done +fi [[ -n "${HG_SERVER_PD_PEERS:-}" ]] && set_prop "pd.peers" "${HG_SERVER_PD_PEERS}" "${GRAPH_CONF}" [[ -n "${HG_SERVER_USE_PD:-}" ]] && \ set_prop "usePD" "${HG_SERVER_USE_PD}" "${REST_SERVER_CONF}" diff --git a/hugegraph-server/hugegraph-dist/docker/test/test-docker-entrypoint-process.sh b/hugegraph-server/hugegraph-dist/docker/test/test-docker-entrypoint-process.sh index 88a7a30b2b..09c35fb27e 100644 --- a/hugegraph-server/hugegraph-dist/docker/test/test-docker-entrypoint-process.sh +++ b/hugegraph-server/hugegraph-dist/docker/test/test-docker-entrypoint-process.sh @@ -287,7 +287,11 @@ sys.exit(int((work / 'child-exit').read_text())) os.kill(int((work / 'bin/pid').read_text()), signal.SIGKILL) else: status = 42 if mode in ['partition42', 'partitionDiagnostic42'] else int(mode[4:]) - (work / 'child-exit').write_text(str(status)) + # Publish the exit status only after its contents are + # complete; the launcher polls for the final path. + pending = work / 'child-exit.pending' + pending.write_text(str(status)) + pending.replace(work / 'child-exit') actual = process.wait(timeout=5) assert actual == expected, (mode, expected, actual, (work / 'output').read_text()) diagnostic = (work / 'output').read_text() diff --git a/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/cmd/InitStore.java b/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/cmd/InitStore.java index 5362a1821d..05a5c75627 100644 --- a/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/cmd/InitStore.java +++ b/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/cmd/InitStore.java @@ -29,6 +29,8 @@ import org.apache.hugegraph.HugeFactory; import org.apache.hugegraph.HugeGraph; +import org.apache.hugegraph.auth.HugeAuthenticator.User; +import org.apache.hugegraph.auth.HugeGraphAuthProxy; import org.apache.hugegraph.auth.StandardAuthenticator; import org.apache.hugegraph.backend.store.BackendStoreInfo; import org.apache.hugegraph.config.CoreOptions; @@ -37,6 +39,7 @@ import org.apache.hugegraph.constant.ServiceConstant; import org.apache.hugegraph.dist.RegisterUtil; import org.apache.hugegraph.meta.PdMetaDriver.PDAuthConfig; +import org.apache.hugegraph.task.TaskManager; import org.apache.hugegraph.util.ConfigUtil; import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.Log; @@ -292,7 +295,7 @@ private static HugeGraph initGraph(String configPath) throws Exception { * Init the required information for creating the admin account * (when switch from non-auth mode to auth mode) */ - graph.initSystemInfo(); + initSystemInfo(graph); LOG.info("Skip init-store due to the backend store of '{}' " + "had been initialized", graph.name()); } else { @@ -309,4 +312,14 @@ private static void initBackend(final HugeGraph graph) { // Only explicitly transient backend failures can be retried safely graph.initBackend(); } + + private static void initSystemInfo(HugeGraph graph) { + // Persisted API-created graphs use the auth proxy. Their task-schema + // initialization runs on a pre-existing executor, so it must receive + // this local bootstrap command's admin context explicitly. + HugeGraphAuthProxy.runAsAdmin(() -> { + TaskManager.setContext(User.ADMIN.toJson()); + graph.initSystemInfo(); + }); + } } diff --git a/hugegraph-server/hugegraph-dist/src/test/java/org/apache/hugegraph/cmd/InitStoreTest.java b/hugegraph-server/hugegraph-dist/src/test/java/org/apache/hugegraph/cmd/InitStoreTest.java index cf11df45f9..793ed15387 100644 --- a/hugegraph-server/hugegraph-dist/src/test/java/org/apache/hugegraph/cmd/InitStoreTest.java +++ b/hugegraph-server/hugegraph-dist/src/test/java/org/apache/hugegraph/cmd/InitStoreTest.java @@ -20,9 +20,15 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; import org.apache.hugegraph.HugeGraph; +import org.apache.hugegraph.auth.AuthContext; +import org.apache.hugegraph.auth.HugeAuthenticator.User; +import org.apache.hugegraph.auth.HugeGraphAuthProxy; +import org.apache.hugegraph.task.TaskManager; import org.junit.Assert; import org.junit.Test; @@ -54,4 +60,82 @@ public void testInitBackendFailsFastForPermanentException() throws Exception { } Assert.assertEquals(1, invocations.get()); } + + @Test + public void testSystemInfoBootstrapPropagatesAdminToExistingTaskExecutor() throws Exception { + ExecutorService executor = Executors.newSingleThreadExecutor(); + // Create the worker before the bootstrap command establishes a context. + executor.submit(HugeGraphAuthProxy::resetContext).get(); + AuthContext.setContext("caller-auth-context"); + TaskManager.setContext("caller-task-context"); + HugeGraphAuthProxy.setRequestGraphSpace("caller_space"); + try { + AtomicInteger invocations = new AtomicInteger(); + HugeGraph graph = initializationGraph(() -> { + try { + executor.submit(new TaskManager.ContextCallable<>(() -> { + Assert.assertEquals(User.ADMIN, + HugeGraphAuthProxy.getContext().user()); + invocations.incrementAndGet(); + return null; + })).get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + initializeSystemInfo(graph); + Assert.assertEquals(1, invocations.get()); + Assert.assertNull(executor.submit(TaskManager::getContext).get()); + assertCallerContextRestored(); + } finally { + executor.shutdownNow(); + HugeGraphAuthProxy.resetContext(); + TaskManager.resetContext(); + } + } + + @Test + public void testSystemInfoBootstrapRestoresContextAfterFailure() throws Exception { + IllegalStateException failure = new IllegalStateException("Task schema initialization failed"); + AuthContext.setContext("caller-auth-context"); + TaskManager.setContext("caller-task-context"); + HugeGraphAuthProxy.setRequestGraphSpace("caller_space"); + try { + try { + initializeSystemInfo(initializationGraph(() -> { + throw failure; + })); + Assert.fail("Expected initialization failure"); + } catch (InvocationTargetException e) { + Assert.assertSame(failure, e.getCause()); + } + assertCallerContextRestored(); + } finally { + HugeGraphAuthProxy.resetContext(); + TaskManager.resetContext(); + } + } + + private static HugeGraph initializationGraph(Runnable initialization) { + return (HugeGraph) Proxy.newProxyInstance( + HugeGraph.class.getClassLoader(), new Class[]{HugeGraph.class}, + (proxy, method, args) -> { + if (method.getName().equals("initSystemInfo")) { + initialization.run(); + } + return null; + }); + } + + private static void initializeSystemInfo(HugeGraph graph) throws Exception { + Method method = InitStore.class.getDeclaredMethod("initSystemInfo", HugeGraph.class); + method.setAccessible(true); + method.invoke(null, graph); + } + + private static void assertCallerContextRestored() { + Assert.assertEquals("caller-auth-context", AuthContext.getContext()); + Assert.assertEquals("caller-task-context", TaskManager.getContext()); + Assert.assertEquals("caller_space", HugeGraphAuthProxy.getRequestGraphSpace()); + } } diff --git a/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBStoreProvider.java b/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBStoreProvider.java index 82075919ad..0b92f6aaaa 100644 --- a/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBStoreProvider.java +++ b/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBStoreProvider.java @@ -23,13 +23,34 @@ import org.apache.hugegraph.backend.store.BackendStore; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.util.ConfigUtil; +import org.apache.hugegraph.util.E; public class RocksDBStoreProvider extends AbstractBackendStoreProvider { + private static final String IMAGE_PROVIDER = "TOPLINGDB_ROCKSDB_PROVIDER"; + protected String database() { return this.graph().toLowerCase(); } + @Override + public BackendStore loadSchemaStore(HugeConfig config) { + applyImageProvider(config); + return super.loadSchemaStore(config); + } + + @Override + public BackendStore loadGraphStore(HugeConfig config) { + applyImageProvider(config); + return super.loadGraphStore(config); + } + + @Override + public BackendStore loadSystemStore(HugeConfig config) { + applyImageProvider(config); + return super.loadSystemStore(config); + } + @Override protected BackendStore newSchemaStore(HugeConfig config, String store) { return new RocksDBStore.RocksDBSchemaStore(this, this.database(), store); @@ -68,6 +89,22 @@ protected BackendStore newSystemStore(HugeConfig config, String store) { return new RocksDBStore.RocksDBSystemStore(this, this.database(), store); } + private static void applyImageProvider(HugeConfig config) { + applyImageProvider(config, System.getenv(IMAGE_PROVIDER)); + } + + private static void applyImageProvider(HugeConfig config, String provider) { + if (config == null || config.containsKey(RocksDBOptions.PROVIDER.name()) || + provider == null || provider.isBlank()) { + return; + } + E.checkArgument("rocksdb".equals(provider) || "topling".equals(provider), + "Unsupported %s value '%s'", IMAGE_PROVIDER, provider); + // Keep image selection above persisted graph/PD configuration so an + // image rollback can switch the runtime without rewriting metadata. + config.setProperty(RocksDBOptions.PROVIDER.name(), provider); + } + @Override public String type() { return "rocksdb"; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/BackendProviderFactoryTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/BackendProviderFactoryTest.java index 58bd977b6e..563fa45e2f 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/BackendProviderFactoryTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/BackendProviderFactoryTest.java @@ -25,11 +25,16 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import org.apache.hugegraph.exception.BackendException; +import org.apache.commons.configuration2.PropertiesConfiguration; import org.apache.hugegraph.backend.store.BackendProviderFactory; import org.apache.hugegraph.backend.store.memory.InMemoryDBStoreProvider; import org.apache.hugegraph.backend.store.raft.RaftBackendStoreProvider; +import org.apache.hugegraph.backend.store.rocksdb.RocksDBOptions; +import org.apache.hugegraph.backend.store.rocksdb.RocksDBStoreProvider; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.exception.BackendException; import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.testutil.Whitebox; import org.apache.hugegraph.unit.BaseUnitTest; import org.junit.Test; @@ -105,4 +110,44 @@ public void testRegisterConcurrently() throws Exception { executor.shutdownNow(); } } + + @Test + public void testRocksDBImageProviderIsRuntimeOnlyAndPreservesExplicitConfig() { + applyImageProvider(null, "topling"); + + HugeConfig implicit = rocksConfig(); + applyImageProvider(implicit, "topling"); + Assert.assertEquals("topling", implicit.get(RocksDBOptions.PROVIDER)); + + HugeConfig standard = rocksConfig(); + applyImageProvider(standard, "rocksdb"); + Assert.assertEquals("rocksdb", standard.get(RocksDBOptions.PROVIDER)); + + HugeConfig explicit = rocksConfig(); + explicit.setProperty(RocksDBOptions.PROVIDER.name(), "rocksdb"); + applyImageProvider(explicit, "topling"); + Assert.assertEquals("rocksdb", explicit.get(RocksDBOptions.PROVIDER)); + + HugeConfig absent = rocksConfig(); + applyImageProvider(absent, null); + applyImageProvider(absent, ""); + Assert.assertFalse(absent.containsKey(RocksDBOptions.PROVIDER.name())); + + HugeConfig invalid = rocksConfig(); + Assert.assertThrows(IllegalArgumentException.class, + () -> applyImageProvider(invalid, "invalid")); + Assert.assertFalse(invalid.containsKey(RocksDBOptions.PROVIDER.name())); + } + + private static HugeConfig rocksConfig() { + PropertiesConfiguration properties = new PropertiesConfiguration(); + properties.setProperty("backend", "rocksdb"); + return new HugeConfig(properties); + } + + private static void applyImageProvider(HugeConfig config, String provider) { + Whitebox.invoke(RocksDBStoreProvider.class, + new Class[]{HugeConfig.class, String.class}, + "applyImageProvider", null, config, provider); + } } diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerConfigTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerConfigTest.java index eea6870048..239e4656e0 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerConfigTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerConfigTest.java @@ -113,6 +113,52 @@ public class GraphManagerConfigTest { + @Test + public void testDynamicRocksGraphInheritsImageProvider() { + OptionSpace.register("core", CoreOptions.instance()); + OptionSpace.register("rocksdb", RocksDBOptions.instance()); + for (String provider : new String[]{"rocksdb", "topling"}) { + PropertiesConfiguration properties = new PropertiesConfiguration(); + properties.setProperty("backend", "rocksdb"); + HugeConfig config = new HugeConfig(properties); + applyImageProvider(config, provider); + Assert.assertEquals(provider, config.get(RocksDBOptions.PROVIDER)); + } + } + + @Test + public void testDynamicProviderPreservesExplicitAndDistributedConfiguration() { + PropertiesConfiguration properties = new PropertiesConfiguration(); + properties.setProperty("backend", "rocksdb"); + properties.setProperty("rocksdb.provider", "rocksdb"); + HugeConfig explicit = new HugeConfig(properties); + applyImageProvider(explicit, "topling"); + Assert.assertEquals("rocksdb", explicit.getProperty("rocksdb.provider")); + properties.clearProperty("rocksdb.provider"); + properties.setProperty("backend", "hstore"); + HugeConfig distributed = new HugeConfig(properties); + applyImageProvider(distributed, "topling"); + Assert.assertFalse(distributed.containsKey("rocksdb.provider")); + } + + @Test + public void testDynamicProviderKeepsDefaultWithoutOptInAndRejectsInvalidSelection() { + PropertiesConfiguration properties = new PropertiesConfiguration(); + properties.setProperty("backend", "rocksdb"); + HugeConfig config = new HugeConfig(properties); + applyImageProvider(config, null); + applyImageProvider(config, ""); + Assert.assertFalse(config.containsKey("rocksdb.provider")); + Assert.assertThrows(IllegalArgumentException.class, + () -> applyImageProvider(config, "invalid")); + Assert.assertFalse(config.containsKey("rocksdb.provider")); + } + + private static void applyImageProvider(HugeConfig config, String provider) { + Whitebox.invoke(GraphManager.class, new Class[]{HugeConfig.class, String.class}, + "applyImageProvider", null, config, provider); + } + @Test public void testGraphSpaceDropCleansPdStatisticsBeforeRemovingConfiguration() throws Exception { this.checkGraphSpaceStatisticsCleanup(null, false); diff --git a/hugegraph-store/Dockerfile b/hugegraph-store/Dockerfile index de69be1c74..386e3ad528 100644 --- a/hugegraph-store/Dockerfile +++ b/hugegraph-store/Dockerfile @@ -17,6 +17,9 @@ # # Dockerfile for HugeGraph Store +# Select the native runtime without changing the source build or deployment layout. +ARG RUNTIME_VARIANT=standard + # 1st stage: build source code FROM --platform=$BUILDPLATFORM maven:3.9.16-eclipse-temurin-17 AS build @@ -29,13 +32,45 @@ ARG MAVEN_PROJECTS="hugegraph-server/hugegraph-dist,hugegraph-pd/hg-pd-dist,huge ARG SOURCE_REVISION=local RUN --mount=type=cache,id=hugegraph-maven-${SOURCE_REVISION},target=/root/.m2,sharing=locked \ + --mount=type=secret,id=proxy_ca,required=false \ + --mount=type=secret,id=maven_settings,required=false \ + set -e; \ + if [ -f /run/secrets/proxy_ca ]; then \ + cp "$JAVA_HOME/lib/security/cacerts" /tmp/hugegraph-build-cacerts; \ + keytool -importcert -noprompt -alias hugegraph-build-proxy \ + -file /run/secrets/proxy_ca -keystore /tmp/hugegraph-build-cacerts -storepass changeit; \ + export MAVEN_OPTS="${MAVEN_OPTS:-} -Djavax.net.ssl.trustStore=/tmp/hugegraph-build-cacerts"; \ + fi; \ + if [ -f /run/secrets/maven_settings ]; then \ + MAVEN_ARGS="-s /run/secrets/maven_settings ${MAVEN_ARGS:-}"; \ + fi; \ mvn install -pl "$MAVEN_PROJECTS" \ - -am $MAVEN_ARGS -e -B -ntp -Dmaven.test.skip=true -Dmaven.javadoc.skip=true \ - && rm ./hugegraph-server/*.tar.gz ./hugegraph-pd/*.tar.gz ./hugegraph-store/*.tar.gz + -am $MAVEN_ARGS -e -B -ntp -Dmaven.test.skip=true -Dmaven.javadoc.skip=true; \ + rm -f /tmp/hugegraph-build-cacerts; \ + rm ./hugegraph-server/*.tar.gz ./hugegraph-pd/*.tar.gz ./hugegraph-store/*.tar.gz + +# Optional local JNI directory: --build-context topling-jni=/path/to/directory. +# An empty default stage keeps ordinary and URL-based builds self-contained. +FROM scratch AS topling-jni # 2nd stage: runtime env # Note: ZGC (The Z Garbage Collector) is only supported on ARM-Mac with java > 13 -FROM eclipse-temurin:17-jdk-jammy +FROM eclipse-temurin:17-jdk-jammy AS standard-base +ENV TOPLINGDB_ROCKSDB_PROVIDER=rocksdb + +# The fork's JNI requires glibc 2.38+ and a Java 17 JDK for its capability probe. +FROM eclipse-temurin:17-jdk-noble AS topling-base +ARG TARGETARCH +RUN test "$TARGETARCH" = amd64 || { echo 'Topling images require linux/amd64' >&2; exit 1; } +ENV TOPLINGDB_ROCKSDB_PROVIDER=topling + +FROM ${RUNTIME_VARIANT}-base AS runtime +ARG RUNTIME_VARIANT +ARG SOURCE_REVISION=local +ARG SOURCE_REPOSITORY=https://github.com/apache/hugegraph +LABEL org.opencontainers.image.source="$SOURCE_REPOSITORY" \ + org.opencontainers.image.revision="$SOURCE_REVISION" \ + org.apache.hugegraph.rocksdb-runtime="$RUNTIME_VARIANT" # Local Arthas attach requires the JDK attach and compiler modules. LABEL maintainer="HugeGraph Docker Maintainers " @@ -52,12 +87,19 @@ WORKDIR /hugegraph-store/ # from hg-store-dist bin/util.sh. Store runs no port preflight, so unlike the # server images this needs no socket-table tool in its place. ARG RUNTIME_DEPS_EPOCH=1 -RUN apt-get -q update \ - && apt-get -q install -y --no-install-recommends --no-install-suggests \ +RUN --mount=type=secret,id=proxy_ca,required=false \ + native_deps="" \ + && if [ "$RUNTIME_VARIANT" = topling ]; then \ + native_deps="libaio1t64 libstdc++6 libsnappy1v5 libbz2-1.0 liblz4-1 libzstd1 zlib1g unzip"; \ + fi \ + && apt_ca=/etc/ssl/certs/ca-certificates.crt \ + && if [ -f /run/secrets/proxy_ca ]; then apt_ca=/run/secrets/proxy_ca; fi \ + && apt-get -q -o Acquire::https::CaInfo="$apt_ca" update \ + && apt-get -q -o Acquire::https::CaInfo="$apt_ca" install -y --no-install-recommends --no-install-suggests \ dumb-init \ procps \ curl \ - vim \ + vim ${native_deps} \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* @@ -77,10 +119,25 @@ COPY --from=build --chown=${HG_UID}:${HG_GID} /pkg/hugegraph-store/apache-hugegr # 3. Init docker script COPY --chown=${HG_UID}:${HG_GID} --chmod=755 hugegraph-store/hg-store-dist/docker/docker-entrypoint.sh . +# Download only during an explicit Topling build, verify and prepare with the +# distribution's normal capability probe. No network access is needed at startup. +ARG TOPLING_JNI_URL +ARG TOPLING_JNI_SHA256 +COPY docker/prepare-topling-image.sh docker/topling-runtime.env docker/toplingdb.yaml /tmp/ +RUN --mount=type=secret,id=proxy_ca,required=false \ + --mount=type=bind,from=topling-jni,target=/run/topling-jni \ + if [ "$RUNTIME_VARIANT" = topling ]; then \ + if [ -f /run/secrets/proxy_ca ]; then export CURL_CA_BUNDLE=/run/secrets/proxy_ca; fi; \ + bash /tmp/prepare-topling-image.sh \ + && chown -R "${HG_UID}:${HG_GID}" topling conf/toplingdb.yaml; \ + fi \ + && rm /tmp/prepare-topling-image.sh /tmp/topling-runtime.env /tmp/toplingdb.yaml + USER ${HG_UID}:${HG_GID} EXPOSE 8520 -VOLUME /hugegraph-store +# Persist data and Raft state without retaining the previous image's runtime. +VOLUME /hugegraph-store/storage HEALTHCHECK --interval=15s --timeout=10s --start-period=90s --retries=3 \ CMD curl -fsS http://localhost:8520/v1/health >/dev/null diff --git a/hugegraph-store/hg-store-dist/docker/docker-entrypoint.sh b/hugegraph-store/hg-store-dist/docker/docker-entrypoint.sh index 8ea9022a33..db1f4f6ebf 100755 --- a/hugegraph-store/hg-store-dist/docker/docker-entrypoint.sh +++ b/hugegraph-store/hg-store-dist/docker/docker-entrypoint.sh @@ -5,7 +5,7 @@ # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at +# the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # @@ -19,6 +19,12 @@ set -euo pipefail log() { echo "[hugegraph-store-entrypoint] $*"; } +ROCKSDB_PROVIDER="${TOPLINGDB_ROCKSDB_PROVIDER:-rocksdb}" +case "${ROCKSDB_PROVIDER}" in + rocksdb | topling) ;; + *) log "ERROR: TOPLINGDB_ROCKSDB_PROVIDER must be rocksdb or topling"; exit 2 ;; +esac + require_env() { local name="$1" if [[ -z "${!name:-}" ]]; then @@ -55,9 +61,17 @@ require_env "HG_STORE_RAFT_ADDRESS" : "${HG_STORE_REST_PORT:=8520}" : "${HG_STORE_DATA_PATH:=/hugegraph-store/storage}" +# Preserve mounted business configuration in the ordinary image. An explicitly +# selected runtime (including the Topling image's default) configures both sides. +ROCKSDB_JSON="" +if [[ -n "${TOPLINGDB_ROCKSDB_PROVIDER:-}" ]]; then + ROCKSDB_JSON="\"rocksdb\": { \"provider\": \"${ROCKSDB_PROVIDER}\" }," +fi + # ── Build SPRING_APPLICATION_JSON ───────────────────────────────────── SPRING_APPLICATION_JSON="$(cat <" +fi log " pdserver.address=${HG_STORE_PD_ADDRESS}" log " grpc.host=${HG_STORE_GRPC_HOST}" log " grpc.port=${HG_STORE_GRPC_PORT}" diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/listener/PdConfigureListener.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/listener/PdConfigureListener.java index 070d3c15ee..c9b7a734b8 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/listener/PdConfigureListener.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/listener/PdConfigureListener.java @@ -63,11 +63,14 @@ public class PdConfigureListener implements + "restart-hugegraph-store.sh"; private ConfigurableApplicationContext context; private File pdConfFile; + private String selectedProvider; // private String restartPath = workDir + fileSeparator + "lib" + fileSeparator; @Override public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { MutablePropertySources sources = event.getEnvironment().getPropertySources(); + this.selectedProvider = event.getEnvironment().getProperty("TOPLINGDB_ROCKSDB_PROVIDER"); + this.prioritizeSelectedProvider(sources); String pdAddress = event.getEnvironment().getProperty("pdserver.address"); pdConfFile = new File(configFilePath); @@ -123,9 +126,23 @@ private void updatePdConfig(MutablePropertySources sources, KvClient client, writeYml(pdConfig); PropertiesPropertySource source = new PropertiesPropertySource("pd-config", configs); sources.addFirst(source); + this.prioritizeSelectedProvider(sources); } } + private void prioritizeSelectedProvider(MutablePropertySources sources) { + if (StringUtils.isEmpty(this.selectedProvider)) { + return; + } + if (!"rocksdb".equals(this.selectedProvider) && !"topling".equals(this.selectedProvider)) { + throw new IllegalArgumentException("Invalid TOPLINGDB_ROCKSDB_PROVIDER: " + + this.selectedProvider); + } + Properties override = new Properties(); + override.setProperty("rocksdb.provider", this.selectedProvider); + sources.addFirst(new PropertiesPropertySource("rocksdb-runtime-provider", override)); + } + private Properties getYmlConfig(String yml) { Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions())); Iterable load = yaml.loadAll(yml); diff --git a/hugegraph-store/hg-store-node/src/test/java/org/apache/hugegraph/store/node/listener/PdConfigureListenerYamlTest.java b/hugegraph-store/hg-store-node/src/test/java/org/apache/hugegraph/store/node/listener/PdConfigureListenerYamlTest.java index b54ce9efc0..755f5cf37b 100644 --- a/hugegraph-store/hg-store-node/src/test/java/org/apache/hugegraph/store/node/listener/PdConfigureListenerYamlTest.java +++ b/hugegraph-store/hg-store-node/src/test/java/org/apache/hugegraph/store/node/listener/PdConfigureListenerYamlTest.java @@ -19,24 +19,107 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.lang.reflect.Field; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Arrays; import java.util.List; import java.util.Properties; +import javax.tools.ToolProvider; + +import org.apache.hugegraph.pd.client.KvClient; +import org.apache.hugegraph.pd.grpc.kv.KResponse; +import org.apache.hugegraph.util.RocksDBRuntime; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mockito; import org.springframework.boot.SpringApplication; import org.springframework.boot.WebApplicationType; import org.springframework.boot.env.YamlPropertySourceLoader; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.PropertySource; +import org.springframework.core.env.PropertiesPropertySource; +import org.springframework.core.env.StandardEnvironment; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.FileSystemResource; import org.yaml.snakeyaml.error.YAMLException; public class PdConfigureListenerYamlTest { + @TempDir + public Path temporary; + + @Test + public void testSelectedRuntimeSurvivesRetainedPdConfiguration() throws Exception { + for (String provider : new String[]{"rocksdb", "topling"}) { + StandardEnvironment environment = new StandardEnvironment(); + Properties image = new Properties(); + image.setProperty("rocksdb.provider", provider); + environment.getPropertySources().addFirst(new PropertiesPropertySource("spring.application.json", image)); + String retained = "rocksdb:\n provider: " + + ("topling".equals(provider) ? "rocksdb" : "topling") + + "\n max_background_jobs: 8\n"; + Path file = this.temporary.resolve(provider + "-application-pd.yml"); + Files.writeString(file, retained); + PdConfigureListener listener = new PdConfigureListener(); + set(listener, "pdConfFile", file.toFile()); + set(listener, "selectedProvider", provider); + KvClient client = Mockito.mock(KvClient.class); + Mockito.when(client.get("S:Timestamp")).thenReturn( + KResponse.newBuilder().setValue(Long.toString(System.currentTimeMillis() + 60000)).build()); + Method update = PdConfigureListener.class.getDeclaredMethod( + "updatePdConfig", org.springframework.core.env.MutablePropertySources.class, + KvClient.class, String.class); + update.setAccessible(true); + update.invoke(listener, environment.getPropertySources(), client, retained); + Assertions.assertEquals(provider, environment.getProperty("rocksdb.provider")); + Assertions.assertEquals("8", environment.getProperty("rocksdb.max_background_jobs")); + Assertions.assertEquals(retained, Files.readString(file)); + verifyRuntime(provider, environment.getProperty("rocksdb.provider")); + } + } + + @Test + public void testNoRuntimeSelectionDoesNotOverrideManagedProvider() throws Exception { + StandardEnvironment environment = new StandardEnvironment(); + Properties configured = new Properties(); + configured.setProperty("rocksdb.provider", "topling"); + environment.getPropertySources().addFirst(new PropertiesPropertySource("pd-config", configured)); + Method prioritize = PdConfigureListener.class.getDeclaredMethod( + "prioritizeSelectedProvider", org.springframework.core.env.MutablePropertySources.class); + prioritize.setAccessible(true); + prioritize.invoke(new PdConfigureListener(), environment.getPropertySources()); + Assertions.assertEquals("topling", environment.getProperty("rocksdb.provider")); + Assertions.assertFalse(environment.getPropertySources().contains("rocksdb-runtime-provider")); + } + + private static void set(PdConfigureListener listener, String name, Object value) throws Exception { + Field field = PdConfigureListener.class.getDeclaredField(name); + field.setAccessible(true); + field.set(listener, value); + } + + private void verifyRuntime(String runtime, String configured) throws Exception { + Path classes = Files.createDirectory(this.temporary.resolve(runtime)); + Path source = classes.resolve("RocksDB.java"); + Files.writeString(source, "package org.rocksdb; public class RocksDB {}" + + ("topling".equals(runtime) ? " class SidePluginRepo {}" : "")); + Assertions.assertEquals(0, ToolProvider.getSystemJavaCompiler().run( + null, null, null, "-d", classes.toString(), source.toString())); + ClassLoader previous = Thread.currentThread().getContextClassLoader(); + try (URLClassLoader loader = new URLClassLoader(new URL[]{classes.toUri().toURL()}, null)) { + Thread.currentThread().setContextClassLoader(loader); + RocksDBRuntime.verify(configured); + } finally { + Thread.currentThread().setContextClassLoader(previous); + } + } + @Test public void testFlattenMapsAndMultipleDocuments() throws Exception { Properties properties = parse("rocksdb:\n max_background_jobs: 8\n" + diff --git a/install-dist/scripts/dependency/optional-image-dependencies.txt b/install-dist/scripts/dependency/optional-image-dependencies.txt new file mode 100644 index 0000000000..5ccf58cb8b --- /dev/null +++ b/install-dist/scripts/dependency/optional-image-dependencies.txt @@ -0,0 +1,19 @@ +# Optional image-only dependency; excluded from the standard Maven/lib inventory. +# The API version does not identify immutable native contents: use the URL and +# SHA-256 pin in docker/topling-runtime.env and the embedded source provenance. + +runtime=topling-community +version=8.10.2 +source_repository=https://github.com/hugegraph/toplingdb +artifact_filename=rocksdbjni-8.10.2-linux64.jar +artifact_pin=docker/topling-runtime.env:TOPLING_JNI_URL,TOPLING_JNI_SHA256 +installed_server=/hugegraph-server/topling/rocksdbjni.jar +installed_pd=/hugegraph-pd/topling/rocksdbjni.jar +installed_store=/hugegraph-store/topling/rocksdbjni.jar +provenance=META-INF/topling-build-info.txt +embedded_licenses=META-INF/licenses/ +embedded_license_checksums=META-INF/licenses/manifest.sha256 +installed_license_directory=topling/licenses/ +installed_license_summary=topling/LICENSE +installed_notice=topling/NOTICE +build_flags=WITH_TOPLING_ROCKS=0 WITH_CSPP_MEMTABLE=0 WITH_TOPLING_DCOMPACT=0