#!/usr/bin/env python3 """Create and re-fetch a bounded encrypted telemetry object; no delete/list operations.""" import argparse import base64 from datetime import datetime, timezone import hashlib import json import os from pathlib import Path import secrets from state_hub_preflight_lane import assert_cluster, command, data ENDPOINT = 'https://s3.nl-ams.scw.cloud' BUCKET = 'railiance-platform-pg-backup' PREFIX = 'platform-pg/application-archives/telemetry/' LIMIT = 256 * 1024**2 def digest(path): with path.open('rb') as stream: return hashlib.file_digest(stream, 'sha256').hexdigest() def run(args, receipt): source = json.loads(args.source_receipt.read_text()) if source.get('status') != 'captured' or not source.get('production_resumed'): raise ValueError('verified_capture_required') if not 0 < args.source.stat().st_size <= LIMIT or digest(args.source) != source['ciphertext_sha256']: raise ValueError('source_mismatch') with args.source.open('rb') as stream: if stream.read(22) != b'age-encryption.org/v1\n': raise ValueError('encrypted_artifact_required') k = ['kubectl', '--kubeconfig', args.kubeconfig] assert_cluster(k) values = data(command(k + ['-n', 'databases', 'get', 'secret', 'platform-pg-backup-s3', '-o', 'json']))['data'] import boto3 from botocore.config import Config client = boto3.client('s3', endpoint_url=ENDPOINT, region_name='nl-ams', aws_access_key_id=base64.b64decode(values['ACCESS_KEY_ID']).decode(), aws_secret_access_key=base64.b64decode(values['ACCESS_SECRET_KEY']).decode(), config=Config(signature_version='s3v4', connect_timeout=15, read_timeout=60, retries={'max_attempts': 2}, request_checksum_calculation='when_required', response_checksum_validation='when_required', s3={'addressing_style': 'path'})) key = PREFIX + datetime.now(timezone.utc).strftime('%Y/%m/%d/%H%M%S-') + secrets.token_hex(16) + '.tar.age' receipt.update(destination='s3://' + BUCKET + '/' + key, ciphertext_sha256=source['ciphertext_sha256'], ciphertext_bytes=args.source.stat().st_size) with args.source.open('rb') as stream: uploaded = client.put_object(Bucket=BUCKET, Key=key, Body=stream, ContentLength=args.source.stat().st_size, ContentType='application/octet-stream', Metadata={'sha256': source['ciphertext_sha256'], 'profile': 'telemetry-essentials-v1'}) version = uploaded.get('VersionId') receipt['version_id'] = version if not version or version == 'null': raise ValueError('version_pin_missing') response = client.get_object(Bucket=BUCKET, Key=key, VersionId=version) if response['ContentLength'] != args.source.stat().st_size: raise ValueError('download_length_mismatch') with response['Body'] as body, args.output.open('xb') as output: args.output.chmod(0o600) total = 0 while chunk := body.read(1024 * 1024): total += len(chunk) if total > LIMIT: raise ValueError('download_limit_exceeded') output.write(chunk) if digest(args.output) != source['ciphertext_sha256']: raise ValueError('download_digest_mismatch') receipt.update(status='fetched_pending_restore', version_pinned=True, download_hash_matches=True) def main(): p = argparse.ArgumentParser(description=__doc__) p.add_argument('--kubeconfig', required=True) for name in ['source', 'source-receipt', 'output', 'receipt']: p.add_argument('--' + name, type=Path, required=True) args = p.parse_args() if args.output.exists(): p.error('output already exists') fd = os.open(args.receipt, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) receipt = {'schema': 'platform.telemetry-primary-archive.v1', 'status': 'failed', 'started_at': datetime.now(timezone.utc).isoformat()} try: run(args, receipt) except Exception as error: receipt['error_type'] = type(error).__name__ finally: receipt['finished_at'] = datetime.now(timezone.utc).isoformat() with os.fdopen(fd, 'w') as output: json.dump(receipt, output, indent=2) return int(receipt['status'] != 'fetched_pending_restore') if __name__ == '__main__': raise SystemExit(main())