v0.27.0
This commit is contained in:
1 parent
a9f9450f80
commit
762eb9e28b
81 files changed
+1242
-1095
No files matched your search
+74
-38
@@ -24,7 +24,7 @@ def read_docker_agent_source() -> str:
|
||||
|
||||
preflight(root)
|
||||
manifest = tomllib.loads((root / "Cargo.toml").read_text())
|
||||
assert manifest["package"]["version"] == "0.26.7"
|
||||
assert manifest["package"]["version"] == "0.27.0"
|
||||
assert manifest["dependencies"]["quinn"]["version"] == "0.11.12"
|
||||
h3dep = manifest["dependencies"]["h3"]
|
||||
assert h3dep["package"] == "hpx-h3" and h3dep["version"] == "=2.5.19" and "quinn" in h3dep["features"]
|
||||
@@ -32,14 +32,14 @@ assert "h3-quinn" not in manifest["dependencies"]
|
||||
lock_path = root / "Cargo.lock"
|
||||
if lock_path.exists():
|
||||
lock = lock_path.read_text()
|
||||
assert 'name = "rust-proxy"\nversion = "0.26.7"' in lock
|
||||
assert 'name = "rust-proxy"\nversion = "0.27.0"' in lock
|
||||
assert 'name = "systemstat"' in lock and 'name = "metrics-process"' in lock and 'name = "sysinfo"' in lock, "Cargo.lock must be resolver-generated after adding cross-platform statistics libraries"
|
||||
for release_doc, marker in {
|
||||
"README.md": "# RustProxy 0.26.7",
|
||||
"CHANGELOG.md": "## 0.26.7",
|
||||
"ROADMAP.md": "## 0.26.7",
|
||||
"AGENTS.md": "Current source version: 0.26.7.",
|
||||
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.26.7",
|
||||
"README.md": "# RustProxy 0.27.0",
|
||||
"CHANGELOG.md": "## 0.27.0",
|
||||
"ROADMAP.md": "## 0.27.0",
|
||||
"AGENTS.md": "Current source version: 0.27.0.",
|
||||
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.27.0",
|
||||
}.items():
|
||||
assert marker in (root / release_doc).read_text(), f"stale release identity in {release_doc}"
|
||||
assert tomllib.loads((root / "rust-toolchain.toml").read_text())["toolchain"]["channel"] == "stable"
|
||||
@@ -73,7 +73,7 @@ for marker in [
|
||||
]:
|
||||
assert marker in update_check_source, marker
|
||||
assert 'pub last_error:' not in update_check_source, "update-check errors must not change dashboard/API state"
|
||||
for name in ["README.md", "ROADMAP.md", "AGENTS.md", "CHANGELOG.md", "UPDATE_CHECK.toml", "Dockerfile", ".dockerignore", "docs/TEST_REPORT.md", "docs/V0.24.8_AUDIT.md", "docs/V0.24.7_AUDIT.md", "docs/V0.24.6_AUDIT.md", "docs/V0.24.5_AUDIT.md", "docs/V0.24.1_AUDIT.md", "docs/V0.24.0_AUDIT.md", "docs/V0.23.2_AUDIT.md", "docs/V0.23.1_AUDIT.md", "docs/V0.22.3_AUDIT.md", "docs/LIVE_LOGS.md", "docs/README.md", "docs/API.md", "docs/ARCHITECTURE.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/VHOSTS.md", "docs/STREAMS.md", "docs/REDIRECTS.md", "docs/CONTAINER.md", "docs/NPM_MIGRATION.md", "docs/DOCKER_DISCOVERY.md", "docker-agent/main.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/docker-agent.env.example", "docker-agent/install.sh", "src/docker.rs", "src/admin/web/docker.js", "scripts/migrate_npm.py", "docs/CUSTOM_LOCATIONS.md", "nagios/README.md", "nagios/check_rustproxy.py", "nagios/check_rustproxy_api", "nagios/check_rustproxy_backends", "nagios/check_rustproxy_latency", "nagios/check_rustproxy_queues", "src/admin/web/error_401.html", "src/admin/web/error_404.html", "src/admin/web/streams.js", "src/admin/web/redirects.js", "src/admin/web/host404.js", "docs/NOT_FOUND_HOSTS.md", "debian/control", "debian/rules", "debian/rustproxy.service", "debian/rustproxy.logrotate", "debian/build-package.sh", "debian/build-repository.sh", "grafana/rustproxy-overview.json", "grafana/README.md"]:
|
||||
for name in ["README.md", "ROADMAP.md", "AGENTS.md", "CHANGELOG.md", "UPDATE_CHECK.toml", "Dockerfile", ".dockerignore", "docs/TEST_REPORT.md", "docs/V0.24.8_AUDIT.md", "docs/V0.24.7_AUDIT.md", "docs/V0.24.6_AUDIT.md", "docs/V0.24.5_AUDIT.md", "docs/V0.24.1_AUDIT.md", "docs/V0.24.0_AUDIT.md", "docs/V0.23.2_AUDIT.md", "docs/V0.23.1_AUDIT.md", "docs/V0.22.3_AUDIT.md", "docs/LIVE_LOGS.md", "docs/README.md", "docs/API.md", "docs/ARCHITECTURE.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/VHOSTS.md", "docs/STREAMS.md", "docs/REDIRECTS.md", "docs/CONTAINER.md", "docs/NPM_MIGRATION.md", "docs/DOCKER_DISCOVERY.md", "docker-agent/main.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/docker-agent.env.example", "docker-agent/install.sh", "src/docker.rs", "src/admin/web/docker.js", "scripts/migrate_npm.py", "docs/CUSTOM_LOCATIONS.md", "nagios/README.md", "nagios/check_rustproxy.py", "nagios/check_rustproxy_api", "nagios/check_rustproxy_backends", "nagios/check_rustproxy_latency", "nagios/check_rustproxy_queues", "src/admin/web/error_401.html", "src/admin/web/error_404.html", "src/admin/web/streams.js", "src/admin/web/redirects.js", "src/admin/web/host404.js", "docs/NOT_FOUND_HOSTS.md", "grafana/rustproxy-overview.json", "grafana/README.md"]:
|
||||
assert (root / name).is_file(), name
|
||||
npm_doc = (root / "docs/NPM_MIGRATION.md").read_text(encoding="utf-8")
|
||||
for marker in ["scripts/migrate_npm_core.py", "scripts/migrate_npm_api.py", '-v "$PWD/scripts:/rustproxy-scripts:ro"', "python /rustproxy-scripts/migrate_npm.py"]:
|
||||
@@ -104,7 +104,7 @@ for path in [root / "make_zip.py", *(root / "scripts").glob("*.py")]:
|
||||
ast.parse(path.read_text(), str(path))
|
||||
subprocess.run(["bash", "-n", str(root / "dev.sh")], check=True)
|
||||
subprocess.run(["bash", "-n", str(root / "upgrade.sh")], check=True)
|
||||
for name in ["install.sh", "install-debian13.sh", "update.sh", "debian/build-package.sh", "debian/build-repository.sh", "debian/rustproxy.postinst", "nagios/check_rustproxy_api", "nagios/check_rustproxy_backends", "nagios/check_rustproxy_latency", "nagios/check_rustproxy_queues", "docker-agent/install.sh"]:
|
||||
for name in ["install.sh", "install-debian13.sh", "update.sh", "nagios/check_rustproxy_api", "nagios/check_rustproxy_backends", "nagios/check_rustproxy_latency", "nagios/check_rustproxy_queues", "docker-agent/install.sh"]:
|
||||
subprocess.run(["bash", "-n", str(root / name)], check=True)
|
||||
bootstrap = (root / "install-debian13.sh").read_text()
|
||||
for marker in [
|
||||
@@ -124,7 +124,6 @@ assert dev.count('cargo generate-lockfile') == 1, "dev.sh must only create Cargo
|
||||
unit = (root / "deploy/rustproxy.service").read_text()
|
||||
assert "LogsDirectory=rustproxy" in unit and "/var/log/rustproxy" in unit
|
||||
assert "Environment=MALLOC_ARENA_MAX=2" in unit
|
||||
assert "Environment=MALLOC_ARENA_MAX=2" in (root / "debian/rustproxy.service").read_text()
|
||||
assert "Environment=MALLOC_ARENA_MAX=2" in read_deploy_source()
|
||||
assert not re.search(r"\b20\d{2}[-/]\d{1,2}[-/]\d{1,2}\b", (root / "CHANGELOG.md").read_text()), "changelog contains a release date"
|
||||
subprocess.run(["python3", str(root / "scripts/langs_check.py")], check=True)
|
||||
@@ -268,7 +267,7 @@ assert 'Opt-in health decisions based on ordinary user traffic. Defaults to disa
|
||||
assert '"force_ssl"' in openapi and '"forward_client_ip"' in openapi
|
||||
assert '"backend_header"' in openapi and '"backend_timing_header"' in openapi
|
||||
assert '"StreamMetrics"' in openapi and '"streams"' in openapi
|
||||
for marker in ['"version": "0.26.7"', '"/api/traffic"', '"capture_user_agent"', '"capture_country"', '"hot_auto_trim_enabled"', '"hot_auto_trim_after_seconds"', '"hot_auto_trim_target_percent"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']:
|
||||
for marker in ['"version": "0.27.0"', '"/api/traffic"', '"capture_user_agent"', '"capture_country"', '"hot_auto_trim_enabled"', '"hot_auto_trim_after_seconds"', '"hot_auto_trim_target_percent"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']:
|
||||
assert marker in openapi, marker
|
||||
for marker in ['"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
|
||||
assert marker in openapi, marker
|
||||
@@ -395,7 +394,7 @@ for marker in ['effectiveCacheMemory', 'cacheMemoryHeadroom', 'cachePresetValues
|
||||
for marker in ['selectedTransformPreset', 'transformPresetValues', 'applyTransformPreset', 'updateTransformResourceSizing', 'memory_budget_bytes', 'max_inflight']:
|
||||
assert marker in js, marker
|
||||
assert 'envelope?.config.cache?.enabled === true && cacheToggle.value !== "off"' in vhosts_js
|
||||
for marker in ['transform: { enabled: false }', 'vhTransform', 'collectTransform']:
|
||||
for marker in ['transform: { enabled: false }', 'vhTransform', 'collectTransform', 'compression: { enabled: null, gzip: null, br: null, zstd: null, deflate: null }', 'vh-compression-enabled', 'vh-compression-zstd', 'vh-compression-br', 'vh-compression-gzip', 'vh-compression-deflate', 'collectCompression']:
|
||||
assert marker in vhosts_js, marker
|
||||
docker_js = (root / 'src/admin/web/docker.js').read_text()
|
||||
assert 'transform_enabled' in docker_js
|
||||
@@ -406,6 +405,9 @@ for marker in ['cp-hot-entries-meter', 'cp-memory', 'cp-cold-entries-meter', 'cp
|
||||
assert marker in html and marker in cache_panel_js, marker
|
||||
for marker in ['data.settings?.max_entries', 'data.settings?.cold_max_entries', 'data.settings?.cold_max_bytes', 'setMeter(']:
|
||||
assert marker in cache_panel_js, marker
|
||||
compact_css = css.replace(" ", "")
|
||||
for marker in ['progress::-webkit-progress-bar', 'progress::-webkit-progress-value', 'progress::-moz-progress-bar', '-webkit-appearance:none']:
|
||||
assert marker in compact_css, marker
|
||||
transform_panel_js = (root / 'src/admin/web/transform-panel.js').read_text()
|
||||
for marker in ['/api/transform', 'tp-cache-hits', 'transform-reason-list']:
|
||||
assert marker in transform_panel_js, marker
|
||||
@@ -752,21 +754,10 @@ assert 'minimal_welcome' in proxy_source and 'cache-control' in proxy_source
|
||||
make_zip = (root / "make_zip.py").read_text()
|
||||
for marker in ["SOURCE_DATE_EPOCH", "ZipInfo", "deterministic_mode", "compresslevel=9"]:
|
||||
assert marker in make_zip, marker
|
||||
assert "'debian'" in make_zip and "'grafana'" in make_zip and "'docker-agent'" in make_zip
|
||||
assert "'debian'" not in make_zip and "'grafana'" in make_zip and "'docker-agent'" in make_zip
|
||||
assert "'UPDATE_CHECK.toml'" in make_zip
|
||||
# Debian package contracts.
|
||||
debian_service = (root / 'debian/rustproxy.service').read_text()
|
||||
for marker in ['User=rustproxy', 'AmbientCapabilities=CAP_NET_BIND_SERVICE', 'StateDirectory=rustproxy', 'LogsDirectory=rustproxy', 'ProtectSystem=strict', 'RUST_PROXY_ADMIN_LISTEN']:
|
||||
if marker.startswith('RUST_PROXY_'):
|
||||
assert marker in (root / 'debian/rustproxy.environment').read_text(), marker
|
||||
else:
|
||||
assert marker in debian_service, marker
|
||||
logrotate = (root / 'debian/rustproxy.logrotate').read_text()
|
||||
for marker in ['daily', 'rotate 3', 'maxage 3', 'compresscmd /usr/bin/gzip', 'compressoptions -9', 'copytruncate', 'su rustproxy rustproxy']:
|
||||
assert marker in logrotate, marker
|
||||
repo_script = (root / 'debian/build-repository.sh').read_text()
|
||||
for marker in ['dpkg-scanpackages', 'apt-ftparchive', 'Packages.gz', 'RUSTPROXY_REPO_GPG_KEY', 'rustproxy.linuxiarz.pl']:
|
||||
assert marker in repo_script, marker
|
||||
# Debian packaging is maintained outside this source tree.
|
||||
assert not (root / 'debian').exists()
|
||||
grafana = json.loads((root / 'grafana/rustproxy-overview.json').read_text())
|
||||
assert grafana['uid'] == 'rustproxy-0170' and grafana['schemaVersion'] >= 39
|
||||
expressions = '\n'.join(target.get('expr','') for panel in grafana['panels'] for target in panel.get('targets',[]))
|
||||
@@ -776,7 +767,7 @@ assert any(variable.get('name') == 'traffic_vhost' for variable in grafana['temp
|
||||
assert any(panel.get('title') == 'Traffic analytics and Web Server' for panel in grafana['panels'])
|
||||
assert any(panel.get('title') == 'Traffic capture state' for panel in grafana['panels'])
|
||||
assert any(panel.get('title') == 'Top configured routes' for panel in grafana['panels'])
|
||||
# 0.19.0 compression/protocol/dashboard UX regressions.
|
||||
# 0.27.0 compression/protocol/dashboard UX regressions.
|
||||
compression_source = rust_source(root, "src/compression.rs")
|
||||
metrics_source = rust_source(root, "src/metrics.rs")
|
||||
admin_source = rust_source(root, "src/admin/mod.rs")
|
||||
@@ -784,21 +775,61 @@ style_source = (root / "src/admin/web/style.css").read_text()
|
||||
docker_ui = (root / "src/admin/web/docker.js").read_text()
|
||||
for marker in ["pub enum AppliedEncoding", "AppliedEncoding::Gzip", "AppliedEncoding::Brotli", "AppliedEncoding::Zstd", "AppliedEncoding::Deflate"]:
|
||||
assert marker in compression_source, marker
|
||||
assert "quality_weights_can_select_each_enabled_codec" in compression_source
|
||||
for marker in [
|
||||
"pub struct AllowCookie",
|
||||
'response.extensions().get::<AllowCookie>().is_none()',
|
||||
"cache_safe_cookie_marker_allows_compression_without_weakening_default_guard",
|
||||
"gzip: true", "br: true", "zstd: true", "deflate: false",
|
||||
"const PREFERENCE: [Self; 4] = [Self::Zstd, Self::Brotli, Self::Gzip, Self::Deflate]",
|
||||
"fn preferred_encoding", "pub struct Overrides", "pub fn scoped", "pub fn resolve",
|
||||
"defaults_enable_modern_compression_without_deflate",
|
||||
"equal_quality_uses_zstd_then_br_then_gzip_then_deflate",
|
||||
"quality_weights_win_before_server_priority",
|
||||
"cookies_authorization_and_private_cache_policy_do_not_disable_compression",
|
||||
"existing_content_encoding_is_never_recompressed",
|
||||
"no_transform_range_and_sse_are_not_compressed",
|
||||
"age_header_survives_downstream_compression",
|
||||
]:
|
||||
assert marker in compression_source, marker
|
||||
for forbidden in ["pub struct AllowCookie", "private_or_no_transform"]:
|
||||
assert forbidden not in compression_source, forbidden
|
||||
compression_finalize_source = rust_source(root, "src/proxy/upstream_helpers.rs")
|
||||
for marker in [
|
||||
'exchange.cache_status.load(Ordering::Relaxed) == 1',
|
||||
'response.extensions().get::<StaticSourceResponse>().is_some()',
|
||||
'.insert(crate::compression::AllowCookie)',
|
||||
"let global_compression = &snapshot.envelope.config.compression",
|
||||
"frontend.compression.resolve(global_compression, &compression)",
|
||||
"service.compression.resolve(global_compression, &compression)",
|
||||
]:
|
||||
assert marker in compression_finalize_source, marker
|
||||
assert "AllowCookie" not in compression_finalize_source
|
||||
openapi_0270 = json.loads((root / "src/admin/openapi.json").read_text())
|
||||
compression_override_properties = openapi_0270["components"]["schemas"]["CompressionOverrides"]["properties"]
|
||||
for prop in ["enabled", "zstd", "br", "gzip", "deflate"]:
|
||||
assert prop in compression_override_properties, prop
|
||||
docker_assignment_properties = openapi_0270["components"]["schemas"]["DockerAssignment"]["properties"]
|
||||
for prop in ["compression_enabled", "compression_zstd", "compression_br", "compression_gzip", "compression_deflate"]:
|
||||
assert prop in docker_assignment_properties, prop
|
||||
web_service_ui_0270 = (root / "src/admin/web/web-server.js").read_text()
|
||||
for marker in [
|
||||
'vh-compression-options',
|
||||
'compressionOptions.hidden = disabled',
|
||||
'input.disabled = disabled',
|
||||
]:
|
||||
assert marker in vhosts_js, marker
|
||||
for marker in [
|
||||
'const compressionFields = [[labeled("compressionZstd"',
|
||||
'field.hidden = disabled; control.disabled = disabled',
|
||||
]:
|
||||
assert marker in docker_ui, marker
|
||||
assert 'id="web-service-compression-options"' in html
|
||||
for marker in [
|
||||
'function syncCompression()',
|
||||
'$("web-service-compression-options").hidden = disabled',
|
||||
'$( `web-service-compression-${name}` ).disabled = disabled',
|
||||
'$("web-service-compression").addEventListener("change", syncCompression)',
|
||||
]:
|
||||
assert marker in web_service_ui_0270, marker
|
||||
settings_source_0270 = rust_source(root, "src/settings.rs")
|
||||
config_model_0270 = rust_source(root, "src/config/model.rs")
|
||||
for marker in ['mode: ServerMode::Custom', 'value: "RustProxy".into()', 'legacy_compat_default']:
|
||||
assert marker in settings_source_0270, marker
|
||||
assert 'default = "legacy_server_header_default"' in config_model_0270
|
||||
headers_source_0267 = rust_source(root, "src/headers.rs")
|
||||
vhost_core_0267 = (root / "src/admin/web/vhosts/core.js").read_text()
|
||||
for marker in [
|
||||
@@ -833,10 +864,15 @@ assert "dockerOpenApp" in docker_ui and "proxiedUrl(agent, hostname)" in docker_
|
||||
# 0.19.0 Docker-vhost controls: chip aliases, default agent ACL, redirect/WebSockets/cache.
|
||||
docker_source = rust_source(root, "src/docker.rs")
|
||||
management_ui = (root / "src/admin/web/management.js").read_text()
|
||||
for marker in ["function aliasEditor(", "host-chip-editor docker-alias-editor", ".docker-force-ssl", ".docker-websockets", ".docker-cache-enabled", ".docker-agent-acl-profile"]:
|
||||
for marker in ["function aliasEditor(", "host-chip-editor docker-alias-editor", ".docker-force-ssl", ".docker-websockets", ".docker-cache-enabled", ".docker-compression-enabled", ".docker-compression-zstd", ".docker-compression-br", ".docker-compression-gzip", ".docker-compression-deflate", ".docker-agent-acl-profile"]:
|
||||
assert marker in docker_ui, marker
|
||||
for marker in ['"force_ssl": assignment.force_ssl.unwrap_or(false)', '"websockets": assignment.websockets', '"cache": {"enabled": assignment.cache_enabled}', 'assignment.acl_profile_id.clone().or_else(|| agent.acl_profile_id.clone())']:
|
||||
for marker in ['"force_ssl": assignment.force_ssl.unwrap_or(false)', '"websockets": assignment.websockets', '"enabled": assignment.compression_enabled', '"zstd": assignment.compression_zstd', '"br": assignment.compression_br', '"gzip": assignment.compression_gzip', '"deflate": assignment.compression_deflate', '"cache": {"enabled": assignment.cache_enabled}', 'assignment.acl_profile_id.clone().or_else(|| agent.acl_profile_id.clone())', 'host.transient_unavailable = target.is_none()', 'inventory: previous_inventory']:
|
||||
assert marker in docker_source, marker
|
||||
for marker in ["published_assignment_stays_routable_as_503_when_service_is_missing", "failed_agent_observation_with_last_inventory_keeps_backend_materialized", "disabling_agent_stops_discovery_but_keeps_published_hostname_as_503"]:
|
||||
assert marker in docker_source, marker
|
||||
route_cache_0270 = rust_source(root, "src/proxy/forward/route_cache.rs")
|
||||
for marker in ['if route.unavailable', 'StatusCode::SERVICE_UNAVAILABLE', 'published Docker service is currently unavailable', 'retry-after']:
|
||||
assert marker in route_cache_0270, marker
|
||||
assert "agent.acl_profile_id === profile.id" in management_ui
|
||||
assert 'payload["backend_pool_domains"]' in admin_source and "status.backend_pool_domains" in vhosts_js
|
||||
assert 'body[data-density="compact"]' in style_source and '--ui-control: 22px' in style_source
|
||||
@@ -1285,7 +1321,7 @@ assert '"error_log" => {' not in admin_0241 and 'request("/api/config/error_log"
|
||||
assert '"error_log"' in openapi and 'logging, error_log, forwarding' not in openapi
|
||||
assert 'logging_section_persists_error_log_settings_atomically' in admin_0241 and 'error_log_is_not_a_standalone_configuration_section' in admin_0241
|
||||
assert 'location.pathname === "/logging"' in settings_js_0241
|
||||
assert '"RuntimeInformation"' in openapi and '"version": "0.26.7"' in openapi
|
||||
assert '"RuntimeInformation"' in openapi and '"version": "0.27.0"' in openapi
|
||||
|
||||
# 0.24.1 compiler and hostname-cardinality regressions.
|
||||
tls_0241 = rust_source(root, "src/tls.rs")
|
||||
@@ -1330,5 +1366,5 @@ for schema_name in ["RedirectHost", "NotFoundHost"]:
|
||||
node = openapi_0241["components"]["schemas"][schema_name]["properties"]["frontend"]["properties"]["hosts"]
|
||||
assert "maxItems" not in node, schema_name
|
||||
|
||||
print("PASS: 0.26.7 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, Debian/Grafana/Prometheus/404-host/custom-location/path/cache/dashboard/Nagios UI/source contracts, stopped legacy-renewal removal, session/admin/transport contracts, deterministic package source and embedded SQLite schema-v8 migrations.")
|
||||
print("PASS: 0.27.0 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, external-packaging/Grafana/Prometheus/404-host/custom-location/path/cache/dashboard/Nagios UI/source contracts, stopped legacy-renewal removal, session/admin/transport contracts, deterministic package source and embedded SQLite schema-v8 migrations.")
|
||||
print("NOT VERIFIED: Rust parsing, types, compilation, linking or runtime behavior. Run ./dev.sh test.")
|
||||
@@ -97,7 +97,7 @@ def digest(path: Path) -> str:
|
||||
def fetch_https(url: str, max_bytes: int) -> bytes:
|
||||
if urllib.parse.urlparse(url).scheme != 'https':
|
||||
raise ValueError('GeoIP downloads require HTTPS.')
|
||||
request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer/0.26.7'})
|
||||
request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer/0.27.0'})
|
||||
with urllib.request.urlopen(request, timeout=30) as response:
|
||||
if urllib.parse.urlparse(response.geturl()).scheme != 'https':
|
||||
raise ValueError('GeoIP download redirected away from HTTPS.')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rust-independent documentation/Reader consistency checks for the 0.26.7 release."""
|
||||
"""Rust-independent documentation/Reader consistency checks for the 0.27.0 release."""
|
||||
from pathlib import Path
|
||||
import re
|
||||
from rust_source import rust_source
|
||||
@@ -14,6 +14,7 @@ required = [
|
||||
DOCS / "README.md",
|
||||
DOCS / "WEB_SERVER.md",
|
||||
DOCS / "TODO.md",
|
||||
DOCS / "V0.27.0_IMPLEMENTATION.md",
|
||||
DOCS / "V0.26.7_IMPLEMENTATION.md",
|
||||
DOCS / "V0.25.5_AUDIT.md",
|
||||
DOCS / "V0.25.4_AUDIT.md",
|
||||
@@ -56,16 +57,16 @@ assert '"/docs/content.json"' in admin
|
||||
readme = (ROOT / "README.md").read_text(encoding="utf-8")
|
||||
roadmap = (ROOT / "ROADMAP.md").read_text(encoding="utf-8")
|
||||
changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
|
||||
assert readme.startswith("# RustProxy 0.26.7")
|
||||
assert readme.startswith("# RustProxy 0.27.0")
|
||||
assert "docs/WEB_SERVER.md" in readme and "docs/README.md" in readme
|
||||
assert readme.count("## What changed in ") == 1, "README duplicates historical release notes; use CHANGELOG/archive"
|
||||
assert "## Release history" in readme and "docs/ROADMAP_HISTORY.md" in readme
|
||||
assert roadmap.startswith("# RustProxy roadmap") and "## 0.26.7" in roadmap and "## 0.26.6" in roadmap and "## 0.26.5" in roadmap and "## 0.26.4" in roadmap and "## 0.26.0" in roadmap and "## 0.25.5" in roadmap and "## 0.25.4" in roadmap and "## 0.25.3" in roadmap and "## 0.25.2" in roadmap and "## 0.25.1" in roadmap and "## 0.25.0" in roadmap
|
||||
assert roadmap.startswith("# RustProxy roadmap") and "## 0.27.0" in roadmap and "## 0.26.7" in roadmap and "## 0.26.6" in roadmap and "## 0.26.5" in roadmap and "## 0.26.4" in roadmap and "## 0.26.0" in roadmap and "## 0.25.5" in roadmap and "## 0.25.4" in roadmap and "## 0.25.3" in roadmap and "## 0.25.2" in roadmap and "## 0.25.1" in roadmap and "## 0.25.0" in roadmap
|
||||
assert "## 0.24." not in roadmap and "## Web Server follow-up" in roadmap and "docs/ROADMAP_HISTORY.md" in roadmap
|
||||
assert changelog.startswith("# Changelog\n")
|
||||
for release in ["0.26.7", "0.26.6", "0.26.5", "0.26.4", "0.26.1", "0.26.0", "0.25.6", "0.25.5", "0.25.4", "0.25.3"]:
|
||||
for release in ["0.27.0", "0.26.7", "0.26.6", "0.26.5", "0.26.4", "0.26.1", "0.26.0", "0.25.6", "0.25.5", "0.25.4", "0.25.3"]:
|
||||
assert f"## {release}" in changelog, release
|
||||
assert changelog.index("## 0.26.7") < changelog.index("## 0.26.6") < changelog.index("## 0.26.5") < changelog.index("## 0.26.4") < changelog.index("## 0.26.1") < changelog.index("## 0.26.0") < changelog.index("## 0.25.6") < changelog.index("## 0.25.5")
|
||||
assert changelog.index("## 0.27.0") < changelog.index("## 0.26.7") < changelog.index("## 0.26.6") < changelog.index("## 0.26.5") < changelog.index("## 0.26.4") < changelog.index("## 0.26.1") < changelog.index("## 0.26.0") < changelog.index("## 0.25.6") < changelog.index("## 0.25.5")
|
||||
assert "CHANGELOG_TECHNICAL_ARCHIVE.md" in changelog
|
||||
for developer_only in ["Clean the remaining compiler warnings", "Update package/source/UI/OpenAPI/release identity", "source regressions and update release identity"]:
|
||||
assert developer_only not in changelog, f"developer-only CHANGELOG detail: {developer_only}"
|
||||
|
||||
@@ -73,12 +73,12 @@ def check(root: Path, repair_known: bool = False) -> list[str]:
|
||||
flat = module.parent.with_suffix(".rs")
|
||||
if flat.is_file():
|
||||
raise ValueError(f"Conflicting Rust modules: {flat.relative_to(root)} and {module.relative_to(root)}")
|
||||
for path in ["src/auth/mod.rs", "src/auth/store.rs", "src/backup/snapshot.rs", "src/upstream.rs", "src/tls.rs", "src/proxy/websocket.rs", "src/cache/mod.rs", "src/cache/model.rs", "src/cache/lifecycle.rs", "src/cache/cold.rs", "src/cache/dashboard.rs", "src/cache/tickets.rs", "src/cache/lookup.rs", "src/cache/policy.rs", "src/cache/fill.rs", "src/cache/tests.rs", "src/settings.rs", "src/compression.rs", "src/diagnostics.rs", "src/traffic.rs", "src/system_stats/mod.rs", "src/system_stats/linux.rs", "src/vhost.rs", "src/web_server/mod.rs", "src/web_server/config.rs", "src/web_server/filesystem.rs", "src/web_server/security.rs", "src/web_server/response.rs", "src/web_server/request.rs", "src/web_server/routing.rs", "src/web_server/fastcgi.rs", "src/web_server/htaccess.rs", "src/web_server/serve.rs", "src/web_server/tests.rs", "src/balance.rs", "src/security.rs", "src/admin/web/vhosts/core.js", "src/admin/web/vhosts/editor.js", "src/admin/web/vhosts/render.js", "src/admin/web/web-server.js", "src/admin/web/streams.js", "src/admin/web/host404.js", "src/admin/web/app/core.js", "src/admin/web/app/preferences.js", "src/admin/web/app/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/management.js", "src/admin/web/drafts.js", "src/pages.rs", "scripts/mock_server.py", "scripts/mock_auth.py", "src/forwarding.rs", "src/tinyauth.rs", "src/http3.rs", "src/stream.rs", "src/admin/web/integrations.js", "src/admin/web/traffic-panel.js", "src/prometheus.rs", "src/admin/web/settings.js", "src/admin/web/docker.js", "src/docker.rs", "docker-agent/main.go", "docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go", "docker-agent/certificate.go", "docker-agent/server.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/install.sh", "docs/DOCKER_DISCOVERY.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/README.md", "docs/TODO.md", "docs/V0.25.5_AUDIT.md", "docs/V0.25.4_AUDIT.md", "docs/V0.25.3_AUDIT.md", "docs/V0.25.2_AUDIT.md", "docs/CHANGELOG_TECHNICAL_ARCHIVE.md", "docs/ROADMAP_HISTORY.md", "scripts/docs_check.py", "scripts/web_server_contracts.py", "scripts/rust_source.py", "scripts/rust_module_contracts.py", "scripts/source_module_contracts.py", "scripts/acme_provider.py", "src/basic_auth.rs", "scripts/deploy.py", "scripts/deploy_core.py", "scripts/deploy_runtime.py", "scripts/migrate_npm.py", "scripts/migrate_npm_core.py", "scripts/migrate_npm_api.py", "docs/NPM_MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/swagger_assets.py", "src/admin/documentation.rs", "src/admin/openapi.json", "langs/en_US.json", "langs/pl_pl.json", "Dockerfile", ".dockerignore", "docs/CONTAINER.md", "docs/PROMETHEUS.md", "docs/NOT_FOUND_HOSTS.md", "debian/control", "debian/rules", "debian/rustproxy.service", "debian/rustproxy.logrotate", "grafana/rustproxy-overview.json"]:
|
||||
for path in ["src/auth/mod.rs", "src/auth/store.rs", "src/backup/snapshot.rs", "src/upstream.rs", "src/tls.rs", "src/proxy/websocket.rs", "src/cache/mod.rs", "src/cache/model.rs", "src/cache/lifecycle.rs", "src/cache/cold.rs", "src/cache/dashboard.rs", "src/cache/tickets.rs", "src/cache/lookup.rs", "src/cache/policy.rs", "src/cache/fill.rs", "src/cache/tests.rs", "src/settings.rs", "src/compression.rs", "src/diagnostics.rs", "src/traffic.rs", "src/system_stats/mod.rs", "src/system_stats/linux.rs", "src/vhost.rs", "src/web_server/mod.rs", "src/web_server/config.rs", "src/web_server/filesystem.rs", "src/web_server/security.rs", "src/web_server/response.rs", "src/web_server/request.rs", "src/web_server/routing.rs", "src/web_server/fastcgi.rs", "src/web_server/htaccess.rs", "src/web_server/serve.rs", "src/web_server/tests.rs", "src/balance.rs", "src/security.rs", "src/admin/web/vhosts/core.js", "src/admin/web/vhosts/editor.js", "src/admin/web/vhosts/render.js", "src/admin/web/web-server.js", "src/admin/web/streams.js", "src/admin/web/host404.js", "src/admin/web/app/core.js", "src/admin/web/app/preferences.js", "src/admin/web/app/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/management.js", "src/admin/web/drafts.js", "src/pages.rs", "scripts/mock_server.py", "scripts/mock_auth.py", "src/forwarding.rs", "src/tinyauth.rs", "src/http3.rs", "src/stream.rs", "src/admin/web/integrations.js", "src/admin/web/traffic-panel.js", "src/prometheus.rs", "src/admin/web/settings.js", "src/admin/web/docker.js", "src/docker.rs", "docker-agent/main.go", "docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go", "docker-agent/certificate.go", "docker-agent/server.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/install.sh", "docs/DOCKER_DISCOVERY.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/README.md", "docs/TODO.md", "docs/V0.25.5_AUDIT.md", "docs/V0.25.4_AUDIT.md", "docs/V0.25.3_AUDIT.md", "docs/V0.25.2_AUDIT.md", "docs/CHANGELOG_TECHNICAL_ARCHIVE.md", "docs/ROADMAP_HISTORY.md", "scripts/docs_check.py", "scripts/web_server_contracts.py", "scripts/rust_source.py", "scripts/rust_module_contracts.py", "scripts/source_module_contracts.py", "scripts/acme_provider.py", "src/basic_auth.rs", "scripts/deploy.py", "scripts/deploy_core.py", "scripts/deploy_runtime.py", "scripts/migrate_npm.py", "scripts/migrate_npm_core.py", "scripts/migrate_npm_api.py", "docs/NPM_MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/swagger_assets.py", "src/admin/documentation.rs", "src/admin/openapi.json", "langs/en_US.json", "langs/pl_pl.json", "Dockerfile", ".dockerignore", "docs/CONTAINER.md", "docs/PROMETHEUS.md", "docs/NOT_FOUND_HOSTS.md", "grafana/rustproxy-overview.json"]:
|
||||
if not (root / path).is_file():
|
||||
raise ValueError(f"Incomplete RustProxy 0.26.7 source tree: missing {path}")
|
||||
raise ValueError(f"Incomplete RustProxy 0.27.0 source tree: missing {path}")
|
||||
text = (root / "src/lib.rs").read_text(encoding="utf-8")
|
||||
if '#[path = "auth/mod.rs"]' not in text:
|
||||
raise ValueError("src/lib.rs is not the 0.26.7 entry point; extract the complete package, not individual files.")
|
||||
raise ValueError("src/lib.rs is not the 0.27.0 entry point; extract the complete package, not individual files.")
|
||||
return messages
|
||||
|
||||
|
||||
|
||||
+27
-29
@@ -123,7 +123,7 @@ def upgrade_smoke(tmp: Path) -> None:
|
||||
assert backup.is_dir()
|
||||
assert (backup / "src/obsolete.rs").is_file()
|
||||
assert not (target / "src/obsolete.rs").exists()
|
||||
assert 'version = "0.26.7"' in (target / "Cargo.toml").read_text(encoding="utf-8")
|
||||
assert 'version = "0.27.0"' in (target / "Cargo.toml").read_text(encoding="utf-8")
|
||||
assert (target / "data/keep.txt").read_text(encoding="utf-8") == "preserve\n"
|
||||
assert (target / ".env").read_text(encoding="utf-8") == "RUST_PROXY_DEMO=0\n"
|
||||
assert (target / "langs/site.json").is_file()
|
||||
@@ -134,7 +134,7 @@ def upgrade_smoke(tmp: Path) -> None:
|
||||
assert (target / "docker-agent/certificate.go").is_file()
|
||||
assert (target / "docker-agent/server.go").is_file()
|
||||
manifest = json.loads((backup / "upgrade.json").read_text(encoding="utf-8"))
|
||||
assert manifest["to_version"] == "0.26.7"
|
||||
assert manifest["to_version"] == "0.27.0"
|
||||
assert manifest["removed_obsolete_runtime_state"] is True
|
||||
assert not legacy.exists()
|
||||
assert not (backup / "legacy-data").exists()
|
||||
@@ -571,7 +571,7 @@ def source_contract_smoke() -> None:
|
||||
assert marker in admin, marker
|
||||
anonymous_block = admin.split("fn anonymous_allowed", 1)[1].split("const CERTIFICATE_API_KEY_HEADER", 1)[0]
|
||||
assert '"/api/status"' not in anonymous_block
|
||||
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.26.7"' in openapi_0241
|
||||
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.27.0"' in openapi_0241
|
||||
assert 'id="panel-logging"' not in html
|
||||
for marker in ['id="settings-tab-logging"', 'id="settings-page-logging"', 'id="logging-form"', 'id="error-log-form"']:
|
||||
assert marker in html, marker
|
||||
@@ -985,7 +985,16 @@ def source_contract_smoke() -> None:
|
||||
vhosts_ui = read_vhosts_js(ROOT)
|
||||
for marker in ['pub enum AppliedEncoding', 'AppliedEncoding::Gzip', 'AppliedEncoding::Brotli', 'AppliedEncoding::Zstd', 'AppliedEncoding::Deflate']:
|
||||
assert marker in compression_source, marker
|
||||
assert 'quality_weights_can_select_each_enabled_codec' in compression_source
|
||||
for marker in [
|
||||
'gzip: true', 'br: true', 'zstd: true', 'deflate: false',
|
||||
'const PREFERENCE: [Self; 4] = [Self::Zstd, Self::Brotli, Self::Gzip, Self::Deflate]',
|
||||
'quality_weights_win_before_server_priority',
|
||||
'equal_quality_uses_zstd_then_br_then_gzip_then_deflate',
|
||||
'cookies_authorization_and_private_cache_policy_do_not_disable_compression',
|
||||
'existing_content_encoding_is_never_recompressed',
|
||||
]:
|
||||
assert marker in compression_source, marker
|
||||
assert 'AllowCookie' not in compression_source
|
||||
diagnostics_source = rust_source(ROOT, 'src/diagnostics.rs') + rust_source(ROOT, 'src/system_stats/mod.rs') + rust_source(ROOT, 'src/system_stats/linux.rs')
|
||||
for marker in ['pub struct NetworkInterfaceView', 'pub network_interfaces: Vec<NetworkInterfaceView>', 'sysinfo::Networks::new_with_refreshed_list', 'system.network_stats', 'collect_network_systemstat', 'normalize_network_interfaces', 'merge_network_interfaces']:
|
||||
assert marker in diagnostics_source, marker
|
||||
@@ -1003,13 +1012,19 @@ def source_contract_smoke() -> None:
|
||||
assert 'compressionCountersHelp' not in ui_html
|
||||
assert 'sparkline-h1' in (ROOT / 'src/admin/web/index.html').read_text(encoding='utf-8')
|
||||
assert 'body[data-density="compact"]' in style_source and '--ui-control: 22px' in style_source
|
||||
compact_style_0270 = style_source.replace(' ', '')
|
||||
for marker in ['progress::-webkit-progress-bar', 'progress::-webkit-progress-value', 'progress::-moz-progress-bar', '-webkit-appearance:none']:
|
||||
assert marker in compact_style_0270, marker
|
||||
assert 'status.backend_pool_domains' in vhosts_ui and 'payload["backend_pool_domains"]' in admin
|
||||
docker_source = rust_source(ROOT, 'src/docker.rs')
|
||||
management_ui = (ROOT / 'src/admin/web/management.js').read_text(encoding='utf-8')
|
||||
for marker in ['function aliasEditor(', 'host-chip-editor docker-alias-editor', '.docker-force-ssl', '.docker-websockets', '.docker-cache-enabled', '.docker-passive-enabled', '.docker-passive-5xx', '.docker-agent-acl-profile']:
|
||||
for marker in ['function aliasEditor(', 'host-chip-editor docker-alias-editor', '.docker-force-ssl', '.docker-websockets', '.docker-cache-enabled', '.docker-compression-enabled', '.docker-compression-zstd', '.docker-compression-br', '.docker-compression-gzip', '.docker-compression-deflate', '.docker-passive-enabled', '.docker-passive-5xx', '.docker-agent-acl-profile']:
|
||||
assert marker in docker_ui, marker
|
||||
for marker in ['"force_ssl": assignment.force_ssl.unwrap_or(false)', '"websockets": assignment.websockets', '"cache": {"enabled": assignment.cache_enabled}', '"passive_health": assignment.passive_health.clone()', 'assignment.acl_profile_id.clone().or_else(|| agent.acl_profile_id.clone())']:
|
||||
for marker in ['"force_ssl": assignment.force_ssl.unwrap_or(false)', '"websockets": assignment.websockets', '"enabled": assignment.compression_enabled', '"zstd": assignment.compression_zstd', '"br": assignment.compression_br', '"gzip": assignment.compression_gzip', '"deflate": assignment.compression_deflate', '"cache": {"enabled": assignment.cache_enabled}', '"passive_health": assignment.passive_health.clone()', 'assignment.acl_profile_id.clone().or_else(|| agent.acl_profile_id.clone())', 'host.transient_unavailable = target.is_none()', 'inventory: previous_inventory']:
|
||||
assert marker in docker_source, marker
|
||||
route_cache_0270 = rust_source(ROOT, 'src/proxy/forward/route_cache.rs')
|
||||
for marker in ['if route.unavailable', 'StatusCode::SERVICE_UNAVAILABLE', 'published Docker service is currently unavailable', 'retry-after']:
|
||||
assert marker in route_cache_0270, marker
|
||||
assert 'agent.acl_profile_id === profile.id' in management_ui
|
||||
vhost_source = rust_source(ROOT, 'src/vhost.rs')
|
||||
for marker in ['DEFAULT_PROXY_CONNECT_TIMEOUT_SECONDS: u64 = 10', 'DEFAULT_PROXY_RESPONSE_HEADER_TIMEOUT_SECONDS: u64 = 120', 'DEFAULT_PROXY_READ_TIMEOUT_SECONDS: u64 = 60', 'DEFAULT_PROXY_SEND_TIMEOUT_SECONDS: u64 = 60', 'proxy_timeouts_have_explicit_safe_defaults_but_keep_null_compatibility']:
|
||||
@@ -1028,7 +1043,7 @@ def source_contract_smoke() -> None:
|
||||
assert '.managed-update' in main and "flush_hot_to_cold_for_update" in main
|
||||
openapi = (ROOT / "src/admin/openapi.json").read_text(encoding="utf-8")
|
||||
assert '"/api/config/history/restore"' in openapi
|
||||
for marker in ['"version": "0.26.7"', '"memory_preset"', '"cold_preset"', '"memory_budget_bytes"', '"max_inflight"', '"preset"', '"session_days"', '"session_lifetime_seconds"', '"accept_local_listener_risk"', '"backend_header"', '"backend_timing_header"', '"cold_on_age"', '"match_type"', '"regex_insensitive"', '"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
|
||||
for marker in ['"version": "0.27.0"', '"memory_preset"', '"cold_preset"', '"memory_budget_bytes"', '"max_inflight"', '"preset"', '"session_days"', '"session_lifetime_seconds"', '"accept_local_listener_risk"', '"backend_header"', '"backend_timing_header"', '"cold_on_age"', '"match_type"', '"regex_insensitive"', '"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
|
||||
assert marker in openapi, marker
|
||||
assert '"software_update"' in openapi and '"update_available"' in openapi
|
||||
for marker in ['"/api/certificates/deploy"', '"CertificateDeploy"', '"CertificateDeployResult"', '"/api/certificates/reload"']:
|
||||
@@ -1116,24 +1131,9 @@ def container_contract_smoke() -> None:
|
||||
assert marker in ignore, marker
|
||||
assert (ROOT / "docs/CONTAINER.md").is_file()
|
||||
|
||||
def debian_grafana_contract_smoke() -> None:
|
||||
control = (ROOT / "debian/control").read_text(encoding="utf-8")
|
||||
rules = (ROOT / "debian/rules").read_text(encoding="utf-8")
|
||||
service = (ROOT / "debian/rustproxy.service").read_text(encoding="utf-8")
|
||||
environment = (ROOT / "debian/rustproxy.environment").read_text(encoding="utf-8")
|
||||
logrotate = (ROOT / "debian/rustproxy.logrotate").read_text(encoding="utf-8")
|
||||
postinst = (ROOT / "debian/rustproxy.postinst").read_text(encoding="utf-8")
|
||||
repo = (ROOT / "debian/build-repository.sh").read_text(encoding="utf-8")
|
||||
def grafana_contract_smoke() -> None:
|
||||
assert not (ROOT / "debian").exists(), "Debian packaging belongs to the external packaging repository"
|
||||
dashboard = json.loads((ROOT / "grafana/rustproxy-overview.json").read_text(encoding="utf-8"))
|
||||
assert "debhelper-compat (= 13)" in control and "cargo" in control and "rustc" in control
|
||||
assert "cargo build --release --locked" in rules and "cargo test --locked" in rules
|
||||
for marker in ["User=rustproxy", "Group=rustproxy", "Environment=MALLOC_ARENA_MAX=2", "AmbientCapabilities=CAP_NET_BIND_SERVICE", "StateDirectory=rustproxy", "LogsDirectory=rustproxy", "ProtectSystem=strict", "ReadWritePaths=/var/lib/rustproxy /var/log/rustproxy"]:
|
||||
assert marker in service, marker
|
||||
assert "RUST_PROXY_ADMIN_LISTEN=127.0.0.1:9090" in environment
|
||||
for marker in ["daily", "rotate 3", "maxage 3", "compresscmd /usr/bin/gzip", "compressoptions -9", "copytruncate", "su rustproxy rustproxy"]:
|
||||
assert marker in logrotate, marker
|
||||
assert "/var/log/rustproxy" in postinst and "-o rustproxy -g rustproxy" in postinst
|
||||
assert "rustproxy.linuxiarz.pl" in repo and "dpkg-scanpackages" in repo and "gzip -9 -n" in repo
|
||||
assert dashboard.get("uid") == "rustproxy-0170"
|
||||
queries = json.dumps(dashboard)
|
||||
for marker in ["rust_proxy_process_rss_bytes", "rust_proxy_cache_transient_bytes", "rust_proxy_backend_ewma_latency_seconds", "rust_proxy_certificate_renewal_not_after_timestamp_seconds", "rust_proxy_not_found_hosts_configured", "rust_proxy_http1_requests_total", "rust_proxy_backend_upstream_client_generation_requests", "rust_proxy_backend_upstream_client_generation_age_seconds", "rust_proxy_backend_retry_attempts_total", "rust_proxy_backend_upstream_client_rotations_total", "rust_proxy_docker_backend_upstream_client_generation_requests", "rust_proxy_docker_backend_retry_attempts_total"]:
|
||||
@@ -1181,9 +1181,7 @@ def reproducible_archive_smoke(tmp: Path) -> None:
|
||||
assert "RustProxy/nagios/check_rustproxy_api" in names
|
||||
assert "RustProxy/src/admin/web/host404.js" in names
|
||||
assert "RustProxy/docs/NOT_FOUND_HOSTS.md" in names
|
||||
assert "RustProxy/debian/control" in names
|
||||
assert "RustProxy/debian/rustproxy.service" in names
|
||||
assert "RustProxy/debian/rustproxy.logrotate" in names
|
||||
assert not any(name.startswith("RustProxy/debian/") for name in names)
|
||||
assert "RustProxy/grafana/rustproxy-overview.json" in names
|
||||
assert "RustProxy/UPDATE_CHECK.toml" in names
|
||||
assert "RustProxy/src/update_check.rs" in names
|
||||
@@ -1208,8 +1206,8 @@ def main() -> None:
|
||||
ui_contract_smoke()
|
||||
source_contract_smoke()
|
||||
container_contract_smoke()
|
||||
debian_grafana_contract_smoke()
|
||||
print("PASS: 0.26.7 SQLite/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, Debian packaging, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.")
|
||||
grafana_contract_smoke()
|
||||
print("PASS: 0.27.0 SQLite/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, external Debian-packaging separation, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -85,7 +85,7 @@ def download(destination: Path) -> None:
|
||||
"""TLS verification uses Python's default trust store; never executes a shell."""
|
||||
started = time.monotonic()
|
||||
opener = build_opener(HttpsRedirects())
|
||||
request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build/0.26.7"})
|
||||
request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build/0.27.0"})
|
||||
with opener.open(request, timeout=30) as response, destination.open("wb") as output:
|
||||
length = response.headers.get("Content-Length")
|
||||
if length and int(length) > MAX_ARCHIVE:
|
||||
|
||||
+3
-3
@@ -46,7 +46,7 @@ def upgrade(source: Path, target: Path, *, stopped: bool, data_dir: Path | None
|
||||
for name in MANAGED:
|
||||
if (target / name).is_symlink():
|
||||
raise ValueError(f"Managed target {name} is a symlink; refusing to replace it.")
|
||||
backup = target / ".upgrade-backup" / ("before-0.26.7-" + uuid.uuid4().hex[:12])
|
||||
backup = target / ".upgrade-backup" / ("before-0.27.0-" + uuid.uuid4().hex[:12])
|
||||
backup.mkdir(parents=True, mode=0o700)
|
||||
installed: list[str] = []
|
||||
moved: list[str] = []
|
||||
@@ -84,7 +84,7 @@ def upgrade(source: Path, target: Path, *, stopped: bool, data_dir: Path | None
|
||||
remove_legacy = legacy.exists()
|
||||
if remove_legacy and (legacy.is_symlink() or not legacy.is_file()):
|
||||
raise ValueError("Obsolete renewal observation sidecar must be a regular file.")
|
||||
(backup / "upgrade.json").write_text(json.dumps({"from_directory":str(target), "to_version":"0.26.7", "managed_paths":MANAGED, "removed_obsolete_runtime_state": remove_legacy}, indent=2)+"\n", encoding="utf-8")
|
||||
(backup / "upgrade.json").write_text(json.dumps({"from_directory":str(target), "to_version":"0.27.0", "managed_paths":MANAGED, "removed_obsolete_runtime_state": remove_legacy}, indent=2)+"\n", encoding="utf-8")
|
||||
if remove_legacy:
|
||||
legacy.unlink()
|
||||
except BaseException:
|
||||
@@ -115,7 +115,7 @@ def main() -> None:
|
||||
backup = upgrade(Path(__file__).resolve().parents[1], args.target, stopped=args.stopped, data_dir=data_dir)
|
||||
except (OSError, ValueError) as error:
|
||||
parser.exit(2, f"Upgrade refused or rolled back: {error}\n")
|
||||
print(f"Installed RustProxy 0.26.7. Previous managed source files: {backup}")
|
||||
print(f"Installed RustProxy 0.27.0. Previous managed source files: {backup}")
|
||||
print("Preserved data, .env, .git, target and other unmanaged files. No database migration was run.")
|
||||
print(f"Next: cd {args.target}; ./dev.sh test; ./dev.sh")
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ need(web,
|
||||
'HeaderValue::from_static("no-store")',
|
||||
"builtin_error_response(StatusCode::FORBIDDEN, &method)",
|
||||
"empty_index_can_go_directly_to_directory_listing",
|
||||
"RustProxy <b>0.26.7</b>",
|
||||
"RustProxy <b>0.27.0</b>",
|
||||
"traversal_and_symlink_escape_are_not_served",
|
||||
"try_files_rewrites_cache_rules_headers_and_downloads_work",
|
||||
"precompressed_sidecars_keep_mime_validators_and_range_semantics",
|
||||
@@ -117,9 +117,12 @@ need(proxy,
|
||||
"finish_headers(",
|
||||
"crate::compression::encode(")
|
||||
assert "crate::web_server::serve(service, &request" not in proxy
|
||||
assert proxy.index("basic_identity =") < proxy.index("CompiledSource::WebServer(service)")
|
||||
assert proxy.index("let identity =") < proxy.index("CompiledSource::WebServer(service)")
|
||||
assert proxy.index("CompiledSource::WebServer(service)") < proxy.index("let (backend, backend_lease, affinity_cookie)")
|
||||
basic_identity_pos = proxy.index("basic_identity =")
|
||||
identity_pos = proxy.index("let identity =")
|
||||
web_source_pos = proxy.index("CompiledSource::WebServer(service)", basic_identity_pos)
|
||||
assert basic_identity_pos < web_source_pos
|
||||
assert identity_pos < web_source_pos
|
||||
assert web_source_pos < proxy.index("let (backend, backend_lease, affinity_cookie)")
|
||||
custom_error_pos = proxy.index("let custom_error =")
|
||||
assert proxy.find("apply_response_rules(", custom_error_pos) > custom_error_pos
|
||||
need(acl, "ambiguous encoded path character", '"/%2e%2e/secret"', '"/a%2fb"', '"/a%5cb"', '"/bad%"')
|
||||
@@ -129,7 +132,7 @@ need(html, 'data-panel="web-server"', 'id="panel-web-server"', 'id="web-service-
|
||||
'data-web-service-page="mime"', 'data-web-service-page="cache"', 'data-web-service-page="delivery"',
|
||||
'data-web-service-page="php"', 'data-web-service-page="security"', 'data-web-service-page="htaccess"',
|
||||
'id="web-service-htaccess-enabled"', 'id="web-service-htaccess-help"', 'id="htaccess-help-dialog"', 'id="web-service-try-files"',
|
||||
'id="web-service-cache-rules"', 'id="web-service-precompressed"', 'id="web-service-php-enabled"',
|
||||
'id="web-service-cache-rules"', 'id="web-service-precompressed"', 'id="web-service-compression"', 'id="web-service-compression-zstd"', 'id="web-service-compression-br"', 'id="web-service-compression-gzip"', 'id="web-service-compression-deflate"', 'id="web-service-php-enabled"',
|
||||
'id="web-service-php-pass-authorization"', 'id="web-service-php-body-limit"',
|
||||
'id="web-service-php-header-limit"', 'id="web-service-php-stderr-limit"',
|
||||
'id="web-service-php-custom-params"', 'id="web-service-php-values"', 'id="php-help-dialog"',
|
||||
@@ -138,7 +141,7 @@ need(html, 'data-panel="web-server"', 'id="panel-web-server"', 'id="web-service-
|
||||
'data-logging-settings-tab="format"', 'data-logging-settings-tab="errors"')
|
||||
need(settings_ui, "selectLoggingTab", 'data-logging-settings-page', 'selectLoggingTab("outputs")')
|
||||
need(web_ui, 'saveSection("web-services"', "directory_listing", "web_service_id", "mime_types", "default_mime_type",
|
||||
"cache_control", "cache_rules", "response_headers", "try_files", "rewrites", "precompressed",
|
||||
"cache_control", "cache_rules", "response_headers", "try_files", "rewrites", "precompressed", "compression",
|
||||
"download_extensions", "symlink_policy", "php", "htaccess", "hardening", "presetConfig", "phpFrontController", "staticCacheRules",
|
||||
"pass_authorization", "max_request_body_bytes", "max_response_header_bytes", "stderr_log_bytes", "custom_params", "php_values",
|
||||
"syncHtaccess", "htaccess-help-dialog", "php-help-dialog", "parseFastCgiParams", "parsePhpValues", "validatePhpCustomData", "activateTab", "initTabs", "presetChanged",
|
||||
@@ -157,7 +160,11 @@ assert web_service_schema["properties"]["index"]["maxItems"] == 16
|
||||
assert web_service_schema["properties"]["mime_types"]["maxProperties"] == 64
|
||||
assert web_service_schema["properties"]["default_mime_type"]["default"] == "application/octet-stream"
|
||||
assert web_service_schema["properties"]["cache_control"]["nullable"] is True
|
||||
for prop in ["cache_rules", "response_headers", "try_files", "rewrites", "precompressed", "download_extensions", "symlink_policy", "php", "htaccess", "hardening"]:
|
||||
assert web_service_schema["properties"]["compression"]["$ref"].endswith("/CompressionOverrides")
|
||||
compression_override_schema = openapi["components"]["schemas"]["CompressionOverrides"]
|
||||
for prop in ["enabled", "zstd", "br", "gzip", "deflate"]:
|
||||
assert prop in compression_override_schema["properties"], prop
|
||||
for prop in ["cache_rules", "response_headers", "try_files", "rewrites", "precompressed", "compression", "download_extensions", "symlink_policy", "php", "htaccess", "hardening"]:
|
||||
assert prop in web_service_schema["properties"], prop
|
||||
assert web_service_schema["properties"]["symlink_policy"]["default"] == "deny"
|
||||
assert web_service_schema["properties"]["php"]["properties"]["enabled"]["default"] is False
|
||||
|
||||
@@ -45,7 +45,7 @@ const ids = [
|
||||
"web-service-php-connect-timeout","web-service-php-enabled","web-service-php-endpoint","web-service-php-extensions",
|
||||
"web-service-php-request-timeout","web-service-php-pass-authorization","web-service-php-body-limit","web-service-php-header-limit",
|
||||
"web-service-php-stderr-limit","web-service-php-custom-params","web-service-php-values","web-service-php-help",
|
||||
"web-service-precompressed","web-service-precompressed-br","web-service-precompressed-gzip",
|
||||
"web-service-compression","web-service-compression-options","web-service-compression-zstd","web-service-compression-br","web-service-compression-gzip","web-service-compression-deflate","web-service-precompressed","web-service-precompressed-br","web-service-precompressed-gzip",
|
||||
"web-service-preset","web-service-preset-apply","web-service-preset-summary","web-service-reset",
|
||||
"web-service-response-headers","web-service-rewrites","web-service-root","web-service-search",
|
||||
"web-service-symlink-policy","web-service-tabs","web-service-try-files"
|
||||
@@ -63,6 +63,8 @@ elements.get("web-service-php-pass-authorization").checked = true;
|
||||
elements.get("web-service-php-body-limit").value = "0";
|
||||
elements.get("web-service-php-header-limit").value = "65536";
|
||||
elements.get("web-service-php-stderr-limit").value = "4096";
|
||||
elements.get("web-service-compression").value = "";
|
||||
for (const name of ["zstd", "br", "gzip", "deflate"]) elements.get(`web-service-compression-${name}`).value = "";
|
||||
|
||||
const document = {
|
||||
body: new Element("body"),
|
||||
@@ -100,6 +102,23 @@ vm.createContext(context);
|
||||
vm.runInContext(fs.readFileSync("src/admin/web/drafts.js", "utf8"), context, { filename: "drafts.js" });
|
||||
vm.runInContext(fs.readFileSync("src/admin/web/web-server.js", "utf8"), context, { filename: "web-server.js" });
|
||||
|
||||
// Explicitly disabling scoped dynamic compression must hide and disable codec
|
||||
// inheritance controls without erasing their saved selections.
|
||||
const compressionMode = elements.get("web-service-compression");
|
||||
const compressionOptions = elements.get("web-service-compression-options");
|
||||
const compressionZstd = elements.get("web-service-compression-zstd");
|
||||
compressionZstd.value = "off";
|
||||
compressionMode.value = "off";
|
||||
compressionMode.dispatch("change");
|
||||
if (!compressionOptions.hidden) throw new Error("Disabled Web Service compression did not hide codec overrides");
|
||||
for (const name of ["zstd", "br", "gzip", "deflate"]) {
|
||||
if (!elements.get(`web-service-compression-${name}`).disabled) throw new Error(`Disabled Web Service compression left ${name} override active`);
|
||||
}
|
||||
compressionMode.value = "on";
|
||||
compressionMode.dispatch("change");
|
||||
if (compressionOptions.hidden) throw new Error("Re-enabled Web Service compression did not restore codec overrides");
|
||||
if (compressionZstd.value !== "off") throw new Error("Compression visibility toggle erased the saved codec selection");
|
||||
|
||||
const preset = elements.get("web-service-preset");
|
||||
preset.value = "wordpress";
|
||||
preset.dispatch("change");
|
||||
@@ -133,6 +152,8 @@ elements.get("web-service-php-custom-params").value = "APP_ENV = production";
|
||||
elements.get("web-service-php-values").value = "memory_limit = 256M";
|
||||
preset.value = "wordpress";
|
||||
preset.dispatch("change");
|
||||
elements.get("web-service-compression").value = "on";
|
||||
elements.get("web-service-compression-gzip").value = "off";
|
||||
await elements.get("web-service-form").dispatch("submit");
|
||||
if (savedSection?.name !== "web-services" || savedSection.value.length !== 1) throw new Error("WordPress preset save did not persist a Web Service");
|
||||
const saved = savedSection.value[0];
|
||||
@@ -140,6 +161,7 @@ if (saved.index.join("\n") !== expectedIndex) throw new Error("Saved WordPress p
|
||||
if (saved.try_files.join("\n") !== expectedTry) throw new Error("Saved WordPress preset lost try_files");
|
||||
if (!saved.php.enabled) throw new Error("Saved WordPress preset lost PHP-FPM enablement");
|
||||
if (!saved.precompressed.enabled) throw new Error("Saved WordPress preset lost precompressed enablement");
|
||||
if (saved.compression?.enabled !== true || saved.compression?.gzip !== false) throw new Error("Saved WordPress preset lost compression/codec override");
|
||||
if (saved.php.pass_authorization !== false) throw new Error("Saved WordPress preset lost Authorization forwarding choice");
|
||||
if (saved.php.max_request_body_bytes !== 1048576 || saved.php.max_response_header_bytes !== 131072 || saved.php.stderr_log_bytes !== 8192) throw new Error("Saved WordPress preset lost advanced FastCGI limits");
|
||||
if (saved.php.custom_params.APP_ENV !== "production") throw new Error("Saved WordPress preset lost custom FastCGI parameters");
|
||||
|
||||
Reference in new issue
Block a user