titan-iac/services/veles/migration-jobs/cassandra-artifact-export.yaml

160 lines
5.9 KiB
YAML

# services/veles/migration-jobs/cassandra-artifact-export.yaml
# Scale to one only during the Veles -> Cassandra artifact copy, then return to
# zero after the Cassandra import job verifies the copied content.
apiVersion: v1
kind: Service
metadata:
name: veles-artifact-export-for-cassandra
namespace: veles
spec:
selector:
app: veles-artifact-export-for-cassandra
ports:
- name: http
port: 8765
targetPort: http
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: veles-artifact-export-for-cassandra
namespace: veles
spec:
replicas: 0
selector:
matchLabels:
app: veles-artifact-export-for-cassandra
template:
metadata:
labels:
app: veles-artifact-export-for-cassandra
spec:
automountServiceAccountToken: false
nodeSelector:
cassandra.bstein.dev/node-pool: oceanus
tolerations:
- key: veles.bstein.dev/simulation
operator: Equal
value: "true"
effect: NoSchedule
containers:
- name: exporter
image: python:3.12-alpine
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8765
env:
- name: SOURCE_ROOT
value: /source
command: ["python", "-c"]
args:
- |
from __future__ import annotations
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
import hashlib
import json
import os
import tarfile
ROOT = Path(os.environ.get("SOURCE_ROOT", "/source")).resolve()
EXCLUDE_TOP = {"lost+found", ".cassandra-artifact-migration"}
def iter_files():
for path in sorted(ROOT.rglob("*")):
if not path.is_file():
continue
rel = path.relative_to(ROOT).as_posix()
if rel.split("/", 1)[0] in EXCLUDE_TOP:
continue
yield path, rel
def digest_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def build_manifest() -> dict[str, object]:
files = []
total_bytes = 0
for path, rel in iter_files():
size = path.stat().st_size
total_bytes += size
files.append({"path": rel, "size": size, "sha256": digest_file(path)})
manifest_text = "\n".join(
f"{item['sha256']} {item['size']} {item['path']}" for item in files
)
return {
"schema": "cassandra.artifact_manifest.v1",
"source": "veles-artifacts",
"file_count": len(files),
"total_bytes": total_bytes,
"manifest_sha256": hashlib.sha256(manifest_text.encode()).hexdigest(),
"files": files,
}
MANIFEST = build_manifest()
MANIFEST_PATHS = [str(item["path"]) for item in MANIFEST["files"]]
class Handler(BaseHTTPRequestHandler):
def do_GET(self) -> None:
if self.path == "/healthz":
self.send_response(200)
self.end_headers()
self.wfile.write(b"ok\n")
return
if self.path == "/manifest.json":
payload = json.dumps(MANIFEST, sort_keys=True).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
return
if self.path == "/archive.tar":
self.send_response(200)
self.send_header("Content-Type", "application/x-tar")
self.end_headers()
with tarfile.open(fileobj=self.wfile, mode="w|") as archive:
for rel in MANIFEST_PATHS:
path = ROOT / rel
if not path.is_file():
raise FileNotFoundError(rel)
with path.open("rb") as handle:
info = archive.gettarinfo(str(path), arcname=rel)
archive.addfile(info, handle)
return
self.send_response(404)
self.end_headers()
def log_message(self, fmt: str, *args: object) -> None:
print(fmt % args, flush=True)
print(
"ready file_count={file_count} total_bytes={total_bytes} manifest={manifest_sha256}".format(
**MANIFEST
),
flush=True,
)
ThreadingHTTPServer(("0.0.0.0", 8765), Handler).serve_forever()
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: "2"
memory: 2Gi
volumeMounts:
- name: artifacts
mountPath: /source
readOnly: true
volumes:
- name: artifacts
persistentVolumeClaim:
claimName: veles-artifacts
readOnly: true