修复 Native 观测性兼容并补齐分层验证流程
This commit is contained in:
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
cache_dir=${IAM_GRADLE_CACHE:-${XDG_CACHE_HOME:-$HOME/.cache}/iam-login/gradle}
|
||||
mkdir -p -- "$cache_dir"
|
||||
cache_dir=$(cd -- "$cache_dir" && pwd)
|
||||
docker_cmd=(docker)
|
||||
if [[ ${IAM_DOCKER_USE_SUDO:-0} == 1 ]]; then
|
||||
docker_cmd=(sudo -n docker)
|
||||
fi
|
||||
if (($# == 0)); then
|
||||
set -- test
|
||||
fi
|
||||
|
||||
# Linux host networking is needed for Testcontainers' published ports.
|
||||
exec "${docker_cmd[@]}" run --rm --network host \
|
||||
--cpus "${IAM_BUILD_CPUS:-4}" --memory "${IAM_BUILD_MEMORY:-8g}" \
|
||||
--user "$(id -u):$(id -g)" \
|
||||
--group-add "$(stat -c %g /var/run/docker.sock)" \
|
||||
-e LANG=C.UTF-8 \
|
||||
-e HOME=/gradle \
|
||||
-e JAVA_TOOL_OPTIONS=-Duser.home=/gradle \
|
||||
-e GRADLE_USER_HOME=/gradle \
|
||||
-e TESTCONTAINERS_HOST_OVERRIDE=127.0.0.1 \
|
||||
-v "$cache_dir:/gradle" \
|
||||
-v "$repo_root:/workspace" \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-w /workspace --entrypoint /workspace/gradlew \
|
||||
ghcr.io/graalvm/native-image-community@sha256:0d936f32bb8acb5bc60c41b33e05f064d7a6aaf36b726538296c54949bd4a3c0 \
|
||||
--no-daemon --max-workers="${IAM_BUILD_CPUS:-4}" "$@"
|
||||
Executable
+123
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exercise the real native executable without a JVM or production services."""
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import secrets
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
|
||||
def health_requests(metrics):
|
||||
total = 0.0
|
||||
for line in metrics.splitlines():
|
||||
match = re.match(r'^http_server_requests_seconds_count\{([^}]*)\}\s+([\d.eE+-]+)', line)
|
||||
if match and re.search(r'uri="/actuator/health[^"]*"', match[1]):
|
||||
total += float(match[2])
|
||||
return total
|
||||
|
||||
|
||||
def main():
|
||||
binary = Path(sys.argv[1] if len(sys.argv) > 1 else 'build/native/nativeCompile/iam-login').resolve()
|
||||
with binary.open('rb') as source:
|
||||
if source.read(4) != b'\x7fELF':
|
||||
raise RuntimeError('Expected a Linux native ELF executable')
|
||||
report_dir = Path('build/reports/native-smoke')
|
||||
report_dir.mkdir(parents=True, exist_ok=True)
|
||||
log_path = report_dir / 'application.log'
|
||||
result_path = report_dir / 'result.json'
|
||||
result_path.unlink(missing_ok=True)
|
||||
password = secrets.token_urlsafe(32)
|
||||
env = os.environ.copy()
|
||||
env.update(SPRING_SECURITY_USER_NAME='native-smoke', SPRING_SECURITY_USER_PASSWORD=password)
|
||||
started = time.monotonic()
|
||||
process = None
|
||||
try:
|
||||
with log_path.open('w') as log:
|
||||
process = subprocess.Popen([
|
||||
str(binary), '--server.address=127.0.0.1', '--server.port=0',
|
||||
], env=env, stdout=log, stderr=subprocess.STDOUT)
|
||||
deadline = started + 60
|
||||
while True:
|
||||
if process.poll() is not None:
|
||||
raise RuntimeError(f'Native process exited before startup; see {log_path}')
|
||||
match = re.search(r'Tomcat started on port (\d+)', log_path.read_text(errors='replace'))
|
||||
if match:
|
||||
break
|
||||
if time.monotonic() > deadline:
|
||||
raise RuntimeError(f'Native startup timed out; see {log_path}')
|
||||
time.sleep(0.1)
|
||||
base_url = f'http://127.0.0.1:{match[1]}'
|
||||
basic = base64.b64encode(f'native-smoke:{password}'.encode()).decode()
|
||||
# Keep loopback traffic independent of developer proxy settings.
|
||||
client = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
||||
|
||||
def request(path, authenticated=False):
|
||||
headers = {'Accept': 'text/plain' if authenticated and path == '/actuator/prometheus' else 'application/json'}
|
||||
if authenticated:
|
||||
headers['Authorization'] = f'Basic {basic}'
|
||||
req = urllib.request.Request(base_url + path, headers=headers)
|
||||
try:
|
||||
with client.open(req, timeout=10) as response:
|
||||
return response.status, response.read().decode()
|
||||
except urllib.error.HTTPError as error:
|
||||
return error.code, error.read().decode()
|
||||
|
||||
while True:
|
||||
status, body = request('/actuator/health/liveness')
|
||||
if status == 200 and json.loads(body)['status'] == 'UP':
|
||||
break
|
||||
if time.monotonic() > deadline:
|
||||
raise RuntimeError('Liveness endpoint did not become UP')
|
||||
time.sleep(0.1)
|
||||
ready_seconds = time.monotonic() - started
|
||||
assert request('/actuator/prometheus')[0] == 401, 'Anonymous metrics must be rejected'
|
||||
assert request('/')[0] == 401, 'Anonymous application access must be rejected'
|
||||
status, before = request('/actuator/prometheus', authenticated=True)
|
||||
assert status == 200, 'Authenticated Prometheus scrape failed'
|
||||
for _ in range(3):
|
||||
assert request('/actuator/health/liveness')[0] == 200
|
||||
metric_deadline = time.monotonic() + 5
|
||||
while True:
|
||||
status, after = request('/actuator/prometheus', authenticated=True)
|
||||
assert status == 200
|
||||
delta = health_requests(after) - health_requests(before)
|
||||
if delta >= 3:
|
||||
break
|
||||
if time.monotonic() > metric_deadline:
|
||||
raise RuntimeError(f'HTTP metric did not count health requests: {delta}')
|
||||
time.sleep(0.1)
|
||||
memory = Path(f'/proc/{process.pid}/status').read_text()
|
||||
rss = re.search(r'^VmRSS:\s+(\d+) kB', memory, re.MULTILINE)
|
||||
report = {
|
||||
'binary_bytes': binary.stat().st_size,
|
||||
'ready_seconds': round(ready_seconds, 3),
|
||||
'rss_after_requests_kib': int(rss[1]) if rss else None,
|
||||
'health_request_count_delta': delta,
|
||||
'liveness': 'UP',
|
||||
'anonymous_application_and_metrics': 401,
|
||||
'authenticated_metrics': 200,
|
||||
'scope': 'native bootstrap only; no directory readiness, AD/MFA/Hydra or trace-export validation',
|
||||
}
|
||||
finally:
|
||||
if process is not None and process.poll() is None:
|
||||
process.terminate()
|
||||
try:
|
||||
process.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
process.wait()
|
||||
runtime_log = log_path.read_text(errors='replace')
|
||||
if re.search(r'MissingReflectionRegistrationError|MissingResourceRegistrationError|UnsupportedFeatureError', runtime_log):
|
||||
raise RuntimeError(f'Native runtime registration error, including during shutdown; see {log_path}')
|
||||
result_path.write_text(json.dumps(report, indent=2) + '\n')
|
||||
print(json.dumps(report, indent=2))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user