Skip to content

Commit 54c50d3

Browse files
committed
feat: Add clickhouse operator and base cluster
1 parent d4d3c89 commit 54c50d3

File tree

6 files changed

+359
-0
lines changed

6 files changed

+359
-0
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Operator goes in the clickhouse namespace and watches that namespace only.
2+
createCRDs: true
3+
watchNamespaces:
4+
- clickhouse
5+
6+
# Schedule the operator only on worker nodes.
7+
nodeSelector:
8+
node-role.kubernetes.io/worker: ""
9+
10+
# Spread away from other operator pods (if HA operator is enabled later).
11+
affinity:
12+
podAntiAffinity:
13+
preferredDuringSchedulingIgnoredDuringExecution:
14+
- weight: 100
15+
podAffinityTerm:
16+
labelSelector:
17+
matchExpressions:
18+
- key: app.kubernetes.io/name
19+
operator: In
20+
values: ["altinity-clickhouse-operator"]
21+
topologyKey: "kubernetes.io/hostname"
22+
23+
# Keep it quiet-ish but observable
24+
env:
25+
- name: LOG_LEVEL
26+
value: "info"
27+
28+
metrics:
29+
enabled: true
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
apiVersion: clickhouse.altinity.com/v1
2+
kind: ClickHouseInstallation
3+
metadata:
4+
name: ch
5+
namespace: clickhouse
6+
spec:
7+
taskID: "1"
8+
9+
configuration:
10+
# Use Keeper for replication
11+
zookeeper:
12+
nodes:
13+
- host: keeper-ck
14+
port: 2181
15+
16+
users:
17+
# Disable dangerous defaults; create reader/writer explicitly.
18+
default/readonly: 1
19+
20+
writer/password_sha256_hex:
21+
valueFrom:
22+
secretKeyRef:
23+
name: clickhouse-db-passwords
24+
key: writer_password_sha256
25+
writer/profile: default
26+
writer/quota: default
27+
writer/networks/ip: "::/0"
28+
29+
reader/password_sha256_hex:
30+
valueFrom:
31+
secretKeyRef:
32+
name: clickhouse-db-passwords
33+
key: reader_password_sha256
34+
reader/profile: readonly
35+
reader/quota: default
36+
reader/networks/ip: "::/0"
37+
38+
profiles:
39+
readonly/readonly: 1
40+
41+
clusters:
42+
- name: main
43+
layout:
44+
shardsCount: 1
45+
replicasCount: 3
46+
templates:
47+
podTemplate: ch-pod
48+
volumeClaimTemplate: ch-data
49+
50+
# Create DB + tables for IPFIX rollups on first boot.
51+
files:
52+
10-init-ipfix.sql: |
53+
CREATE DATABASE IF NOT EXISTS ipfix;
54+
55+
-- Raw hourly rollups coming from edge nodes (per node).
56+
CREATE TABLE IF NOT EXISTS ipfix.vip_hourly_node
57+
(
58+
hour_ts DateTime,
59+
vip LowCardinality(String), -- store IP as text to support v4/v6 uniformly
60+
dir LowCardinality(String), -- 'to' | 'from'
61+
bytes UInt64,
62+
packets UInt64,
63+
node LowCardinality(String)
64+
)
65+
ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/{shard}/vip_hourly_node','{replica}')
66+
PARTITION BY toYYYYMMDD(hour_ts)
67+
ORDER BY (hour_ts, vip, dir, node);
68+
69+
-- Cluster-wide aggregation (fast to query).
70+
CREATE TABLE IF NOT EXISTS ipfix.vip_hourly_mv
71+
(
72+
hour_ts DateTime,
73+
vip LowCardinality(String),
74+
dir LowCardinality(String),
75+
bytes UInt64,
76+
packets UInt64
77+
)
78+
ENGINE = ReplicatedSummingMergeTree('/clickhouse/tables/{shard}/vip_hourly_mv','{replica}')
79+
PARTITION BY toYYYYMMDD(hour_ts)
80+
ORDER BY (hour_ts, vip, dir);
81+
82+
-- Materialized View to keep vip_hourly_mv in sync.
83+
CREATE MATERIALIZED VIEW IF NOT EXISTS ipfix.vip_hourly_mv__mv
84+
TO ipfix.vip_hourly_mv
85+
AS
86+
SELECT
87+
hour_ts,
88+
vip,
89+
dir,
90+
sum(bytes) AS bytes,
91+
sum(packets) AS packets
92+
FROM ipfix.vip_hourly_node
93+
GROUP BY hour_ts, vip, dir;
94+
95+
templates:
96+
podTemplates:
97+
- name: ch-pod
98+
spec:
99+
nodeSelector:
100+
node-role.kubernetes.io/worker: ""
101+
affinity:
102+
podAntiAffinity:
103+
requiredDuringSchedulingIgnoredDuringExecution:
104+
- labelSelector:
105+
matchLabels:
106+
clickhouse.altinity.com/chi: ch
107+
topologyKey: "kubernetes.io/hostname"
108+
containers:
109+
- name: clickhouse
110+
# Injected by envsubst in install script from helm-chart-versions.yaml
111+
image: ${CLICKHOUSE_SERVER_IMAGE}
112+
imagePullPolicy: IfNotPresent
113+
ports:
114+
- name: http
115+
containerPort: 8123
116+
- name: native
117+
containerPort: 9000
118+
- name: inter
119+
containerPort: 9009
120+
volumeClaimTemplates:
121+
- name: ch-data
122+
spec:
123+
accessModes: ["ReadWriteOnce"]
124+
storageClassName: general
125+
resources:
126+
requests:
127+
storage: 80Gi
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
apiVersion: clickhouse-keeper.altinity.com/v1
2+
kind: ClickHouseKeeperInstallation
3+
metadata:
4+
name: keeper
5+
namespace: clickhouse
6+
spec:
7+
configuration:
8+
clusters:
9+
- name: ck
10+
layout:
11+
replicasCount: 3
12+
templates:
13+
podTemplates:
14+
- name: keeper-pod
15+
spec:
16+
nodeSelector:
17+
node-role.kubernetes.io/worker: ""
18+
affinity:
19+
podAntiAffinity:
20+
requiredDuringSchedulingIgnoredDuringExecution:
21+
- labelSelector:
22+
matchLabels:
23+
clickhouse-keeper.altinity.com/chi: keeper
24+
topologyKey: "kubernetes.io/hostname"
25+
containers:
26+
- name: clickhouse-keeper
27+
# Injected by envsubst in install script
28+
image: ${CLICKHOUSE_KEEPER_IMAGE}
29+
imagePullPolicy: IfNotPresent
30+
command: ["/usr/bin/clickhouse-keeper","--config-file=/etc/clickhouse-keeper/keeper-config.xml"]
31+
volumeClaimTemplates:
32+
- name: keeper-data
33+
spec:
34+
accessModes: ["ReadWriteOnce"]
35+
storageClassName: general
36+
resources:
37+
requests:
38+
storage: 80Gi
39+
defaults:
40+
templates:
41+
podTemplate: keeper-pod
42+
volumeClaimTemplate: keeper-data
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
sortOptions:
3+
order: fifo
4+
resources:
5+
- all.yaml
6+
- chk-keeper.yaml
7+
- chi-cluster.yaml
8+
- svc-clickhouse-http.yaml
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
apiVersion: v1
2+
kind: Service
3+
metadata:
4+
name: clickhouse-http
5+
namespace: clickhouse
6+
labels:
7+
app: clickhouse
8+
spec:
9+
type: ClusterIP
10+
selector:
11+
clickhouse.altinity.com/chi: ch
12+
ports:
13+
- name: http
14+
port: 8123
15+
targetPort: 8123

