Skip to content

Commit 3e0cde0

Browse files
committed
Code cleanup
1 parent f1f3854 commit 3e0cde0

File tree

7 files changed

+137
-40
lines changed

7 files changed

+137
-40
lines changed

crate/operator/bootstrap.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,8 @@ async def _bootstrap_user_via_sidecar(
194194
args=command_grant["args"],
195195
logger=logger,
196196
)
197-
except Exception:
198-
logger.exception("... failed")
197+
except Exception as e:
198+
exception_logger("... failed. %s", str(e))
199199
raise _temporary_error()
200200
else:
201201
if "rowcount" in result:
@@ -313,10 +313,6 @@ async def pod_exec(cmd):
313313
raise _temporary_error()
314314

315315

316-
def get_control_service_host(namespace: str, name: str) -> str:
317-
return f"crate-control-{name}.{namespace}.svc.cluster.local"
318-
319-
320316
async def bootstrap_gc_admin_user(core: CoreV1Api, namespace: str, name: str):
321317
"""
322318
Create the gc_admin user, which is used by Grand Central to run

crate/operator/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@
6161
TERMINATION_GRACE_PERIOD_SECONDS = 1200
6262
DECOMMISSION_TIMEOUT = "720s"
6363

64+
# Port on which the crate-control sidecar listens.
65+
CRATE_CONTROL_PORT = 5050
66+
6467
# dcutil fileserver URL for dynamic architecture detection
6568
DCUTIL_FILESERVER_URL = "http://dc-util-fileserver.dcutil.svc.cluster.local/latest"
6669

crate/operator/create.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
from crate.operator.config import config
9595
from crate.operator.constants import (
9696
API_GROUP,
97+
CRATE_CONTROL_PORT,
9798
DATA_PVC_NAME_PREFIX,
9899
DCUTIL_FILESERVER_URL,
99100
DECOMMISSION_TIMEOUT,
@@ -322,6 +323,12 @@ def get_statefulset_containers(
322323
sql_exporter_image = config.SQL_EXPORTER_IMAGE
323324
crate_control_image = config.CRATE_CONTROL_IMAGE
324325

326+
if config.CLOUD_PROVIDER == CloudProvider.OPENSHIFT and not crate_control_image:
327+
raise kopf.PermanentError(
328+
"CRATEDB_OPERATOR_CRATE_CONTROL_IMAGE must be set when "
329+
"CRATEDB_OPERATOR_CLOUD_PROVIDER=openshift"
330+
)
331+
325332
crate_container = V1Container(
326333
command=crate_command,
327334
env=crate_env,
@@ -434,7 +441,7 @@ def get_statefulset_containers(
434441
name="crate-control",
435442
image=crate_control_image,
436443
ports=[
437-
V1ContainerPort(container_port=5050, name="control"),
444+
V1ContainerPort(container_port=CRATE_CONTROL_PORT, name="control"),
438445
],
439446
env=[
440447
V1EnvVar(
@@ -452,11 +459,21 @@ def get_statefulset_containers(
452459
),
453460
],
454461
readiness_probe=V1Probe(
455-
http_get=V1HTTPGetAction(path="/health", port=5050),
462+
http_get=V1HTTPGetAction(path="/health", port=CRATE_CONTROL_PORT),
456463
initial_delay_seconds=20,
457464
period_seconds=5,
458465
failure_threshold=6,
459466
),
467+
resources=V1ResourceRequirements(
468+
limits={
469+
"cpu": "100m",
470+
"memory": "64Mi",
471+
},
472+
requests={
473+
"cpu": "50m",
474+
"memory": "32Mi",
475+
},
476+
),
460477
)
461478
)
462479

@@ -857,8 +874,6 @@ def get_statefulset(
857874
node_labels.update(node_spec.get("labels", {}))
858875
# This is to identify pods of the same cluster but with a different node type
859876
node_labels[LABEL_NODE_NAME] = node_name
860-
if config.CLOUD_PROVIDER == CloudProvider.OPENSHIFT:
861-
node_labels["tuned.openshift.io/cratedb"] = ""
862877
full_pod_name_prefix = f"crate-{node_name_prefix}{name}"
863878
image_registry, version = crate_image.rsplit(":", 1)
864879

@@ -999,10 +1014,10 @@ async def create_crate_scc(
9991014
plural="securitycontextconstraints",
10001015
body=scc,
10011016
)
1002-
logger.info(f"Created SCC {scc_name}")
1017+
logger.info("Created SCC %s", scc_name)
10031018
except ApiException as e:
10041019
if e.status == 409: # Already exists
1005-
logger.info(f"SCC {scc_name} already exists")
1020+
logger.info("SCC %s already exists", scc_name)
10061021
else:
10071022
raise
10081023

@@ -1303,7 +1318,7 @@ def get_crate_control_service(
13031318
name: str,
13041319
namespace: str,
13051320
labels: Dict[str, str],
1306-
port: int = 5050,
1321+
port: int = CRATE_CONTROL_PORT,
13071322
) -> V1Service:
13081323
"""
13091324
Create a headless service that exposes the crate-control sidecar of a
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# CrateDB Kubernetes Operator
2+
#
3+
# Licensed to Crate.IO GmbH ("Crate") under one or more contributor
4+
# license agreements. See the NOTICE file distributed with this work for
5+
# additional information regarding copyright ownership. Crate licenses
6+
# this file to you under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License. You may
8+
# obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15+
# License for the specific language governing permissions and limitations
16+
# under the License.
17+
#
18+
# However, if you have executed another commercial license agreement
19+
# with Crate these terms will supersede the license and you may use the
20+
# software solely pursuant to the terms of the relevant commercial agreement.
21+
22+
import logging
23+
24+
from kubernetes_asyncio.client import ApiException, CustomObjectsApi
25+
26+
from crate.operator.config import config
27+
from crate.operator.constants import CloudProvider
28+
from crate.operator.utils.k8s_api_client import GlobalApiClient
29+
30+
31+
async def delete_cratedb(
32+
namespace: str,
33+
name: str,
34+
logger: logging.Logger,
35+
) -> None:
36+
"""
37+
Clean up cluster-scoped resources that cannot be garbage-collected
38+
via Kubernetes owner references.
39+
40+
Namespace-scoped resources (Services, Secrets, StatefulSets, etc.)
41+
are cleaned up automatically via owner references. However,
42+
SecurityContextConstraints are cluster-scoped and require explicit
43+
deletion here.
44+
"""
45+
await _delete_crate_scc(namespace, name, logger)
46+
47+
48+
async def _delete_crate_scc(
49+
namespace: str,
50+
name: str,
51+
logger: logging.Logger,
52+
) -> None:
53+
"""
54+
Delete the OpenShift SecurityContextConstraint for a CrateDB cluster.
55+
"""
56+
if config.CLOUD_PROVIDER != CloudProvider.OPENSHIFT:
57+
return
58+
59+
scc_name = f"crate-anyuid-{namespace}-{name}"
60+
61+
async with GlobalApiClient() as api_client:
62+
custom_api = CustomObjectsApi(api_client)
63+
try:
64+
await custom_api.delete_cluster_custom_object(
65+
group="security.openshift.io",
66+
version="v1",
67+
plural="securitycontextconstraints",
68+
name=scc_name,
69+
)
70+
logger.info("Deleted SCC %s", scc_name)
71+
except ApiException as e:
72+
if e.status == 404:
73+
logger.info("SCC %s already deleted", scc_name)
74+
else:
75+
logger.warning(
76+
"Could not delete SCC %s (status=%s reason=%s) — "
77+
"it may need to be removed manually",
78+
scc_name,
79+
e.status,
80+
e.reason,
81+
)

