You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@impala.apache.org by st...@apache.org on 2026/07/22 03:43:26 UTC

(impala) branch master updated (779a8771b -> 01ca03c31)

This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/impala.git


    from 779a8771b IMPALA-14783: Add time travel support for SHOW FILES on Iceberg tables
     new d4b0c117e IMPALA-14981: Add operator reconciler packaging
     new 01ca03c31 IMPALA-14982: Add Impala-on-K8s deployment guide

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 bin/rat_exclude_files.txt                          |   1 +
 helm/impala/README.md                              | 271 ++++++++++++++++++
 operator/impala-operator/Dockerfile                |  49 ++++
 operator/impala-operator/main.py                   | 316 +++++++++++++++++++++
 operator/impala-operator/manifests/deployment.yaml |   2 +-
 .../impala-operator/requirements.txt               |   4 +-
 6 files changed, 640 insertions(+), 3 deletions(-)
 create mode 100644 helm/impala/README.md
 create mode 100644 operator/impala-operator/Dockerfile
 create mode 100644 operator/impala-operator/main.py
 copy testdata/cluster/ranger/java_home.sh => operator/impala-operator/requirements.txt (96%)
 mode change 100755 => 100644


(impala) 01/02: IMPALA-14981: Add operator reconciler packaging

Posted by st...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/impala.git

commit d4b0c117e6b18b88d071ae5164bc5d39c92925d8
Author: Anubhav Jindal <an...@gmail.com>
AuthorDate: Tue May 26 14:41:16 2026 -0700

    IMPALA-14981: Add operator reconciler packaging
    
    Add the operator reconciliation implementation and packaging assets needed
    to run chart-backed Impala and optional LDAP reconciliation.
    
    Handle LDAP disable transitions declaratively by uninstalling an existing
    LDAP release when ldapEnabled is set to false.
    
    Add ASF license headers to the new operator source and packaging files.
    
    Testing:
    - python3 -m py_compile operator/impala-operator/main.py
    - kubectl config current-context (k3d-impala-live)
    - kubectl apply -f operator/impala-operator/manifests/crd-impalacluster.yaml
    - kubectl apply -f operator/impala-operator/manifests/rbac.yaml
    - kubectl create namespace impala-14981-live
    - /tmp/impala-op-venv/bin/python reconcile driver invoking _ensure_namespace, _ensure_ldap, and _ensure_impala in operator/impala-operator/main.py with spec.set persistence.accessModes[0]=ReadWriteOnce
    - kubectl rollout status deployment/impala-14981-live-impala-{statestored,catalogd,impalad,hms} -n impala-14981-live
    - helm -n impala-14981-live status impala-14981-live (STATUS: deployed)
    - kubectl get pods -n impala-14981-live (all core pods Running/Ready)
    - helm create /tmp/ldap-dummy-14981 && helm upgrade --install impala-ldap-14981-live /tmp/ldap-dummy-14981 -n impala-14981-live
    - /tmp/impala-op-venv/bin/python reconcile driver invoking _ensure_ldap with ldapEnabled=false and ldapReleaseName=impala-ldap-14981-live
    - helm -n impala-14981-live status impala-ldap-14981-live (fails after operator path; release removed)
    
    Change-Id: I9b66e32b00724221ead2f4975802a26b2be7e248
    Assisted-by: GPT-5.3 (Cursor)
    Co-authored-by: Cursor <cu...@cursor.com>
    Reviewed-on: http://gerrit.cloudera.org:8080/24363
    Reviewed-by: Jason Fehr <jf...@cloudera.com>
    Tested-by: Jason Fehr <jf...@cloudera.com>
---
 operator/impala-operator/Dockerfile                |  49 ++++
 operator/impala-operator/main.py                   | 316 +++++++++++++++++++++
 operator/impala-operator/manifests/deployment.yaml |   2 +-
 operator/impala-operator/requirements.txt          |  19 ++
 4 files changed, 385 insertions(+), 1 deletion(-)