bin/install-clickhouse.sh

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Service paths
5+
GLOBAL_OVERRIDES_DIR="/etc/genestack/helm-configs/global_overrides"
6+
SERVICE_CONFIG_DIR="/etc/genestack/helm-configs/clickhouse-helm-overrides.yaml"
7+
BASE_OVERRIDES="/opt/genestack/base-helm-configs/clickhouse/clickhouse-helm-overrides.yaml"
8+
KUSTOMIZE_DIR="/etc/genestack/kustomize/clickhouse/overlay"
9+
VERSIONS_FILE="/etc/genestack/helm-chart-versions.yaml"
10+
11+
NS="clickhouse"
12+
OP_RELEASE="altinity-operator"
13+
14+
need() { command -v "$1" >/dev/null || { echo "Missing required command: $1" >&2; exit 1; }; }
15+
need helm
16+
need kubectl
17+
need kustomize
18+
need awk
19+
need sha256sum
20+
need openssl
21+
need envsubst
22+
23+
echo "==> Ensuring namespace '${NS}' exists"
24+
kubectl get ns "${NS}" >/dev/null 2>&1 || kubectl create ns "${NS}"
25+
26+
# --- Create/reuse DB password secret ---
27+
echo "==> Ensuring DB password secret exists in namespace '${NS}'"
28+
if ! kubectl -n "${NS}" get secret clickhouse-db-passwords >/dev/null 2>&1; then
29+
WRITER_PLAIN="$(openssl rand -hex 16)"
30+
READER_PLAIN="$(openssl rand -hex 16)"
31+
WRITER_SHA256="$(printf "%s" "${WRITER_PLAIN}" | sha256sum | awk "{print \$1}")"
32+
READER_SHA256="$(printf "%s" "${READER_PLAIN}" | sha256sum | awk "{print \$1}")"
33+
kubectl -n "${NS}" apply -f - <<EOF
34+
apiVersion: v1
35+
kind: Secret
36+
metadata:
37+
name: clickhouse-db-passwords
38+
type: Opaque
39+
stringData:
40+
writer_password_sha256: "${WRITER_SHA256}"
41+
reader_password_sha256: "${READER_SHA256}"
42+
writer_password_plain: "${WRITER_PLAIN}"
43+
reader_password_plain: "${READER_PLAIN}"
44+
EOF
45+
else
46+
echo " Secret 'clickhouse-db-passwords' already exists; reusing."
47+
fi
48+
49+
# --- Read versions from YAML without yq ---
50+
# Simple awk-based extractor: get "key: value" lines.
51+
get_yaml_val() {
52+
local key="$1"
53+
awk -v k="$key" '
54+
$1 ~ k ":" {
55+
# value may have quotes
56+
sub(/^[^:]+:[[:space:]]*/,"")
57+
gsub(/"/,"")
58+
print
59+
exit
60+
}' "${VERSIONS_FILE}"
61+
}
62+
63+
OP_CHART="altinity/altinity-clickhouse-operator"
64+
OP_VERSION="$(get_yaml_val "clickhouse-operator")"
65+
CH_SERVER_IMAGE="altinity/clickhouse-server:$(get_yaml_val "clickhouse-server")"
66+
CH_KEEPER_IMAGE="clickhouse/clickhouse-keeper:$(get_yaml_val "clickhouse-keeper")"
67+
68+
if [[ -z "${OP_VERSION}" || -z "${CH_SERVER_IMAGE}" || -z "${CH_KEEPER_IMAGE}" ]]; then
69+
echo "Failed to parse ${VERSIONS_FILE}. Please verify keys." >&2
70+
exit 1
71+
fi
72+
73+
echo "==> Helm repo add/update for operator chart: ${OP_CHART} @ ${OP_VERSION}"
74+
helm repo add altinity https://helm.altinity.com >/dev/null
75+
helm repo update >/dev/null
76+
77+
echo "==> Installing/Upgrading ClickHouse Operator release '${OP_RELEASE}'"
78+
HELM_CMD="helm upgrade --install ${OP_RELEASE} ${OP_CHART} \
79+
--version ${OP_VERSION} \
80+
-n ${NS}"
81+
82+
HELM_CMD+=" -f ${BASE_OVERRIDES}"
83+
84+
for dir in "$GLOBAL_OVERRIDES_DIR" "$SERVICE_CONFIG_DIR"; do
85+
if compgen -G "${dir}/*.yaml" > /dev/null; then
86+
for yaml_file in "${dir}"/*.yaml; do
87+
HELM_CMD+=" -f ${yaml_file}"
88+
done
89+
fi
90+
done
91+
92+
HELM_CMD+=" $@"
93+
94+
echo "==> Executing Helm command:"
95+
echo "${HELM_CMD}"
96+
eval "${HELM_CMD}"
97+
98+
echo "==> Waiting for operator to be ready"
99+
kubectl -n "${NS}" rollout status deploy/${OP_RELEASE} --timeout=300s
100+
101+
# --- Apply Kustomize with envsubsted images from versions file ---
102+
export CLICKHOUSE_SERVER_IMAGE="${CH_SERVER_IMAGE}"
103+
export CLICKHOUSE_KEEPER_IMAGE="${CH_KEEPER_IMAGE}"
104+
105+
echo "==> Applying ClickHouse Keeper + Cluster (kustomize + envsubst)"
106+
# We envsubst only image placeholders present in manifests.
107+
kubectl kustomize "${KUSTOMIZE_DIR}" | envsubst '${CLICKHOUSE_SERVER_IMAGE} ${CLICKHOUSE_KEEPER_IMAGE}' | kubectl apply -n "${NS}" -f -
108+
109+
echo "==> Waiting for ClickHouse cluster pods (CHI=ch) to be Ready"
110+
kubectl -n "${NS}" wait --for=condition=Ready pod -l clickhouse.altinity.com/chi=ch --timeout=900s
111+
112+
echo "==> Service endpoint (HTTP 8123)"
113+
kubectl -n "${NS}" get svc clickhouse-http -o wide
114+
115+
# Print connection hints using stored plaintext (if present)
116+
WRITER_PLAIN="$(kubectl -n "${NS}" get secret clickhouse-db-passwords -o jsonpath='{.data.writer_password_plain}' 2>/dev/null | base64 -d || true)"
117+
READER_PLAIN="$(kubectl -n "${NS}" get secret clickhouse-db-passwords -o jsonpath='{.data.reader_password_plain}' 2>/dev/null | base64 -d || true)"
118+
119+
# Print out the in-cluster endpoint, and various service info
120+
cat <<EOF
121+
122+
ClickHouse installed.
123+
124+
In-cluster HTTP endpoint:
125+
http://clickhouse-http.${NS}.svc.cluster.local:8123
126+
127+
Example queries:
128+
kubectl -n ${NS} port-forward svc/clickhouse-http 8123:8123 &
129+
curl -s "http://localhost:8123/?user=reader&password=${READER_PLAIN}&query=SELECT%201"
130+
131+
Users (from Secret clickhouse-db-passwords):
132+
reader / ${READER_PLAIN}
133+
writer / ${WRITER_PLAIN}
134+
135+
To rotate passwords: update the Secret and bump taskID in chi-cluster.yaml (or patch):
136+
kubectl -n ${NS} patch chi ch --type=merge -p '{"spec":{"taskID":"2"}}'
137+
138+
EOF

0 commit comments

Comments
 (0)