crate/operator/main.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
)
3939
from crate.operator.handlers.handle_create_cratedb import create_cratedb
4040
from crate.operator.handlers.handle_create_grand_central import create_grand_central
41+
from crate.operator.handlers.handle_delete_cratedb import delete_cratedb
4142
from crate.operator.handlers.handle_notify_external_ip_changed import (
4243
external_ip_changed,
4344
)
@@ -149,6 +150,22 @@ async def cluster_create(
149150
await create_cratedb(namespace, meta, spec, patch, status, logger)
150151

151152

153+
@kopf.on.delete(API_GROUP, "v1", RESOURCE_CRATEDB, annotations=annotation_filter())
154+
async def cluster_delete(
155+
namespace: str,
156+
name: str,
157+
logger: logging.Logger,
158+
**_kwargs,
159+
):
160+
"""
161+
Handles deletion of CrateDB Clusters.
162+
163+
Cleans up cluster-scoped resources that cannot be garbage-collected
164+
via owner references (e.g. OpenShift SecurityContextConstraints).
165+
"""
166+
await delete_cratedb(namespace, name, logger)
167+
168+
152169
@kopf.on.update(
153170
API_GROUP,
154171
"v1",

crate/operator/sql.py

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from kubernetes_asyncio.stream import WsApiClient
1212

1313
from crate.operator.config import config
14+
from crate.operator.constants import CRATE_CONTROL_PORT, CloudProvider
1415
from crate.operator.utils.k8s_api_client import GlobalApiClient
1516
from crate.operator.utils.kubeapi import resolve_secret_key_ref
1617

@@ -195,7 +196,7 @@ async def execute_sql_via_crate_control(
195196
logger: logging.Logger,
196197
):
197198
control_host = f"crate-control-{name}.{namespace}.svc.cluster.local"
198-
url = f"http://{control_host}:5050/exec"
199+
url = f"http://{control_host}:{CRATE_CONTROL_PORT}/exec"
199200

200201
async with GlobalApiClient() as api_client:
201202
core = CoreV1Api(api_client)
@@ -209,7 +210,7 @@ async def execute_sql_via_crate_control(
209210
payload: Dict[str, Any] = {"stmt": sql}
210211
if args is not None:
211212
payload["args"] = args
212-
logger.info("Payload for sidecar request %s", payload)
213+
logger.info("Sending SQL to crate-control sidecar: %s", sql[:80])
213214

214215
async with httpx.AsyncClient(timeout=5.0) as client:
215216
resp = await client.post(
@@ -228,22 +229,6 @@ async def execute_sql_via_crate_control(
228229
return data
229230

230231

231-
async def crate_control_available(
232-
namespace: str,
233-
name: str,
234-
) -> bool:
235-
svc_name = f"crate-control-{name}"
236-
async with GlobalApiClient() as api_client:
237-
core = CoreV1Api(api_client)
238-
try:
239-
await core.read_namespaced_service(svc_name, namespace)
240-
return True
241-
except ApiException as e:
242-
if e.status == 404:
243-
return False
244-
raise
245-
246-
247232
async def execute_sql(
248233
*,
249234
namespace: str,
@@ -254,7 +239,7 @@ async def execute_sql(
254239
args: list | None,
255240
logger: logging.Logger,
256241
) -> SQLResult:
257-
if await crate_control_available(namespace, name):
242+
if config.CLOUD_PROVIDER == CloudProvider.OPENSHIFT:
258243
logger.info("Using sidecar SQL execution")
259244
resp = await execute_sql_via_crate_control(
260245
namespace=namespace,

sidecars/cratecontrol/main.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
# with Crate these terms will supersede the license and you may use the
2121
# software solely pursuant to the terms of the relevant commercial agreement.
2222

23+
import hmac
2324
import json
2425
import logging
2526
import os
@@ -65,9 +66,12 @@ def abort_json(status: int, payload: dict):
6566
def verify_token():
6667
"""
6768
Verify bootstrap token from request header.
69+
70+
Uses hmac.compare_digest() for timing-safe comparison to prevent
71+
timing attacks that could leak the token value.
6872
"""
69-
token = request.headers.get("Token")
70-
if not token or token != BOOTSTRAP_TOKEN:
73+
token = request.headers.get("Token") or ""
74+
if not hmac.compare_digest(token, BOOTSTRAP_TOKEN):
7175
logger.warning("unauthorized_request")
7276
return False
7377
return True
@@ -108,11 +112,7 @@ def health():
108112
"""
109113
Health check endpoint.
110114
"""
111-
return {
112-
"status": "ok",
113-
"crate_url": CRATE_HTTP_URL,
114-
"ssl_verification": VERIFY_SSL,
115-
}
115+
return {"status": "ok"}
116116

117117

118118
@app.post("/exec")
@@ -137,7 +137,7 @@ def exec_sql():
137137
response.content_type = "application/json"
138138

139139
if status >= 400:
140-
logger.error("query_failed status=%d: %s", status, body[:200])
140+
logger.error("query_failed status=%d", status)
141141
response.status = status
142142
return body
143143

0 commit comments

Comments
 (0)