From a841f0bf13f161ebb3007450122a93ca58033583 Mon Sep 17 00:00:00 2001 From: Himanshu Verma Date: Thu, 30 Jul 2026 21:20:01 +0530 Subject: [PATCH 1/8] feat(helm): add HStore deployment chart --- .github/workflows/helm-chart-ci.yml | 82 ++ README.md | 11 +- helm/hugegraph/.helmignore | 33 + helm/hugegraph/Chart.yaml | 36 + helm/hugegraph/README.md | 396 +++++++++ helm/hugegraph/templates/NOTES.txt | 46 ++ helm/hugegraph/templates/_helpers.tpl | 283 +++++++ helm/hugegraph/templates/pd-pdb.yaml | 32 + .../templates/pd-service-client.yaml | 36 + .../templates/pd-service-headless.yaml | 41 + helm/hugegraph/templates/pd-statefulset.yaml | 158 ++++ .../templates/server-deployment.yaml | 242 ++++++ helm/hugegraph/templates/server-hpa.yaml | 40 + helm/hugegraph/templates/server-ingress.yaml | 53 ++ helm/hugegraph/templates/server-pdb.yaml | 33 + helm/hugegraph/templates/server-service.yaml | 41 + helm/hugegraph/templates/serviceaccount.yaml | 35 + helm/hugegraph/templates/store-pdb.yaml | 32 + .../templates/store-service-headless.yaml | 41 + .../templates/store-statefulset.yaml | 193 +++++ .../templates/tests/test-connection.yaml | 67 ++ .../testdata/values-pre-hardening.yaml | 128 +++ helm/hugegraph/values-cluster.yaml | 86 ++ helm/hugegraph/values-single.yaml | 40 + helm/hugegraph/values.schema.json | 765 ++++++++++++++++++ helm/hugegraph/values.yaml | 256 ++++++ 26 files changed, 3204 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/helm-chart-ci.yml create mode 100644 helm/hugegraph/.helmignore create mode 100644 helm/hugegraph/Chart.yaml create mode 100644 helm/hugegraph/README.md create mode 100644 helm/hugegraph/templates/NOTES.txt create mode 100644 helm/hugegraph/templates/_helpers.tpl create mode 100644 helm/hugegraph/templates/pd-pdb.yaml create mode 100644 helm/hugegraph/templates/pd-service-client.yaml create mode 100644 helm/hugegraph/templates/pd-service-headless.yaml create mode 100644 helm/hugegraph/templates/pd-statefulset.yaml create mode 100644 helm/hugegraph/templates/server-deployment.yaml create mode 100644 helm/hugegraph/templates/server-hpa.yaml create mode 100644 helm/hugegraph/templates/server-ingress.yaml create mode 100644 helm/hugegraph/templates/server-pdb.yaml create mode 100644 helm/hugegraph/templates/server-service.yaml create mode 100644 helm/hugegraph/templates/serviceaccount.yaml create mode 100644 helm/hugegraph/templates/store-pdb.yaml create mode 100644 helm/hugegraph/templates/store-service-headless.yaml create mode 100644 helm/hugegraph/templates/store-statefulset.yaml create mode 100644 helm/hugegraph/templates/tests/test-connection.yaml create mode 100644 helm/hugegraph/testdata/values-pre-hardening.yaml create mode 100644 helm/hugegraph/values-cluster.yaml create mode 100644 helm/hugegraph/values-single.yaml create mode 100644 helm/hugegraph/values.schema.json create mode 100644 helm/hugegraph/values.yaml diff --git a/.github/workflows/helm-chart-ci.yml b/.github/workflows/helm-chart-ci.yml new file mode 100644 index 0000000000..a8ad116a48 --- /dev/null +++ b/.github/workflows/helm-chart-ci.yml @@ -0,0 +1,82 @@ +# +# 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. +# + +name: "Helm Chart CI" + +on: + push: + branches: [ master, 'release-*' ] + paths: [ 'helm/**', '.github/workflows/helm-chart-ci.yml' ] + pull_request: + paths: [ 'helm/**', '.github/workflows/helm-chart-ci.yml' ] + +permissions: + contents: read + +jobs: + lint-and-render: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: azure/setup-helm@v4 + with: + version: v3.16.2 + + - name: helm lint + run: | + helm lint helm/hugegraph + helm lint helm/hugegraph -f helm/hugegraph/values-single.yaml + helm lint helm/hugegraph -f helm/hugegraph/values-cluster.yaml + + - name: helm template + run: | + for preset in "" "-f helm/hugegraph/values-single.yaml" "-f helm/hugegraph/values-cluster.yaml"; do + helm template ci helm/hugegraph $preset > /dev/null + done + + - name: reject invalid values + run: | + # each of these must fail; the schema and helpers are the contract + ! helm template ci helm/hugegraph --set pd.replicas=100 2>/dev/null + ! helm template ci helm/hugegraph --set pd.pdb.minAvailable=3 2>/dev/null + ! helm template ci helm/hugegraph --set server.hpa.enabled=true 2>/dev/null + ! helm template ci helm/hugegraph --set server.auth.enabled=true 2>/dev/null + + - name: kubeconform + shell: bash + run: | + set -o pipefail + curl -sSLo /tmp/kc.tar.gz https://github.com/yannh/kubeconform/releases/download/v0.6.7/kubeconform-linux-amd64.tar.gz + tar -xzf /tmp/kc.tar.gz -C /tmp + for preset in "" "-f helm/hugegraph/values-single.yaml" "-f helm/hugegraph/values-cluster.yaml"; do + helm template ci helm/hugegraph $preset | /tmp/kubeconform -strict -summary -kubernetes-version 1.23.0 + done + + - name: legacy --reuse-values compatibility + run: | + # A release created before a field existed must still render. This has + # regressed five times, so it is guarded here rather than by review. + # `-f` is NOT equivalent: it merges over the new defaults, whereas + # --reuse-values discards them, so the fixture must become values.yaml. + cp -R helm/hugegraph /tmp/legacy + cp helm/hugegraph/testdata/values-pre-hardening.yaml /tmp/legacy/values.yaml + helm template legacy /tmp/legacy > /dev/null + helm install legacy /tmp/legacy --dry-run=client > /dev/null + + - name: helm package + run: helm package helm/hugegraph -d /tmp/chart diff --git a/README.md b/README.md index adf9792776..a876069740 100644 --- a/README.md +++ b/README.md @@ -207,9 +207,16 @@ For advanced Docker configurations, see: > > **Version Tags**: Use release tags (e.g., `1.7.0`) for stable deployments. The `latest` tag should only be used for testing or development. +### Option 2: Kubernetes with Helm + +The HStore Helm chart deploys HugeGraph PD, Store, and Server as a distributed +Kubernetes cluster. See the [chart documentation](helm/hugegraph/README.md) for +single-node and highly available presets, configuration, and +upgrade guidance. +
-Option 2: Download Binary Package +Option 3: Download Binary Package Download pre-built packages from the [Download Page](https://hugegraph.apache.org/docs/download/download/): @@ -242,7 +249,7 @@ For detailed instructions, see the [Binary Installation Guide](https://hugegraph
-Option 3: Build from Source +Option 4: Build from Source Build from source for development or customization: diff --git a/helm/hugegraph/.helmignore b/helm/hugegraph/.helmignore new file mode 100644 index 0000000000..8016b1b85d --- /dev/null +++ b/helm/hugegraph/.helmignore @@ -0,0 +1,33 @@ +# 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. + +# Patterns to ignore when building packages. +.DS_Store +.git/ +.gitignore +*.swp +*.bak +*.tmp +*.orig +*~ +.idea/ +.vscode/ + +# Contributor tooling, if present in a working tree. Never ship it. +scripts/ +testdata/ + +# Never package a chart archive inside a chart. +*.tgz diff --git a/helm/hugegraph/Chart.yaml b/helm/hugegraph/Chart.yaml new file mode 100644 index 0000000000..0b7f7ccde1 --- /dev/null +++ b/helm/hugegraph/Chart.yaml @@ -0,0 +1,36 @@ +# +# 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. +# + +apiVersion: v2 +name: hugegraph +description: Helm chart for Apache HugeGraph HStore cluster (PD + Store + Server) +type: application +version: 0.1.0 +appVersion: "latest" +kubeVersion: ">=1.23.0-0" +keywords: + - hugegraph + - graph + - hstore + - raft +home: https://hugegraph.apache.org/ +sources: + - https://github.com/apache/hugegraph +maintainers: + - name: HugeGraph Community + url: https://hugegraph.apache.org/ + email: dev@hugegraph.apache.org diff --git a/helm/hugegraph/README.md b/helm/hugegraph/README.md new file mode 100644 index 0000000000..6d03f7677b --- /dev/null +++ b/helm/hugegraph/README.md @@ -0,0 +1,396 @@ +# HugeGraph HStore Helm Chart + +[Apache HugeGraph](https://hugegraph.apache.org/) - an open source, distributed graph database. + +## Documentation + +This chart deploys a distributed HugeGraph cluster - PD, Store, and Server - on +Kubernetes. For HugeGraph itself see . + +Note that this chart requires Helm 3. `--reset-then-reuse-values`, referenced +under Upgrading, requires Helm 3.14 or later. + +## Prerequisites Details + +* Kubernetes 1.23+ (the chart renders `autoscaling/v2` and `policy/v1`) +* PV support on the underlying infrastructure: a default StorageClass, or an + explicit `storageClassName` for PD and Store +* Sufficient memory for nine JVM processes in the default topology. + Insufficient memory causes OOM kills that surface as silent Raft failures + rather than as clear errors. + +## Chart Details + +| Component | Workload | Purpose | +|---|---|---| +| PD | StatefulSet + PVC | Placement driver; Raft group tracking Stores and partitions | +| Store | StatefulSet + PVC | Graph data storage (HStore) | +| Server | Deployment | Gremlin and REST query layer | + +A distributed HugeGraph cluster has a startup contract that this chart encodes +so operators do not have to: + +- **Server does not run `init-store`.** The chart injects + `HG_SERVER_INIT_STORE_ENABLED=false`, and the image's `init-store` exits when + `init_store.enabled=false`, after which Server registers with PD normally. + This matters because nothing serializes Server replicas: without the gate, + every replica would initialize the same backend concurrently. The chart + creates no init Job and does not set `HG_SERVER_SKIP_INIT`. Standalone + behavior is unchanged, because the option defaults to `true` when unset. +- **Store waits for PD quorum** in an init container before starting, so Store + never registers against an incomplete PD Raft group. +- **The Server startup probe allows at least 450 seconds.** The image may spend + 300 seconds waiting for storage and a further 120 seconds in the start + command. A lower configured `failureThreshold` is raised to this floor rather + than being rejected. +- **The image entrypoint keeps ownership of `PASSWORD` handling and + `auth.admin_pa`.** When authentication or a custom port or REST tuning is + configured, the chart's wrapper only ensures `usePD=true` and `pd.peers` are + present, then hands off to the image entrypoint. +- **Resource names reserve their suffix and StatefulSet ordinal before + truncation,** so a long release name cannot produce colliding or over-long + Pod and Service names, and PD/Store identities stay fixed when replicas + change. + +## Installing the Chart + +```bash +helm install hugegraph ./helm/hugegraph --namespace hugegraph --create-namespace +``` + +This deploys 3 PD + 3 Store + 3 Server, preserves the image's automatic JVM +sizing, and sets no resource requests or limits. Set resources before +production use. + +This first chart is version `0.1.0`. While the contribution is a draft, its +component image tags and `appVersion` track `latest` with pull policy `Always`. +Before stable publication, pin all three component tags and `appVersion` to the +next HugeGraph release and switch the component pull policies to +`IfNotPresent`. + +Verify the release: + +```bash +helm test hugegraph --namespace hugegraph +``` + +### Values Presets + +| File | Purpose | +|---|---| +| `values.yaml` | Default 3+3+3 topology | +| `values-single.yaml` | Single-node 1+1+1 example | +| `values-cluster.yaml` | Production 3+3+3 starting point with JVM/resources, PD/Store PDBs, and required anti-affinity | + +`values-cluster.yaml` is a production starting point, not a capacity +guarantee. Recalculate capacity for the graph size, traffic, failure budget, +node topology, and storage class before production use. + +## Upgrading the Chart + +```bash +helm upgrade hugegraph ./helm/hugegraph --namespace hugegraph --reuse-values +``` + +Every optional field stays optional, so a release created by an earlier +revision continues to render under `--reuse-values`. Note that `--reuse-values` +keeps the old release's values as the complete base, so a release created +before a field existed does **not** pick up its new default — including the +hardened `securityContext`, ServiceAccounts, and `terminationGracePeriodSeconds`. +Pod-level token mounting is the one exception: it is disabled unconditionally. +Use `-f` with your own values, or `--reset-then-reuse-values`, to adopt them. + +PD and Store resource names reserve room for their StatefulSet ordinal before truncation, so +identities stay fixed across replica changes and scaling never renames a +PersistentVolumeClaim. + +## Uninstalling the Chart + +```bash +helm uninstall hugegraph --namespace hugegraph +``` + +Helm does not remove PersistentVolumeClaims created by StatefulSets. Delete +them explicitly, and only when the data is no longer needed. + +## Configuration + +The following table lists the configurable parameters of the chart and their +default values. + +### Global + +| Parameter | Description | Default | +|---|---|---| +| `nameOverride` | Override the chart name in generated resource names | `""` | +| `fullnameOverride` | Override the full generated resource name | `""` | +| `imagePullSecrets` | Secrets used to pull the PD, Store, and Server images | `[]` | + +### PD + +| Parameter | Description | Default | +|---|---|---| +| `pd.replicas` | PD StatefulSet replicas. Maximum `99` | `3` | +| `pd.image.repository` | PD image repository | `hugegraph/pd` | +| `pd.image.tag` | PD image tag. Tracks the development image until the next release is pinned | `latest` | +| `pd.image.pullPolicy` | PD image pull policy | `Always` | +| `pd.javaOpts` | Empty preserves the image's automatic JVM sizing | `""` | +| `pd.ports.grpc` | PD gRPC port | `8686` | +| `pd.ports.rest` | PD REST port, also used by probes | `8620` | +| `pd.ports.raft` | PD Raft port | `8610` | +| `pd.dataPath` | PD data directory inside the container | `/hugegraph-pd/pd_data` | +| `pd.storage.size` | PD PersistentVolumeClaim size | `10Gi` | +| `pd.storage.storageClassName` | Empty uses the cluster default StorageClass | `""` | +| `pd.resources` | PD container resources. Set these for production | `{}` | +| `pd.podSecurityContext` | Pod-level securityContext, rendered only when set | `{}` | +| `pd.securityContext` | Container-level securityContext. Hardened by default; `runAsNonRoot` is not set because the published images run as root | `allowPrivilegeEscalation: false`, `capabilities.drop: [ALL]`, `seccompProfile: RuntimeDefault` | +| `pd.antiAffinity` | One of `required`, `preferred`, `disabled` | `required` | +| `pd.nodeSelector` | Node selector for pd Pods | `{}` | +| `pd.tolerations` | Tolerations for pd Pods | `[]` | +| `pd.affinity` | Raw affinity; overrides `pd.antiAffinity` when set | `{}` | +| `pd.topologySpreadConstraints` | Topology spread constraints for pd Pods | `[]` | +| `pd.priorityClassName` | PriorityClass for pd Pods | `""` | +| `pd.podAnnotations` | Extra annotations on pd Pods | `{}` | +| `pd.podLabels` | Extra labels on pd Pods | `{}` | +| `pd.extraEnv` | Extra environment variables for the PD container | `[]` | +| `pd.terminationGracePeriodSeconds` | Shutdown grace period | `300` | +| `pd.serviceAccount.create` | Create a ServiceAccount for pd | `true` | +| `pd.serviceAccount.name` | Use an existing ServiceAccount instead | `""` | +| `pd.serviceAccount.annotations` | Annotations on the created ServiceAccount | `{}` | +| `pd.serviceAccount.automountServiceAccountToken` | Mount an API token. The chart makes no API calls | `false` | +| `pd.pdb.enabled` | Create a PodDisruptionBudget for PD | `true` | +| `pd.pdb.minAvailable` | Must be strictly less than `pd.replicas`. No PDB is rendered when `pd.replicas` is 1 | `2` | +| `pd.probes.*.periodSeconds` | Probe interval | see `values.yaml` | +| `pd.probes.*.failureThreshold` | Probe failure threshold | see `values.yaml` | +| `pd.probes.*.timeoutSeconds` | Probe timeout. Defaults to `5` on readiness/liveness; Kubernetes would otherwise apply `1` | `5` | +| `pd.probes.*.initialDelaySeconds` | Optional probe start delay | unset | +| `pd.probes.*.successThreshold` | Optional probe success threshold | unset | + +### Store + +| Parameter | Description | Default | +|---|---|---| +| `store.replicas` | Store StatefulSet replicas. Maximum `99` | `3` | +| `store.image.repository` | Store image repository | `hugegraph/store` | +| `store.image.tag` | Store image tag. Tracks the development image until the next release is pinned | `latest` | +| `store.image.pullPolicy` | Store image pull policy | `Always` | +| `store.javaOpts` | Empty preserves the image's automatic JVM sizing | `""` | +| `store.ports.grpc` | Store gRPC port | `8500` | +| `store.ports.raft` | Store Raft port | `8510` | +| `store.ports.rest` | Store REST port | `8520` | +| `store.dataPath` | Store data directory | `/hugegraph-store/storage` | +| `store.storage.size` | Store PersistentVolumeClaim size | `50Gi` | +| `store.storage.storageClassName` | Empty uses the cluster default StorageClass | `""` | +| `store.resources` | Store container resources. Set these for production | `{}` | +| `store.podSecurityContext` | Pod-level securityContext, rendered only when set | `{}` | +| `store.securityContext` | Container-level securityContext; also applied to the PD-quorum init container. Hardened by default; `runAsNonRoot` is not set because the published images run as root | `allowPrivilegeEscalation: false`, `capabilities.drop: [ALL]`, `seccompProfile: RuntimeDefault` | +| `store.waitTimeoutSeconds` | Bound on the PD-quorum wait before the init container fails | `900` | +| `store.antiAffinity` | One of `required`, `preferred`, `disabled` | `required` | +| `store.nodeSelector` | Node selector for store Pods | `{}` | +| `store.tolerations` | Tolerations for store Pods | `[]` | +| `store.affinity` | Raw affinity; overrides `store.antiAffinity` when set | `{}` | +| `store.topologySpreadConstraints` | Topology spread constraints for store Pods | `[]` | +| `store.priorityClassName` | PriorityClass for store Pods | `""` | +| `store.podAnnotations` | Extra annotations on store Pods | `{}` | +| `store.podLabels` | Extra labels on store Pods | `{}` | +| `store.extraEnv` | Extra environment variables for the Store container | `[]` | +| `store.terminationGracePeriodSeconds` | Shutdown grace period | `300` | +| `store.serviceAccount.create` | Create a ServiceAccount for store | `true` | +| `store.serviceAccount.name` | Use an existing ServiceAccount instead | `""` | +| `store.serviceAccount.annotations` | Annotations on the created ServiceAccount | `{}` | +| `store.serviceAccount.automountServiceAccountToken` | Mount an API token. The chart makes no API calls | `false` | +| `store.pdb.enabled` | Create a PodDisruptionBudget for Store | `true` | +| `store.pdb.minAvailable` | Must be strictly less than `store.replicas`. No PDB is rendered when `store.replicas` is 1 | `2` | +| `store.waitImage` | Image for the PD-quorum init container | `curlimages/curl:8.5.0` | +| `store.waitResources` | Resources for the init container | `{}` | +| `store.probes.*` | Same probe keys as PD | see `values.yaml` | + +### Server + +| Parameter | Description | Default | +|---|---|---| +| `server.replicas` | Server Deployment replicas. Ignored when `server.hpa.enabled` | `3` | +| `server.image.repository` | Server image repository | `hugegraph/server` | +| `server.image.tag` | Server image tag. Tracks the development image until the next release is pinned | `latest` | +| `server.image.pullPolicy` | Server image pull policy | `Always` | +| `server.javaOpts` | Empty preserves the image's automatic JVM sizing | `""` | +| `server.port` | Server REST port, container port, and Service port | `8080` | +| `server.backend` | Storage backend | `hstore` | +| `server.resources` | Server resources. `requests.cpu` is required when HPA is enabled | `{}` | +| `server.podSecurityContext` | Pod-level securityContext, rendered only when set | `{}` | +| `server.securityContext` | Container-level securityContext. Hardened by default; `runAsNonRoot` is not set because the published images run as root | `allowPrivilegeEscalation: false`, `capabilities.drop: [ALL]`, `seccompProfile: RuntimeDefault` | +| `server.pdb.enabled` | Create a PodDisruptionBudget for Server. Off by default: Server holds no quorum | `false` | +| `server.pdb.minAvailable` | Must be strictly less than `server.replicas` | `2` | +| `server.antiAffinity` | One of `required`, `preferred`, `disabled`. Defaults to `preferred` rather than `required` because HPA may scale Server past the node count; set `required` when replicas always stay below it | `preferred` | +| `server.nodeSelector` | Node selector for server Pods | `{}` | +| `server.tolerations` | Tolerations for server Pods | `[]` | +| `server.affinity` | Raw affinity; overrides `server.antiAffinity` when set | `{}` | +| `server.topologySpreadConstraints` | Topology spread constraints for server Pods | `[]` | +| `server.priorityClassName` | PriorityClass for server Pods | `""` | +| `server.podAnnotations` | Extra annotations on server Pods | `{}` | +| `server.podLabels` | Extra labels on server Pods | `{}` | +| `server.extraEnv` | Extra environment variables for the Server container | `[]` | +| `server.terminationGracePeriodSeconds` | Shutdown grace period | `60` | +| `server.serviceAccount.create` | Create a ServiceAccount for server | `true` | +| `server.serviceAccount.name` | Use an existing ServiceAccount instead | `""` | +| `server.serviceAccount.annotations` | Annotations on the created ServiceAccount | `{}` | +| `server.serviceAccount.automountServiceAccountToken` | Mount an API token. The chart makes no API calls | `false` | +| `server.waitImage` | Image for the Helm test hook | `curlimages/curl:8.5.0` | +| `server.restServer.minFreeMemory` | Empty preserves the image default | `""` | +| `server.restServer.batchMaxWriteThreads` | Empty preserves the image default | `""` | +| `server.initStoreEnabled` | Must remain `false` for distributed HStore | `false` | +| `server.auth.enabled` | Enable admin authentication | `false` | +| `server.auth.existingSecret` | Required when auth is enabled; must contain key `password` | `""` | +| `server.ingress.enabled` | Create an Ingress for the Server Service | `false` | +| `server.ingress.className` | IngressClass name | `""` | +| `server.ingress.annotations` | Ingress annotations (cert-manager, nginx, ALB) | `{}` | +| `server.service.type` | Server Service type | `ClusterIP` | +| `server.service.annotations` | Server Service annotations | `{}` | +| `server.ingress.hosts` | Ingress hosts and paths | see `values.yaml` | +| `server.ingress.tls` | Ingress TLS configuration | `[]` | +| `server.hpa.enabled` | Create a HorizontalPodAutoscaler | `false` | +| `server.hpa.minReplicas` | HPA minimum replicas | `3` | +| `server.hpa.maxReplicas` | HPA maximum replicas | `10` | +| `server.hpa.targetCPUUtilizationPercentage` | HPA CPU utilization target | `70` | +| `server.probes.startup.failureThreshold` | Raised automatically so the budget is at least 450s | `90` | +| `server.probes.startup.periodSeconds` | Startup probe interval | `5` | +| `server.probes.*` | Same optional probe keys as PD | see `values.yaml` | + +When `server.hpa.enabled` is `true` the Deployment omits `spec.replicas`, so a +Helm upgrade does not overwrite the autoscaler's live replica count. Enabling +utilization-based HPA requires a strictly positive +`server.resources.requests.cpu`. + +Specify each parameter with `--set`, or supply a YAML file with `-f`: + +```bash +helm install hugegraph ./helm/hugegraph --set server.replicas=5 +``` + +`values.schema.json` and template helpers reject invalid input at render time, +before anything reaches the cluster: + +- Unknown keys and wrong types are rejected. +- `server.initStoreEnabled` must remain `false` for a distributed deployment. +- With authentication enabled, `server.auth.existingSecret` must name a Secret + containing a `password` key. With authentication disabled it must be empty, + so a configured but inactive Secret reference cannot be overlooked. A missing + Secret fails when Kubernetes configures the container; an empty `password` + fails in the Server startup wrapper. +- `server.hpa.minReplicas` must not exceed `maxReplicas`, and enabling + utilization-based HPA requires a strictly positive + `server.resources.requests.cpu`. +- `pdb.minAvailable` must be less than the matching `replicas`, so a + PodDisruptionBudget cannot permanently block node drains. +- `pd.replicas` and `store.replicas` are capped at 99. + +## Deep Dive + +### Connecting to the Cluster + +```bash +kubectl port-forward -n hugegraph svc/hugegraph-server 8080:8080 +curl http://127.0.0.1:8080/versions +curl http://127.0.0.1:8080/graphs +``` + +### Cluster Health + +| Component | Port | Purpose | +|------|-------------|---------| +| PD | `8686` | gRPC (Store and Server clients) | +| PD | `8620` | REST / health probes | +| PD | `8610` | Raft | +| Store | `8500` | gRPC | +| Store | `8510` | Raft | +| Store | `8520` | REST / health probes | +| Server | `8080` | Gremlin and REST API | + +All ports are configurable through `values.yaml`. Changing `server.port` updates +the listener, container port, and Service together. + +--- + +### Scaling + +PD and Store reserve the maximum StatefulSet ordinal in their resource names, +so scaling never renames a PersistentVolumeClaim or shifts a Pod identity. +Both are capped at 99 replicas. + +Server scales through `server.replicas`, or by enabling `server.hpa`. With HPA +enabled the Deployment omits `spec.replicas`, so a Helm upgrade does not +overwrite the autoscaler's live replica count. + +## Troubleshooting + +### Store Pods Stuck in `Init:0/1` + +The Store init container waits for PD to reach Raft quorum. Check PD first: + +```bash +kubectl get pods -l app.kubernetes.io/component=pd +kubectl logs -c wait-for-pd +``` + +The wait is bounded by `store.waitTimeoutSeconds` (default 900). On timeout the +init container exits with a message naming the peers it polled, so the failure +appears in `kubectl describe pod` instead of hanging silently. + +### PersistentVolumeClaims Stay `Pending` + +No default StorageClass, or the provisioner is unhealthy: + +```bash +kubectl get sc +kubectl get pvc -l app.kubernetes.io/instance= +kubectl -n get pods +``` + +### Server Ready but Queries Fail + +The Server readiness probe uses `/versions`, which can report ready before the +graph is fully able to serve index-backed queries. Confirm the graph is live: + +```bash +kubectl exec -c server -- curl -s localhost:8080/graphs +``` + +### Pods OOM Killed or Restarting + +The default `values.yaml` sets **no** resource requests or limits and preserves +the image's automatic JVM sizing. Set resources explicitly before production +use; see `values-cluster.yaml`. + +```bash +kubectl get pods -o wide +kubectl describe pod | grep -A5 "Last State" +``` + +### Release Name Too Long + +Helm itself rejects release names longer than 53 characters, before this chart +renders anything: + +``` +invalid release name ... the length must not be longer than 53 +``` + +Within that limit the chart is safe: resource names reserve their suffix and +StatefulSet ordinal before truncation, so every generated Service and Pod name +stays inside the 63-character DNS label limit, and PD/Store identities do not +shift when replicas change. Use `fullnameOverride` to shorten generated names +independently of the release name. + +--- + +## Limitations + +- No TLS, backups, Operator, multi-cluster support, automatic leader transfer, + or a complete monitoring stack. +- The published images run as root, so `runAsNonRoot` and + `readOnlyRootFilesystem` are not chart defaults. The container + `securityContext` does default to `allowPrivilegeEscalation: false`, + `capabilities.drop: [ALL]`, and `seccompProfile: RuntimeDefault`, which are + valid for a root image; `podSecurityContext` and `securityContext` are fully + configurable per component. +- `values-cluster.yaml` is a starting point, not a capacity guarantee. diff --git a/helm/hugegraph/templates/NOTES.txt b/helm/hugegraph/templates/NOTES.txt new file mode 100644 index 0000000000..288c8d6f5f --- /dev/null +++ b/helm/hugegraph/templates/NOTES.txt @@ -0,0 +1,46 @@ +{{/* +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. +*/}} +{{ .Release.Name }} installed in namespace {{ .Release.Namespace }}. + + PD {{ .Values.pd.replicas }} replica(s) + Store {{ .Values.store.replicas }} replica(s) + Server {{ if .Values.server.hpa.enabled }}HPA {{ .Values.server.hpa.minReplicas }}-{{ .Values.server.hpa.maxReplicas }}{{ else }}{{ .Values.server.replicas }} replica(s){{ end }} + +Watch the cluster come up: + + kubectl get pods -n {{ .Release.Namespace }} -l app.kubernetes.io/instance={{ .Release.Name }} -w + +PD reaches Raft quorum before Store registers, so Store Pods stay in Init until +PD is ready. + +Verify the release: + + helm test {{ .Release.Name }} --namespace {{ .Release.Namespace }} + +Reach the Server API: + + kubectl port-forward -n {{ .Release.Namespace }} svc/{{ include "hugegraph.server.name" . }} {{ .Values.server.port }}:{{ .Values.server.port }} + curl http://127.0.0.1:{{ .Values.server.port }}/versions +{{- if not .Values.server.auth.enabled }} + +Authentication is disabled. Do not expose this release to untrusted networks. +{{- end }} +{{- if or (empty .Values.pd.resources) (empty .Values.store.resources) (empty .Values.server.resources) }} + +One or more components have no resource requests or limits. Set them before +production use; see values-cluster.yaml. +{{- end }} diff --git a/helm/hugegraph/templates/_helpers.tpl b/helm/hugegraph/templates/_helpers.tpl new file mode 100644 index 0000000000..3f64b2b2b4 --- /dev/null +++ b/helm/hugegraph/templates/_helpers.tpl @@ -0,0 +1,283 @@ +# +# 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. +# + +{{/* +Expand the name of the chart. +*/}} +{{- define "hugegraph.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "hugegraph.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{- define "hugegraph.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "hugegraph.labels" -}} +helm.sh/chart: {{ include "hugegraph.chart" . }} +{{ include "hugegraph.selectorLabels" . }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{- define "hugegraph.selectorLabels" -}} +app.kubernetes.io/name: {{ include "hugegraph.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{- define "hugegraph.pd.name" -}} +{{- printf "%s-pd" (include "hugegraph.fullname" . | trunc 57 | trimSuffix "-") }} +{{- end }} + +{{- define "hugegraph.pd.clientName" -}} +{{- printf "%s-pd-client" (include "hugegraph.fullname" . | trunc 53 | trimSuffix "-") }} +{{- end }} + +{{- define "hugegraph.store.name" -}} +{{- printf "%s-store" (include "hugegraph.fullname" . | trunc 54 | trimSuffix "-") }} +{{- end }} + +{{- define "hugegraph.server.name" -}} +{{- printf "%s-server" (include "hugegraph.fullname" . | trunc 56 | trimSuffix "-") }} +{{- end }} + +{{- define "hugegraph.test.name" -}} +{{- printf "%s-test-connection" (include "hugegraph.fullname" . | trunc 47 | trimSuffix "-") }} +{{- end }} + +{{/* +PD Raft peers list: pod-0.svc.ns.svc:8610,... +Uses short headless DNS (cluster.local optional) resolvable inside the namespace. +*/}} +{{- define "hugegraph.pd.raftPeersList" -}} +{{- $peers := list -}} +{{- $replicas := int .Values.pd.replicas -}} +{{- $name := include "hugegraph.pd.name" . -}} +{{- $ns := .Release.Namespace -}} +{{- $port := int .Values.pd.ports.raft -}} +{{- range $i := until $replicas -}} + {{- $peers = append $peers (printf "%s-%d.%s.%s.svc:%d" $name $i $name $ns $port) -}} +{{- end -}} +{{- join "," $peers -}} +{{- end }} + +{{/* +PD gRPC peers for Store/Server. +*/}} +{{- define "hugegraph.pd.grpcPeersList" -}} +{{- $peers := list -}} +{{- $replicas := int .Values.pd.replicas -}} +{{- $name := include "hugegraph.pd.name" . -}} +{{- $ns := .Release.Namespace -}} +{{- $port := int .Values.pd.ports.grpc -}} +{{- range $i := until $replicas -}} + {{- $peers = append $peers (printf "%s-%d.%s.%s.svc:%d" $name $i $name $ns $port) -}} +{{- end -}} +{{- join "," $peers -}} +{{- end }} + +{{/* +PD REST endpoints for Server storage-readiness checks. +*/}} +{{- define "hugegraph.pd.restPeersList" -}} +{{- $peers := list -}} +{{- $replicas := int .Values.pd.replicas -}} +{{- $name := include "hugegraph.pd.name" . -}} +{{- $ns := .Release.Namespace -}} +{{- $port := int .Values.pd.ports.rest -}} +{{- range $i := until $replicas -}} + {{- $peers = append $peers (printf "%s-%d.%s.%s.svc:%d" $name $i $name $ns $port) -}} +{{- end -}} +{{- join "," $peers -}} +{{- end }} + +{{/* +Initial store list for PD bootstrap: store-0.svc.ns.svc:8500,... +*/}} +{{- define "hugegraph.store.initialStoreList" -}} +{{- $peers := list -}} +{{- $replicas := int .Values.store.replicas -}} +{{- $name := include "hugegraph.store.name" . -}} +{{- $ns := .Release.Namespace -}} +{{- $port := int .Values.store.ports.grpc -}} +{{- range $i := until $replicas -}} + {{- $peers = append $peers (printf "%s-%d.%s.%s.svc:%d" $name $i $name $ns $port) -}} +{{- end -}} +{{- join "," $peers -}} +{{- end }} + +{{/* +First store REST endpoint for STORE_REST / wait-partition. +*/}} +{{- define "hugegraph.store.restPrimary" -}} +{{- $name := include "hugegraph.store.name" . -}} +{{- $ns := .Release.Namespace -}} +{{- printf "%s-0.%s.%s.svc:%d" $name $name $ns (int .Values.store.ports.rest) -}} +{{- end }} + +{{/* +Quorum size: floor(replicas/2)+1 +*/}} +{{- define "hugegraph.pd.quorum" -}} +{{- add (div (int .Values.pd.replicas) 2) 1 -}} +{{- end }} + +{{/* +Render JAVA_OPTS only when explicitly configured. An empty value preserves the +image entrypoint's existing automatic JVM sizing behavior. +*/}} +{{- define "hugegraph.javaOptsEnv" -}} +{{- $javaOpts := default "" . -}} +{{- if ne (trim $javaOpts) "" -}} +- name: JAVA_OPTS + value: {{ $javaOpts | quote }} +{{- end -}} +{{- end }} + +{{/* +Keep the startup probe alive for the 300-second storage wait, the Server's +120-second start timeout, and 30 seconds of process overhead. Older stored +values remain accepted, but their rendered threshold is raised to this floor. +*/}} +{{- define "hugegraph.server.startupFailureThreshold" -}} +{{- $period := int .Values.server.probes.startup.periodSeconds -}} +{{- $configured := int .Values.server.probes.startup.failureThreshold -}} +{{- $minimum := div (add 449 $period) $period -}} +{{- max $configured $minimum -}} +{{- end }} + +{{/* +Optional probe tunables, emitted only when explicitly set. Kubernetes defaults +timeoutSeconds to 1 second, which a garbage-collection pause can exceed on a +loaded Server; operators need a supported way to raise it without forking the +chart. Only explicitly configured fields are rendered. +*/}} +{{- define "hugegraph.probeTuning" -}} +{{- if hasKey . "timeoutSeconds" }} +timeoutSeconds: {{ .timeoutSeconds }} +{{- end }} +{{- if hasKey . "initialDelaySeconds" }} +initialDelaySeconds: {{ .initialDelaySeconds }} +{{- end }} +{{- if hasKey . "successThreshold" }} +successThreshold: {{ .successThreshold }} +{{- end }} +{{- end }} + +{{/* +Resolve the ServiceAccount name for a component: an explicit name wins, +otherwise the generated one when create is true, otherwise "default". +*/}} +{{- define "hugegraph.serviceAccountName" -}} +{{- $sa := get .component "serviceAccount" | default dict -}} +{{- if get $sa "name" -}} +{{- get $sa "name" -}} +{{- else if (get $sa "create" | default false) -}} +{{- .name -}} +{{- else -}} +default +{{- end -}} +{{- end }} + +{{/* +Cross-field validation that JSON Schema draft-07 cannot express. +*/}} +{{- define "hugegraph.validateValues" -}} +{{- $networkPolicy := get .Values "networkPolicy" | default dict -}} +{{- if (get $networkPolicy "enabled" | default false) -}} +{{- fail "networkPolicy.enabled=true is unsupported because this chart does not implement NetworkPolicy resources" -}} +{{- end -}} +{{- if and .Values.server.hpa.enabled (gt (int .Values.server.hpa.minReplicas) (int .Values.server.hpa.maxReplicas)) -}} +{{- fail "server.hpa.minReplicas must be less than or equal to server.hpa.maxReplicas" -}} +{{- end -}} +{{- if .Values.server.hpa.enabled -}} +{{- $serverResources := .Values.server.resources | default dict -}} +{{- $serverRequests := get $serverResources "requests" | default dict -}} +{{- if not (hasKey $serverRequests "cpu") -}} +{{- fail "server.resources.requests.cpu is required when server.hpa.enabled=true" -}} +{{- end -}} +{{- $cpuRequest := trim (toString (get $serverRequests "cpu")) -}} +{{- if or (eq $cpuRequest "") (hasPrefix "-" $cpuRequest) (regexMatch "^[+]?((0+([.]0*)?)|([.]0+))(([KMGTPE]i)|[numkMGTPE]|[eE][+-]?[0-9]+)?$" $cpuRequest) -}} +{{- fail "server.resources.requests.cpu must be strictly positive when server.hpa.enabled=true" -}} +{{- end -}} +{{- end -}} +{{/* +Only validate minAvailable where a PDB is actually rendered. The pd/store PDB +templates require replicas > 1, so a single-replica release never creates one +and must not be failed for a value that has no effect. +*/}} +{{- if and .Values.pd.pdb.enabled (gt (int .Values.pd.replicas) 1) (ge (int .Values.pd.pdb.minAvailable) (int .Values.pd.replicas)) -}} +{{- fail "pd.pdb.minAvailable must be less than pd.replicas, otherwise the PDB permanently blocks voluntary disruptions such as node drains" -}} +{{- end -}} +{{- if and .Values.store.pdb.enabled (gt (int .Values.store.replicas) 1) (ge (int .Values.store.pdb.minAvailable) (int .Values.store.replicas)) -}} +{{- fail "store.pdb.minAvailable must be less than store.replicas, otherwise the PDB permanently blocks voluntary disruptions such as node drains" -}} +{{- end -}} +{{- $svc := get .Values.server "service" | default dict -}} +{{- if and (get $svc "nodePort") (not (has (get $svc "type" | default "ClusterIP") (list "NodePort" "LoadBalancer"))) -}} +{{- fail "server.service.nodePort requires server.service.type to be NodePort or LoadBalancer" -}} +{{- end -}} +{{- $serverPdb := get .Values.server "pdb" | default dict -}} +{{- if and (get $serverPdb "enabled" | default false) (gt (int .Values.server.replicas) 1) (ge (int (get $serverPdb "minAvailable" | default 1)) (int .Values.server.replicas)) -}} +{{- fail "server.pdb.minAvailable must be less than server.replicas, otherwise the PDB permanently blocks voluntary disruptions such as node drains" -}} +{{- end -}} +{{- end }} + +{{/* +podAntiAffinity snippet for a component label key. +mode: required | preferred | disabled +*/}} +{{- define "hugegraph.antiAffinity" -}} +{{- $mode := .mode -}} +{{- $component := .component -}} +{{- $labels := .labels -}} +{{- if eq $mode "required" }} +affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: + {{- toYaml $labels | nindent 12 }} + app.kubernetes.io/component: {{ $component }} + topologyKey: kubernetes.io/hostname +{{- else if eq $mode "preferred" }} +affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + {{- toYaml $labels | nindent 14 }} + app.kubernetes.io/component: {{ $component }} + topologyKey: kubernetes.io/hostname +{{- end }} +{{- end }} diff --git a/helm/hugegraph/templates/pd-pdb.yaml b/helm/hugegraph/templates/pd-pdb.yaml new file mode 100644 index 0000000000..a0d93a098e --- /dev/null +++ b/helm/hugegraph/templates/pd-pdb.yaml @@ -0,0 +1,32 @@ +# +# 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. +# + +{{- if and .Values.pd.pdb.enabled (gt (int .Values.pd.replicas) 1) }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "hugegraph.pd.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: pd +spec: + minAvailable: {{ .Values.pd.pdb.minAvailable }} + selector: + matchLabels: + {{- include "hugegraph.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: pd +{{- end }} diff --git a/helm/hugegraph/templates/pd-service-client.yaml b/helm/hugegraph/templates/pd-service-client.yaml new file mode 100644 index 0000000000..d40cc9325c --- /dev/null +++ b/helm/hugegraph/templates/pd-service-client.yaml @@ -0,0 +1,36 @@ +# +# 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. +# + +apiVersion: v1 +kind: Service +metadata: + name: {{ include "hugegraph.pd.clientName" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: pd +spec: + type: ClusterIP + selector: + {{- include "hugegraph.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: pd + ports: + - name: rest + port: {{ .Values.pd.ports.rest }} + targetPort: rest + - name: grpc + port: {{ .Values.pd.ports.grpc }} + targetPort: grpc diff --git a/helm/hugegraph/templates/pd-service-headless.yaml b/helm/hugegraph/templates/pd-service-headless.yaml new file mode 100644 index 0000000000..3b724f9dcf --- /dev/null +++ b/helm/hugegraph/templates/pd-service-headless.yaml @@ -0,0 +1,41 @@ +# +# 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. +# + +apiVersion: v1 +kind: Service +metadata: + name: {{ include "hugegraph.pd.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: pd +spec: + clusterIP: None + # Mandatory: Raft bootstrap needs DNS for not-yet-ready pods + publishNotReadyAddresses: true + selector: + {{- include "hugegraph.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: pd + ports: + - name: grpc + port: {{ .Values.pd.ports.grpc }} + targetPort: grpc + - name: rest + port: {{ .Values.pd.ports.rest }} + targetPort: rest + - name: raft + port: {{ .Values.pd.ports.raft }} + targetPort: raft diff --git a/helm/hugegraph/templates/pd-statefulset.yaml b/helm/hugegraph/templates/pd-statefulset.yaml new file mode 100644 index 0000000000..01ef705006 --- /dev/null +++ b/helm/hugegraph/templates/pd-statefulset.yaml @@ -0,0 +1,158 @@ +# +# 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. +# + +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "hugegraph.pd.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: pd +spec: + serviceName: {{ include "hugegraph.pd.name" . }} + replicas: {{ .Values.pd.replicas }} + podManagementPolicy: Parallel + selector: + matchLabels: + {{- include "hugegraph.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: pd + template: + metadata: + labels: + {{- include "hugegraph.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: pd + {{- with .Values.pd.podLabels }}{{ toYaml . | nindent 8 }}{{- end }} + {{- with .Values.pd.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + automountServiceAccountToken: {{ get (get .Values.pd "serviceAccount" | default dict) "automountServiceAccountToken" | default false }} + serviceAccountName: {{ include "hugegraph.serviceAccountName" (dict "component" .Values.pd "name" (include "hugegraph.pd.name" .) ) }} + {{- with .Values.pd.terminationGracePeriodSeconds }} + terminationGracePeriodSeconds: {{ . }} + {{- end }} + {{- with .Values.pd.priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} + {{- with .Values.pd.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.pd.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.pd.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.pd.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if .Values.pd.affinity }} + affinity: + {{- toYaml .Values.pd.affinity | nindent 8 }} + {{- else }} + {{- with include "hugegraph.antiAffinity" (dict "mode" .Values.pd.antiAffinity "component" "pd" "labels" (include "hugegraph.selectorLabels" . | fromYaml)) }}{{ . | trim | nindent 6 }}{{- end }} + {{- end }} + containers: + - name: pd + image: "{{ .Values.pd.image.repository }}:{{ .Values.pd.image.tag | default $.Chart.AppVersion }}" + imagePullPolicy: {{ .Values.pd.image.pullPolicy }} + {{- with .Values.pd.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: grpc + containerPort: {{ .Values.pd.ports.grpc }} + - name: rest + containerPort: {{ .Values.pd.ports.rest }} + - name: raft + containerPort: {{ .Values.pd.ports.raft }} + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: HG_PD_GRPC_HOST + value: "$(POD_NAME).{{ include "hugegraph.pd.name" . }}.$(NAMESPACE).svc" + - name: HG_PD_GRPC_PORT + value: {{ .Values.pd.ports.grpc | quote }} + - name: HG_PD_REST_PORT + value: {{ .Values.pd.ports.rest | quote }} + - name: HG_PD_RAFT_ADDRESS + value: "$(POD_NAME).{{ include "hugegraph.pd.name" . }}.$(NAMESPACE).svc:{{ .Values.pd.ports.raft }}" + - name: HG_PD_RAFT_PEERS_LIST + value: {{ include "hugegraph.pd.raftPeersList" . | quote }} + - name: HG_PD_INITIAL_STORE_LIST + value: {{ include "hugegraph.store.initialStoreList" . | quote }} + - name: HG_PD_INITIAL_STORE_COUNT + value: {{ .Values.store.replicas | quote }} + - name: HG_PD_DATA_PATH + value: {{ .Values.pd.dataPath | quote }} + {{- with .Values.pd.extraEnv }}{{ toYaml . | nindent 12 }}{{- end }} + {{- with include "hugegraph.javaOptsEnv" .Values.pd.javaOpts }}{{ . | trim | nindent 12 }}{{- end }} + volumeMounts: + - name: pd-data + mountPath: {{ .Values.pd.dataPath }} + startupProbe: + httpGet: + path: /v1/health + port: rest + failureThreshold: {{ .Values.pd.probes.startup.failureThreshold }} + periodSeconds: {{ .Values.pd.probes.startup.periodSeconds }} + {{- with include "hugegraph.probeTuning" .Values.pd.probes.startup }}{{ . | trim | nindent 12 }}{{- end }} + readinessProbe: + httpGet: + path: /v1/health + port: rest + periodSeconds: {{ .Values.pd.probes.readiness.periodSeconds }} + failureThreshold: {{ .Values.pd.probes.readiness.failureThreshold }} + {{- with include "hugegraph.probeTuning" .Values.pd.probes.readiness }}{{ . | trim | nindent 12 }}{{- end }} + livenessProbe: + httpGet: + path: /v1/health + port: rest + periodSeconds: {{ .Values.pd.probes.liveness.periodSeconds }} + failureThreshold: {{ .Values.pd.probes.liveness.failureThreshold }} + {{- with include "hugegraph.probeTuning" .Values.pd.probes.liveness }}{{ . | trim | nindent 12 }}{{- end }} + {{- with .Values.pd.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumeClaimTemplates: + - metadata: + name: pd-data + spec: + accessModes: ["ReadWriteOnce"] + {{- if .Values.pd.storage.storageClassName }} + storageClassName: {{ .Values.pd.storage.storageClassName | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.pd.storage.size }} diff --git a/helm/hugegraph/templates/server-deployment.yaml b/helm/hugegraph/templates/server-deployment.yaml new file mode 100644 index 0000000000..6445c80c75 --- /dev/null +++ b/helm/hugegraph/templates/server-deployment.yaml @@ -0,0 +1,242 @@ +# +# 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.validateValues" . }} +{{- $restServer := .Values.server.restServer | default dict }} +{{- $minFreeMemory := "" }} +{{- $batchMaxWriteThreads := "" }} +{{- if hasKey $restServer "minFreeMemory" }} +{{- $minFreeMemory = toString (get $restServer "minFreeMemory") }} +{{- end }} +{{- if hasKey $restServer "batchMaxWriteThreads" }} +{{- $batchMaxWriteThreads = toString (get $restServer "batchMaxWriteThreads") }} +{{- end }} +{{- $customPort := ne (int .Values.server.port) 8080 }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "hugegraph.server.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: server +spec: + {{- if not .Values.server.hpa.enabled }} + replicas: {{ .Values.server.replicas }} + {{- end }} + selector: + matchLabels: + {{- include "hugegraph.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: server + template: + metadata: + labels: + {{- include "hugegraph.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: server + {{- with .Values.server.podLabels }}{{ toYaml . | nindent 8 }}{{- end }} + {{- with .Values.server.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + automountServiceAccountToken: {{ get (get .Values.server "serviceAccount" | default dict) "automountServiceAccountToken" | default false }} + serviceAccountName: {{ include "hugegraph.serviceAccountName" (dict "component" .Values.server "name" (include "hugegraph.server.name" .) ) }} + {{- with .Values.server.terminationGracePeriodSeconds }} + terminationGracePeriodSeconds: {{ . }} + {{- end }} + {{- with .Values.server.priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} + {{- with .Values.server.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.server.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.server.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.server.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if .Values.server.affinity }} + affinity: + {{- toYaml .Values.server.affinity | nindent 8 }} + {{- else }} + {{- with include "hugegraph.antiAffinity" (dict "mode" (.Values.server.antiAffinity | default "preferred") "component" "server" "labels" (include "hugegraph.selectorLabels" . | fromYaml)) }}{{ . | trim | nindent 6 }}{{- end }} + {{- end }} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: server + image: "{{ .Values.server.image.repository }}:{{ .Values.server.image.tag | default $.Chart.AppVersion }}" + imagePullPolicy: {{ .Values.server.image.pullPolicy }} + {{- with .Values.server.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if or .Values.server.auth.enabled + $customPort + (ne $minFreeMemory "") + (ne $batchMaxWriteThreads "") }} + command: + - /usr/bin/dumb-init + - -- + - /bin/bash + - -c + args: + - | + set -euo pipefail + {{- if .Values.server.auth.enabled }} + : "${PASSWORD:?auth Secret key 'password' must not be empty}" + {{- end }} + CONF=./conf/rest-server.properties + TMP=$(mktemp) + {{- if .Values.server.auth.enabled }} + FOUND_USE_PD=false + FOUND_PD_PEERS=false + {{- end }} + {{- if $customPort }} + FOUND_RESTSERVER_URL=false + {{- end }} + {{- if ne $minFreeMemory "" }} + FOUND_MIN_FREE_MEMORY=false + {{- end }} + {{- if ne $batchMaxWriteThreads "" }} + FOUND_BATCH_MAX_WRITE_THREADS=false + {{- end }} + while IFS= read -r LINE || [[ -n "${LINE}" ]]; do + case "${LINE}" in + {{- if .Values.server.auth.enabled }} + usePD=*) + printf 'usePD=true\n' >>"${TMP}" + FOUND_USE_PD=true + ;; + pd.peers=*) + printf 'pd.peers=%s\n' "${HG_SERVER_PD_PEERS}" >>"${TMP}" + FOUND_PD_PEERS=true + ;; + {{- end }} + {{- if $customPort }} + restserver.url=*) + printf 'restserver.url=http://0.0.0.0:%s\n' \ + {{ .Values.server.port | quote }} >>"${TMP}" + FOUND_RESTSERVER_URL=true + ;; + {{- end }} + {{- if ne $minFreeMemory "" }} + restserver.min_free_memory=*) + printf 'restserver.min_free_memory=%s\n' \ + {{ $minFreeMemory | quote }} >>"${TMP}" + FOUND_MIN_FREE_MEMORY=true + ;; + {{- end }} + {{- if ne $batchMaxWriteThreads "" }} + batch.max_write_threads=*) + printf 'batch.max_write_threads=%s\n' \ + {{ $batchMaxWriteThreads | quote }} >>"${TMP}" + FOUND_BATCH_MAX_WRITE_THREADS=true + ;; + {{- end }} + *) + printf '%s\n' "${LINE}" >>"${TMP}" + ;; + esac + done <"${CONF}" + {{- if .Values.server.auth.enabled }} + if [[ "${FOUND_USE_PD}" == false ]]; then + printf 'usePD=true\n' >>"${TMP}" + fi + if [[ "${FOUND_PD_PEERS}" == false ]]; then + printf 'pd.peers=%s\n' "${HG_SERVER_PD_PEERS}" >>"${TMP}" + fi + {{- end }} + {{- if $customPort }} + if [[ "${FOUND_RESTSERVER_URL}" == false ]]; then + printf 'restserver.url=http://0.0.0.0:%s\n' \ + {{ .Values.server.port | quote }} >>"${TMP}" + fi + {{- end }} + {{- if ne $minFreeMemory "" }} + if [[ "${FOUND_MIN_FREE_MEMORY}" == false ]]; then + printf 'restserver.min_free_memory=%s\n' \ + {{ $minFreeMemory | quote }} >>"${TMP}" + fi + {{- end }} + {{- if ne $batchMaxWriteThreads "" }} + if [[ "${FOUND_BATCH_MAX_WRITE_THREADS}" == false ]]; then + printf 'batch.max_write_threads=%s\n' \ + {{ $batchMaxWriteThreads | quote }} >>"${TMP}" + fi + {{- end }} + chmod 600 "${TMP}" + mv "${TMP}" "${CONF}" + exec ./docker-entrypoint.sh + {{- end }} + ports: + - name: http + containerPort: {{ .Values.server.port }} + env: + - name: HG_SERVER_BACKEND + value: {{ .Values.server.backend | quote }} + - name: HG_SERVER_PD_PEERS + value: {{ include "hugegraph.pd.grpcPeersList" . | quote }} + - name: HG_SERVER_PD_REST_ENDPOINT + value: {{ include "hugegraph.pd.restPeersList" . | quote }} + - name: STORE_REST + value: {{ include "hugegraph.store.restPrimary" . | quote }} + - name: HG_SERVER_INIT_STORE_ENABLED + value: {{ .Values.server.initStoreEnabled | quote }} + {{- with .Values.server.extraEnv }}{{ toYaml . | nindent 12 }}{{- end }} + {{- with include "hugegraph.javaOptsEnv" .Values.server.javaOpts }}{{ . | trim | nindent 12 }}{{- end }} + {{- if and .Values.server.auth.enabled .Values.server.auth.existingSecret }} + - name: PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.server.auth.existingSecret | quote }} + key: password + {{- end }} + startupProbe: + httpGet: + path: /versions + port: http + failureThreshold: {{ include "hugegraph.server.startupFailureThreshold" . }} + periodSeconds: {{ .Values.server.probes.startup.periodSeconds }} + {{- with include "hugegraph.probeTuning" .Values.server.probes.startup }}{{ . | trim | nindent 12 }}{{- end }} + readinessProbe: + httpGet: + path: /versions + port: http + periodSeconds: {{ .Values.server.probes.readiness.periodSeconds }} + failureThreshold: {{ .Values.server.probes.readiness.failureThreshold }} + {{- with include "hugegraph.probeTuning" .Values.server.probes.readiness }}{{ . | trim | nindent 12 }}{{- end }} + livenessProbe: + httpGet: + path: /versions + port: http + periodSeconds: {{ .Values.server.probes.liveness.periodSeconds }} + failureThreshold: {{ .Values.server.probes.liveness.failureThreshold }} + {{- with include "hugegraph.probeTuning" .Values.server.probes.liveness }}{{ . | trim | nindent 12 }}{{- end }} + {{- with .Values.server.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} diff --git a/helm/hugegraph/templates/server-hpa.yaml b/helm/hugegraph/templates/server-hpa.yaml new file mode 100644 index 0000000000..40e95c0d99 --- /dev/null +++ b/helm/hugegraph/templates/server-hpa.yaml @@ -0,0 +1,40 @@ +# +# 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. +# + +{{- if .Values.server.hpa.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "hugegraph.server.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: server +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "hugegraph.server.name" . }} + minReplicas: {{ .Values.server.hpa.minReplicas }} + maxReplicas: {{ .Values.server.hpa.maxReplicas }} + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.server.hpa.targetCPUUtilizationPercentage }} +{{- end }} diff --git a/helm/hugegraph/templates/server-ingress.yaml b/helm/hugegraph/templates/server-ingress.yaml new file mode 100644 index 0000000000..08a8c6a7ea --- /dev/null +++ b/helm/hugegraph/templates/server-ingress.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. +# + +{{- if .Values.server.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "hugegraph.server.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: server + {{- with (get .Values.server.ingress "annotations") }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.server.ingress.className }} + ingressClassName: {{ .Values.server.ingress.className | quote }} + {{- end }} + {{- with .Values.server.ingress.tls }} + tls: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + {{- range .Values.server.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "hugegraph.server.name" $ }} + port: + number: {{ $.Values.server.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/hugegraph/templates/server-pdb.yaml b/helm/hugegraph/templates/server-pdb.yaml new file mode 100644 index 0000000000..3c1f0c4847 --- /dev/null +++ b/helm/hugegraph/templates/server-pdb.yaml @@ -0,0 +1,33 @@ +# +# 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. +# + +{{- $pdb := get .Values.server "pdb" | default dict }} +{{- if and (get $pdb "enabled" | default false) (gt (int .Values.server.replicas) 1) }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "hugegraph.server.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: server +spec: + minAvailable: {{ get $pdb "minAvailable" | default 1 }} + selector: + matchLabels: + {{- include "hugegraph.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: server +{{- end }} diff --git a/helm/hugegraph/templates/server-service.yaml b/helm/hugegraph/templates/server-service.yaml new file mode 100644 index 0000000000..b4053dc4ba --- /dev/null +++ b/helm/hugegraph/templates/server-service.yaml @@ -0,0 +1,41 @@ +# +# 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. +# + +apiVersion: v1 +kind: Service +metadata: + name: {{ include "hugegraph.server.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: server + {{- $svc := get .Values.server "service" | default dict }} + {{- with (get $svc "annotations") }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ get $svc "type" | default "ClusterIP" }} + selector: + {{- include "hugegraph.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: server + ports: + - name: http + port: {{ .Values.server.port }} + targetPort: http + {{- with (get $svc "nodePort") }} + nodePort: {{ . }} + {{- end }} diff --git a/helm/hugegraph/templates/serviceaccount.yaml b/helm/hugegraph/templates/serviceaccount.yaml new file mode 100644 index 0000000000..9889414d4b --- /dev/null +++ b/helm/hugegraph/templates/serviceaccount.yaml @@ -0,0 +1,35 @@ +{{- /* +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. +*/ -}} +{{- range $component := list "pd" "store" "server" }} +{{- $values := index $.Values $component }} +{{- $sa := get $values "serviceAccount" | default dict }} +{{- if and (get $sa "create" | default false) (not (get $sa "name")) }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include (printf "hugegraph.%s.name" $component) $ }} + labels: + {{- include "hugegraph.labels" $ | nindent 4 }} + app.kubernetes.io/component: {{ $component }} + {{- with (get $sa "annotations") }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ get $sa "automountServiceAccountToken" | default false }} +{{- end }} +{{- end }} diff --git a/helm/hugegraph/templates/store-pdb.yaml b/helm/hugegraph/templates/store-pdb.yaml new file mode 100644 index 0000000000..9fb1e536da --- /dev/null +++ b/helm/hugegraph/templates/store-pdb.yaml @@ -0,0 +1,32 @@ +# +# 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. +# + +{{- if and .Values.store.pdb.enabled (gt (int .Values.store.replicas) 1) }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "hugegraph.store.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: store +spec: + minAvailable: {{ .Values.store.pdb.minAvailable }} + selector: + matchLabels: + {{- include "hugegraph.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: store +{{- end }} diff --git a/helm/hugegraph/templates/store-service-headless.yaml b/helm/hugegraph/templates/store-service-headless.yaml new file mode 100644 index 0000000000..4c82023b5d --- /dev/null +++ b/helm/hugegraph/templates/store-service-headless.yaml @@ -0,0 +1,41 @@ +# +# 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. +# + +apiVersion: v1 +kind: Service +metadata: + name: {{ include "hugegraph.store.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: store +spec: + clusterIP: None + # Mandatory: Raft / self-FQDN resolution before readiness + publishNotReadyAddresses: true + selector: + {{- include "hugegraph.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: store + ports: + - name: grpc + port: {{ .Values.store.ports.grpc }} + targetPort: grpc + - name: raft + port: {{ .Values.store.ports.raft }} + targetPort: raft + - name: rest + port: {{ .Values.store.ports.rest }} + targetPort: rest diff --git a/helm/hugegraph/templates/store-statefulset.yaml b/helm/hugegraph/templates/store-statefulset.yaml new file mode 100644 index 0000000000..973ecacdff --- /dev/null +++ b/helm/hugegraph/templates/store-statefulset.yaml @@ -0,0 +1,193 @@ +# +# 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. +# + +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "hugegraph.store.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: store +spec: + serviceName: {{ include "hugegraph.store.name" . }} + replicas: {{ .Values.store.replicas }} + podManagementPolicy: Parallel + selector: + matchLabels: + {{- include "hugegraph.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: store + template: + metadata: + labels: + {{- include "hugegraph.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: store + {{- with .Values.store.podLabels }}{{ toYaml . | nindent 8 }}{{- end }} + {{- with .Values.store.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + automountServiceAccountToken: {{ get (get .Values.store "serviceAccount" | default dict) "automountServiceAccountToken" | default false }} + serviceAccountName: {{ include "hugegraph.serviceAccountName" (dict "component" .Values.store "name" (include "hugegraph.store.name" .) ) }} + {{- with .Values.store.terminationGracePeriodSeconds }} + terminationGracePeriodSeconds: {{ . }} + {{- end }} + {{- with .Values.store.priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} + {{- with .Values.store.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.store.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.store.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.store.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if .Values.store.affinity }} + affinity: + {{- toYaml .Values.store.affinity | nindent 8 }} + {{- else }} + {{- with include "hugegraph.antiAffinity" (dict "mode" .Values.store.antiAffinity "component" "store" "labels" (include "hugegraph.selectorLabels" . | fromYaml)) }}{{ . | trim | nindent 6 }}{{- end }} + {{- end }} + initContainers: + - name: wait-for-pd + image: {{ .Values.store.waitImage | quote }} + {{- with .Values.store.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + command: + - sh + - -c + - | + set -eu + REQUIRED={{ include "hugegraph.pd.quorum" . }} + HEALTH_PEERS=$(echo "{{ include "hugegraph.pd.restPeersList" . }}" | tr ',' ' ') + TIMEOUT={{ .Values.store.waitTimeoutSeconds | default 900 }} + DEADLINE=$(( $(date +%s) + TIMEOUT )) + echo "Waiting for PD quorum (${REQUIRED}) among: ${HEALTH_PEERS}" + until [ "$( + ok=0 + for peer in ${HEALTH_PEERS}; do + if curl -fsS "http://${peer}/v1/health" >/dev/null 2>&1; then + ok=$((ok+1)) + fi + done + echo "$ok" + )" -ge "${REQUIRED}" ]; do + if [ "$(date +%s)" -ge "${DEADLINE}" ]; then + echo "Timed out after ${TIMEOUT}s waiting for PD quorum (${REQUIRED}) among: ${HEALTH_PEERS}" >&2 + echo "Check PD Pods: kubectl get pods -l app.kubernetes.io/component=pd" >&2 + exit 1 + fi + echo "Waiting for PD quorum..." + sleep 5 + done + echo "PD quorum reached." + {{- with .Values.store.waitResources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + containers: + - name: store + image: "{{ .Values.store.image.repository }}:{{ .Values.store.image.tag | default $.Chart.AppVersion }}" + imagePullPolicy: {{ .Values.store.image.pullPolicy }} + {{- with .Values.store.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: grpc + containerPort: {{ .Values.store.ports.grpc }} + - name: raft + containerPort: {{ .Values.store.ports.raft }} + - name: rest + containerPort: {{ .Values.store.ports.rest }} + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: HG_STORE_PD_ADDRESS + value: {{ include "hugegraph.pd.grpcPeersList" . | quote }} + - name: HG_STORE_GRPC_HOST + value: "$(POD_NAME).{{ include "hugegraph.store.name" . }}.$(NAMESPACE).svc" + - name: HG_STORE_GRPC_PORT + value: {{ .Values.store.ports.grpc | quote }} + - name: HG_STORE_REST_PORT + value: {{ .Values.store.ports.rest | quote }} + - name: HG_STORE_RAFT_ADDRESS + value: "$(POD_NAME).{{ include "hugegraph.store.name" . }}.$(NAMESPACE).svc:{{ .Values.store.ports.raft }}" + - name: HG_STORE_DATA_PATH + value: {{ .Values.store.dataPath | quote }} + {{- with .Values.store.extraEnv }}{{ toYaml . | nindent 12 }}{{- end }} + {{- with include "hugegraph.javaOptsEnv" .Values.store.javaOpts }}{{ . | trim | nindent 12 }}{{- end }} + volumeMounts: + - name: store-data + mountPath: {{ .Values.store.dataPath }} + startupProbe: + httpGet: + path: /v1/health + port: rest + failureThreshold: {{ .Values.store.probes.startup.failureThreshold }} + periodSeconds: {{ .Values.store.probes.startup.periodSeconds }} + {{- with include "hugegraph.probeTuning" .Values.store.probes.startup }}{{ . | trim | nindent 12 }}{{- end }} + readinessProbe: + httpGet: + path: /v1/health + port: rest + periodSeconds: {{ .Values.store.probes.readiness.periodSeconds }} + failureThreshold: {{ .Values.store.probes.readiness.failureThreshold }} + {{- with include "hugegraph.probeTuning" .Values.store.probes.readiness }}{{ . | trim | nindent 12 }}{{- end }} + livenessProbe: + httpGet: + path: /v1/health + port: rest + periodSeconds: {{ .Values.store.probes.liveness.periodSeconds }} + failureThreshold: {{ .Values.store.probes.liveness.failureThreshold }} + {{- with include "hugegraph.probeTuning" .Values.store.probes.liveness }}{{ . | trim | nindent 12 }}{{- end }} + {{- with .Values.store.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumeClaimTemplates: + - metadata: + name: store-data + spec: + accessModes: ["ReadWriteOnce"] + {{- if .Values.store.storage.storageClassName }} + storageClassName: {{ .Values.store.storage.storageClassName | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.store.storage.size }} diff --git a/helm/hugegraph/templates/tests/test-connection.yaml b/helm/hugegraph/templates/tests/test-connection.yaml new file mode 100644 index 0000000000..b5c431b659 --- /dev/null +++ b/helm/hugegraph/templates/tests/test-connection.yaml @@ -0,0 +1,67 @@ +# +# 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. +# + +# Official Helm chart test hook — only runs with: helm test +# Not an install-time hook. Safe with --wait (no init Job in no-init design). +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "hugegraph.test.name" . }}" + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: test + annotations: + "helm.sh/hook": test + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +spec: + restartPolicy: Never + containers: + - name: curl + image: {{ .Values.server.waitImage | quote }} + {{- if .Values.server.auth.enabled }} + env: + - name: PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.server.auth.existingSecret | quote }} + key: password + {{- end }} + command: + - sh + - -c + - | + set -eu + SVC="{{ include "hugegraph.server.name" . }}" + PORT="{{ .Values.server.port }}" + {{- if .Values.server.auth.enabled }} + AUTH_HEADER=$(printf 'admin:%s' "${PASSWORD}" | base64 | tr -d '\r\n') + request() { + printf 'header = "Authorization: Basic %s"\n' "${AUTH_HEADER}" | + curl --config - -fsS "$1" + } + {{- else }} + request() { + curl -fsS "$1" + } + {{- end }} + echo "helm test: GET http://${SVC}:${PORT}/versions" + request "http://${SVC}:${PORT}/versions" + echo + echo "helm test: GET http://${SVC}:${PORT}/graphs" + request "http://${SVC}:${PORT}/graphs" + echo + echo "helm test: OK" diff --git a/helm/hugegraph/testdata/values-pre-hardening.yaml b/helm/hugegraph/testdata/values-pre-hardening.yaml new file mode 100644 index 0000000000..d3d34cea1f --- /dev/null +++ b/helm/hugegraph/testdata/values-pre-hardening.yaml @@ -0,0 +1,128 @@ +# +# 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. +# + +# Values shape shipped before the production-hardening fields were added. +# validate-chart.sh installs this as the chart default values to emulate a +# Helm --reuse-values upgrade whose stored release values lack all new keys. + +fullnameOverride: "" +nameOverride: "" + +imagePullSecrets: [] + +pd: + replicas: 3 + image: + repository: hugegraph/pd + tag: latest + pullPolicy: IfNotPresent + ports: + grpc: 8686 + rest: 8620 + raft: 8610 + dataPath: /hugegraph-pd/pd_data + storage: + size: 10Gi + storageClassName: "" + resources: {} + antiAffinity: required + pdb: + enabled: true + minAvailable: 2 + probes: + startup: + failureThreshold: 30 + periodSeconds: 10 + readiness: + periodSeconds: 10 + failureThreshold: 3 + liveness: + periodSeconds: 20 + failureThreshold: 3 + +store: + replicas: 3 + image: + repository: hugegraph/store + tag: latest + pullPolicy: IfNotPresent + ports: + grpc: 8500 + raft: 8510 + rest: 8520 + dataPath: /hugegraph-store/storage + storage: + size: 50Gi + storageClassName: "" + resources: {} + antiAffinity: required + pdb: + enabled: true + minAvailable: 2 + waitImage: curlimages/curl:8.5.0 + probes: + startup: + failureThreshold: 40 + periodSeconds: 10 + readiness: + periodSeconds: 10 + failureThreshold: 3 + liveness: + periodSeconds: 20 + failureThreshold: 3 + +server: + replicas: 3 + image: + repository: hugegraph/server + tag: latest + pullPolicy: IfNotPresent + port: 8080 + backend: hstore + resources: {} + waitImage: curlimages/curl:8.5.0 + initStoreEnabled: false + auth: + enabled: false + existingSecret: "" + ingress: + enabled: false + className: "" + hosts: + - host: hugegraph.local + paths: + - path: / + pathType: Prefix + tls: [] + hpa: + enabled: false + minReplicas: 3 + maxReplicas: 10 + targetCPUUtilizationPercentage: 70 + probes: + startup: + failureThreshold: 30 + periodSeconds: 5 + readiness: + periodSeconds: 10 + failureThreshold: 3 + liveness: + periodSeconds: 20 + failureThreshold: 3 + +networkPolicy: + enabled: false diff --git a/helm/hugegraph/values-cluster.yaml b/helm/hugegraph/values-cluster.yaml new file mode 100644 index 0000000000..91ae09a9c6 --- /dev/null +++ b/helm/hugegraph/values-cluster.yaml @@ -0,0 +1,86 @@ +# +# 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. +# + +# Production starting point: 3 Server + 3 PD + 3 Store. +# Size this profile again for the real graph, traffic, and failure budget. +# Usage: helm install hg ./helm/hugegraph -f values-cluster.yaml + +pd: + replicas: 3 + javaOpts: >- + -Xms256m -Xmx512m + -XX:MaxMetaspaceSize=256m + -XX:MaxDirectMemorySize=256m + -XX:+UseContainerSupport + resources: + requests: + cpu: 500m + memory: 1Gi + limits: + cpu: "2" + memory: 2Gi + antiAffinity: required + pdb: + enabled: true + minAvailable: 2 + storage: + size: 10Gi + +store: + replicas: 3 + javaOpts: >- + -Xms512m -Xmx1024m + -XX:MaxMetaspaceSize=256m + -XX:MaxDirectMemorySize=512m + -XX:+UseContainerSupport + resources: + requests: + cpu: "1" + memory: 2Gi + limits: + cpu: "4" + memory: 4Gi + waitResources: + requests: + cpu: 25m + memory: 32Mi + limits: + cpu: 250m + memory: 64Mi + antiAffinity: required + pdb: + enabled: true + minAvailable: 2 + storage: + size: 50Gi + +server: + replicas: 3 + javaOpts: >- + -Xms512m -Xmx1024m + -XX:MaxMetaspaceSize=256m + -XX:MaxDirectMemorySize=256m + -XX:+UseContainerSupport + resources: + requests: + cpu: "1" + memory: 1Gi + limits: + cpu: "4" + memory: 2Gi + hpa: + enabled: false diff --git a/helm/hugegraph/values-single.yaml b/helm/hugegraph/values-single.yaml new file mode 100644 index 0000000000..86dd71abd6 --- /dev/null +++ b/helm/hugegraph/values-single.yaml @@ -0,0 +1,40 @@ +# +# 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. +# + +# Convenience preset: single-node / local kind-minikube development. +# Usage: helm install hg ./helm/hugegraph -f values-single.yaml + +pd: + replicas: 1 + antiAffinity: disabled + pdb: + enabled: false + storage: + size: 5Gi + +store: + replicas: 1 + antiAffinity: disabled + pdb: + enabled: false + storage: + size: 10Gi + +server: + replicas: 1 + hpa: + enabled: false diff --git a/helm/hugegraph/values.schema.json b/helm/hugegraph/values.schema.json new file mode 100644 index 0000000000..23ff6f73d8 --- /dev/null +++ b/helm/hugegraph/values.schema.json @@ -0,0 +1,765 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "HugeGraph HStore Helm values", + "type": "object", + "required": [ + "pd", + "store", + "server" + ], + "properties": { + "fullnameOverride": { + "type": "string" + }, + "nameOverride": { + "type": "string" + }, + "imagePullSecrets": { + "type": "array", + "items": { + "type": "object" + } + }, + "pd": { + "$ref": "#/definitions/pd" + }, + "store": { + "$ref": "#/definitions/store" + }, + "server": { + "$ref": "#/definitions/server" + }, + "global": { + "type": "object" + }, + "networkPolicy": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + } + } + }, + "definitions": { + "image": { + "type": "object", + "additionalProperties": false, + "required": [ + "repository", + "tag", + "pullPolicy" + ], + "properties": { + "repository": { + "type": "string", + "minLength": 1 + }, + "tag": { + "type": "string" + }, + "pullPolicy": { + "type": "string", + "enum": [ + "Always", + "IfNotPresent", + "Never" + ] + } + } + }, + "resources": { + "type": "object", + "additionalProperties": false, + "properties": { + "requests": { + "$ref": "#/definitions/resourceList" + }, + "limits": { + "$ref": "#/definitions/resourceList" + } + } + }, + "resourceList": { + "type": "object", + "additionalProperties": { + "type": [ + "string", + "number" + ] + }, + "properties": { + "cpu": { + "type": [ + "string", + "number" + ] + }, + "memory": { + "type": [ + "string", + "number" + ] + }, + "ephemeral-storage": { + "type": [ + "string", + "number" + ] + } + } + }, + "storage": { + "type": "object", + "additionalProperties": false, + "required": [ + "size", + "storageClassName" + ], + "properties": { + "size": { + "type": "string", + "minLength": 1 + }, + "storageClassName": { + "type": "string" + } + } + }, + "pdb": { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled", + "minAvailable" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "minAvailable": { + "type": "integer", + "minimum": 1 + } + } + }, + "probe": { + "type": "object", + "additionalProperties": false, + "required": [ + "periodSeconds", + "failureThreshold" + ], + "properties": { + "periodSeconds": { + "type": "integer", + "minimum": 1 + }, + "failureThreshold": { + "type": "integer", + "minimum": 1 + }, + "timeoutSeconds": { + "type": "integer", + "minimum": 1 + }, + "initialDelaySeconds": { + "type": "integer", + "minimum": 0 + }, + "successThreshold": { + "type": "integer", + "minimum": 1 + } + } + }, + "probes": { + "type": "object", + "additionalProperties": false, + "required": [ + "startup", + "readiness", + "liveness" + ], + "properties": { + "startup": { + "$ref": "#/definitions/probe" + }, + "readiness": { + "$ref": "#/definitions/probe" + }, + "liveness": { + "$ref": "#/definitions/probe" + } + } + }, + "ports": { + "type": "object", + "additionalProperties": false, + "required": [ + "grpc", + "rest", + "raft" + ], + "properties": { + "grpc": { + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "rest": { + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "raft": { + "type": "integer", + "minimum": 1, + "maximum": 65535 + } + } + }, + "pd": { + "type": "object", + "additionalProperties": false, + "required": [ + "replicas", + "image", + "ports", + "dataPath", + "storage", + "resources", + "antiAffinity", + "pdb", + "probes" + ], + "properties": { + "replicas": { + "type": "integer", + "minimum": 1, + "maximum": 99 + }, + "image": { + "$ref": "#/definitions/image" + }, + "javaOpts": { + "type": "string" + }, + "ports": { + "$ref": "#/definitions/ports" + }, + "dataPath": { + "type": "string", + "minLength": 1 + }, + "storage": { + "$ref": "#/definitions/storage" + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "antiAffinity": { + "type": "string", + "enum": [ + "required", + "preferred", + "disabled" + ] + }, + "pdb": { + "$ref": "#/definitions/pdb" + }, + "probes": { + "$ref": "#/definitions/probes" + }, + "podSecurityContext": { + "type": "object" + }, + "securityContext": { + "type": "object" + }, + "nodeSelector": { + "type": "object" + }, + "tolerations": { + "type": "array" + }, + "affinity": { + "type": "object" + }, + "topologySpreadConstraints": { + "type": "array" + }, + "priorityClassName": { + "type": "string" + }, + "podAnnotations": { + "type": "object" + }, + "podLabels": { + "type": "object" + }, + "extraEnv": { + "type": "array" + }, + "terminationGracePeriodSeconds": { + "type": "integer", + "minimum": 0 + }, + "serviceAccount": { + "type": "object", + "properties": { + "create": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "annotations": { + "type": "object" + }, + "automountServiceAccountToken": { + "type": "boolean" + } + } + } + } + }, + "store": { + "type": "object", + "additionalProperties": false, + "required": [ + "replicas", + "image", + "ports", + "dataPath", + "storage", + "resources", + "antiAffinity", + "pdb", + "waitImage", + "probes" + ], + "properties": { + "replicas": { + "type": "integer", + "minimum": 1, + "maximum": 99 + }, + "image": { + "$ref": "#/definitions/image" + }, + "javaOpts": { + "type": "string" + }, + "ports": { + "$ref": "#/definitions/ports" + }, + "dataPath": { + "type": "string", + "minLength": 1 + }, + "storage": { + "$ref": "#/definitions/storage" + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "antiAffinity": { + "type": "string", + "enum": [ + "required", + "preferred", + "disabled" + ] + }, + "pdb": { + "$ref": "#/definitions/pdb" + }, + "waitImage": { + "type": "string", + "minLength": 1 + }, + "waitResources": { + "$ref": "#/definitions/resources" + }, + "probes": { + "$ref": "#/definitions/probes" + }, + "podSecurityContext": { + "type": "object" + }, + "securityContext": { + "type": "object" + }, + "waitTimeoutSeconds": { + "type": "integer", + "minimum": 1 + }, + "nodeSelector": { + "type": "object" + }, + "tolerations": { + "type": "array" + }, + "affinity": { + "type": "object" + }, + "topologySpreadConstraints": { + "type": "array" + }, + "priorityClassName": { + "type": "string" + }, + "podAnnotations": { + "type": "object" + }, + "podLabels": { + "type": "object" + }, + "extraEnv": { + "type": "array" + }, + "terminationGracePeriodSeconds": { + "type": "integer", + "minimum": 0 + }, + "serviceAccount": { + "type": "object", + "properties": { + "create": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "annotations": { + "type": "object" + }, + "automountServiceAccountToken": { + "type": "boolean" + } + } + } + } + }, + "server": { + "type": "object", + "additionalProperties": false, + "required": [ + "replicas", + "image", + "port", + "backend", + "resources", + "waitImage", + "initStoreEnabled", + "auth", + "ingress", + "hpa", + "probes" + ], + "properties": { + "replicas": { + "type": "integer", + "minimum": 1 + }, + "image": { + "$ref": "#/definitions/image" + }, + "javaOpts": { + "type": "string" + }, + "port": { + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "backend": { + "type": "string", + "const": "hstore" + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "waitImage": { + "type": "string", + "minLength": 1, + "description": "Image used by the Helm test hook" + }, + "waitResources": { + "$ref": "#/definitions/resources" + }, + "restServer": { + "type": "object", + "additionalProperties": false, + "properties": { + "minFreeMemory": { + "oneOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "string", + "const": "" + } + ] + }, + "batchMaxWriteThreads": { + "oneOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "string", + "const": "" + } + ] + } + } + }, + "initStoreEnabled": { + "type": "boolean", + "const": false + }, + "auth": { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled", + "existingSecret" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "existingSecret": { + "type": "string" + } + }, + "allOf": [ + { + "if": { + "properties": { + "enabled": { + "const": true + } + } + }, + "then": { + "properties": { + "existingSecret": { + "minLength": 1 + } + } + } + }, + { + "if": { + "properties": { + "enabled": { + "const": false + } + } + }, + "then": { + "properties": { + "existingSecret": { + "const": "" + } + } + } + } + ] + }, + "ingress": { + "type": "object", + "required": [ + "enabled", + "className", + "hosts", + "tls" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "className": { + "type": "string" + }, + "hosts": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "host", + "paths" + ], + "properties": { + "host": { + "type": "string", + "minLength": 1 + }, + "paths": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "path", + "pathType" + ], + "properties": { + "path": { + "type": "string" + }, + "pathType": { + "type": "string", + "enum": [ + "Exact", + "Prefix", + "ImplementationSpecific" + ] + } + } + } + } + } + } + }, + "tls": { + "type": "array", + "items": { + "type": "object" + } + }, + "annotations": { + "type": "object" + } + } + }, + "hpa": { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled", + "minReplicas", + "maxReplicas", + "targetCPUUtilizationPercentage" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "minReplicas": { + "type": "integer", + "minimum": 1 + }, + "maxReplicas": { + "type": "integer", + "minimum": 1 + }, + "targetCPUUtilizationPercentage": { + "type": "integer", + "minimum": 1, + "maximum": 100 + } + } + }, + "probes": { + "$ref": "#/definitions/probes" + }, + "antiAffinity": { + "type": "string", + "enum": [ + "required", + "preferred", + "disabled" + ] + }, + "podSecurityContext": { + "type": "object" + }, + "securityContext": { + "type": "object" + }, + "pdb": { + "$ref": "#/definitions/pdb" + }, + "nodeSelector": { + "type": "object" + }, + "tolerations": { + "type": "array" + }, + "affinity": { + "type": "object" + }, + "topologySpreadConstraints": { + "type": "array" + }, + "priorityClassName": { + "type": "string" + }, + "podAnnotations": { + "type": "object" + }, + "podLabels": { + "type": "object" + }, + "extraEnv": { + "type": "array" + }, + "terminationGracePeriodSeconds": { + "type": "integer", + "minimum": 0 + }, + "serviceAccount": { + "type": "object", + "properties": { + "create": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "annotations": { + "type": "object" + }, + "automountServiceAccountToken": { + "type": "boolean" + } + } + }, + "service": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "enum": [ + "ClusterIP", + "NodePort", + "LoadBalancer" + ] + }, + "annotations": { + "type": "object" + }, + "nodePort": { + "type": [ + "integer", + "null" + ], + "minimum": 30000, + "maximum": 32767 + } + } + } + } + } + } +} diff --git a/helm/hugegraph/values.yaml b/helm/hugegraph/values.yaml new file mode 100644 index 0000000000..a43ae7b98b --- /dev/null +++ b/helm/hugegraph/values.yaml @@ -0,0 +1,256 @@ +# +# 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. +# + +# Default values for HugeGraph HStore Helm chart. +# See values-single.yaml and values-cluster.yaml for presets. + +fullnameOverride: "" +nameOverride: "" + +imagePullSecrets: [] + +pd: + replicas: 3 + image: + repository: hugegraph/pd + # The draft tracks latest until the next HugeGraph release tag is available. + # Pin the release tag and switch to IfNotPresent before stable publication. + tag: latest + pullPolicy: Always + # Empty preserves the image entrypoint's automatic JVM sizing. + javaOpts: "" + ports: + grpc: 8686 + rest: 8620 + raft: 8610 + dataPath: /hugegraph-pd/pd_data + storage: + size: 10Gi + storageClassName: "" + resources: {} + # Pod-level and container-level securityContext. Empty by default because the + # published images run as root; set these to satisfy a restricted namespace. + podSecurityContext: {} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + seccompProfile: + type: RuntimeDefault + # required | preferred | disabled + antiAffinity: required + # Scheduling. `affinity` takes precedence over the antiAffinity preset. + nodeSelector: {} + tolerations: [] + affinity: {} + topologySpreadConstraints: [] + priorityClassName: "" + podAnnotations: {} + podLabels: {} + # Extra environment variables appended to the pd container. + extraEnv: [] + # Gives a JVM with an on-disk store time to shut down cleanly on drain. + terminationGracePeriodSeconds: 300 + serviceAccount: + create: true + name: "" + annotations: {} + # This chart makes no Kubernetes API calls, so no token is mounted. + automountServiceAccountToken: false + pdb: + enabled: true + minAvailable: 2 + # Startup can take a while during Raft bootstrap + probes: + startup: + failureThreshold: 30 + periodSeconds: 10 + timeoutSeconds: 5 + readiness: + periodSeconds: 10 + failureThreshold: 3 + timeoutSeconds: 5 + liveness: + periodSeconds: 20 + failureThreshold: 3 + timeoutSeconds: 5 + +store: + replicas: 3 + image: + repository: hugegraph/store + # The draft tracks latest until the next HugeGraph release tag is available. + # Pin the release tag and switch to IfNotPresent before stable publication. + tag: latest + pullPolicy: Always + # Empty preserves the image entrypoint's automatic JVM sizing. + javaOpts: "" + ports: + grpc: 8500 + raft: 8510 + rest: 8520 + dataPath: /hugegraph-store/storage + storage: + size: 50Gi + storageClassName: "" + resources: {} + podSecurityContext: {} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + seccompProfile: + type: RuntimeDefault + antiAffinity: required + # Scheduling. `affinity` takes precedence over the antiAffinity preset. + nodeSelector: {} + tolerations: [] + affinity: {} + topologySpreadConstraints: [] + priorityClassName: "" + podAnnotations: {} + podLabels: {} + # Extra environment variables appended to the store container. + extraEnv: [] + # Gives a JVM with an on-disk store time to shut down cleanly on drain. + terminationGracePeriodSeconds: 300 + serviceAccount: + create: true + name: "" + annotations: {} + # This chart makes no Kubernetes API calls, so no token is mounted. + automountServiceAccountToken: false + pdb: + enabled: true + minAvailable: 2 + waitImage: curlimages/curl:8.5.0 + # Bound the PD-quorum wait so a cluster that never reaches quorum fails + # visibly instead of sitting in Init:0/1 forever. + waitTimeoutSeconds: 900 + # Optional bounds for the PD-quorum wait init container. + waitResources: {} + probes: + startup: + failureThreshold: 40 + periodSeconds: 10 + timeoutSeconds: 5 + readiness: + periodSeconds: 10 + failureThreshold: 3 + timeoutSeconds: 5 + liveness: + periodSeconds: 20 + failureThreshold: 3 + timeoutSeconds: 5 + +server: + replicas: 3 + image: + repository: hugegraph/server + # The draft tracks latest until the next HugeGraph release tag is available. + # Pin the release tag and switch to IfNotPresent before stable publication. + tag: latest + pullPolicy: Always + # Empty preserves the image entrypoint's automatic JVM sizing. + javaOpts: "" + port: 8080 + backend: hstore + resources: {} + # Server is stateless and may scale past the node count via HPA, so the + # default only prefers spreading. Use "required" when replicas are always + # fewer than schedulable nodes. + # required | preferred | disabled + antiAffinity: preferred + # Scheduling. `affinity` takes precedence over the antiAffinity preset. + nodeSelector: {} + tolerations: [] + affinity: {} + topologySpreadConstraints: [] + priorityClassName: "" + podAnnotations: {} + podLabels: {} + # Extra environment variables appended to the server container. + extraEnv: [] + # Gives a JVM with an on-disk store time to shut down cleanly on drain. + terminationGracePeriodSeconds: 60 + serviceAccount: + create: true + name: "" + annotations: {} + # This chart makes no Kubernetes API calls, so no token is mounted. + automountServiceAccountToken: false + podSecurityContext: {} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + seccompProfile: + type: RuntimeDefault + # Server has no quorum to preserve, so no PodDisruptionBudget by default. + pdb: + enabled: false + minAvailable: 2 + # Image used by the Helm test hook. + waitImage: curlimages/curl:8.5.0 + restServer: + # Empty preserves the image's restserver.min_free_memory default. + minFreeMemory: "" + # Empty preserves the image's batch.max_write_threads default. + batchMaxWriteThreads: "" + # Distributed HStore: the init-store gate must be explicitly false, so that + # concurrent Server replicas never initialize the same backend. No + # HG_SERVER_SKIP_INIT and no init Job. + initStoreEnabled: false + auth: + enabled: false + # Secret must contain key "password" + existingSecret: "" + service: + type: ClusterIP + annotations: {} + ingress: + enabled: false + className: "" + annotations: {} + hosts: + - host: hugegraph.local + paths: + - path: / + pathType: Prefix + tls: [] + hpa: + enabled: false + minReplicas: 3 + maxReplicas: 10 + targetCPUUtilizationPercentage: 70 + probes: + startup: + # 450s covers the 300s storage wait and Server process startup. + failureThreshold: 90 + periodSeconds: 5 + timeoutSeconds: 5 + readiness: + periodSeconds: 10 + failureThreshold: 3 + timeoutSeconds: 5 + liveness: + periodSeconds: 20 + failureThreshold: 3 + timeoutSeconds: 5 + +# No init Job; see README.md for the HStore initialization contract. +# Install with: helm install ... --wait (no --wait-for-jobs) From 6ddce9a9766ea06050cff80e1768a8eca9323093 Mon Sep 17 00:00:00 2001 From: Himanshu Verma Date: Fri, 31 Jul 2026 12:08:37 +0530 Subject: [PATCH 2/8] fix(ci): keep legacy Helm validation cluster-independent --- .github/workflows/helm-chart-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/helm-chart-ci.yml b/.github/workflows/helm-chart-ci.yml index a8ad116a48..a8d93c4601 100644 --- a/.github/workflows/helm-chart-ci.yml +++ b/.github/workflows/helm-chart-ci.yml @@ -76,7 +76,7 @@ jobs: cp -R helm/hugegraph /tmp/legacy cp helm/hugegraph/testdata/values-pre-hardening.yaml /tmp/legacy/values.yaml helm template legacy /tmp/legacy > /dev/null - helm install legacy /tmp/legacy --dry-run=client > /dev/null + helm template legacy /tmp/legacy --is-upgrade > /dev/null - name: helm package run: helm package helm/hugegraph -d /tmp/chart From 00ed46489815a1c2e6e4afbebf8dc23740ef15c7 Mon Sep 17 00:00:00 2001 From: Himanshu Verma Date: Fri, 31 Jul 2026 13:41:19 +0530 Subject: [PATCH 3/8] fix(helm): address chart review feedback --- .github/workflows/helm-chart-ci.yml | 8 ++++++++ helm/hugegraph/README.md | 5 +++-- helm/hugegraph/templates/NOTES.txt | 5 +++++ helm/hugegraph/templates/_helpers.tpl | 16 ++++++++++++++-- helm/hugegraph/templates/server-ingress.yaml | 2 +- helm/hugegraph/templates/server-pdb.yaml | 3 ++- .../templates/tests/test-connection.yaml | 16 ++++++++++++++++ helm/hugegraph/values.schema.json | 12 ++++-------- helm/hugegraph/values.yaml | 4 +++- 9 files changed, 56 insertions(+), 15 deletions(-) diff --git a/.github/workflows/helm-chart-ci.yml b/.github/workflows/helm-chart-ci.yml index a8d93c4601..0291b4d6ba 100644 --- a/.github/workflows/helm-chart-ci.yml +++ b/.github/workflows/helm-chart-ci.yml @@ -32,6 +32,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: azure/setup-helm@v4 with: @@ -55,6 +57,12 @@ jobs: ! helm template ci helm/hugegraph --set pd.replicas=100 2>/dev/null ! helm template ci helm/hugegraph --set pd.pdb.minAvailable=3 2>/dev/null ! helm template ci helm/hugegraph --set server.hpa.enabled=true 2>/dev/null + ! helm template ci helm/hugegraph \ + --set server.hpa.enabled=true \ + --set server.hpa.minReplicas=2 \ + --set server.resources.requests.cpu=100m \ + --set server.pdb.enabled=true \ + --set server.pdb.minAvailable=2 2>/dev/null ! helm template ci helm/hugegraph --set server.auth.enabled=true 2>/dev/null - name: kubeconform diff --git a/helm/hugegraph/README.md b/helm/hugegraph/README.md index 6d03f7677b..b68a102893 100644 --- a/helm/hugegraph/README.md +++ b/helm/hugegraph/README.md @@ -220,7 +220,7 @@ default values. | `server.podSecurityContext` | Pod-level securityContext, rendered only when set | `{}` | | `server.securityContext` | Container-level securityContext. Hardened by default; `runAsNonRoot` is not set because the published images run as root | `allowPrivilegeEscalation: false`, `capabilities.drop: [ALL]`, `seccompProfile: RuntimeDefault` | | `server.pdb.enabled` | Create a PodDisruptionBudget for Server. Off by default: Server holds no quorum | `false` | -| `server.pdb.minAvailable` | Must be strictly less than `server.replicas` | `2` | +| `server.pdb.minAvailable` | Must be less than `server.hpa.minReplicas` when HPA is enabled, otherwise less than `server.replicas` | `2` | | `server.antiAffinity` | One of `required`, `preferred`, `disabled`. Defaults to `preferred` rather than `required` because HPA may scale Server past the node count; set `required` when replicas always stay below it | `preferred` | | `server.nodeSelector` | Node selector for server Pods | `{}` | | `server.tolerations` | Tolerations for server Pods | `[]` | @@ -236,6 +236,7 @@ default values. | `server.serviceAccount.annotations` | Annotations on the created ServiceAccount | `{}` | | `server.serviceAccount.automountServiceAccountToken` | Mount an API token. The chart makes no API calls | `false` | | `server.waitImage` | Image for the Helm test hook | `curlimages/curl:8.5.0` | +| `server.testResources` | Resources for the Helm test hook container | `{}` | | `server.restServer.minFreeMemory` | Empty preserves the image default | `""` | | `server.restServer.batchMaxWriteThreads` | Empty preserves the image default | `""` | | `server.initStoreEnabled` | Must remain `false` for distributed HStore | `false` | @@ -371,7 +372,7 @@ kubectl describe pod | grep -A5 "Last State" Helm itself rejects release names longer than 53 characters, before this chart renders anything: -``` +```text invalid release name ... the length must not be longer than 53 ``` diff --git a/helm/hugegraph/templates/NOTES.txt b/helm/hugegraph/templates/NOTES.txt index 288c8d6f5f..a0d410951d 100644 --- a/helm/hugegraph/templates/NOTES.txt +++ b/helm/hugegraph/templates/NOTES.txt @@ -34,7 +34,12 @@ Verify the release: Reach the Server API: kubectl port-forward -n {{ .Release.Namespace }} svc/{{ include "hugegraph.server.name" . }} {{ .Values.server.port }}:{{ .Values.server.port }} +{{- if .Values.server.auth.enabled }} + PASSWORD="$(kubectl get secret -n {{ .Release.Namespace }} {{ .Values.server.auth.existingSecret }} -o jsonpath='{.data.password}' | base64 --decode)" + curl --user "admin:${PASSWORD}" http://127.0.0.1:{{ .Values.server.port }}/versions +{{- else }} curl http://127.0.0.1:{{ .Values.server.port }}/versions +{{- end }} {{- if not .Values.server.auth.enabled }} Authentication is disabled. Do not expose this release to untrusted networks. diff --git a/helm/hugegraph/templates/_helpers.tpl b/helm/hugegraph/templates/_helpers.tpl index 3f64b2b2b4..cc9a24b59f 100644 --- a/helm/hugegraph/templates/_helpers.tpl +++ b/helm/hugegraph/templates/_helpers.tpl @@ -208,6 +208,17 @@ default {{- end -}} {{- end }} +{{/* +The minimum Server replica count that a PDB must remain valid against. +*/}} +{{- define "hugegraph.server.replicaFloor" -}} +{{- if .Values.server.hpa.enabled -}} +{{- .Values.server.hpa.minReplicas -}} +{{- else -}} +{{- .Values.server.replicas -}} +{{- end -}} +{{- end }} + {{/* Cross-field validation that JSON Schema draft-07 cannot express. */}} @@ -246,8 +257,9 @@ and must not be failed for a value that has no effect. {{- fail "server.service.nodePort requires server.service.type to be NodePort or LoadBalancer" -}} {{- end -}} {{- $serverPdb := get .Values.server "pdb" | default dict -}} -{{- if and (get $serverPdb "enabled" | default false) (gt (int .Values.server.replicas) 1) (ge (int (get $serverPdb "minAvailable" | default 1)) (int .Values.server.replicas)) -}} -{{- fail "server.pdb.minAvailable must be less than server.replicas, otherwise the PDB permanently blocks voluntary disruptions such as node drains" -}} +{{- $serverReplicaFloor := include "hugegraph.server.replicaFloor" . | int -}} +{{- if and (get $serverPdb "enabled" | default false) (gt $serverReplicaFloor 1) (ge (int (get $serverPdb "minAvailable" | default 1)) $serverReplicaFloor) -}} +{{- fail "server.pdb.minAvailable must be less than the active Server replica floor (server.hpa.minReplicas when HPA is enabled, otherwise server.replicas), otherwise the PDB permanently blocks voluntary disruptions such as node drains" -}} {{- end -}} {{- end }} diff --git a/helm/hugegraph/templates/server-ingress.yaml b/helm/hugegraph/templates/server-ingress.yaml index 08a8c6a7ea..ad473fde73 100644 --- a/helm/hugegraph/templates/server-ingress.yaml +++ b/helm/hugegraph/templates/server-ingress.yaml @@ -41,7 +41,7 @@ spec: http: paths: {{- range .paths }} - - path: {{ .path }} + - path: {{ .path | quote }} pathType: {{ .pathType }} backend: service: diff --git a/helm/hugegraph/templates/server-pdb.yaml b/helm/hugegraph/templates/server-pdb.yaml index 3c1f0c4847..7fb4826aec 100644 --- a/helm/hugegraph/templates/server-pdb.yaml +++ b/helm/hugegraph/templates/server-pdb.yaml @@ -16,7 +16,8 @@ # {{- $pdb := get .Values.server "pdb" | default dict }} -{{- if and (get $pdb "enabled" | default false) (gt (int .Values.server.replicas) 1) }} +{{- $replicaFloor := include "hugegraph.server.replicaFloor" . | int }} +{{- if and (get $pdb "enabled" | default false) (gt $replicaFloor 1) }} apiVersion: policy/v1 kind: PodDisruptionBudget metadata: diff --git a/helm/hugegraph/templates/tests/test-connection.yaml b/helm/hugegraph/templates/tests/test-connection.yaml index b5c431b659..beef5c1d47 100644 --- a/helm/hugegraph/templates/tests/test-connection.yaml +++ b/helm/hugegraph/templates/tests/test-connection.yaml @@ -28,10 +28,26 @@ metadata: "helm.sh/hook": test "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded spec: + automountServiceAccountToken: false restartPolicy: Never + securityContext: + seccompProfile: + type: RuntimeDefault containers: - name: curl image: {{ .Values.server.waitImage | quote }} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + readOnlyRootFilesystem: true + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + {{- with .Values.server.testResources }} + resources: + {{- toYaml . | nindent 8 }} + {{- end }} {{- if .Values.server.auth.enabled }} env: - name: PASSWORD diff --git a/helm/hugegraph/values.schema.json b/helm/hugegraph/values.schema.json index 23ff6f73d8..7c2185c5be 100644 --- a/helm/hugegraph/values.schema.json +++ b/helm/hugegraph/values.schema.json @@ -31,14 +31,6 @@ }, "global": { "type": "object" - }, - "networkPolicy": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - } - } } }, "definitions": { @@ -576,6 +568,7 @@ }, "ingress": { "type": "object", + "additionalProperties": false, "required": [ "enabled", "className", @@ -734,6 +727,9 @@ } } }, + "testResources": { + "type": "object" + }, "service": { "type": "object", "additionalProperties": false, diff --git a/helm/hugegraph/values.yaml b/helm/hugegraph/values.yaml index a43ae7b98b..83bb1f3c74 100644 --- a/helm/hugegraph/values.yaml +++ b/helm/hugegraph/values.yaml @@ -185,7 +185,7 @@ server: podLabels: {} # Extra environment variables appended to the server container. extraEnv: [] - # Gives a JVM with an on-disk store time to shut down cleanly on drain. + # Allows in-flight requests to drain before the Server is stopped. terminationGracePeriodSeconds: 60 serviceAccount: create: true @@ -206,6 +206,8 @@ server: minAvailable: 2 # Image used by the Helm test hook. waitImage: curlimages/curl:8.5.0 + # Optional resources for the Helm test hook container. + testResources: {} restServer: # Empty preserves the image's restserver.min_free_memory default. minFreeMemory: "" From 4628f0835fa6bee5f181b7f6f645c2b0d86ddc93 Mon Sep 17 00:00:00 2001 From: Himanshu Verma Date: Fri, 31 Jul 2026 13:49:40 +0530 Subject: [PATCH 4/8] fix(helm): validate test hook resources --- helm/hugegraph/values.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/hugegraph/values.schema.json b/helm/hugegraph/values.schema.json index 7c2185c5be..56c1284bca 100644 --- a/helm/hugegraph/values.schema.json +++ b/helm/hugegraph/values.schema.json @@ -728,7 +728,7 @@ } }, "testResources": { - "type": "object" + "$ref": "#/definitions/resources" }, "service": { "type": "object", From 34234e33fa843a168b0c05d92e40ebe1c8be512f Mon Sep 17 00:00:00 2001 From: Himanshu Verma Date: Mon, 3 Aug 2026 17:12:50 +0530 Subject: [PATCH 5/8] feat(helm): add optional Hubble UI and fix auth bootstrap with init-store disabled The Server wrapper now writes auth.admin_pa from the auth Secret alongside usePD and pd.peers, so an auth-enabled release keeps its configured admin password with init_store.enabled=false instead of silently falling back to the public default. The Secret value is rejected when it contains properties-parser metacharacters that would inject config lines or store a different password than the Secret holds. The new hubble component deploys the Hubble UI as a single-replica Deployment with pd and direct wiring modes, optional Ingress and H2 persistence, schema validation, render-time guards, docs, and CI coverage. PD-meta installs (auth enabled, or Hubble in pd mode) also announce the Server client Service URL to PD via server.urls_to_pd and server.deploy_in_k8s so discovery clients receive a resolvable address instead of the 0.0.0.0 default, and the Hubble wrapper writes server.host so current images bind all interfaces. Because current Hubble images authenticate their login against the cluster, rendering Hubble without server.auth fails unless explicitly overridden. The CI invalid-value step now fails on every case rather than only its last line, and positive renders cover both Hubble modes. Validated against a composition of master 1716c774 plus the current heads of #3119 (edf07d0f), #3126 (b40c42fb), and #3130 (198de19e): fresh auth-enabled installs reach Ready with zero restarts, the admin credential comes from the Secret while unauthenticated and default-password requests get 401, and Hubble logs in with the Secret credential and reads cluster metadata through PD discovery, with its H2 metadata persisted on the PVC. --- .github/workflows/helm-chart-ci.yml | 63 +++- helm/hugegraph/README.md | 112 ++++++- helm/hugegraph/templates/NOTES.txt | 26 ++ helm/hugegraph/templates/_helpers.tpl | 62 ++++ .../templates/hubble-deployment.yaml | 268 ++++++++++++++++ helm/hugegraph/templates/hubble-ingress.yaml | 54 ++++ helm/hugegraph/templates/hubble-pvc.yaml | 41 +++ helm/hugegraph/templates/hubble-service.yaml | 43 +++ .../templates/server-deployment.yaml | 76 ++++- helm/hugegraph/templates/serviceaccount.yaml | 6 +- helm/hugegraph/values-cluster.yaml | 18 ++ helm/hugegraph/values.schema.json | 292 +++++++++++++----- helm/hugegraph/values.yaml | 90 +++++- 13 files changed, 1051 insertions(+), 100 deletions(-) create mode 100644 helm/hugegraph/templates/hubble-deployment.yaml create mode 100644 helm/hugegraph/templates/hubble-ingress.yaml create mode 100644 helm/hugegraph/templates/hubble-pvc.yaml create mode 100644 helm/hugegraph/templates/hubble-service.yaml diff --git a/.github/workflows/helm-chart-ci.yml b/.github/workflows/helm-chart-ci.yml index 0291b4d6ba..b052c86bd7 100644 --- a/.github/workflows/helm-chart-ci.yml +++ b/.github/workflows/helm-chart-ci.yml @@ -50,20 +50,55 @@ jobs: for preset in "" "-f helm/hugegraph/values-single.yaml" "-f helm/hugegraph/values-cluster.yaml"; do helm template ci helm/hugegraph $preset > /dev/null done + # Positive coverage for every hubble wrapper branch: pd mode with + # auth, direct mode with a custom port, and a TLS-less ingress with + # the explicit opt-in. + helm template ci helm/hugegraph \ + --set hubble.enabled=true \ + --set server.auth.enabled=true \ + --set server.auth.existingSecret=ci-auth > /dev/null + helm template ci helm/hugegraph \ + --set hubble.enabled=true \ + --set hubble.allowWithoutServerAuth=true \ + --set hubble.mode=direct \ + --set hubble.port=9090 \ + --set hubble.persistence.enabled=true \ + --set hubble.ingress.enabled=true \ + --set hubble.ingress.allowPlainHttp=true > /dev/null - name: reject invalid values run: | - # each of these must fail; the schema and helpers are the contract - ! helm template ci helm/hugegraph --set pd.replicas=100 2>/dev/null - ! helm template ci helm/hugegraph --set pd.pdb.minAvailable=3 2>/dev/null - ! helm template ci helm/hugegraph --set server.hpa.enabled=true 2>/dev/null - ! helm template ci helm/hugegraph \ + # Each case must fail to render; the schema and helpers are the + # contract. `! cmd` alone is NOT enforced under `set -e` (errexit + # ignores inverted commands), so every case checks explicitly. + must_fail() { + if helm template ci helm/hugegraph "$@" >/dev/null 2>&1; then + echo "expected render failure for: $*" >&2 + exit 1 + fi + } + must_fail --set pd.replicas=100 + must_fail --set pd.pdb.minAvailable=3 + must_fail --set server.hpa.enabled=true + must_fail \ --set server.hpa.enabled=true \ --set server.hpa.minReplicas=2 \ --set server.resources.requests.cpu=100m \ --set server.pdb.enabled=true \ - --set server.pdb.minAvailable=2 2>/dev/null - ! helm template ci helm/hugegraph --set server.auth.enabled=true 2>/dev/null + --set server.pdb.minAvailable=2 + must_fail --set server.auth.enabled=true + must_fail --set hubble.enabled=true + A="--set hubble.enabled=true --set hubble.allowWithoutServerAuth=true" + must_fail $A --set hubble.port=0 + must_fail $A \ + --set hubble.persistence.enabled=true \ + --set hubble.persistence.size="" + must_fail $A --set hubble.service.nodePort=30080 + must_fail $A --set hubble.mode=bogus + must_fail $A --set hubble.image.tag="" + must_fail $A --set hubble.ingress.enabled=true + must_fail --set server.ingress.enabled=true \ + --set server.ingress.allowPlainHttp=true - name: kubeconform shell: bash @@ -74,6 +109,20 @@ jobs: for preset in "" "-f helm/hugegraph/values-single.yaml" "-f helm/hugegraph/values-cluster.yaml"; do helm template ci helm/hugegraph $preset | /tmp/kubeconform -strict -summary -kubernetes-version 1.23.0 done + helm template ci helm/hugegraph \ + --set hubble.enabled=true \ + --set hubble.allowWithoutServerAuth=true \ + --set hubble.mode=direct \ + --set hubble.port=9090 \ + --set hubble.persistence.enabled=true \ + --set hubble.ingress.enabled=true \ + --set hubble.ingress.allowPlainHttp=true \ + | /tmp/kubeconform -strict -summary -kubernetes-version 1.23.0 + helm template ci helm/hugegraph \ + --set hubble.enabled=true \ + --set server.auth.enabled=true \ + --set server.auth.existingSecret=ci-auth \ + | /tmp/kubeconform -strict -summary -kubernetes-version 1.23.0 - name: legacy --reuse-values compatibility run: | diff --git a/helm/hugegraph/README.md b/helm/hugegraph/README.md index b68a102893..c2c78693fe 100644 --- a/helm/hugegraph/README.md +++ b/helm/hugegraph/README.md @@ -43,10 +43,18 @@ so operators do not have to: 300 seconds waiting for storage and a further 120 seconds in the start command. A lower configured `failureThreshold` is raised to this floor rather than being rejected. -- **The image entrypoint keeps ownership of `PASSWORD` handling and - `auth.admin_pa`.** When authentication or a custom port or REST tuning is - configured, the chart's wrapper only ensures `usePD=true` and `pd.peers` are - present, then hands off to the image entrypoint. +- **The wrapper writes `auth.admin_pa` from the auth Secret.** With + `init_store.enabled=false` the admin credential is created on the PD startup + path from `auth.admin_pa`, not from the Docker `PASSWORD` stdin path. When + authentication is enabled, the chart's wrapper therefore writes + `auth.admin_pa` from the mounted Secret alongside `usePD=true` and + `pd.peers`, then hands off to the image entrypoint. Two caveats: + `auth.admin_pa` applies only when the admin is first created, so changing the + Secret does not rotate an existing cluster's password, and the value lands in + `rest-server.properties` inside the container (file mode 600). Because the + Java properties parser reinterprets them, the Secret value must not contain + newlines, carriage returns, or backslashes; the wrapper refuses to start if + it does. - **Resource names reserve their suffix and StatefulSet ordinal before truncation,** so a long release name cannot produce colliding or over-long Pod and Service names, and PD/Store identities stay fixed when replicas @@ -80,7 +88,7 @@ helm test hugegraph --namespace hugegraph |---|---| | `values.yaml` | Default 3+3+3 topology | | `values-single.yaml` | Single-node 1+1+1 example | -| `values-cluster.yaml` | Production 3+3+3 starting point with JVM/resources, PD/Store PDBs, and required anti-affinity | +| `values-cluster.yaml` | Production 3+3+3 starting point with JVM/resources, PD/Store PDBs, and required anti-affinity; Hubble stays opt-in because the preset does not enable authentication | `values-cluster.yaml` is a production starting point, not a capacity guarantee. Recalculate capacity for the graph size, traffic, failure budget, @@ -262,6 +270,82 @@ Helm upgrade does not overwrite the autoscaler's live replica count. Enabling utilization-based HPA requires a strictly positive `server.resources.requests.cpu`. +### Hubble (optional UI) + +Set `hubble.enabled=true` to deploy [HugeGraph Hubble](https://hugegraph.apache.org/docs/quickstart/toolchain/hugegraph-hubble/), +the web UI for graph management, schema browsing, Gremlin queries, and the +cluster operations view. `hubble.mode` selects the wiring. In the default +`pd` mode the chart points `pd.peers` at the PD gRPC peers, `pd.server` at +the PD client Service REST port, and the Store metrics allow-list at the +Store REST endpoints, so the cluster view works without manual wiring; the +Server is additionally configured to register its client Service URL with PD +(see below). In `direct` mode Hubble only receives `server.direct_url` +pointing at the Server client Service; there is no PD discovery and no +operations view. Everything else in `hugegraph-hubble.properties` keeps the +image default. + +Enabling `pd`-mode Hubble switches the Server into PD meta mode (`usePD`, +`server.urls_to_pd`, `server.deploy_in_k8s`) so that PD can hand Hubble a +resolvable Server address; auth-enabled installs already run in this mode. +On an existing release this change rolls the Server Deployment once. The +Store allow-list is computed from `store.replicas` at render time, so scale +Store with `helm upgrade`, not `kubectl scale`, or the list goes stale until +the next upgrade. + +Hubble is one replica by design: it keeps UI connection metadata, including +any graph credentials entered in the UI, in an embedded per-instance H2 +database. Enable `hubble.persistence` to keep that metadata across Pod +replacement; the chart then redirects the H2 location into the mounted +volume through Spring's environment binding. The Deployment uses the +`Recreate` strategy so two Hubble instances never attach the same database. +The PVC is kept on `helm uninstall` (delete it explicitly to discard the +stored metadata), `size` and `storageClassName` apply at install time only, +and a non-root `podSecurityContext` needs a matching `fsGroup` so H2 can +write the volume. + +**Enable `server.auth` when using Hubble.** Current Hubble images gate the +UI behind a login that authenticates against the cluster; with server +authentication disabled the login cannot complete (the server rejects +`/auth/login` with "Unconfigured authenticator"), so Hubble is only useful +on an auth-enabled deployment, where the admin credential from +`server.auth.existingSecret` logs in. The chart therefore refuses to render +`hubble.enabled=true` without `server.auth` unless +`hubble.allowWithoutServerAuth=true` explicitly overrides it for images +whose login does not need cluster authentication. + +**Hubble serves plain HTTP.** Reach it with `kubectl port-forward` or behind +an HTTPS-terminating Ingress; never expose the port directly to an untrusted +network. An Ingress without `tls` is rejected at render time unless +`hubble.ingress.allowPlainHttp=true` explicitly accepts plain HTTP for a +trusted network. + +| Parameter | Description | Default | +|---|---|---| +| `hubble.enabled` | Deploy the Hubble UI. Requires `server.auth` (see below) | `false` | +| `hubble.mode` | `pd` discovers the cluster through PD and enables the operations view; `direct` talks to the Server client Service only | `pd` | +| `hubble.allowWithoutServerAuth` | Renders Hubble without `server.auth`, for future images whose login does not require cluster authentication | `false` | +| `hubble.image.repository` | Hubble image repository | `hugegraph/hubble` | +| `hubble.image.tag` | Hubble image tag. Tracks the development image until the next release is pinned | `latest` | +| `hubble.image.pullPolicy` | Hubble image pull policy | `Always` | +| `hubble.port` | Hubble HTTP port, container port, and Service port | `8088` | +| `hubble.persistence.enabled` | Persist UI connection metadata in a PVC | `false` | +| `hubble.persistence.size` | PVC size | `1Gi` | +| `hubble.persistence.storageClassName` | Empty uses the cluster default StorageClass | `""` | +| `hubble.resources` | Hubble resources | `{}` | +| `hubble.podSecurityContext` | Pod-level securityContext, rendered only when set | `{}` | +| `hubble.securityContext` | Container-level securityContext, hardened like the other components | see `values.yaml` | +| `hubble.service.type` | Hubble Service type | `ClusterIP` | +| `hubble.service.annotations` | Hubble Service annotations | `{}` | +| `hubble.service.nodePort` | Requires a `NodePort` or `LoadBalancer` Service type | unset | +| `hubble.ingress.*` | Same Ingress keys as `server.ingress.*`, plus `allowPlainHttp`, which applies to the Hubble Ingress only | `enabled: false` | +| `hubble.serviceAccount.*` | Same ServiceAccount keys as the other components | `create: true` | +| `hubble.nodeSelector` / `tolerations` / `affinity` / `topologySpreadConstraints` | Scheduling controls | unset | +| `hubble.priorityClassName` | PriorityClass for the hubble Pod | `""` | +| `hubble.podAnnotations` / `hubble.podLabels` | Extra Pod metadata | `{}` | +| `hubble.extraEnv` | Extra environment variables for the hubble container | `[]` | +| `hubble.terminationGracePeriodSeconds` | Shutdown grace period | `30` | +| `hubble.probes.*` | Same probe keys as PD; startup and readiness check `/actuator/health`, liveness is a TCP check | see `values.yaml` | + Specify each parameter with `--set`, or supply a YAML file with `-f`: ```bash @@ -284,6 +368,16 @@ before anything reaches the cluster: - `pdb.minAvailable` must be less than the matching `replicas`, so a PodDisruptionBudget cannot permanently block node drains. - `pd.replicas` and `store.replicas` are capped at 99. +- `hubble.port` must be a valid port, `hubble.persistence.size` must be + non-empty, and `hubble.service.nodePort` requires a `NodePort` or + `LoadBalancer` Service type. +- `hubble.image.tag` must be non-empty (the chart `appVersion` tracks the + Server release, not Hubble), and a Hubble Ingress without `tls` is rejected + unless `hubble.ingress.allowPlainHttp=true`. +- `hubble.enabled` without `server.auth.enabled` is rejected unless + `hubble.allowWithoutServerAuth=true`, and setting + `server.ingress.allowPlainHttp` is rejected because the plain-HTTP opt-in + applies to the Hubble Ingress only. ## Deep Dive @@ -395,3 +489,11 @@ independently of the release name. valid for a root image; `podSecurityContext` and `securityContext` are fully configurable per component. - `values-cluster.yaml` is a starting point, not a capacity guarantee. +- The auth Secret sets the admin password only at first creation via + `auth.admin_pa`; the chart cannot rotate an existing cluster's admin + password. +- Hubble is single-replica, serves plain HTTP, requires `server.auth` to be + enabled for its login to complete, and keeps UI connection metadata, + including any graph credentials entered in the UI, in an embedded H2 + database that is lost on Pod replacement unless `hubble.persistence` is + enabled. diff --git a/helm/hugegraph/templates/NOTES.txt b/helm/hugegraph/templates/NOTES.txt index a0d410951d..72b8f54cf9 100644 --- a/helm/hugegraph/templates/NOTES.txt +++ b/helm/hugegraph/templates/NOTES.txt @@ -14,11 +14,15 @@ 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. */}} +{{- $hubbleEnabled := get (get .Values "hubble" | default dict) "enabled" | default false }} {{ .Release.Name }} installed in namespace {{ .Release.Namespace }}. PD {{ .Values.pd.replicas }} replica(s) Store {{ .Values.store.replicas }} replica(s) Server {{ if .Values.server.hpa.enabled }}HPA {{ .Values.server.hpa.minReplicas }}-{{ .Values.server.hpa.maxReplicas }}{{ else }}{{ .Values.server.replicas }} replica(s){{ end }} +{{- if $hubbleEnabled }} + Hubble 1 replica (UI) +{{- end }} Watch the cluster come up: @@ -40,6 +44,28 @@ Reach the Server API: {{- else }} curl http://127.0.0.1:{{ .Values.server.port }}/versions {{- end }} +{{- if $hubbleEnabled }} + +Reach the Hubble UI: + + kubectl port-forward -n {{ .Release.Namespace }} svc/{{ include "hugegraph.hubble.name" . }} {{ .Values.hubble.port }}:{{ .Values.hubble.port }} + + then open http://127.0.0.1:{{ .Values.hubble.port }} + +{{- if not .Values.server.auth.enabled }} + +Server authentication is disabled, so the Hubble login cannot complete on +this release: current Hubble images authenticate against the cluster. +Enable server.auth to use Hubble. +{{- end }} + +Hubble serves plain HTTP. Reach it through port-forward or an +HTTPS-terminating Ingress; never expose it directly to an untrusted network. +{{- if not .Values.hubble.persistence.enabled }} +Hubble persistence is disabled: UI connection metadata is lost when the +Hubble Pod is replaced. Graph data is unaffected. +{{- end }} +{{- end }} {{- if not .Values.server.auth.enabled }} Authentication is disabled. Do not expose this release to untrusted networks. diff --git a/helm/hugegraph/templates/_helpers.tpl b/helm/hugegraph/templates/_helpers.tpl index cc9a24b59f..c32d261fcd 100644 --- a/helm/hugegraph/templates/_helpers.tpl +++ b/helm/hugegraph/templates/_helpers.tpl @@ -70,6 +70,14 @@ app.kubernetes.io/instance: {{ .Release.Name }} {{- printf "%s-server" (include "hugegraph.fullname" . | trunc 56 | trimSuffix "-") }} {{- end }} +{{- define "hugegraph.hubble.name" -}} +{{- printf "%s-hubble" (include "hugegraph.fullname" . | trunc 56 | trimSuffix "-") }} +{{- end }} + +{{- define "hugegraph.hubble.dataName" -}} +{{- printf "%s-hubble-data" (include "hugegraph.fullname" . | trunc 51 | trimSuffix "-") }} +{{- end }} + {{- define "hugegraph.test.name" -}} {{- printf "%s-test-connection" (include "hugegraph.fullname" . | trunc 47 | trimSuffix "-") }} {{- end }} @@ -144,6 +152,38 @@ First store REST endpoint for STORE_REST / wait-partition. {{- printf "%s-0.%s.%s.svc:%d" $name $name $ns (int .Values.store.ports.rest) -}} {{- end }} +{{/* +Server REST URL reached through the client Service. Announced to PD via +server.urls_to_pd so PD-discovered clients (Hubble) get a resolvable address +instead of the in-pod 0.0.0.0 default. +*/}} +{{- define "hugegraph.server.clientUrl" -}} +{{- printf "http://%s.%s.svc:%d" (include "hugegraph.server.name" .) .Release.Namespace (int .Values.server.port) -}} +{{- end }} + +{{/* +PD REST endpoint reached through the client Service, for Hubble's pd.server. +*/}} +{{- define "hugegraph.pd.restClientEndpoint" -}} +{{- printf "%s.%s.svc:%d" (include "hugegraph.pd.clientName" .) .Release.Namespace (int .Values.pd.ports.rest) -}} +{{- end }} + +{{/* +Store REST origins in Hubble's bracketed allow-list form: +[http://store-0.svc.ns.svc:8520,...] +*/}} +{{- define "hugegraph.store.restOriginsList" -}} +{{- $origins := list -}} +{{- $replicas := int .Values.store.replicas -}} +{{- $name := include "hugegraph.store.name" . -}} +{{- $ns := .Release.Namespace -}} +{{- $port := int .Values.store.ports.rest -}} +{{- range $i := until $replicas -}} + {{- $origins = append $origins (printf "http://%s-%d.%s.%s.svc:%d" $name $i $name $ns $port) -}} +{{- end -}} +{{- printf "[%s]" (join "," $origins) -}} +{{- end }} + {{/* Quorum size: floor(replicas/2)+1 */}} @@ -261,6 +301,28 @@ and must not be failed for a value that has no effect. {{- if and (get $serverPdb "enabled" | default false) (gt $serverReplicaFloor 1) (ge (int (get $serverPdb "minAvailable" | default 1)) $serverReplicaFloor) -}} {{- fail "server.pdb.minAvailable must be less than the active Server replica floor (server.hpa.minReplicas when HPA is enabled, otherwise server.replicas), otherwise the PDB permanently blocks voluntary disruptions such as node drains" -}} {{- end -}} +{{- $serverIngress := get .Values.server "ingress" | default dict -}} +{{- if hasKey $serverIngress "allowPlainHttp" -}} +{{- fail "server.ingress.allowPlainHttp has no effect; the plain-HTTP opt-in applies to hubble.ingress only" -}} +{{- end -}} +{{- $hubble := get .Values "hubble" | default dict -}} +{{- if get $hubble "enabled" | default false -}} +{{- if and (not .Values.server.auth.enabled) (not (get $hubble "allowWithoutServerAuth" | default false)) -}} +{{- fail "hubble.enabled requires server.auth: current Hubble images authenticate against the cluster and cannot complete their login on an auth-less deployment. Enable server.auth, or set hubble.allowWithoutServerAuth=true for images that support it" -}} +{{- end -}} +{{- $hubbleSvc := get $hubble "service" | default dict -}} +{{- if and (get $hubbleSvc "nodePort") (not (has (get $hubbleSvc "type" | default "ClusterIP") (list "NodePort" "LoadBalancer"))) -}} +{{- fail "hubble.service.nodePort requires hubble.service.type to be NodePort or LoadBalancer" -}} +{{- end -}} +{{- $hubbleImage := get $hubble "image" | default dict -}} +{{- if eq (trim (get $hubbleImage "tag" | default "")) "" -}} +{{- fail "hubble.image.tag must not be empty: the chart appVersion tracks the Server release, not Hubble, so there is no meaningful fallback" -}} +{{- end -}} +{{- $hubbleIngress := get $hubble "ingress" | default dict -}} +{{- if and (get $hubbleIngress "enabled" | default false) (empty (get $hubbleIngress "tls")) (not (get $hubbleIngress "allowPlainHttp" | default false)) -}} +{{- fail "hubble.ingress.enabled without tls publishes the plain-HTTP, unauthenticated Hubble UI; configure hubble.ingress.tls, or set hubble.ingress.allowPlainHttp=true to accept that on a trusted network" -}} +{{- end -}} +{{- end -}} {{- end }} {{/* diff --git a/helm/hugegraph/templates/hubble-deployment.yaml b/helm/hugegraph/templates/hubble-deployment.yaml new file mode 100644 index 0000000000..d094db4a45 --- /dev/null +++ b/helm/hugegraph/templates/hubble-deployment.yaml @@ -0,0 +1,268 @@ +# +# 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. +# + +{{- if get (get .Values "hubble" | default dict) "enabled" | default false }} +{{- include "hugegraph.validateValues" . }} +{{- $customPort := ne (int .Values.hubble.port) 8088 }} +{{- $mode := .Values.hubble.mode | default "pd" }} +{{- if not (has $mode (list "pd" "direct")) }} +{{- fail "hubble.mode must be one of: pd, direct" }} +{{- end }} +{{- $pdMode := eq $mode "pd" }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "hugegraph.hubble.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: hubble +spec: + # Hubble keeps its connection metadata in an embedded per-instance H2 + # database, so this Deployment is fixed at one replica and replaces the Pod + # instead of rolling, which also keeps a persistent volume single-attached. + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + {{- include "hugegraph.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: hubble + template: + metadata: + labels: + {{- include "hugegraph.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: hubble + {{- with .Values.hubble.podLabels }}{{ toYaml . | nindent 8 }}{{- end }} + {{- with .Values.hubble.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + automountServiceAccountToken: {{ get (get .Values.hubble "serviceAccount" | default dict) "automountServiceAccountToken" | default false }} + serviceAccountName: {{ include "hugegraph.serviceAccountName" (dict "component" .Values.hubble "name" (include "hugegraph.hubble.name" .) ) }} + {{- with .Values.hubble.terminationGracePeriodSeconds }} + terminationGracePeriodSeconds: {{ . }} + {{- end }} + {{- with .Values.hubble.priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} + {{- with .Values.hubble.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.hubble.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.hubble.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.hubble.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.hubble.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: hubble + image: "{{ .Values.hubble.image.repository }}:{{ .Values.hubble.image.tag }}" + imagePullPolicy: {{ .Values.hubble.image.pullPolicy }} + {{- with .Values.hubble.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + # The Hubble image reads conf/hugegraph-hubble.properties only; it + # has no environment mapping for these keys, so the wrapper rewrites + # them before handing off to the image's own start script. The image + # ships no dumb-init; start-hubble.sh -f execs java, which then + # receives signals directly. + command: + - /bin/bash + - -c + args: + - | + set -euo pipefail + CONF=./conf/hugegraph-hubble.properties + if [[ ! -r "${CONF}" ]]; then + echo "missing or unreadable ${CONF}; refusing to write a stub config" >&2 + exit 1 + fi + TMP=$(mktemp) + FOUND_PD_ENABLED=false + {{- if $pdMode }} + FOUND_PD_PEERS=false + FOUND_PD_SERVER=false + FOUND_STORE_TARGETS=false + {{- else }} + FOUND_DIRECT_URL=false + {{- end }} + FOUND_SERVER_HOST=false + {{- if $customPort }} + FOUND_HUBBLE_PORT=false + FOUND_SERVER_PORT=false + {{- end }} + while IFS= read -r LINE || [[ -n "${LINE}" ]]; do + case "${LINE}" in + pd.enabled=*) + printf 'pd.enabled={{ $pdMode }}\n' >>"${TMP}" + FOUND_PD_ENABLED=true + ;; + {{- if $pdMode }} + pd.peers=*) + printf 'pd.peers=%s\n' "${HG_HUBBLE_PD_PEERS}" >>"${TMP}" + FOUND_PD_PEERS=true + ;; + pd.server=*) + printf 'pd.server=%s\n' "${HG_HUBBLE_PD_SERVER}" >>"${TMP}" + FOUND_PD_SERVER=true + ;; + operations.store.allowed_targets=*) + printf 'operations.store.allowed_targets=%s\n' \ + "${HG_HUBBLE_STORE_TARGETS}" >>"${TMP}" + FOUND_STORE_TARGETS=true + ;; + {{- else }} + server.direct_url=*) + printf 'server.direct_url=%s\n' "${HG_HUBBLE_SERVER_URL}" >>"${TMP}" + FOUND_DIRECT_URL=true + ;; + {{- end }} + server.host=*) + printf 'server.host=0.0.0.0\n' >>"${TMP}" + FOUND_SERVER_HOST=true + ;; + {{- if $customPort }} + hubble.port=*) + printf 'hubble.port=%s\n' {{ .Values.hubble.port | quote }} >>"${TMP}" + FOUND_HUBBLE_PORT=true + ;; + server.port=*) + printf 'server.port=%s\n' {{ .Values.hubble.port | quote }} >>"${TMP}" + FOUND_SERVER_PORT=true + ;; + {{- end }} + *) + printf '%s\n' "${LINE}" >>"${TMP}" + ;; + esac + done <"${CONF}" + if [[ "${FOUND_PD_ENABLED}" == false ]]; then + printf 'pd.enabled={{ $pdMode }}\n' >>"${TMP}" + fi + {{- if $pdMode }} + if [[ "${FOUND_PD_PEERS}" == false ]]; then + printf 'pd.peers=%s\n' "${HG_HUBBLE_PD_PEERS}" >>"${TMP}" + fi + if [[ "${FOUND_PD_SERVER}" == false ]]; then + printf 'pd.server=%s\n' "${HG_HUBBLE_PD_SERVER}" >>"${TMP}" + fi + if [[ "${FOUND_STORE_TARGETS}" == false ]]; then + printf 'operations.store.allowed_targets=%s\n' \ + "${HG_HUBBLE_STORE_TARGETS}" >>"${TMP}" + fi + {{- else }} + if [[ "${FOUND_DIRECT_URL}" == false ]]; then + printf 'server.direct_url=%s\n' "${HG_HUBBLE_SERVER_URL}" >>"${TMP}" + fi + {{- end }} + # Current Hubble binds server.host, which defaults to + # localhost; the shipped conf's hubble.host=0.0.0.0 line is + # legacy, is ignored by current images, and is preserved + # verbatim above for images that still read it. + if [[ "${FOUND_SERVER_HOST}" == false ]]; then + printf 'server.host=0.0.0.0\n' >>"${TMP}" + fi + {{- if $customPort }} + if [[ "${FOUND_HUBBLE_PORT}" == false ]]; then + printf 'hubble.port=%s\n' {{ .Values.hubble.port | quote }} >>"${TMP}" + fi + if [[ "${FOUND_SERVER_PORT}" == false ]]; then + printf 'server.port=%s\n' {{ .Values.hubble.port | quote }} >>"${TMP}" + fi + {{- end }} + chmod 600 "${TMP}" + mv "${TMP}" "${CONF}" + exec ./bin/start-hubble.sh -f + ports: + - name: http + containerPort: {{ .Values.hubble.port }} + env: + {{- if $pdMode }} + - name: HG_HUBBLE_PD_PEERS + value: {{ include "hugegraph.pd.grpcPeersList" . | quote }} + - name: HG_HUBBLE_PD_SERVER + value: {{ include "hugegraph.pd.restClientEndpoint" . | quote }} + - name: HG_HUBBLE_STORE_TARGETS + value: {{ include "hugegraph.store.restOriginsList" . | quote }} + {{- else }} + - name: HG_HUBBLE_SERVER_URL + value: {{ include "hugegraph.server.clientUrl" . | quote }} + {{- end }} + {{- if .Values.hubble.persistence.enabled }} + # Hubble's H2 location is fixed to ./db inside the image classpath + # config; Spring Boot's environment binding is the supported way to + # point it into the mounted volume. + - name: SPRING_DATASOURCE_URL + value: "jdbc:h2:file:/hubble-data/db;DB_CLOSE_ON_EXIT=FALSE" + {{- end }} + {{- with .Values.hubble.extraEnv }}{{ toYaml . | nindent 12 }}{{- end }} + {{- if .Values.hubble.persistence.enabled }} + volumeMounts: + - name: data + mountPath: /hubble-data + {{- end }} + startupProbe: + httpGet: + path: /actuator/health + port: http + failureThreshold: {{ .Values.hubble.probes.startup.failureThreshold }} + periodSeconds: {{ .Values.hubble.probes.startup.periodSeconds }} + {{- with include "hugegraph.probeTuning" .Values.hubble.probes.startup }}{{ . | trim | nindent 12 }}{{- end }} + readinessProbe: + httpGet: + path: /actuator/health + port: http + periodSeconds: {{ .Values.hubble.probes.readiness.periodSeconds }} + failureThreshold: {{ .Values.hubble.probes.readiness.failureThreshold }} + {{- with include "hugegraph.probeTuning" .Values.hubble.probes.readiness }}{{ . | trim | nindent 12 }}{{- end }} + # TCP for liveness: /actuator/health aggregates datasource health, + # and a transiently slow persistent volume must not get a healthy + # JVM killed (Recreate + RWO makes an overlapping restart worse). + livenessProbe: + tcpSocket: + port: http + periodSeconds: {{ .Values.hubble.probes.liveness.periodSeconds }} + failureThreshold: {{ .Values.hubble.probes.liveness.failureThreshold }} + {{- with include "hugegraph.probeTuning" .Values.hubble.probes.liveness }}{{ . | trim | nindent 12 }}{{- end }} + {{- with .Values.hubble.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if .Values.hubble.persistence.enabled }} + volumes: + - name: data + persistentVolumeClaim: + claimName: {{ include "hugegraph.hubble.dataName" . }} + {{- end }} +{{- end }} diff --git a/helm/hugegraph/templates/hubble-ingress.yaml b/helm/hugegraph/templates/hubble-ingress.yaml new file mode 100644 index 0000000000..845730619d --- /dev/null +++ b/helm/hugegraph/templates/hubble-ingress.yaml @@ -0,0 +1,54 @@ +# +# 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. +# + +{{- $hubble := get .Values "hubble" | default dict }} +{{- if and (get $hubble "enabled" | default false) .Values.hubble.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "hugegraph.hubble.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: hubble + {{- with (get .Values.hubble.ingress "annotations") }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.hubble.ingress.className }} + ingressClassName: {{ .Values.hubble.ingress.className | quote }} + {{- end }} + {{- with .Values.hubble.ingress.tls }} + tls: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + {{- range .Values.hubble.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path | quote }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "hugegraph.hubble.name" $ }} + port: + number: {{ $.Values.hubble.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/hugegraph/templates/hubble-pvc.yaml b/helm/hugegraph/templates/hubble-pvc.yaml new file mode 100644 index 0000000000..8143fab844 --- /dev/null +++ b/helm/hugegraph/templates/hubble-pvc.yaml @@ -0,0 +1,41 @@ +# +# 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. +# + +{{- $hubble := get .Values "hubble" | default dict }} +{{- if and (get $hubble "enabled" | default false) .Values.hubble.persistence.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "hugegraph.hubble.dataName" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: hubble + # Survive helm uninstall, matching the PD/Store volumeClaimTemplates + # behavior; delete the PVC explicitly to discard the stored connection + # metadata and credentials. + annotations: + helm.sh/resource-policy: keep +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.hubble.persistence.size }} + {{- if .Values.hubble.persistence.storageClassName }} + storageClassName: {{ .Values.hubble.persistence.storageClassName | quote }} + {{- end }} +{{- end }} diff --git a/helm/hugegraph/templates/hubble-service.yaml b/helm/hugegraph/templates/hubble-service.yaml new file mode 100644 index 0000000000..53eed8ec4a --- /dev/null +++ b/helm/hugegraph/templates/hubble-service.yaml @@ -0,0 +1,43 @@ +# +# 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. +# + +{{- if get (get .Values "hubble" | default dict) "enabled" | default false }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "hugegraph.hubble.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: hubble + {{- $svc := get .Values.hubble "service" | default dict }} + {{- with (get $svc "annotations") }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ get $svc "type" | default "ClusterIP" }} + selector: + {{- include "hugegraph.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: hubble + ports: + - name: http + port: {{ .Values.hubble.port }} + targetPort: http + {{- with (get $svc "nodePort") }} + nodePort: {{ . }} + {{- end }} +{{- end }} diff --git a/helm/hugegraph/templates/server-deployment.yaml b/helm/hugegraph/templates/server-deployment.yaml index 6445c80c75..2d9b1c333a 100644 --- a/helm/hugegraph/templates/server-deployment.yaml +++ b/helm/hugegraph/templates/server-deployment.yaml @@ -26,6 +26,16 @@ {{- $batchMaxWriteThreads = toString (get $restServer "batchMaxWriteThreads") }} {{- end }} {{- $customPort := ne (int .Values.server.port) 8080 }} +{{- $hubble := get .Values "hubble" | default dict }} +{{- $hubblePdMode := and (get $hubble "enabled" | default false) (ne (get $hubble "mode" | default "pd") "direct") }} +{{/* +PD meta mode (usePD) is required on two paths: the built-in authenticator +creates the admin on the PD startup path, and PD-mode Hubble discovers the +Server through the service URL the Server registers with PD. Both need the +same four properties, so they share one condition. +*/}} +{{- $pdMeta := or .Values.server.auth.enabled $hubblePdMode }} +{{- $wrapper := or $pdMeta $customPort (ne $minFreeMemory "") (ne $batchMaxWriteThreads "") }} apiVersion: apps/v1 kind: Deployment metadata: @@ -94,10 +104,7 @@ spec: securityContext: {{- toYaml . | nindent 12 }} {{- end }} - {{- if or .Values.server.auth.enabled - $customPort - (ne $minFreeMemory "") - (ne $batchMaxWriteThreads "") }} + {{- if $wrapper }} command: - /usr/bin/dumb-init - -- @@ -108,12 +115,34 @@ spec: set -euo pipefail {{- if .Values.server.auth.enabled }} : "${PASSWORD:?auth Secret key 'password' must not be empty}" + # The value is written into a Java properties file, whose parser + # treats CR as a line terminator, unescapes backslashes, and + # skips whitespace after the separator, so these shapes would + # inject config lines or silently store a password that differs + # from the Secret. + case "${PASSWORD}" in + *$'\n'* | *$'\r'* | *\\* | ' '* | $'\t'*) + echo "auth Secret key 'password' must not contain newlines," \ + "carriage returns, or backslashes, or start with" \ + "whitespace" >&2 + exit 1 + ;; + esac {{- end }} CONF=./conf/rest-server.properties + if [[ ! -r "${CONF}" ]]; then + echo "missing or unreadable ${CONF}; refusing to write a stub config" >&2 + exit 1 + fi TMP=$(mktemp) - {{- if .Values.server.auth.enabled }} + {{- if $pdMeta }} FOUND_USE_PD=false FOUND_PD_PEERS=false + FOUND_URLS_TO_PD=false + FOUND_DEPLOY_IN_K8S=false + {{- end }} + {{- if .Values.server.auth.enabled }} + FOUND_AUTH_ADMIN_PA=false {{- end }} {{- if $customPort }} FOUND_RESTSERVER_URL=false @@ -126,7 +155,7 @@ spec: {{- end }} while IFS= read -r LINE || [[ -n "${LINE}" ]]; do case "${LINE}" in - {{- if .Values.server.auth.enabled }} + {{- if $pdMeta }} usePD=*) printf 'usePD=true\n' >>"${TMP}" FOUND_USE_PD=true @@ -135,6 +164,20 @@ spec: printf 'pd.peers=%s\n' "${HG_SERVER_PD_PEERS}" >>"${TMP}" FOUND_PD_PEERS=true ;; + server.urls_to_pd=*) + printf 'server.urls_to_pd=%s\n' "${HG_SERVER_URLS_TO_PD}" >>"${TMP}" + FOUND_URLS_TO_PD=true + ;; + server.deploy_in_k8s=*) + printf 'server.deploy_in_k8s=true\n' >>"${TMP}" + FOUND_DEPLOY_IN_K8S=true + ;; + {{- end }} + {{- if .Values.server.auth.enabled }} + auth.admin_pa=*) + printf 'auth.admin_pa=%s\n' "${PASSWORD}" >>"${TMP}" + FOUND_AUTH_ADMIN_PA=true + ;; {{- end }} {{- if $customPort }} restserver.url=*) @@ -162,13 +205,28 @@ spec: ;; esac done <"${CONF}" - {{- if .Values.server.auth.enabled }} + {{- if $pdMeta }} if [[ "${FOUND_USE_PD}" == false ]]; then printf 'usePD=true\n' >>"${TMP}" fi if [[ "${FOUND_PD_PEERS}" == false ]]; then printf 'pd.peers=%s\n' "${HG_SERVER_PD_PEERS}" >>"${TMP}" fi + # PD hands this URL to discovery clients such as Hubble. The + # k8s branch is taken only when server.deploy_in_k8s is true; + # otherwise the announcement falls back to restserver.url, + # whose 0.0.0.0 is never resolvable from another Pod. + if [[ "${FOUND_URLS_TO_PD}" == false ]]; then + printf 'server.urls_to_pd=%s\n' "${HG_SERVER_URLS_TO_PD}" >>"${TMP}" + fi + if [[ "${FOUND_DEPLOY_IN_K8S}" == false ]]; then + printf 'server.deploy_in_k8s=true\n' >>"${TMP}" + fi + {{- end }} + {{- if .Values.server.auth.enabled }} + if [[ "${FOUND_AUTH_ADMIN_PA}" == false ]]; then + printf 'auth.admin_pa=%s\n' "${PASSWORD}" >>"${TMP}" + fi {{- end }} {{- if $customPort }} if [[ "${FOUND_RESTSERVER_URL}" == false ]]; then @@ -206,6 +264,10 @@ spec: value: {{ include "hugegraph.store.restPrimary" . | quote }} - name: HG_SERVER_INIT_STORE_ENABLED value: {{ .Values.server.initStoreEnabled | quote }} + {{- if $pdMeta }} + - name: HG_SERVER_URLS_TO_PD + value: {{ include "hugegraph.server.clientUrl" . | quote }} + {{- end }} {{- with .Values.server.extraEnv }}{{ toYaml . | nindent 12 }}{{- end }} {{- with include "hugegraph.javaOptsEnv" .Values.server.javaOpts }}{{ . | trim | nindent 12 }}{{- end }} {{- if and .Values.server.auth.enabled .Values.server.auth.existingSecret }} diff --git a/helm/hugegraph/templates/serviceaccount.yaml b/helm/hugegraph/templates/serviceaccount.yaml index 9889414d4b..0dbf4c302c 100644 --- a/helm/hugegraph/templates/serviceaccount.yaml +++ b/helm/hugegraph/templates/serviceaccount.yaml @@ -14,7 +14,11 @@ 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. */ -}} -{{- range $component := list "pd" "store" "server" }} +{{- $components := list "pd" "store" "server" }} +{{- if get (get .Values "hubble" | default dict) "enabled" | default false }} +{{- $components = append $components "hubble" }} +{{- end }} +{{- range $component := $components }} {{- $values := index $.Values $component }} {{- $sa := get $values "serviceAccount" | default dict }} {{- if and (get $sa "create" | default false) (not (get $sa "name")) }} diff --git a/helm/hugegraph/values-cluster.yaml b/helm/hugegraph/values-cluster.yaml index 91ae09a9c6..b3d37b2ced 100644 --- a/helm/hugegraph/values-cluster.yaml +++ b/helm/hugegraph/values-cluster.yaml @@ -84,3 +84,21 @@ server: memory: 2Gi hpa: enabled: false + +# The optional Hubble UI is not enabled here: this preset does not enable +# server authentication, and current Hubble images cannot complete their +# login against an auth-less cluster. Enable it together with server.auth, +# starting from: +# +# hubble: +# enabled: true +# persistence: +# enabled: true +# size: 1Gi +# resources: +# requests: +# cpu: 250m +# memory: 768Mi +# limits: +# cpu: "1" +# memory: 1536Mi diff --git a/helm/hugegraph/values.schema.json b/helm/hugegraph/values.schema.json index 56c1284bca..9ad8d96de7 100644 --- a/helm/hugegraph/values.schema.json +++ b/helm/hugegraph/values.schema.json @@ -31,6 +31,9 @@ }, "global": { "type": "object" + }, + "hubble": { + "$ref": "#/definitions/hubble" } }, "definitions": { @@ -567,72 +570,7 @@ ] }, "ingress": { - "type": "object", - "additionalProperties": false, - "required": [ - "enabled", - "className", - "hosts", - "tls" - ], - "properties": { - "enabled": { - "type": "boolean" - }, - "className": { - "type": "string" - }, - "hosts": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "required": [ - "host", - "paths" - ], - "properties": { - "host": { - "type": "string", - "minLength": 1 - }, - "paths": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "required": [ - "path", - "pathType" - ], - "properties": { - "path": { - "type": "string" - }, - "pathType": { - "type": "string", - "enum": [ - "Exact", - "Prefix", - "ImplementationSpecific" - ] - } - } - } - } - } - } - }, - "tls": { - "type": "array", - "items": { - "type": "object" - } - }, - "annotations": { - "type": "object" - } - } + "$ref": "#/definitions/ingress" }, "hpa": { "type": "object", @@ -731,29 +669,225 @@ "$ref": "#/definitions/resources" }, "service": { + "$ref": "#/definitions/service" + } + } + }, + "service": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "enum": [ + "ClusterIP", + "NodePort", + "LoadBalancer" + ] + }, + "annotations": { + "type": "object" + }, + "nodePort": { + "type": [ + "integer", + "null" + ], + "minimum": 30000, + "maximum": 32767 + } + } + }, + "ingress": { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled", + "className", + "hosts", + "tls" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "className": { + "type": "string" + }, + "hosts": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "host", + "paths" + ], + "properties": { + "host": { + "type": "string", + "minLength": 1 + }, + "paths": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "path", + "pathType" + ], + "properties": { + "path": { + "type": "string" + }, + "pathType": { + "type": "string", + "enum": [ + "Exact", + "Prefix", + "ImplementationSpecific" + ] + } + } + } + } + } + } + }, + "tls": { + "type": "array", + "items": { + "type": "object" + } + }, + "annotations": { + "type": "object" + }, + "allowPlainHttp": { + "type": "boolean" + } + } + }, + "hubble": { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled", + "mode", + "image", + "port", + "persistence", + "resources", + "service", + "ingress", + "probes" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "mode": { + "type": "string", + "enum": [ + "pd", + "direct" + ] + }, + "allowWithoutServerAuth": { + "type": "boolean" + }, + "image": { + "$ref": "#/definitions/image" + }, + "port": { + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "persistence": { "type": "object", "additionalProperties": false, + "required": [ + "enabled", + "size", + "storageClassName" + ], "properties": { - "type": { + "enabled": { + "type": "boolean" + }, + "size": { "type": "string", - "enum": [ - "ClusterIP", - "NodePort", - "LoadBalancer" - ] + "minLength": 1 + }, + "storageClassName": { + "type": "string" + } + } + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "podSecurityContext": { + "type": "object" + }, + "securityContext": { + "type": "object" + }, + "nodeSelector": { + "type": "object" + }, + "tolerations": { + "type": "array" + }, + "affinity": { + "type": "object" + }, + "topologySpreadConstraints": { + "type": "array" + }, + "priorityClassName": { + "type": "string" + }, + "podAnnotations": { + "type": "object" + }, + "podLabels": { + "type": "object" + }, + "extraEnv": { + "type": "array" + }, + "terminationGracePeriodSeconds": { + "type": "integer", + "minimum": 0 + }, + "serviceAccount": { + "type": "object", + "properties": { + "create": { + "type": "boolean" + }, + "name": { + "type": "string" }, "annotations": { "type": "object" }, - "nodePort": { - "type": [ - "integer", - "null" - ], - "minimum": 30000, - "maximum": 32767 + "automountServiceAccountToken": { + "type": "boolean" } } + }, + "service": { + "$ref": "#/definitions/service" + }, + "ingress": { + "$ref": "#/definitions/ingress" + }, + "probes": { + "$ref": "#/definitions/probes" } } } diff --git a/helm/hugegraph/values.yaml b/helm/hugegraph/values.yaml index 83bb1f3c74..88142c23dd 100644 --- a/helm/hugegraph/values.yaml +++ b/helm/hugegraph/values.yaml @@ -219,7 +219,9 @@ server: initStoreEnabled: false auth: enabled: false - # Secret must contain key "password" + # Secret must contain key "password" (no newlines). Applied as + # auth.admin_pa when the admin account is first created; changing the + # Secret later does not rotate an existing cluster's password. existingSecret: "" service: type: ClusterIP @@ -254,5 +256,91 @@ server: failureThreshold: 3 timeoutSeconds: 5 +# Optional Hubble UI. Serves plain HTTP; keep it on a ClusterIP Service or +# behind an HTTPS-terminating Ingress, never directly on an untrusted network. +hubble: + enabled: false + # pd: discover the cluster through PD (enables the cluster operations view). + # direct: talk to the Server client Service only, without PD discovery. + # Current Hubble images require server.auth to be enabled: the UI login + # authenticates against the cluster, so rendering fails otherwise unless + # allowWithoutServerAuth explicitly overrides for images that support it. + mode: pd + allowWithoutServerAuth: false + image: + repository: hugegraph/hubble + # The draft tracks latest until the next HugeGraph release tag is available. + # Pin the release tag and switch to IfNotPresent before stable publication. + tag: latest + pullPolicy: Always + port: 8088 + # Hubble keeps UI connection metadata, including any graph credentials + # entered in the UI, in an embedded per-instance H2 database, so the + # Deployment is fixed at a single replica (pointing SPRING_DATASOURCE_URL + # at an external database via extraEnv is not a supported configuration). + # Without persistence that metadata is lost on Pod replacement; graph data + # is unaffected. size and storageClassName apply at install time only, and + # the PVC is kept on helm uninstall. + persistence: + enabled: false + size: 1Gi + storageClassName: "" + resources: {} + # When persistence is enabled and the pod runs as non-root, set a matching + # fsGroup here so H2 can write /hubble-data. + podSecurityContext: {} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + seccompProfile: + type: RuntimeDefault + nodeSelector: {} + tolerations: [] + affinity: {} + topologySpreadConstraints: [] + priorityClassName: "" + podAnnotations: {} + podLabels: {} + # Extra environment variables appended to the hubble container. + extraEnv: [] + terminationGracePeriodSeconds: 30 + serviceAccount: + create: true + name: "" + annotations: {} + # This chart makes no Kubernetes API calls, so no token is mounted. + automountServiceAccountToken: false + service: + type: ClusterIP + annotations: {} + ingress: + enabled: false + className: "" + annotations: {} + hosts: + - host: hubble.local + paths: + - path: / + pathType: Prefix + tls: [] + # Hubble serves plain HTTP, so an Ingress without tls is rejected at + # render time unless this is explicitly set to true for a trusted + # network. + allowPlainHttp: false + probes: + startup: + failureThreshold: 30 + periodSeconds: 5 + timeoutSeconds: 5 + readiness: + periodSeconds: 10 + failureThreshold: 3 + timeoutSeconds: 5 + liveness: + periodSeconds: 20 + failureThreshold: 3 + timeoutSeconds: 5 + # No init Job; see README.md for the HStore initialization contract. # Install with: helm install ... --wait (no --wait-for-jobs) From ad52e70bc222a8b4d2ab416e4fde7c248269046f Mon Sep 17 00:00:00 2001 From: Himanshu Verma Date: Mon, 3 Aug 2026 17:20:57 +0530 Subject: [PATCH 6/8] fix(helm): enforce the PD PDB quorum floor and reject chart-managed extraEnv overrides A PD PodDisruptionBudget with minAvailable below floor(replicas/2)+1 permits voluntary evictions that leave PD without a Raft majority, so the render now requires the quorum floor in addition to the existing blocks-all-drains upper bound. With 2 replicas no valid budget exists; the README documents the even/odd contract. extraEnv entries render after the chart-owned variables and Kubernetes lets the last duplicate win, so a duplicate name could silently override a validated contract such as HG_SERVER_INIT_STORE_ENABLED=false. Each component's extraEnv now rejects its chart-managed variable names. Boundary and negative render cases for both rules are part of the CI invalid-value step. --- .github/workflows/helm-chart-ci.yml | 14 ++++++++++++++ helm/hugegraph/README.md | 8 ++++++++ helm/hugegraph/templates/_helpers.tpl | 22 ++++++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/.github/workflows/helm-chart-ci.yml b/.github/workflows/helm-chart-ci.yml index b052c86bd7..a632a9de69 100644 --- a/.github/workflows/helm-chart-ci.yml +++ b/.github/workflows/helm-chart-ci.yml @@ -99,6 +99,20 @@ jobs: must_fail $A --set hubble.ingress.enabled=true must_fail --set server.ingress.enabled=true \ --set server.ingress.allowPlainHttp=true + # PD PDB must keep the Raft majority: floor(replicas/2)+1 + must_fail --set pd.replicas=5 --set pd.pdb.minAvailable=2 + must_fail --set pd.replicas=4 --set pd.pdb.minAvailable=2 + helm template ci helm/hugegraph \ + --set pd.replicas=5 --set pd.pdb.minAvailable=3 > /dev/null + helm template ci helm/hugegraph \ + --set pd.replicas=5 --set pd.pdb.minAvailable=4 > /dev/null + # extraEnv must not override chart-managed variables + must_fail --set 'server.extraEnv[0].name=HG_SERVER_INIT_STORE_ENABLED' \ + --set 'server.extraEnv[0].value=true' + must_fail --set 'pd.extraEnv[0].name=HG_PD_RAFT_PEERS_LIST' \ + --set 'pd.extraEnv[0].value=x' + must_fail --set 'store.extraEnv[0].name=HG_STORE_PD_ADDRESS' \ + --set 'store.extraEnv[0].value=x' - name: kubeconform shell: bash diff --git a/helm/hugegraph/README.md b/helm/hugegraph/README.md index c2c78693fe..323be460ed 100644 --- a/helm/hugegraph/README.md +++ b/helm/hugegraph/README.md @@ -367,6 +367,14 @@ before anything reaches the cluster: `server.resources.requests.cpu`. - `pdb.minAvailable` must be less than the matching `replicas`, so a PodDisruptionBudget cannot permanently block node drains. +- `pd.pdb.minAvailable` must also be at least the PD Raft majority, + `floor(replicas/2)+1`, so the budget cannot permit evictions that drop PD + below quorum. With 2 PD replicas no valid budget exists (the majority is + the whole membership); disable the PD PDB or use an odd replica count. +- `extraEnv` must not set chart-managed variable names (for example + `HG_SERVER_INIT_STORE_ENABLED` or the PD/Store identity and topology + variables): entries render after the chart-owned variables and the last + duplicate wins, so an override would silently bypass a validated contract. - `pd.replicas` and `store.replicas` are capped at 99. - `hubble.port` must be a valid port, `hubble.persistence.size` must be non-empty, and `hubble.service.nodePort` requires a `NodePort` or diff --git a/helm/hugegraph/templates/_helpers.tpl b/helm/hugegraph/templates/_helpers.tpl index c32d261fcd..5b077bf3b3 100644 --- a/helm/hugegraph/templates/_helpers.tpl +++ b/helm/hugegraph/templates/_helpers.tpl @@ -289,6 +289,9 @@ and must not be failed for a value that has no effect. {{- if and .Values.pd.pdb.enabled (gt (int .Values.pd.replicas) 1) (ge (int .Values.pd.pdb.minAvailable) (int .Values.pd.replicas)) -}} {{- fail "pd.pdb.minAvailable must be less than pd.replicas, otherwise the PDB permanently blocks voluntary disruptions such as node drains" -}} {{- end -}} +{{- if and .Values.pd.pdb.enabled (gt (int .Values.pd.replicas) 1) (lt (int .Values.pd.pdb.minAvailable) (include "hugegraph.pd.quorum" . | int)) -}} +{{- fail "pd.pdb.minAvailable must be at least the PD Raft majority, floor(replicas/2)+1, otherwise the budget permits voluntary evictions that drop PD below quorum" -}} +{{- end -}} {{- if and .Values.store.pdb.enabled (gt (int .Values.store.replicas) 1) (ge (int .Values.store.pdb.minAvailable) (int .Values.store.replicas)) -}} {{- fail "store.pdb.minAvailable must be less than store.replicas, otherwise the PDB permanently blocks voluntary disruptions such as node drains" -}} {{- end -}} @@ -305,6 +308,25 @@ and must not be failed for a value that has no effect. {{- if hasKey $serverIngress "allowPlainHttp" -}} {{- fail "server.ingress.allowPlainHttp has no effect; the plain-HTTP opt-in applies to hubble.ingress only" -}} {{- end -}} +{{/* +extraEnv entries render after the chart-owned variables and Kubernetes lets +the last duplicate win, so a duplicate name would silently override a +validated contract (for example re-enabling init-store across Server +replicas). Reserved names are rejected instead. +*/}} +{{- $reservedEnv := dict + "pd" (list "HG_PD_GRPC_HOST" "HG_PD_GRPC_PORT" "HG_PD_REST_PORT" "HG_PD_RAFT_ADDRESS" "HG_PD_RAFT_PEERS_LIST" "HG_PD_INITIAL_STORE_LIST" "HG_PD_INITIAL_STORE_COUNT" "HG_PD_DATA_PATH" "JAVA_OPTS") + "store" (list "HG_STORE_PD_ADDRESS" "HG_STORE_GRPC_HOST" "HG_STORE_GRPC_PORT" "HG_STORE_REST_PORT" "HG_STORE_RAFT_ADDRESS" "HG_STORE_DATA_PATH" "JAVA_OPTS") + "server" (list "HG_SERVER_BACKEND" "HG_SERVER_PD_PEERS" "HG_SERVER_PD_REST_ENDPOINT" "STORE_REST" "HG_SERVER_INIT_STORE_ENABLED" "HG_SERVER_URLS_TO_PD" "PASSWORD" "JAVA_OPTS") + "hubble" (list "HG_HUBBLE_PD_PEERS" "HG_HUBBLE_PD_SERVER" "HG_HUBBLE_STORE_TARGETS" "HG_HUBBLE_SERVER_URL" "SPRING_DATASOURCE_URL") -}} +{{- range $component, $reserved := $reservedEnv -}} +{{- $componentValues := get $.Values $component | default dict -}} +{{- range $entry := get $componentValues "extraEnv" | default list -}} +{{- if has (get $entry "name") $reserved -}} +{{- fail (printf "%s.extraEnv must not set the chart-managed variable %s" $component (get $entry "name")) -}} +{{- end -}} +{{- end -}} +{{- end -}} {{- $hubble := get .Values "hubble" | default dict -}} {{- if get $hubble "enabled" | default false -}} {{- if and (not .Values.server.auth.enabled) (not (get $hubble "allowWithoutServerAuth" | default false)) -}} From f58de3fffcba431af86472052e986db095a4a58e Mon Sep 17 00:00:00 2001 From: Himanshu Verma Date: Mon, 3 Aug 2026 22:40:33 +0530 Subject: [PATCH 7/8] ci(helm): fix workflow startup failure The run failed at startup because azure/setup-helm@v4 is not on the ASF-approved actions allowlist; install the pinned helm release from the official tarball in a plain run step instead. --- .github/workflows/helm-chart-ci.yml | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/.github/workflows/helm-chart-ci.yml b/.github/workflows/helm-chart-ci.yml index a632a9de69..20105e104c 100644 --- a/.github/workflows/helm-chart-ci.yml +++ b/.github/workflows/helm-chart-ci.yml @@ -35,9 +35,13 @@ jobs: with: persist-credentials: false - - uses: azure/setup-helm@v4 - with: - version: v3.16.2 + # azure/setup-helm is not on the ASF-approved actions allowlist, which + # fails the workflow at startup; install the pinned release directly. + - name: install helm + run: | + curl -fsSL https://get.helm.sh/helm-v3.16.2-linux-amd64.tar.gz | tar -xz -C /tmp + sudo install -m 0755 /tmp/linux-amd64/helm /usr/local/bin/helm + helm version - name: helm lint run: | @@ -48,6 +52,7 @@ jobs: - name: helm template run: | for preset in "" "-f helm/hugegraph/values-single.yaml" "-f helm/hugegraph/values-cluster.yaml"; do + # shellcheck disable=SC2086 # $preset intentionally splits into flags helm template ci helm/hugegraph $preset > /dev/null done # Positive coverage for every hubble wrapper branch: pd mode with @@ -88,15 +93,15 @@ jobs: --set server.pdb.minAvailable=2 must_fail --set server.auth.enabled=true must_fail --set hubble.enabled=true - A="--set hubble.enabled=true --set hubble.allowWithoutServerAuth=true" - must_fail $A --set hubble.port=0 - must_fail $A \ + A=(--set hubble.enabled=true --set hubble.allowWithoutServerAuth=true) + must_fail "${A[@]}" --set hubble.port=0 + must_fail "${A[@]}" \ --set hubble.persistence.enabled=true \ --set hubble.persistence.size="" - must_fail $A --set hubble.service.nodePort=30080 - must_fail $A --set hubble.mode=bogus - must_fail $A --set hubble.image.tag="" - must_fail $A --set hubble.ingress.enabled=true + must_fail "${A[@]}" --set hubble.service.nodePort=30080 + must_fail "${A[@]}" --set hubble.mode=bogus + must_fail "${A[@]}" --set hubble.image.tag="" + must_fail "${A[@]}" --set hubble.ingress.enabled=true must_fail --set server.ingress.enabled=true \ --set server.ingress.allowPlainHttp=true # PD PDB must keep the Raft majority: floor(replicas/2)+1 @@ -121,6 +126,7 @@ jobs: curl -sSLo /tmp/kc.tar.gz https://github.com/yannh/kubeconform/releases/download/v0.6.7/kubeconform-linux-amd64.tar.gz tar -xzf /tmp/kc.tar.gz -C /tmp for preset in "" "-f helm/hugegraph/values-single.yaml" "-f helm/hugegraph/values-cluster.yaml"; do + # shellcheck disable=SC2086 # $preset intentionally splits into flags helm template ci helm/hugegraph $preset | /tmp/kubeconform -strict -summary -kubernetes-version 1.23.0 done helm template ci helm/hugegraph \ From 0ebd2dc09bd23e3f32e2e6ee2f5f0e297354bedf Mon Sep 17 00:00:00 2001 From: Himanshu Verma Date: Tue, 4 Aug 2026 01:08:39 +0530 Subject: [PATCH 8/8] feat(helm): default partition sharding to store-level HA and soften scheduling defaults Fresh installs now seed PD with partition.default-shard-count derived from the Store count (3 when store.replicas is at least 3, otherwise 1, matching PD's odd-only constraint and its 2-to-1 clamp), so a default 3-Store deployment gets store-level HA instead of the image default of one shard replica per partition. The seed is delivered as -D system properties prepended into the PD JAVA_OPTS ahead of pd.javaOpts, preserving the start script's automatic heap sizing; it applies at first bootstrap only, after which PD metadata is authoritative, all documented together with the resulting initial partition count change. pd.antiAffinity and store.antiAffinity default to preferred so the chart schedules on clusters with fewer nodes than replicas; values-cluster.yaml keeps required for both, NOTES warns when PD quorum members may co-locate, and the README documents the upgrade implications. The Disaster Recovery documentation describes what current PD builds actually do: the scheduled patrol only marks silent stores Offline, shard reconciliation and tombstone processing run only via the manual /v1/task/patrolPartitions endpoint, and the pd.patrol-interval and store.max-down-time properties are bound but never read, which is why the chart does not expose them. Periodic leader balancing and recovery metrics are referenced as upstream feature requests. extraEnv now also rejects JAVA_OPTIONS for pd, store, and server, because the start scripts drop the chart-managed JAVA_OPTS entirely when it is set. Schema accepts numeric strings for the new keys, values-file integers at or above one million no longer fail as scientific notation, and CI asserts the rendered -D content, covers both shard-count validation messages, and kubeconforms the sharded render. --- .github/workflows/helm-chart-ci.yml | 29 +++ helm/hugegraph/README.md | 181 ++++++++++++++++++- helm/hugegraph/templates/NOTES.txt | 8 + helm/hugegraph/templates/_helpers.tpl | 115 +++++++++++- helm/hugegraph/templates/pd-statefulset.yaml | 2 +- helm/hugegraph/values-cluster.yaml | 2 + helm/hugegraph/values.schema.json | 24 +++ helm/hugegraph/values.yaml | 37 +++- 8 files changed, 380 insertions(+), 18 deletions(-) diff --git a/.github/workflows/helm-chart-ci.yml b/.github/workflows/helm-chart-ci.yml index 20105e104c..c4d8794abe 100644 --- a/.github/workflows/helm-chart-ci.yml +++ b/.github/workflows/helm-chart-ci.yml @@ -70,6 +70,19 @@ jobs: --set hubble.persistence.enabled=true \ --set hubble.ingress.enabled=true \ --set hubble.ingress.allowPlainHttp=true > /dev/null + # PD sharding knobs rendered as -D system properties, including + # the values-file shape where the count arrives as a numeric string + helm template ci helm/hugegraph \ + --set pd.partition.defaultShardCount=3 \ + --set pd.partition.storeMaxShardCount=12 > /dev/null + helm template ci helm/hugegraph \ + --set-string pd.partition.defaultShardCount=3 > /dev/null + # The derived shard count must land in the PD JAVA_OPTS verbatim: + # 3 on the default topology, 1 on the single-node preset + helm template ci helm/hugegraph \ + | grep -qF 'value: "-Dpartition.default-shard-count=3"' + helm template ci helm/hugegraph -f helm/hugegraph/values-single.yaml \ + | grep -qF 'value: "-Dpartition.default-shard-count=1"' - name: reject invalid values run: | @@ -111,6 +124,14 @@ jobs: --set pd.replicas=5 --set pd.pdb.minAvailable=3 > /dev/null helm template ci helm/hugegraph \ --set pd.replicas=5 --set pd.pdb.minAvailable=4 > /dev/null + # PD sharding knobs: empty or a positive integer; an explicit + # shard count must be odd and stay within the store count + must_fail --set pd.partition.defaultShardCount=0 + must_fail --set pd.partition.defaultShardCount=-1 + must_fail --set-string pd.partition.defaultShardCount=abc + must_fail --set pd.partition.defaultShardCount=2 + must_fail --set pd.partition.defaultShardCount=5 + must_fail --set pd.partition.storeMaxShardCount=0 # extraEnv must not override chart-managed variables must_fail --set 'server.extraEnv[0].name=HG_SERVER_INIT_STORE_ENABLED' \ --set 'server.extraEnv[0].value=true' @@ -118,6 +139,10 @@ jobs: --set 'pd.extraEnv[0].value=x' must_fail --set 'store.extraEnv[0].name=HG_STORE_PD_ADDRESS' \ --set 'store.extraEnv[0].value=x' + # JAVA_OPTIONS is reserved: a preset value makes the start scripts + # skip auto heap sizing and drop the chart's JAVA_OPTS entirely + must_fail --set 'pd.extraEnv[0].name=JAVA_OPTIONS' \ + --set 'pd.extraEnv[0].value=-Xmx1g' - name: kubeconform shell: bash @@ -143,6 +168,10 @@ jobs: --set server.auth.enabled=true \ --set server.auth.existingSecret=ci-auth \ | /tmp/kubeconform -strict -summary -kubernetes-version 1.23.0 + helm template ci helm/hugegraph \ + --set pd.partition.defaultShardCount=3 \ + --set pd.partition.storeMaxShardCount=12 \ + | /tmp/kubeconform -strict -summary -kubernetes-version 1.23.0 - name: legacy --reuse-values compatibility run: | diff --git a/helm/hugegraph/README.md b/helm/hugegraph/README.md index 323be460ed..16884212a6 100644 --- a/helm/hugegraph/README.md +++ b/helm/hugegraph/README.md @@ -70,6 +70,19 @@ This deploys 3 PD + 3 Store + 3 Server, preserves the image's automatic JVM sizing, and sets no resource requests or limits. Set resources before production use. +The default anti-affinity for `pd`, `store`, and `server` is `preferred` +(Server always was; Hubble has no anti-affinity knob because it is +single-replica by design), so the chart schedules even on clusters with +fewer nodes than replicas. Production should pin `pd.antiAffinity` and +`store.antiAffinity` to `required`, as `values-cluster.yaml` does, so one +node failure cannot take out the PD quorum or co-locate shard replicas; see +Scheduling below. + +A fresh install seeds PD with a partition shard count of 3 when +`store.replicas` is at least 3, and 1 otherwise, instead of the image +default of 1. The seed applies at first bootstrap only; see Partition +Sharding below. + This first chart is version `0.1.0`. While the contribution is a draft, its component image tags and `appVersion` track `latest` with pull policy `Always`. Before stable publication, pin all three component tags and `appVersion` to the @@ -86,9 +99,9 @@ helm test hugegraph --namespace hugegraph | File | Purpose | |---|---| -| `values.yaml` | Default 3+3+3 topology | +| `values.yaml` | Default 3+3+3 topology with preferred anti-affinity, so it schedules on clusters of any node count | | `values-single.yaml` | Single-node 1+1+1 example | -| `values-cluster.yaml` | Production 3+3+3 starting point with JVM/resources, PD/Store PDBs, and required anti-affinity; Hubble stays opt-in because the preset does not enable authentication | +| `values-cluster.yaml` | Production 3+3+3 starting point with JVM/resources, PD/Store PDBs, and required anti-affinity for PD and Store; Hubble stays opt-in because the preset does not enable authentication | `values-cluster.yaml` is a production starting point, not a capacity guarantee. Recalculate capacity for the graph size, traffic, failure budget, @@ -103,10 +116,27 @@ helm upgrade hugegraph ./helm/hugegraph --namespace hugegraph --reuse-values Every optional field stays optional, so a release created by an earlier revision continues to render under `--reuse-values`. Note that `--reuse-values` keeps the old release's values as the complete base, so a release created -before a field existed does **not** pick up its new default — including the +before a field existed does **not** pick up its new default, including the hardened `securityContext`, ServiceAccounts, and `terminationGracePeriodSeconds`. -Pod-level token mounting is the one exception: it is disabled unconditionally. Use `-f` with your own values, or `--reset-then-reuse-values`, to adopt them. +That rule covers values-sourced defaults only; the asymmetry is that +template-derived settings **are** applied even under `--reuse-values`, +because they are computed at render time from whatever values are in effect. +Pod-level token mounting (disabled unconditionally) and the derived +`-Dpartition.default-shard-count` in the PD `JAVA_OPTS` are the current +cases. On an already-initialized cluster the seeded shard count is inert +either way; see Partition Sharding. + +Upgrading an existing release to this chart version rolls the PD StatefulSet +once: PD Pods now always carry a `JAVA_OPTS` environment variable with the +chart-derived partition properties, where previous versions set the variable +only when `pd.javaOpts` was non-empty. + +The `pd.antiAffinity` and `store.antiAffinity` defaults changed from +`required` to `preferred` in this version. `--reuse-values` keeps the old +effective value, but installs that relied on the old `required` default +while supplying their own values files must now pin `antiAffinity: required` +explicitly. PD and Store resource names reserve room for their StatefulSet ordinal before truncation, so identities stay fixed across replica changes and scaling never renames a @@ -142,7 +172,9 @@ default values. | `pd.image.repository` | PD image repository | `hugegraph/pd` | | `pd.image.tag` | PD image tag. Tracks the development image until the next release is pinned | `latest` | | `pd.image.pullPolicy` | PD image pull policy | `Always` | -| `pd.javaOpts` | Empty preserves the image's automatic JVM sizing | `""` | +| `pd.javaOpts` | Extra JVM flags, rendered after the chart-derived `-D` properties below so an explicit duplicate here wins. The image's automatic heap sizing is preserved unless heap flags are set | `""` | +| `pd.partition.defaultShardCount` | Shard replicas per partition, seeded into PD's persisted config at first bootstrap only; inert on an initialized cluster (see Partition Sharding). Empty derives 3 when `store.replicas` is at least 3, else 1. An explicit value must be odd and must not exceed `store.replicas` | `""` | +| `pd.partition.storeMaxShardCount` | Maximum shards per Store, seeded at first bootstrap only. Also fixes the initial partition count, `store.replicas x storeMaxShardCount / shardCount` (see Partition Sharding). Empty preserves the image default of `12` | `""` | | `pd.ports.grpc` | PD gRPC port | `8686` | | `pd.ports.rest` | PD REST port, also used by probes | `8620` | | `pd.ports.raft` | PD Raft port | `8610` | @@ -152,7 +184,7 @@ default values. | `pd.resources` | PD container resources. Set these for production | `{}` | | `pd.podSecurityContext` | Pod-level securityContext, rendered only when set | `{}` | | `pd.securityContext` | Container-level securityContext. Hardened by default; `runAsNonRoot` is not set because the published images run as root | `allowPrivilegeEscalation: false`, `capabilities.drop: [ALL]`, `seccompProfile: RuntimeDefault` | -| `pd.antiAffinity` | One of `required`, `preferred`, `disabled` | `required` | +| `pd.antiAffinity` | One of `required`, `preferred`, `disabled`. `preferred` schedules on clusters with fewer nodes than replicas; production should use `required` so one node failure cannot take out the PD quorum | `preferred` | | `pd.nodeSelector` | Node selector for pd Pods | `{}` | | `pd.tolerations` | Tolerations for pd Pods | `[]` | | `pd.affinity` | Raw affinity; overrides `pd.antiAffinity` when set | `{}` | @@ -193,7 +225,7 @@ default values. | `store.podSecurityContext` | Pod-level securityContext, rendered only when set | `{}` | | `store.securityContext` | Container-level securityContext; also applied to the PD-quorum init container. Hardened by default; `runAsNonRoot` is not set because the published images run as root | `allowPrivilegeEscalation: false`, `capabilities.drop: [ALL]`, `seccompProfile: RuntimeDefault` | | `store.waitTimeoutSeconds` | Bound on the PD-quorum wait before the init container fails | `900` | -| `store.antiAffinity` | One of `required`, `preferred`, `disabled` | `required` | +| `store.antiAffinity` | One of `required`, `preferred`, `disabled`. `preferred` schedules on clusters with fewer nodes than replicas; production should use `required` so one node failure cannot co-locate shard replicas | `preferred` | | `store.nodeSelector` | Node selector for store Pods | `{}` | | `store.tolerations` | Tolerations for store Pods | `[]` | | `store.affinity` | Raw affinity; overrides `store.antiAffinity` when set | `{}` | @@ -375,7 +407,15 @@ before anything reaches the cluster: `HG_SERVER_INIT_STORE_ENABLED` or the PD/Store identity and topology variables): entries render after the chart-owned variables and the last duplicate wins, so an override would silently bypass a validated contract. + `JAVA_OPTS` and `JAVA_OPTIONS` are reserved for the same reason: the + component start scripts skip automatic heap sizing and drop the chart's + `JAVA_OPTS` flags entirely when `JAVA_OPTIONS` arrives preset. - `pd.replicas` and `store.replicas` are capped at 99. +- `pd.partition.defaultShardCount` and `pd.partition.storeMaxShardCount` + must each be empty or a positive integer. An explicit shard count must + also be odd (PD's config API rejects even values, and PD clamps 2 to 1) + and must not exceed `store.replicas`, past which PD would silently clamp + it to the live store count. - `hubble.port` must be a valid port, `hubble.persistence.size` must be non-empty, and `hubble.service.nodePort` requires a `NodePort` or `LoadBalancer` Service type. @@ -414,6 +454,107 @@ the listener, container port, and Service together. --- +### Scheduling + +Every component (`pd`, `store`, `server`, `hubble`) exposes the full set of +scheduling controls: `nodeSelector`, `tolerations`, `affinity`, +`topologySpreadConstraints`, and `priorityClassName`. For example, pinning +Store to labeled nodes is just: + +```yaml +store: + nodeSelector: + hugegraph/role: storage +``` + +`antiAffinity` (`required` | `preferred` | `disabled`) renders a hostname +pod-anti-affinity preset for `pd`, `store`, and `server`; Hubble has no +`antiAffinity` key because it is single-replica by design. Setting a raw +`affinity` replaces the preset entirely. All three default to `preferred` +(Server always did; the pd and store defaults changed from `required`), so +the chart schedules on clusters with fewer nodes than replicas (including +single-node development clusters). The trade: `preferred` lets the +scheduler co-locate replicas under node pressure, so a single node failure +can then take more than one PD or Store replica with it. Production +clusters with enough nodes should pin `pd.antiAffinity` and +`store.antiAffinity` to `required`, as `values-cluster.yaml` does. + +### Partition Sharding + +A fresh install seeds PD's persisted configuration with a partition shard +count of 3 when `store.replicas` is at least 3, and 1 otherwise. Without +this the PD image's `conf/application.yml` would pin +`partition.default-shard-count` to 1, leaving chart-deployed clusters +without store-level HA. The derivation never produces 2 because PD clamps a +shard count of 2 to 1: two shards cannot elect a leader. + +The chart renders the setting as `-Dpartition.default-shard-count` in the PD +container's `JAVA_OPTS`; system properties outrank the shipped config file, +and the PD start script appends `JAVA_OPTS` after its automatically computed +heap flags, so the image's JVM auto-sizing is unaffected. + +**The seed applies at first bootstrap only.** PD persists the shard count +into its own metadata the first time it starts with empty storage, and from +then on the stored value is authoritative: every PD leader change re-reads +it from storage, overwriting whatever the `-D` flag says. Changing +`pd.partition.defaultShardCount` later, or scaling `store.replicas` across +the derivation boundary, therefore has **no** effect on an initialized +cluster. Nor is the value frozen at partition creation: PD reconciles +existing shard groups toward the stored value whenever a partition patrol +runs. To change the shard count of a running cluster, use PD's own config +API (which accepts only odd values not exceeding the live store count) and +then trigger `GET /v1/task/patrolPartitions`; expect shard-group +reallocation when the counts differ. + +The shard count also fixes the initial partition count: +`store.replicas x storeMaxShardCount / shardCount`, computed once at +bootstrap. With the image's `store-max-shard-count` default of 12, the +derived shard count moves a default 3-store install from 36 partitions +(shard count 1) to 12 (shard count 3). Set +`pd.partition.storeMaxShardCount` higher to compensate when more partitions +are wanted; it is likewise seeded at first bootstrap only. + +An explicit `pd.partition.defaultShardCount` must be odd and at most +`store.replicas`. The chart rejects other values at render time: PD would +silently clamp a value above the live store count, clamp 2 to 1, and reject +even values at its config API, so an accepted render would not mean an +honored setting. + +### Disaster Recovery + +What PD automates on current builds is narrow. A scheduled patrol runs on a +hardcoded 60-second cadence and only marks Stores that stopped sending +heartbeats as `Offline`; it does not touch partitions. There is **no +automatic re-replication**: re-placing the replicas of a lost Store, +reconciling shard groups against the stored shard count, and processing +tombstoned Stores all run only when a partition patrol is triggered +explicitly. PD's configuration binds `pd.patrol-interval` and +`store.max-down-time` keys, but no code path on current builds reads +either, which is why this chart does not expose them. + +Recovery and rebalancing are operator-triggered. PD exposes REST triggers, +reachable through the PD client Service: + +```bash +kubectl port-forward -n hugegraph svc/hugegraph-pd-client 8620:8620 +curl http://127.0.0.1:8620/v1/task/patrolPartitions # reconcile shard groups, process tombstoned Stores +curl http://127.0.0.1:8620/v1/task/balanceLeaders # spread Raft leaders +curl http://127.0.0.1:8620/v1/task/balancePartitions # spread partition data +``` + +Run `patrolPartitions` after replacing a Store that is not coming back, +`balancePartitions` once the cluster is stable again, and `balanceLeaders` +after restarts that skewed leader placement. + +Periodic balancing and shard-sync progress metrics do not exist upstream +yet and are out of scope for this chart. Periodic leader balancing is +tracked in +[apache/hugegraph#3135](https://github.com/apache/hugegraph/issues/3135); +disaster-recovery metrics are tracked in +[apache/hugegraph#3136](https://github.com/apache/hugegraph/issues/3136). + +--- + ### Scaling PD and Store reserve the maximum StatefulSet ordinal in their resource names, @@ -458,6 +599,25 @@ graph is fully able to serve index-backed queries. Confirm the graph is live: kubectl exec -c server -- curl -s localhost:8080/graphs ``` +### Queries Fail with "Could not rebind" Right After Creating a Graph + +Creating a graph returns before every Server replica has opened and bound it. +The creating Server writes the graph to PD metadata and registers its own +Gremlin binding; the other replicas converge independently through a PD watch +plus a local graph open. Until they do, a Gremlin query routed through the +load-balanced Service to a not-yet-converged replica fails with a 400 error +such as `Could not rebind [g]`. This is upstream behavior, not a chart +setting. Mitigations: + +- Retry with backoff in the client; the window normally closes in seconds. +- Use sticky routing (or `kubectl port-forward` to one Pod) for + create-then-verify flows, so follow-up queries hit the creating replica. +- Poll `/graphs` on each replica until the new graph appears everywhere + before opening query traffic. + +The underlying fix, orchestrating graph creation through PD, is upstream +work. + ### Pods OOM Killed or Restarting The default `values.yaml` sets **no** resource requests or limits and preserves @@ -489,7 +649,12 @@ independently of the release name. ## Limitations - No TLS, backups, Operator, multi-cluster support, automatic leader transfer, - or a complete monitoring stack. + or a complete monitoring stack. Store recovery is manual on current builds: + re-replication after Store loss, leader balancing, and partition + rebalancing run only when triggered (see Disaster Recovery); periodic + balancing and shard-sync metrics are upstream feature work. +- Newly created graphs are visible on all Server replicas only after a short + propagation window (see Troubleshooting: "Could not rebind"). - The published images run as root, so `runAsNonRoot` and `readOnlyRootFilesystem` are not chart defaults. The container `securityContext` does default to `allowPrivilegeEscalation: false`, diff --git a/helm/hugegraph/templates/NOTES.txt b/helm/hugegraph/templates/NOTES.txt index 72b8f54cf9..cd555837e4 100644 --- a/helm/hugegraph/templates/NOTES.txt +++ b/helm/hugegraph/templates/NOTES.txt @@ -70,6 +70,14 @@ Hubble Pod is replaced. Graph data is unaffected. Authentication is disabled. Do not expose this release to untrusted networks. {{- end }} +{{- if and (gt (int .Values.pd.replicas) 1) (ne (get .Values.pd "antiAffinity" | default "") "required") (empty (get .Values.pd "affinity")) }} + +pd.antiAffinity is not "required", so the scheduler may co-locate PD quorum +members on one node and a single node loss can take down the PD quorum. The +PD PodDisruptionBudget only limits voluntary disruption (drains, evictions), +not node failure. On clusters with at least {{ .Values.pd.replicas }} nodes, +set pd.antiAffinity=required; see values-cluster.yaml. +{{- end }} {{- if or (empty .Values.pd.resources) (empty .Values.store.resources) (empty .Values.server.resources) }} One or more components have no resource requests or limits. Set them before diff --git a/helm/hugegraph/templates/_helpers.tpl b/helm/hugegraph/templates/_helpers.tpl index 5b077bf3b3..a7993b7e9b 100644 --- a/helm/hugegraph/templates/_helpers.tpl +++ b/helm/hugegraph/templates/_helpers.tpl @@ -203,6 +203,80 @@ image entrypoint's existing automatic JVM sizing behavior. {{- end -}} {{- end }} +{{/* +String form of a possibly-absent scalar value, preserving zero. sprig's +`default` treats 0 as unset, which would let a zero slip past the named +validation below, so absence is detected explicitly instead. Numbers from a +values file arrive as float64, whose toString switches to scientific +notation at 1e6 or higher (1000000 becomes "1e+06"), so integral float64 +values are formatted without an exponent. Non-integral floats keep their +raw form on purpose: the schema already rejects them, and the raw form +fails the named validation instead of being silently rounded. +*/}} +{{- define "hugegraph.optionalScalar" -}} +{{- if not (kindIs "invalid" .) -}} +{{- if and (kindIs "float64" .) (eq (floor .) .) -}}{{- printf "%.0f" . -}}{{- else -}}{{- trim (toString .) -}}{{- end -}} +{{- end -}} +{{- end }} + +{{/* +Effective PD JAVA_OPTS: chart-derived -D system properties, then pd.javaOpts. + +The -D route is the grounded override mechanism: the PD image's +docker-entrypoint.sh forwards JAVA_OPTS via `-j` into +bin/start-hugegraph-pd.sh, which places it on the java command line ahead of +-Dspring.config.location, and Spring system properties outrank the shipped +conf/application.yml (which pins partition.default-shard-count to 1). + +The shard count is always derived, so PD Pods always carry a JAVA_OPTS +variable, which shadows the PD image's `ENV JAVA_OPTS` default +(-XX:MaxRAMPercentage=50, -XX:+UseContainerSupport, -XshowSettings:vm). +That is acceptable: the start script always computes explicit -Xms/-Xmx +heap flags when the separate JAVA_OPTIONS variable is unset, which makes +MaxRAMPercentage moot, and only the -XshowSettings:vm startup diagnostics +are lost. + +JVM auto-sizing is preserved, verified against the PD dist start script: +`-j` lands in USER_OPTION, while the automatic heap sizing branch is gated on +the separate JAVA_OPTIONS variable and appends USER_OPTION after the computed +-Xms/-Xmx flags. A JAVA_OPTS holding only -D flags therefore still gets +automatic heap sizing, and heap flags in pd.javaOpts land later on the +command line, so they win. The derived -D flags come first for the same +reason: an explicit duplicate in pd.javaOpts overrides them. + +Both -D properties seed PD's persisted config at first bootstrap only: +ConfigService.loadConfig persists them when no stored config exists, and +every leader change re-reads the stored values (updatePDConfig), so on an +initialized cluster the flags are inert and the authoritative values live +in PD metadata, changeable only through PD's own config API. PD reconciles +existing shard groups toward the stored value when a partition patrol is +triggered (TaskScheduleService reallocShards). The empty-value derivation +is 3 when store.replicas is at least 3, else 1, because PD clamps a shard +count of 2 to 1 (two shards cannot elect a leader) and its config API +accepts only odd values. store-max-shard-count is rendered only when set, +keeping the image default. All lookups tolerate absent keys so releases +stored before these values existed keep rendering under --reuse-values. +*/}} +{{- define "hugegraph.pd.effectiveJavaOpts" -}} +{{- $pd := .Values.pd -}} +{{- $partition := get $pd "partition" | default dict -}} +{{- $flags := list -}} +{{- $shardCount := include "hugegraph.optionalScalar" (get $partition "defaultShardCount") -}} +{{- if eq $shardCount "" -}} +{{- $shardCount = ternary "3" "1" (ge (int .Values.store.replicas) 3) -}} +{{- end -}} +{{- $flags = append $flags (printf "-Dpartition.default-shard-count=%s" $shardCount) -}} +{{- $maxShard := include "hugegraph.optionalScalar" (get $partition "storeMaxShardCount") -}} +{{- if ne $maxShard "" -}} +{{- $flags = append $flags (printf "-Dpartition.store-max-shard-count=%s" $maxShard) -}} +{{- end -}} +{{- $userOpts := trim (get $pd "javaOpts" | default "") -}} +{{- if ne $userOpts "" -}} +{{- $flags = append $flags $userOpts -}} +{{- end -}} +{{- join " " $flags -}} +{{- end }} + {{/* Keep the startup probe alive for the 300-second storage wait, the Server's 120-second start timeout, and 30 seconds of process overhead. Older stored @@ -290,11 +364,38 @@ and must not be failed for a value that has no effect. {{- fail "pd.pdb.minAvailable must be less than pd.replicas, otherwise the PDB permanently blocks voluntary disruptions such as node drains" -}} {{- end -}} {{- if and .Values.pd.pdb.enabled (gt (int .Values.pd.replicas) 1) (lt (int .Values.pd.pdb.minAvailable) (include "hugegraph.pd.quorum" . | int)) -}} -{{- fail "pd.pdb.minAvailable must be at least the PD Raft majority, floor(replicas/2)+1, otherwise the budget permits voluntary evictions that drop PD below quorum" -}} +{{- fail "pd.pdb.minAvailable must be at least the PD Raft majority, floor(replicas/2)+1, otherwise the budget permits voluntary disruptions that drop PD below quorum. Note a PDB only limits voluntary disruption such as drains and evictions; it cannot protect quorum from node failure" -}} {{- end -}} {{- if and .Values.store.pdb.enabled (gt (int .Values.store.replicas) 1) (ge (int .Values.store.pdb.minAvailable) (int .Values.store.replicas)) -}} {{- fail "store.pdb.minAvailable must be less than store.replicas, otherwise the PDB permanently blocks voluntary disruptions such as node drains" -}} {{- end -}} +{{/* +PD -D system properties must be empty or a positive integer. The schema +enforces the types; these checks add a named failure for zero, negative, and +nonsense values, and check an explicit shard count against PD's real +constraints: PD's config API accepts only odd shard counts, PD clamps a +count of 2 to 1 (two shards cannot elect a leader), and PD clamps the +effective count to the number of live stores. All lookups tolerate absent +keys for releases stored before the values existed. +*/}} +{{- $pdPartition := get .Values.pd "partition" | default dict -}} +{{- $pdProps := dict + "pd.partition.defaultShardCount" (include "hugegraph.optionalScalar" (get $pdPartition "defaultShardCount")) + "pd.partition.storeMaxShardCount" (include "hugegraph.optionalScalar" (get $pdPartition "storeMaxShardCount")) -}} +{{- range $label, $raw := $pdProps -}} +{{- if and (ne $raw "") (or (not (regexMatch "^[0-9]+$" $raw)) (eq (int $raw) 0)) -}} +{{- fail (printf "%s must be empty or a positive integer" $label) -}} +{{- end -}} +{{- end -}} +{{- $explicitShards := include "hugegraph.optionalScalar" (get $pdPartition "defaultShardCount") -}} +{{- if regexMatch "^[1-9][0-9]*$" $explicitShards -}} +{{- if eq (mod (int $explicitShards) 2) 0 -}} +{{- fail "pd.partition.defaultShardCount must be odd: PD's config API rejects even shard counts, and PD clamps a bootstrap value of 2 to 1 because two shards cannot elect a leader" -}} +{{- end -}} +{{- if gt (int $explicitShards) (int .Values.store.replicas) -}} +{{- fail "pd.partition.defaultShardCount is greater than store.replicas: PD would clamp the effective shard count to the number of live stores, so the extra replicas would silently never be placed. Raise store.replicas or lower the shard count" -}} +{{- end -}} +{{- end -}} {{- $svc := get .Values.server "service" | default dict -}} {{- if and (get $svc "nodePort") (not (has (get $svc "type" | default "ClusterIP") (list "NodePort" "LoadBalancer"))) -}} {{- fail "server.service.nodePort requires server.service.type to be NodePort or LoadBalancer" -}} @@ -312,12 +413,16 @@ and must not be failed for a value that has no effect. extraEnv entries render after the chart-owned variables and Kubernetes lets the last duplicate win, so a duplicate name would silently override a validated contract (for example re-enabling init-store across Server -replicas). Reserved names are rejected instead. +replicas). Reserved names are rejected instead. JAVA_OPTIONS is reserved +for pd, store, and server because each component's start script skips its +automatic heap sizing and drops the chart's JAVA_OPTS flags entirely when +JAVA_OPTIONS arrives preset in the environment (verified in +start-hugegraph-pd.sh, start-hugegraph-store.sh, and hugegraph-server.sh). */}} {{- $reservedEnv := dict - "pd" (list "HG_PD_GRPC_HOST" "HG_PD_GRPC_PORT" "HG_PD_REST_PORT" "HG_PD_RAFT_ADDRESS" "HG_PD_RAFT_PEERS_LIST" "HG_PD_INITIAL_STORE_LIST" "HG_PD_INITIAL_STORE_COUNT" "HG_PD_DATA_PATH" "JAVA_OPTS") - "store" (list "HG_STORE_PD_ADDRESS" "HG_STORE_GRPC_HOST" "HG_STORE_GRPC_PORT" "HG_STORE_REST_PORT" "HG_STORE_RAFT_ADDRESS" "HG_STORE_DATA_PATH" "JAVA_OPTS") - "server" (list "HG_SERVER_BACKEND" "HG_SERVER_PD_PEERS" "HG_SERVER_PD_REST_ENDPOINT" "STORE_REST" "HG_SERVER_INIT_STORE_ENABLED" "HG_SERVER_URLS_TO_PD" "PASSWORD" "JAVA_OPTS") + "pd" (list "HG_PD_GRPC_HOST" "HG_PD_GRPC_PORT" "HG_PD_REST_PORT" "HG_PD_RAFT_ADDRESS" "HG_PD_RAFT_PEERS_LIST" "HG_PD_INITIAL_STORE_LIST" "HG_PD_INITIAL_STORE_COUNT" "HG_PD_DATA_PATH" "JAVA_OPTS" "JAVA_OPTIONS") + "store" (list "HG_STORE_PD_ADDRESS" "HG_STORE_GRPC_HOST" "HG_STORE_GRPC_PORT" "HG_STORE_REST_PORT" "HG_STORE_RAFT_ADDRESS" "HG_STORE_DATA_PATH" "JAVA_OPTS" "JAVA_OPTIONS") + "server" (list "HG_SERVER_BACKEND" "HG_SERVER_PD_PEERS" "HG_SERVER_PD_REST_ENDPOINT" "STORE_REST" "HG_SERVER_INIT_STORE_ENABLED" "HG_SERVER_URLS_TO_PD" "PASSWORD" "JAVA_OPTS" "JAVA_OPTIONS") "hubble" (list "HG_HUBBLE_PD_PEERS" "HG_HUBBLE_PD_SERVER" "HG_HUBBLE_STORE_TARGETS" "HG_HUBBLE_SERVER_URL" "SPRING_DATASOURCE_URL") -}} {{- range $component, $reserved := $reservedEnv -}} {{- $componentValues := get $.Values $component | default dict -}} diff --git a/helm/hugegraph/templates/pd-statefulset.yaml b/helm/hugegraph/templates/pd-statefulset.yaml index 01ef705006..e16ff53b2d 100644 --- a/helm/hugegraph/templates/pd-statefulset.yaml +++ b/helm/hugegraph/templates/pd-statefulset.yaml @@ -116,7 +116,7 @@ spec: - name: HG_PD_DATA_PATH value: {{ .Values.pd.dataPath | quote }} {{- with .Values.pd.extraEnv }}{{ toYaml . | nindent 12 }}{{- end }} - {{- with include "hugegraph.javaOptsEnv" .Values.pd.javaOpts }}{{ . | trim | nindent 12 }}{{- end }} + {{- with include "hugegraph.javaOptsEnv" (include "hugegraph.pd.effectiveJavaOpts" .) }}{{ . | trim | nindent 12 }}{{- end }} volumeMounts: - name: pd-data mountPath: {{ .Values.pd.dataPath }} diff --git a/helm/hugegraph/values-cluster.yaml b/helm/hugegraph/values-cluster.yaml index b3d37b2ced..98202e5f16 100644 --- a/helm/hugegraph/values-cluster.yaml +++ b/helm/hugegraph/values-cluster.yaml @@ -33,6 +33,7 @@ pd: limits: cpu: "2" memory: 2Gi + # Pinned to "required": production must not co-locate PD quorum members. antiAffinity: required pdb: enabled: true @@ -61,6 +62,7 @@ store: limits: cpu: 250m memory: 64Mi + # Pinned to "required": production must not co-locate shard replicas. antiAffinity: required pdb: enabled: true diff --git a/helm/hugegraph/values.schema.json b/helm/hugegraph/values.schema.json index 9ad8d96de7..fa99e12a41 100644 --- a/helm/hugegraph/values.schema.json +++ b/helm/hugegraph/values.schema.json @@ -37,6 +37,18 @@ } }, "definitions": { + "optionalPositiveInteger": { + "oneOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "type": "string", + "pattern": "^([1-9][0-9]*)?$" + } + ] + }, "image": { "type": "object", "additionalProperties": false, @@ -240,6 +252,18 @@ "javaOpts": { "type": "string" }, + "partition": { + "type": "object", + "additionalProperties": false, + "properties": { + "defaultShardCount": { + "$ref": "#/definitions/optionalPositiveInteger" + }, + "storeMaxShardCount": { + "$ref": "#/definitions/optionalPositiveInteger" + } + } + }, "ports": { "$ref": "#/definitions/ports" }, diff --git a/helm/hugegraph/values.yaml b/helm/hugegraph/values.yaml index 88142c23dd..b13a190e50 100644 --- a/helm/hugegraph/values.yaml +++ b/helm/hugegraph/values.yaml @@ -31,8 +31,32 @@ pd: # Pin the release tag and switch to IfNotPresent before stable publication. tag: latest pullPolicy: Always - # Empty preserves the image entrypoint's automatic JVM sizing. + # Empty preserves the image entrypoint's automatic JVM sizing. The chart + # renders its partition settings below as -D system properties ahead of + # this value, so flags placed here win on conflict. javaOpts: "" + # Partition sharding, rendered as -D system properties that outrank the + # image's conf/application.yml. PD copies both values into its own + # persisted metadata at first bootstrap only; from then on the stored + # values are authoritative, so changing these later (or scaling + # store.replicas across the derivation boundary) has no effect on an + # initialized cluster. Post-bootstrap changes go through PD's config API + # and take effect on existing shard groups only when a partition patrol + # is triggered; see the README's Partition Sharding section. + partition: + # Shard replicas per partition, seeding PD at first bootstrap only. + # Empty derives 3 when store.replicas is at least 3, else 1, so a fresh + # multi-store install gets store-level HA instead of the image default + # of 1. The derivation skips 2 because PD clamps a shard count of 2 to + # 1 (two shards cannot elect a leader). An explicit value must be odd + # and must not exceed store.replicas. + defaultShardCount: "" + # Maximum shards per store, seeding PD at first bootstrap only. Also + # fixes the initial partition count: store.replicas x this value / + # shard count. The derived shard count of 3 gives the default topology + # 12 partitions instead of 36; raise this value to compensate when more + # partitions are wanted. Empty preserves the image default of 12. + storeMaxShardCount: "" ports: grpc: 8686 rest: 8620 @@ -51,8 +75,10 @@ pd: drop: ["ALL"] seccompProfile: type: RuntimeDefault - # required | preferred | disabled - antiAffinity: required + # required | preferred | disabled. "preferred" schedules on clusters with + # fewer nodes than replicas; production should use "required" (see + # values-cluster.yaml) so one node failure cannot take out PD quorum. + antiAffinity: preferred # Scheduling. `affinity` takes precedence over the antiAffinity preset. nodeSelector: {} tolerations: [] @@ -115,7 +141,10 @@ store: drop: ["ALL"] seccompProfile: type: RuntimeDefault - antiAffinity: required + # required | preferred | disabled. "preferred" schedules on clusters with + # fewer nodes than replicas; production should use "required" (see + # values-cluster.yaml) so one node failure cannot co-locate shard replicas. + antiAffinity: preferred # Scheduling. `affinity` takes precedence over the antiAffinity preset. nodeSelector: {} tolerations: []