diff --git a/operator/impala-operator/Dockerfile b/operator/impala-operator/Dockerfile
new file mode 100644
index 000000000..7aed0d778
--- /dev/null
+++ b/operator/impala-operator/Dockerfile
@@ -0,0 +1,49 @@
+# 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.
+
+FROM python:3.14.6-slim
+
+ENV PYTHONDONTWRITEBYTECODE=1
+ENV PYTHONUNBUFFERED=1
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+    ca-certificates \
+    curl \
+    tar \
+    && rm -rf /var/lib/apt/lists/*
+
+RUN HELM_VERSION="v3.16.2" && \
+    curl -fsSL "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz" -o /tmp/helm.tgz && \
+    tar -xzf /tmp/helm.tgz -C /tmp && \
+    mv /tmp/linux-amd64/helm /usr/local/bin/helm && \
+    chmod +x /usr/local/bin/helm && \
+    rm -rf /tmp/helm.tgz /tmp/linux-amd64
+
+WORKDIR /app
+
+ENV DEFAULT_IMPALA_CHART_PATH=/charts/impala
+ENV DEFAULT_IMPALA_VALUES_FILE=${DEFAULT_IMPALA_CHART_PATH}/values-example.yaml
+ENV DEFAULT_LDAP_VALUES_FILE=${DEFAULT_IMPALA_CHART_PATH}/values-ldap-example.yaml
+ENV DEFAULT_HELM_TIMEOUT_SECONDS=900
+
+COPY operator/impala-operator/requirements.txt /app/requirements.txt
+RUN pip install --no-cache-dir -r /app/requirements.txt
+
+COPY operator/impala-operator/main.py /app/main.py
+COPY helm/impala ${DEFAULT_IMPALA_CHART_PATH}
+
+CMD ["kopf", "run", "--verbose", "/app/main.py"]
diff --git a/operator/impala-operator/main.py b/operator/impala-operator/main.py
new file mode 100644
index 000000000..396c6635d
--- /dev/null
+++ b/operator/impala-operator/main.py
@@ -0,0 +1,316 @@
+# 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.
+
+import datetime
+import os
+import subprocess
+from typing import Dict, List
+
+import kopf
+from kubernetes import client, config
+from kubernetes.client.exceptions import ApiException
+
+
+GROUP = "impala.apache.org"
+VERSION = "v1alpha1"
+PLURAL = "impalaclusters"
+
+DEFAULT_IMPALA_RELEASE = "impala"
+DEFAULT_LDAP_RELEASE = "impala-ldap"
+DEFAULT_IMPALA_CHART_PATH = os.getenv("DEFAULT_IMPALA_CHART_PATH", "/charts/impala")
+DEFAULT_IMPALA_VALUES_FILE = os.getenv(
+    "DEFAULT_IMPALA_VALUES_FILE", f"{DEFAULT_IMPALA_CHART_PATH}/values-example.yaml"
+)
+DEFAULT_LDAP_VALUES_FILE = os.getenv(
+    "DEFAULT_LDAP_VALUES_FILE", f"{DEFAULT_IMPALA_CHART_PATH}/values-ldap-example.yaml"
+)
+DEFAULT_HELM_TIMEOUT_SECONDS = int(os.getenv("DEFAULT_HELM_TIMEOUT_SECONDS", "900"))
+_SENSITIVE_SET_KEY_PARTS = (
+    "password",
+    "passwd",
+    "secret",
+    "token",
+    "apikey",
+    "privatekey",
+    "keytab",
+    "credential",
+)
+
+
+def _load_kube() -> None:
+    try:
+        config.load_incluster_config()
+    except config.ConfigException:
+        config.load_kube_config()
+
+
+def _is_sensitive_set_key(key: str) -> bool:
+    lowered = key.lower().replace("-", "").replace("_", "")
+    return any(
+        part.replace("-", "").replace("_", "") in lowered
+        for part in _SENSITIVE_SET_KEY_PARTS
+    )
+
+
+def _sanitize_helm_set_arg(arg: str) -> str:
+    if "=" not in arg:
+        return arg
+    key, value = arg.split("=", 1)
+    if _is_sensitive_set_key(key):
+        return f"{key}=<redacted>"
+    return f"{key}={value}"
+
+
+def _sanitize_cmd(cmd: List[str]) -> str:
+    safe: List[str] = []
+    idx = 0
+    while idx < len(cmd):
+        token = cmd[idx]
+        if token in ("--set", "--set-string") and idx + 1 < len(cmd):
+            safe.append(token)
+            safe.append(_sanitize_helm_set_arg(cmd[idx + 1]))
+            idx += 2
+        else:
+            safe.append(token)
+            idx += 1
+    return " ".join(safe)
+
+
+def _run(cmd: List[str], logger, ignore_error: bool = False) -> str:
+    safe_cmd = _sanitize_cmd(cmd)
+    logger.info("Running: %s", safe_cmd)
+    proc = subprocess.run(cmd, capture_output=True, text=True, check=False)
+    stdout = (proc.stdout or "").strip()
+    stderr = (proc.stderr or "").strip()
+    if proc.returncode != 0 and not ignore_error:
+        logger.error("Command failed: %s", safe_cmd)
+        if stdout:
+            logger.error("stdout: %s", stdout)
+        if stderr:
+            logger.error("stderr: %s", stderr)
+        raise RuntimeError(f"command failed ({proc.returncode}): {safe_cmd}")
+    if stdout:
+        logger.info("stdout: %s", stdout)
+    if stderr:
+        logger.info("stderr: %s", stderr)
+    return stdout
+
+
+def _ensure_namespace(name: str) -> None:
+    api = client.CoreV1Api()
+    try:
+        api.read_namespace(name=name)
+    except ApiException as exc:
+        if exc.status != 404:
+            raise
+        api.create_namespace(client.V1Namespace(metadata=client.V1ObjectMeta(name=name)))
+
+
+def _bool_string(value: bool) -> str:
+    return "true" if value else "false"
+
+
+def _helm_set_value(value: str) -> str:
+    # Helm parses --set values as CSV-like entries; escape commas to keep one value.
+    return value.replace("\\", "\\\\").replace(",", "\\,")
+
+
+def _set_args(spec: Dict) -> List[str]:
+    ldap_enabled = spec.get("ldapEnabled", False)
+    ldap_uri = spec.get("ldapUri", "ldaps://impala-ldap-openldap:636")
+    ldap_bind_pattern = spec.get("ldapBindPattern", "cn=#UID,dc=example,dc=org")
+
+    values = {
+        "kudu.enabled": _bool_string(spec.get("kuduEnabled", False)),
+        "ranger.enabled": _bool_string(spec.get("rangerEnabled", False)),
+        "auth.ranger.enabled": _bool_string(spec.get("rangerAuthEnabled", False)),
+        "auth.ldap.enabled": _bool_string(ldap_enabled),
+    }
+    storage_class_name = spec.get("storageClassName")
+    if storage_class_name:
+        values["persistence.storageClassName"] = storage_class_name
+        values["kudu.master.persistence.storageClassName"] = storage_class_name
+        values["kudu.tserver.persistence.storageClassName"] = storage_class_name
+
+    for key, val in (spec.get("set") or {}).items():
+        values[key] = str(val)
+
+    args = []
+    for key, val in values.items():
+        args.extend(["--set", f"{key}={_helm_set_value(str(val))}"])
+    if ldap_enabled:
+        args.extend(["--set-string", f"auth.ldap.uri={_helm_set_value(ldap_uri)}"])
+        bind_pattern_value = _helm_set_value(ldap_bind_pattern)
+        args.extend(
+            ["--set-string", f"auth.ldap.bindPattern={bind_pattern_value}"]
+        )
+    return args
+
+
+def _helm_timeout(spec: Dict) -> str:
+    timeout = int(spec.get("helmTimeoutSeconds", DEFAULT_HELM_TIMEOUT_SECONDS))
+    return f"{timeout}s"
+
+
+def _release_exists(release: str, namespace: str) -> bool:
+    proc = subprocess.run(
+        ["helm", "status", release, "-n", namespace],
+        capture_output=True,
+        text=True,
+        check=False,
+    )
+    return proc.returncode == 0
+
+
+def _ensure_ldap(spec: Dict, target_ns: str, logger) -> None:
+    ldap_release = spec.get("ldapReleaseName", DEFAULT_LDAP_RELEASE)
+    ldap_enabled = spec.get("ldapEnabled", False)
+    if not ldap_enabled:
+        if _release_exists(ldap_release, target_ns):
+            logger.info(
+                "ldapEnabled=false; uninstalling existing LDAP release %s "
+                "in namespace %s",
+                ldap_release,
+                target_ns,
+            )
+            _delete_release(ldap_release, target_ns, logger)
+        return
+
+    ldap_values = spec.get("ldapValuesFile", DEFAULT_LDAP_VALUES_FILE)
+    timeout = _helm_timeout(spec)
+
+    _run(
+        ["helm", "repo", "add", "openldap", "https://jp-gouin.github.io/helm-openldap/"],
+        logger,
+        ignore_error=True,
+    )
+    _run(["helm", "repo", "update"], logger)
+
+    cmd = [
+        "helm",
+        "upgrade" if _release_exists(ldap_release, target_ns) else "install",
+        ldap_release,
+        "openldap/openldap",
+        "-n",
+        target_ns,
+        "-f",
+        ldap_values,
+        "--wait",
+        "--timeout",
+        timeout,
+    ]
+    _run(cmd, logger)
+
+
+def _ensure_impala(spec: Dict, target_ns: str, logger) -> None:
+    release = spec.get("impalaReleaseName", DEFAULT_IMPALA_RELEASE)
+    chart_path = spec.get("impalaChartPath", DEFAULT_IMPALA_CHART_PATH)
+    values_file = spec.get("impalaValuesFile", DEFAULT_IMPALA_VALUES_FILE)
+    timeout = _helm_timeout(spec)
+
+    cmd = [
+        "helm",
+        "upgrade" if _release_exists(release, target_ns) else "install",
+        release,
+        chart_path,
+        "-n",
+        target_ns,
+        "-f",
+        values_file,
+        "--wait",
+        "--timeout",
+        timeout,
+    ]
+    cmd.extend(_set_args(spec))
+    _run(cmd, logger)
+
+
+def _delete_release(release: str, namespace: str, logger) -> None:
+    cmd = ["helm", "uninstall", release, "-n", namespace]
+    logger.info("Running: %s", _sanitize_cmd(cmd))
+    proc = subprocess.run(cmd, capture_output=True, text=True, check=False)
+    stdout = (proc.stdout or "").strip()
+    stderr = (proc.stderr or "").strip()
+    if proc.returncode == 0:
+        if stdout:
+            logger.info("stdout: %s", stdout)
+        if stderr:
+            logger.info("stderr: %s", stderr)
+        return
+    # Treat "release not found" as idempotent success.
+    not_found_error = "release: not found"
+    if not_found_error in stdout.lower() or not_found_error in stderr.lower():
+        logger.info("Release %s already absent.", release)
+        return
+    if stdout:
+        logger.error("stdout: %s", stdout)
+    if stderr:
+        logger.error("stderr: %s", stderr)
+    raise RuntimeError(
+        f"helm uninstall failed ({proc.returncode}) for release {release} in {namespace}"
+    )
+
+
+@kopf.on.startup()
+def on_startup(settings: kopf.OperatorSettings, **_):
+    _load_kube()
+    settings.persistence.finalizer = "impala.apache.org/finalizer"
+
+
+@kopf.on.create(GROUP, VERSION, PLURAL)
+@kopf.on.update(GROUP, VERSION, PLURAL)
+def reconcile(spec, name, namespace, patch, logger, meta, **_):
+    if spec.get("namespace") and spec.get("namespace") != namespace:
+        logger.warning(
+            "Ignoring spec.namespace=%s and reconciling in metadata.namespace=%s",
+            spec.get("namespace"),
+            namespace,
+        )
+    target_ns = namespace
+    _ensure_namespace(target_ns)
+
+    try:
+        _ensure_ldap(spec, target_ns, logger)
+        _ensure_impala(spec, target_ns, logger)
+    except Exception as exc:
+        patch.status["phase"] = "Failed"
+        patch.status["message"] = str(exc)
+        patch.status["lastReconcileTime"] = datetime.datetime.utcnow().isoformat() + "Z"
+        raise
+
+    patch.status["phase"] = "Ready"
+    patch.status["message"] = "Impala releases reconciled successfully."
+    patch.status["targetNamespace"] = target_ns
+    patch.status["observedGeneration"] = meta.get("generation")
+    patch.status["lastReconcileTime"] = datetime.datetime.utcnow().isoformat() + "Z"
+    logger.info("Reconciled ImpalaCluster %s in namespace %s", name, target_ns)
+
+
+@kopf.on.delete(GROUP, VERSION, PLURAL)
+def delete(spec, namespace, logger, **_):
+    if spec.get("namespace") and spec.get("namespace") != namespace:
+        logger.warning(
+            "Ignoring spec.namespace=%s and deleting releases in metadata.namespace=%s",
+            spec.get("namespace"),
+            namespace,
+        )
+    target_ns = namespace
+    impala_release = spec.get("impalaReleaseName", DEFAULT_IMPALA_RELEASE)
+    ldap_release = spec.get("ldapReleaseName", DEFAULT_LDAP_RELEASE)
+
+    _delete_release(impala_release, target_ns, logger)
+    _delete_release(ldap_release, target_ns, logger)
diff --git a/operator/impala-operator/manifests/deployment.yaml b/operator/impala-operator/manifests/deployment.yaml
index 1d8bf37cd..c25153e63 100644
--- a/operator/impala-operator/manifests/deployment.yaml
+++ b/operator/impala-operator/manifests/deployment.yaml
@@ -52,6 +52,6 @@ spec:
             - name: DEFAULT_IMPALA_VALUES_FILE
               value: /charts/impala/values-example.yaml
             - name: DEFAULT_LDAP_VALUES_FILE
-              value: /charts/impala/values-impala-ldap-example.yaml
+              value: /charts/impala/values-ldap-example.yaml
             - name: DEFAULT_HELM_TIMEOUT_SECONDS
               value: "900"
diff --git a/operator/impala-operator/requirements.txt b/operator/impala-operator/requirements.txt
new file mode 100644
index 000000000..f90bace35
--- /dev/null
+++ b/operator/impala-operator/requirements.txt
@@ -0,0 +1,19 @@
+# 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.
+
+kopf==1.37.2
+kubernetes==30.1.0


(impala) 02/02: IMPALA-14982: Add Impala-on-K8s deployment guide

Posted by st...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/impala.git

commit 01ca03c317ebf0371c54d2f2cc5194ef0b7fdbfc
Author: Anubhav Jindal <an...@gmail.com>
AuthorDate: Tue May 26 14:45:51 2026 -0700

    IMPALA-14982: Add Impala-on-K8s deployment guide
    
    Add a generic deployment guide for running Impala on Kubernetes with
    both Helm and operator workflows, including optional component toggles.
    
    Document LDAP bind-pattern escaping for Helm/operator flows, add explicit
    namespace creation in the operator workflow, include shared warehouse RWX
    storage guidance and fallback override instructions, and include
    troubleshooting steps for HMS permissions and metastore event polling
    compatibility.
    
    Add an ASF license header for the new markdown guide.
    
    Testing:
    - N/A (documentation-only change)
    
    Change-Id: I0a7d1a2f0254209143f05bd417c64410cc282985
    Assisted-by: GPT-5.3 (Cursor)
    Co-authored-by: Cursor <cu...@cursor.com>
    Reviewed-on: http://gerrit.cloudera.org:8080/24364
    Reviewed-by: Jason Fehr <jf...@cloudera.com>
    Tested-by: Jason Fehr <jf...@cloudera.com>
---
 bin/rat_exclude_files.txt |   1 +
 helm/impala/README.md     | 271 ++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 272 insertions(+)

diff --git a/bin/rat_exclude_files.txt b/bin/rat_exclude_files.txt
index f4bd50ac8..e949a9ca9 100644
--- a/bin/rat_exclude_files.txt
+++ b/bin/rat_exclude_files.txt
@@ -112,6 +112,7 @@ bin/distcc/README.md
 tests/comparison/POSTGRES.txt
 docs/README.md
 docker/README.md
+helm/impala/README.md
 be/src/thirdparty/pcg-cpp-0.98/README.md
 lib/python/README.md
 lib/python/impala_py_lib/gdb/README.md
diff --git a/helm/impala/README.md b/helm/impala/README.md
new file mode 100644
index 000000000..3058877f7
--- /dev/null
+++ b/helm/impala/README.md
@@ -0,0 +1,271 @@
+# Impala on Kubernetes (Helm and Operator)
+
+## Scope
+
+- Fast deployment with Helm
+- Optional Kudu, Ranger, and LDAP components
+- Equivalent operator workflow through `ImpalaCluster`
+
+## Prerequisites
+
+- Kubernetes cluster access (`kubectl` configured)
+- Helm 3.x
+- A default StorageClass (or set one explicitly)
+
+From repository root:
+
+```bash
+cd /path/to/impala
+```
+
+## Helm chart structure
+
+Chart root:
+
+- `helm/impala/Chart.yaml`
+- `helm/impala/values.yaml` (base defaults)
+- `helm/impala/values-example.yaml` (generic runnable example)
+- `helm/impala/templates/` (core and optional resources)
+
+Core templates:
+
+- `statestored-*`, `catalogd-*`, `impalad-*`, `hms-*`
+- `configmap.yaml`, `pvc.yaml`
+
+Optional templates:
+
+- Kudu: `kudu-master-*`, `kudu-tserver-*`, `kudu-pvc.yaml`
+- Ranger: `ranger-deployment.yaml`, `ranger-service.yaml`
+
+## Deploy with Helm
+
+Create namespace:
+
+```bash
+kubectl create namespace impala --dry-run=client -o yaml | kubectl apply -f -
+```
+
+Minimal Impala (first install):
+
+```bash
+helm install impala ./helm/impala \
+  -n impala \
+  -f ./helm/impala/values-example.yaml
+```
+
+For subsequent updates, use:
+
+```bash
+helm upgrade impala ./helm/impala \
+  -n impala \
+  -f ./helm/impala/values-example.yaml
+```
+
+Verify:
+
+```bash
+kubectl -n impala get pods
+kubectl -n impala get svc
+kubectl -n impala get pvc
+```
+
+Storage note for shared warehouse PVC:
+
+- The chart defaults `persistence.accessModes` to `ReadWriteMany` because
+  HMS, catalogd, and impalad share the same warehouse volume and may run on
+  different nodes.
+- If your cluster does not provide RWX-capable storage classes, override
+  `persistence.accessModes` to `["ReadWriteOnce"]` and ensure these pods are
+  co-located on the same node.
+
+## Configure Impala (Helm)
+
+Update any setting by editing values and running `helm upgrade`, or by `--set`.
+
+Examples:
+
+```bash
+# Update memory/JVM
+helm upgrade impala ./helm/impala -n impala \
+  -f ./helm/impala/values-example.yaml \
+  --set impalad.memLimit=12gb \
+  --set impalad.javaToolOptions="-Xms2g -Xmx4g"
+```
+
+```bash
+# Expose Impala externally
+helm upgrade impala ./helm/impala -n impala \
+  -f ./helm/impala/values-example.yaml \
+  --set service.impalad.type=LoadBalancer
+```
+
+## Optional services (Helm)
+
+### Enable Kudu
+
+```bash
+helm upgrade impala ./helm/impala -n impala \
+  -f ./helm/impala/values-example.yaml \
+  --set kudu.enabled=true
+```
+
+### Enable Ranger service and Ranger auth flags
+
+```bash
+helm upgrade impala ./helm/impala -n impala \
+  -f ./helm/impala/values-example.yaml \
+  --set ranger.enabled=true \
+  --set auth.ranger.enabled=true
+```
+
+### Enable LDAP
+
+`values-ldap-example.yaml` is for the OpenLDAP chart (`openldap/openldap`).
+`values-impala-ldap-example.yaml` is for the Impala chart LDAP auth settings.
+
+Deploy OpenLDAP first install:
+
+```bash
+helm repo add openldap https://jp-gouin.github.io/helm-openldap/
+helm repo update
+helm install impala-ldap openldap/openldap \
+  -n impala \
+  -f ./helm/impala/values-ldap-example.yaml
+```
+
+For subsequent LDAP updates, use:
+
+```bash
+helm upgrade impala-ldap openldap/openldap \
+  -n impala \
+  -f ./helm/impala/values-ldap-example.yaml
+```
+
+Enable LDAP auth on Impala:
+
+```bash
+helm upgrade impala ./helm/impala -n impala \
+  -f ./helm/impala/values-impala-ldap-example.yaml
+```
+
+For custom bind patterns, use `--set-string auth.ldap.bindPattern='cn=#UID\,dc=example\,dc=org'`.
+
+When using `--set` for LDAP bind patterns, escape commas (`\,`) so Helm does
+not split the value into multiple assignments.
+
+## Run Impala shell from laptop (tunnel)
+
+Port-forward HS2:
+
+```bash
+kubectl -n impala port-forward \
+  pod/$(kubectl -n impala get pod -o name | awk -F/ '/impala-impalad/{print $2; exit}') \
+  21050:21050
+```
+
+In another terminal:
+
+```bash
+impala-shell --protocol=hs2 -i 127.0.0.1:21050 -q "select version();"
+```
+
+If LDAP is enabled:
+
+```bash
+impala-shell --protocol=hs2 \
+  --ldap --auth_creds_ok_in_clear \
+  --user impalauser --ldap_password_cmd="echo -n impala123" \
+  -i 127.0.0.1:21050 \
+  -q "select version();"
+```
+
+## Operator structure
+
+Operator root:
+
+- `operator/impala-operator/main.py` (reconciler)
+- `operator/impala-operator/manifests/crd-impalacluster.yaml`
+- `operator/impala-operator/manifests/rbac.yaml`
+- `operator/impala-operator/manifests/deployment.yaml`
+- `operator/impala-operator/manifests/sample-impalacluster.yaml`
+
+Current model: the operator reconciles an `ImpalaCluster` CR and performs Helm installs/upgrades.
+
+## Deploy with Operator
+
+Build and push operator image:
+
+```bash
+docker build -f operator/impala-operator/Dockerfile -t <registry>/impala-operator:latest .
+docker push <registry>/impala-operator:latest
+```
+
+Install CRD/RBAC/deployment:
+
+```bash
+kubectl apply -k operator/impala-operator/manifests
+kubectl -n impala-operator-system set image deploy/impala-operator \
+  operator=<registry>/impala-operator:latest
+kubectl -n impala-operator-system rollout status deploy/impala-operator
+```
+
+Create the target namespace used by the sample CR:
+
+```bash
+kubectl create namespace impala --dry-run=client -o yaml | kubectl apply -f -
+```
+
+Create cluster CR:
+
+```bash
+kubectl apply -f operator/impala-operator/manifests/sample-impalacluster.yaml
+kubectl get impalacluster -n impala
+```
+
+Enable optional services via CR patch:
+
+```bash
+kubectl patch impalacluster impala-demo -n impala --type merge -p '{
+  "spec": {
+    "kuduEnabled": true,
+    "rangerEnabled": true,
+    "rangerAuthEnabled": true,
+    "ldapValuesFile": "/charts/impala/values-ldap-example.yaml",
+    "ldapEnabled": true,
+    "ldapUri": "ldaps://impala-ldap-openldap:636",
+    "ldapBindPattern": "cn=#UID,dc=example,dc=org"
+  }
+}'
+```
+
+For `ImpalaCluster.spec.ldapBindPattern`, provide raw DN syntax (no comma escaping).
+The operator handles Helm `--set-string` escaping internally.
+
+## Troubleshooting
+
+If HMS fails creating directories on the storage volume, set HMS pod user/group:
+
+```bash
+helm upgrade impala ./helm/impala -n impala \
+  -f ./helm/impala/values-example.yaml \
+  --set hms.securityContext.runAsUser=0 \
+  --set hms.securityContext.runAsGroup=0
+```
+
+If catalog startup fails due to HMS notification API compatibility, disable HMS
+event polling:
+
+```bash
+helm upgrade impala ./helm/impala -n impala \
+  -f ./helm/impala/values-example.yaml \
+  --set catalogd.hmsEventPollingIntervalS=0
+```
+
+## Control-plane guidance
+
+Use one control path per release/namespace:
+
+- Helm-managed release: update with Helm commands
+- Operator-managed release: update via `ImpalaCluster` spec
+
+Do not manage the same release with both at the same time.