Skip to content

Commit 97eb2b1

Browse files
committed
feat: Add clickhouse operator and base cluster
1 parent d4d3c89 commit 97eb2b1

File tree

6 files changed

+363
-0
lines changed

6 files changed

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

0 commit comments

Comments
 (0)