set fallback := true export CLICKHOUSE_NAMESPACE := env("CLICKHOUSE_NAMESPACE", "clickhouse") export CLICKHOUSE_HOST := env("CLICKHOUSE_HOST", "") export CLICKHOUSE_CHART_VERSION := env("CLICKHOUSE_CHART_VERSION", "0.25.5") export CLICKHOUSE_IMAGE := env("CLICKHOUSE_IMAGE", "clickhouse/clickhouse-server:25.10") export EXTERNAL_SECRETS_NAMESPACE := env("EXTERNAL_SECRETS_NAMESPACE", "external-secrets") # ClickHouse resource settings export CLICKHOUSE_MEMORY_REQUEST := env("CLICKHOUSE_MEMORY_REQUEST", "1Gi") export CLICKHOUSE_MEMORY_LIMIT := env("CLICKHOUSE_MEMORY_LIMIT", "8Gi") export CLICKHOUSE_CPU_REQUEST := env("CLICKHOUSE_CPU_REQUEST", "200m") export CLICKHOUSE_CPU_LIMIT := env("CLICKHOUSE_CPU_LIMIT", "2") # ClickHouse memory settings (bytes) # max_server_memory_usage: Server-wide limit, should be ~75% of MEMORY_LIMIT (default: 0 = auto 90% of RAM) export CLICKHOUSE_MAX_SERVER_MEMORY := env("CLICKHOUSE_MAX_SERVER_MEMORY", "6442450944") # max_memory_usage: Per-query limit (default: 10GB) export CLICKHOUSE_MAX_MEMORY_USAGE := env("CLICKHOUSE_MAX_MEMORY_USAGE", "4294967296") # max_bytes_before_external_group_by: Spill to disk threshold for GROUP BY (default: 0 = disabled) export CLICKHOUSE_MAX_BYTES_BEFORE_EXTERNAL_GROUP_BY := env("CLICKHOUSE_MAX_BYTES_BEFORE_EXTERNAL_GROUP_BY", "2147483648") # max_bytes_before_external_sort: Spill to disk threshold for ORDER BY (default: 0 = disabled) export CLICKHOUSE_MAX_BYTES_BEFORE_EXTERNAL_SORT := env("CLICKHOUSE_MAX_BYTES_BEFORE_EXTERNAL_SORT", "2147483648") # ClickHouse log sidecar resource settings export CLICKHOUSE_LOG_MEMORY_REQUEST := env("CLICKHOUSE_LOG_MEMORY_REQUEST", "64Mi") export CLICKHOUSE_LOG_MEMORY_LIMIT := env("CLICKHOUSE_LOG_MEMORY_LIMIT", "128Mi") export CLICKHOUSE_LOG_CPU_REQUEST := env("CLICKHOUSE_LOG_CPU_REQUEST", "10m") export CLICKHOUSE_LOG_CPU_LIMIT := env("CLICKHOUSE_LOG_CPU_LIMIT", "100m") # ClickHouse Operator resource settings export CLICKHOUSE_OPERATOR_MEMORY_REQUEST := env("CLICKHOUSE_OPERATOR_MEMORY_REQUEST", "64Mi") export CLICKHOUSE_OPERATOR_MEMORY_LIMIT := env("CLICKHOUSE_OPERATOR_MEMORY_LIMIT", "256Mi") export CLICKHOUSE_OPERATOR_CPU_REQUEST := env("CLICKHOUSE_OPERATOR_CPU_REQUEST", "50m") export CLICKHOUSE_OPERATOR_CPU_LIMIT := env("CLICKHOUSE_OPERATOR_CPU_LIMIT", "500m") [private] default: @just --list --unsorted --list-submodules # Add Helm repository add-helm-repo: helm repo add clickhouse-operator https://docs.altinity.com/clickhouse-operator/ helm repo update # Remove Helm repository remove-helm-repo: helm repo remove clickhouse-operator # Create ClickHouse namespace create-namespace: #!/bin/bash set -euo pipefail if ! kubectl get namespace ${CLICKHOUSE_NAMESPACE} &>/dev/null; then kubectl create namespace ${CLICKHOUSE_NAMESPACE} fi kubectl label namespace ${CLICKHOUSE_NAMESPACE} \ pod-security.kubernetes.io/enforce=baseline \ pod-security.kubernetes.io/enforce-version=latest \ pod-security.kubernetes.io/warn=baseline \ pod-security.kubernetes.io/warn-version=latest \ --overwrite # Delete ClickHouse namespace delete-namespace: @kubectl delete namespace ${CLICKHOUSE_NAMESPACE} --ignore-not-found # Create ClickHouse credentials secret create-credentials: #!/bin/bash set -euo pipefail echo "Setting up ClickHouse credentials..." # Generate admin password ADMIN_PASSWORD=$(just utils::random-password) if helm status external-secrets -n ${EXTERNAL_SECRETS_NAMESPACE} &>/dev/null; then echo "External Secrets available. Storing credentials in Vault and creating ExternalSecret..." just vault::put clickhouse/credentials admin="$ADMIN_PASSWORD" gomplate -f clickhouse-credentials-external-secret.gomplate.yaml -o clickhouse-credentials-external-secret.yaml kubectl apply -f clickhouse-credentials-external-secret.yaml echo "Waiting for credentials secret to be ready..." kubectl wait --for=condition=Ready externalsecret/clickhouse-credentials-external-secret \ -n ${CLICKHOUSE_NAMESPACE} --timeout=60s else echo "External Secrets not available. Creating Kubernetes Secret directly..." kubectl delete secret clickhouse-credentials -n ${CLICKHOUSE_NAMESPACE} --ignore-not-found kubectl create secret generic clickhouse-credentials -n ${CLICKHOUSE_NAMESPACE} \ --from-literal=admin="$ADMIN_PASSWORD" echo "Credentials secret created directly in Kubernetes" fi echo "ClickHouse credentials setup completed" # Delete ClickHouse credentials secret delete-credentials-secret: @kubectl delete secret clickhouse-credentials -n ${CLICKHOUSE_NAMESPACE} --ignore-not-found @kubectl delete externalsecret clickhouse-credentials-external-secret -n ${CLICKHOUSE_NAMESPACE} --ignore-not-found # Install ClickHouse install: #!/bin/bash set -euo pipefail export CLICKHOUSE_HOST=${CLICKHOUSE_HOST:-} while [ -z "${CLICKHOUSE_HOST}" ]; do CLICKHOUSE_HOST=$( gum input --prompt="ClickHouse host (FQDN): " --width=100 \ --placeholder="e.g., clickhouse.example.com" ) done echo "Installing ClickHouse..." just create-namespace just install-zookeeper just create-credentials just add-helm-repo gomplate -f clickhouse-operator-values.gomplate.yaml -o clickhouse-operator-values.yaml helm upgrade --install clickhouse-operator clickhouse-operator/altinity-clickhouse-operator \ --version ${CLICKHOUSE_CHART_VERSION} -n ${CLICKHOUSE_NAMESPACE} \ -f clickhouse-operator-values.yaml --wait gomplate -f clickhouse-installation-template.gomplate.yaml -o clickhouse-installation-template.yaml gomplate -f clickhouse.gomplate.yaml -o clickhouse.yaml kubectl apply -n ${CLICKHOUSE_NAMESPACE} -f ./clickhouse-installation-template.yaml kubectl apply -n ${CLICKHOUSE_NAMESPACE} -f ./clickhouse.yaml echo "Waiting for ClickHouse installation to be ready..." kubectl wait --for=jsonpath='{.status.status}'=Completed \ clickhouseinstallation/clickhouse -n ${CLICKHOUSE_NAMESPACE} --timeout=600s just setup-ingress ${CLICKHOUSE_HOST} echo "ClickHouse installation completed successfully" echo "ClickHouse API at: https://${CLICKHOUSE_HOST}" # Setup ClickHouse Ingress setup-ingress host: #!/bin/bash set -euo pipefail echo "Setting up ClickHouse Ingress for ${CLICKHOUSE_HOST}..." export CLICKHOUSE_HOST="{{ host }}" gomplate -f clickhouse-ingress.gomplate.yaml -o clickhouse-ingress.yaml kubectl apply -n ${CLICKHOUSE_NAMESPACE} -f clickhouse-ingress.yaml echo "ClickHouse Ingress configured successfully" # Uninstall ClickHouse uninstall: #!/bin/bash set -euo pipefail echo "Uninstalling ClickHouse..." if kubectl get clickhouseinstallations.clickhouse.altinity.com \ -n ${CLICKHOUSE_NAMESPACE} &>/dev/null; then echo "Deleting ClickHouseInstallation resources..." kubectl delete clickhouseinstallations.clickhouse.altinity.com --all \ -n ${CLICKHOUSE_NAMESPACE} --timeout=60s --ignore-not-found || { echo "Graceful deletion timed out, forcing finalizer removal..." for chi in $(kubectl get clickhouseinstallations.clickhouse.altinity.com \ -n ${CLICKHOUSE_NAMESPACE} -o name 2>/dev/null); do kubectl patch "$chi" -n ${CLICKHOUSE_NAMESPACE} \ --type='merge' -p='{"metadata":{"finalizers":null}}' || true done } fi helm uninstall clickhouse-operator -n ${CLICKHOUSE_NAMESPACE} --wait --ignore-not-found just uninstall-zookeeper just delete-credentials-secret just delete-namespace echo "ClickHouse uninstalled successfully" # Get ClickHouse admin password admin-password: #!/bin/bash set -euo pipefail kubectl get secret clickhouse-credentials -n ${CLICKHOUSE_NAMESPACE} \ -o jsonpath='{.data.admin}' 2>/dev/null | base64 -d | tr -d '\n\r' echo # Connect to ClickHouse as admin (interactive) connect-admin-interactive: check-env @just utils::check-connection clickhouse-clickhouse.clickhouse 9000 @clickhouse client --host clickhouse-clickhouse.clickhouse --port 9000 \ --user admin --password $(just clickhouse::admin-password) # Execute SQL as admin via kubectl exec connect-admin: #!/bin/bash set -euo pipefail ADMIN_PASSWORD=$(just admin-password) POD_NAME=$(just find-clickhouse-pod) kubectl exec -n ${CLICKHOUSE_NAMESPACE} "${POD_NAME}" -c clickhouse -- \ clickhouse-client --user admin --password "${ADMIN_PASSWORD}" # Connect to ClickHouse (interactive) connect-interactive user: check-env @just utils::check-connection clickhouse-clickhouse.clickhouse 9000 @clickhouse client --host clickhouse-clickhouse.clickhouse --port 9000 \ --user {{ user }} --ask-password # Execute SQL as specific user via kubectl exec connect user password='': #!/bin/bash set -euo pipefail USER="{{ user }}" PASSWORD="{{ password }}" if [ -z "${PASSWORD}" ]; then PASSWORD=$(gum input --prompt="Password for ${USER}: " --password --width=100) fi POD_NAME=$(just find-clickhouse-pod) kubectl exec -n ${CLICKHOUSE_NAMESPACE} "${POD_NAME}" -c clickhouse -- \ clickhouse-client --user "${USER}" --password "${PASSWORD}" # Find ClickHouse pod name [private] find-clickhouse-pod: #!/bin/bash set -euo pipefail for SELECTOR in \ "app=clickhouse-clickhouse" \ "app.kubernetes.io/name=clickhouse" \ "clickhouse.altinity.com/chi=clickhouse" \ "app=clickhouse"; do POD_NAME=$(kubectl get pods -n ${CLICKHOUSE_NAMESPACE} -l "${SELECTOR}" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "") if [ -n "${POD_NAME}" ]; then echo "${POD_NAME}" exit 0 fi done POD_NAME=$(kubectl get pods -n ${CLICKHOUSE_NAMESPACE} -o name | grep -i clickhouse | head -1 | cut -d'/' -f2 2>/dev/null || echo "") if [ -n "${POD_NAME}" ]; then echo "${POD_NAME}" exit 0 fi echo "No ClickHouse pods found in namespace ${CLICKHOUSE_NAMESPACE}" >&2 echo "Available pods:" >&2 kubectl get pods -n ${CLICKHOUSE_NAMESPACE} >&2 exit 1 # Execute SQL command as admin exec-sql-admin sql='': #!/bin/bash set -euo pipefail SQL="{{ sql }}" ADMIN_PASSWORD=$(just admin-password) POD_NAME=$(just find-clickhouse-pod) if [ -n "${SQL}" ]; then # Pass SQL via stdin to avoid quoting issues echo "${SQL}" | kubectl exec -i -n ${CLICKHOUSE_NAMESPACE} "${POD_NAME}" -c clickhouse -- \ sh -c "export CLICKHOUSE_PASSWORD='${ADMIN_PASSWORD}' && clickhouse-client --user admin" else # Read from stdin kubectl exec -i -n ${CLICKHOUSE_NAMESPACE} "${POD_NAME}" -c clickhouse -- \ sh -c "export CLICKHOUSE_PASSWORD='${ADMIN_PASSWORD}' && clickhouse-client --user admin" fi # Create ClickHouse user create-user username='' password='': #!/bin/bash set -euo pipefail USERNAME=${USERNAME:-"{{ username }}"} PASSWORD=${PASSWORD:-"{{ password }}"} while [ -z "${USERNAME}" ]; do USERNAME=$(gum input --prompt="Username: " --width=100) done if just user-exists ${USERNAME} &>/dev/null; then echo "User ${USERNAME} already exists" >&2 exit fi if [ -z "${PASSWORD}" ]; then PASSWORD=$(gum input --prompt="Password: " --password --width=100 \ --placeholder="Empty to generate a random password") fi if [ -z "${PASSWORD}" ]; then PASSWORD=$(just utils::random-password) echo "Generated random password: ${PASSWORD}" fi echo "Creating ClickHouse user '${USERNAME}'..." just exec-sql-admin "CREATE USER '${USERNAME}' IDENTIFIED BY '${PASSWORD}';" echo "User ${USERNAME} created." # Wait a moment for ClickHouse to process the user creation sleep 2 # Create new user for ClickHouse (deprecated - use create-user instead) create-user-old username='' password='' database='': #!/bin/bash set -euo pipefail USERNAME="{{ username }}" while [ -z "${USERNAME}" ]; do USERNAME=$(gum input --prompt="Username: " --width=100) done PASSWORD="{{ password }}" while [ -z "${PASSWORD}" ]; do PASSWORD=$(gum input --prompt="Password: " --password --width=100) done DATABASE="{{ database }}" while [ -z "${DATABASE}" ]; do DATABASE=$(gum input --prompt="Database: " --width=100) done if [ "${DATABASE}" != "default" ]; then if ! just db-exists ${DATABASE}; then echo "Database '${DATABASE}' does not exist" just create-db ${DATABASE} fi fi echo "Creating ClickHouse user '${USERNAME}'..." just exec-sql-admin "CREATE USER '${USERNAME}' IDENTIFIED BY '${PASSWORD}' DEFAULT DATABASE '${DATABASE}';" # Delete ClickHouse user delete-user username='': #!/bin/bash set -euo pipefail USERNAME=${USERNAME:-"{{ username }}"} while [ -z "${USERNAME}" ]; do USERNAME=$(gum input --prompt="Username: " --width=100) done if ! just user-exists ${USERNAME} &>/dev/null; then echo "User ${USERNAME} does not exist." >&2 exit fi echo "Deleting ClickHouse user '${USERNAME}'..." just exec-sql-admin "DROP USER '${USERNAME}';" echo "User ${USERNAME} deleted." # Grant all privileges on database to user grant db_name='' username='': #!/bin/bash set -euo pipefail DB_NAME=${DB_NAME:-"{{ db_name }}"} USERNAME=${USERNAME:-"{{ username }}"} while [ -z "${DB_NAME}" ]; do DB_NAME=$(gum input --prompt="Database name: " --width=100) done while [ -z "${USERNAME}" ]; do USERNAME=$(gum input --prompt="Username: " --width=100) done if ! just db-exists ${DB_NAME}; then echo "Database ${DB_NAME} does not exist." >&2 exit 1 fi if ! just user-exists ${USERNAME}; then echo "User ${USERNAME} does not exist." >&2 exit 1 fi echo "Granting all privileges on '${DB_NAME}' to ClickHouse user '${USERNAME}'..." just exec-sql-admin "GRANT ALL ON ${DB_NAME}.* TO '${USERNAME}';" # Also grant INFORMATION_SCHEMA access for tools like DLT just exec-sql-admin "GRANT SELECT ON INFORMATION_SCHEMA.* TO '${USERNAME}';" echo "Privileges granted." # Revoke all privileges on database from user revoke db_name='' username='': #!/bin/bash set -euo pipefail DB_NAME=${DB_NAME:-"{{ db_name }}"} USERNAME=${USERNAME:-"{{ username }}"} while [ -z "${DB_NAME}" ]; do DB_NAME=$(gum input --prompt="Database name: " --width=100) done while [ -z "${USERNAME}" ]; do USERNAME=$(gum input --prompt="Username: " --width=100) done if ! just db-exists ${DB_NAME}; then echo "Database ${DB_NAME} does not exist." >&2 exit 1 fi if ! just user-exists ${USERNAME}; then echo "User ${USERNAME} does not exist." >&2 exit 1 fi echo "Revoking all privileges on '${DB_NAME}' from ClickHouse user '${USERNAME}'..." just exec-sql-admin "REVOKE ALL ON ${DB_NAME}.* FROM '${USERNAME}';" echo "Privileges revoked." # Create ClickHouse database and user create-user-and-db username='' db_name='' password='': just create-db "{{ db_name }}" just create-user "{{ username }}" "{{ password }}" just grant "{{ db_name }}" "{{ username }}" # Delete ClickHouse database and user delete-user-and-db username='' db_name='': #!/bin/bash set -euo pipefail DB_NAME=${DB_NAME:-"{{ db_name }}"} USERNAME=${USERNAME:-"{{ username }}"} if just db-exists ${DB_NAME} &>/dev/null; then if just user-exists ${USERNAME} &>/dev/null; then just revoke "${DB_NAME}" "${USERNAME}" else echo "User ${USERNAME} does not exist, skipping revoke." fi just delete-db "${DB_NAME}" else echo "Database ${DB_NAME} does not exist, skipping database deletion." fi if just user-exists ${USERNAME} &>/dev/null; then just delete-user "${USERNAME}" else echo "User ${USERNAME} does not exist, skipping user deletion." fi echo "Cleanup completed." # List all users in ClickHouse list-users: #!/bin/bash set -euo pipefail just exec-sql-admin "SHOW USERS;" # Check if user exists in ClickHouse [no-exit-message] user-exists username='': #!/bin/bash set -euo pipefail USERNAME="{{ username }}" while [ -z "${USERNAME}" ]; do USERNAME=$(gum input --prompt="Username: " --width=100) done # Use exec-sql-admin to check if user exists result=$(just exec-sql-admin "SELECT name FROM system.users WHERE name = '${USERNAME}';" 2>/dev/null || echo "") if [[ -n "$result" && "$result" == *"$USERNAME"* ]]; then echo "User '$USERNAME' exists." exit 0 else echo "User '$USERNAME' does not exist." exit 1 fi # Create DB in ClickHouse create-db database='default': #!/bin/bash set -euo pipefail DATABASE="{{ database }}" while [ -z "${DATABASE}" ]; do DATABASE=$(gum input --prompt="Database: " --width=100) done echo "Creating ClickHouse database '${DATABASE}'..." just exec-sql-admin "CREATE DATABASE IF NOT EXISTS ${DATABASE};" # Check if DB exists in ClickHouse [no-exit-message] db-exists database='default': #!/bin/bash set -euo pipefail DATABASE="{{ database }}" while [ -z "${DATABASE}" ]; do DATABASE=$(gum input --prompt="Database: " --width=100) done result=$(just exec-sql-admin "SHOW DATABASES LIKE '${DATABASE}';" 2>/dev/null || echo "") if [[ -n "$result" && "$result" == *"$DATABASE"* ]]; then echo "Database '$DATABASE' exists." exit 0 else echo "Database '$DATABASE' does not exist." exit 1 fi # Print user privilege user-privilege username='': #!/bin/bash set -euo pipefail USERNAME="{{ username }}" while [ -z "${USERNAME}" ]; do USERNAME=$(gum input --prompt="Username: " --width=100) done just exec-sql-admin "SHOW GRANTS FOR '${USERNAME}';" # Change user password change-password username='' password='': #!/bin/bash set -euo pipefail USERNAME="${USERNAME:-"{{ username }}"}" PASSWORD="${PASSWORD:-"{{ password }}"}" while [ -z "${USERNAME}" ]; do USERNAME=$(gum input --prompt="Username: " --width=100) done while [ -z "${PASSWORD}" ]; do PASSWORD=$(gum input --prompt="New Password: " --password --width=100) done if ! just user-exists ${USERNAME} &>/dev/null; then echo "User ${USERNAME} does not exist." >&2 exit 1 fi echo "Changing password for user '${USERNAME}'..." just exec-sql-admin "ALTER USER '${USERNAME}' IDENTIFIED BY '${PASSWORD}';" echo "Password changed for user '${USERNAME}'" # Grant admin privileges to user grant-admin username='': #!/bin/bash set -euo pipefail USERNAME="${USERNAME:-"{{ username }}"}" while [ -z "${USERNAME}" ]; do USERNAME=$(gum input --prompt="Username: " --width=100) done for i in {1..5}; do if just user-exists ${USERNAME} &>/dev/null; then break fi if [ $i -lt 5 ]; then echo "User ${USERNAME} not found, retrying in 1 second... (attempt $i/5)" sleep 1 else echo "User ${USERNAME} does not exist after 5 attempts." >&2 exit 1 fi done echo "Granting admin privileges to user '${USERNAME}'..." just exec-sql-admin " GRANT SOURCES ON *.* TO '${USERNAME}' WITH GRANT OPTION; GRANT TABLE ENGINE ON * TO '${USERNAME}' WITH GRANT OPTION; GRANT CHECK, SHOW, SELECT, INSERT, ALTER, CREATE, DROP, UNDROP TABLE, TRUNCATE, OPTIMIZE, BACKUP, KILL QUERY, KILL TRANSACTION, MOVE PARTITION BETWEEN SHARDS, ROLE ADMIN, CREATE ROW POLICY, ALTER ROW POLICY, DROP ROW POLICY, CREATE QUOTA, ALTER QUOTA, DROP QUOTA, CREATE SETTINGS PROFILE, ALTER SETTINGS PROFILE, DROP SETTINGS PROFILE, ALLOW SQL SECURITY NONE, SHOW ACCESS, SYSTEM, dictGet, displaySecretsInShowAndSelect, INTROSPECTION, CLUSTER, FILE, URL, REMOTE, MONGO, REDIS, MYSQL, POSTGRES, SQLITE, ODBC, JDBC, HDFS, S3, HIVE, AZURE, KAFKA, NATS, RABBITMQ, SOURCES ON *.* TO '${USERNAME}' WITH GRANT OPTION; GRANT SET DEFINER ON * TO '${USERNAME}' WITH GRANT OPTION; GRANT CREATE USER, ALTER USER, DROP USER, CREATE ROLE, ALTER ROLE, DROP ROLE ON * TO '${USERNAME}' WITH GRANT OPTION; " echo "Admin privileges granted to user '${USERNAME}'" # Install ZooKeeper install-zookeeper: #!/bin/bash set -euo pipefail if ! kubectl get namespace zookeeper &>/dev/null; then kubectl create namespace zookeeper fi kubectl apply -n ${CLICKHOUSE_NAMESPACE} -f ./zookeeper.yaml # Uninstall ZooKeeper uninstall-zookeeper: kubectl delete -n ${CLICKHOUSE_NAMESPACE} -f ./zookeeper.yaml --ignore-not-found # Clean up ClickHouse resources cleanup: #!/bin/bash set -euo pipefail echo "This will delete all ClickHouse resources and secrets." if gum confirm "Are you sure you want to proceed?"; then echo "Cleaning up ClickHouse resources..." just vault::delete clickhouse/credentials || true echo "Cleanup completed" else echo "Cleanup cancelled" fi # Check the environment [private] check-env: #!/bin/bash set -euo pipefail if ! command -v clickhouse &>/dev/null; then echo "clickhouse CLI is not installed. Please install it first." exit 1 fi