From dd54c35667a9c490b972b16f9c78890c3565a52d Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 22 Jul 2026 18:09:21 +0800 Subject: [PATCH] fix(signoz): bind alert rules export shape --- agent99-signoz-metadata-executor.ps1 | 4 +- config/signoz/metadata-export-policy.json | 2 +- ...rganization-canonical-route-migration.yaml | 2 +- .../p0-obs-002-post-closure-regression.yaml | 22 ++++-- .../test_signoz_metadata_export_contract.py | 25 +++++-- .../ops/deploy-signoz-metadata-toolchain.sh | 22 ++++++ ...oz_metadata_executor_runtime_entrypoint.py | 2 +- .../test_signoz_metadata_toolchain_deploy.py | 70 ++++++++++++++++++- .../signoz-canonical-route-preflight.py | 20 +++++- .../test_signoz_canonical_route_migration.py | 18 ++++- 10 files changed, 163 insertions(+), 24 deletions(-) diff --git a/agent99-signoz-metadata-executor.ps1 b/agent99-signoz-metadata-executor.ps1 index d46c40551..2cc168513 100644 --- a/agent99-signoz-metadata-executor.ps1 +++ b/agent99-signoz-metadata-executor.ps1 @@ -22,7 +22,7 @@ $EvidenceDir = Join-Path $AgentRoot "evidence" $RuntimeManifestPath = Join-Path $AgentRoot "state\runtime-manifest.json" $EntrypointPath = [string]$MyInvocation.MyCommand.Path $SafeIdPattern = "^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$" -$BundleId = "bb6d78b67f506072b2e45e92bc1d415364e25852282a9b068900665c578df011" +$BundleId = "337b5f86921d9b0af1a49e3bfe3e16c6a359f8fdfa1521c9dcbf230582ec286e" $ToolchainRoot = "/backup/toolchains/signoz-metadata/$BundleId" $PolicyPath = "$ToolchainRoot/metadata-export-policy.json" $ExporterPath = "$ToolchainRoot/signoz-metadata-export.py" @@ -56,7 +56,7 @@ $script:EvidenceReserved = $false $script:ApplyDispatchAttempted = $false $ToolchainFiles = [ordered]@{ - $PolicyPath = "df09dbf91d30bcf4d1a2119b1c301240a252fddd446c56ecba253d5399526533" + $PolicyPath = "3ac9dbc1fb74bd7c6a15e99c54c8119e930df229e9d7cf8e3e323596bebe17fb" $ExporterPath = "b184d55bd5601377d25e78e422de172d01c3f8b8ea2de948b65c915201094956" $SharedContractPath = "f719d3f2ec86acfafe12be1883e57e6675b8b64caab0ee16d6054c8b383fa213" $VerifierPath = "cbab56dd3919866a96550c1dce46ec67783cd6f00473e4491861e17d1430048c" diff --git a/config/signoz/metadata-export-policy.json b/config/signoz/metadata-export-policy.json index d26b0ac77..486ea6ecc 100644 --- a/config/signoz/metadata-export-policy.json +++ b/config/signoz/metadata-export-policy.json @@ -107,7 +107,7 @@ "id": "alert_rules", "endpoint": "/api/v1/rules", "method": "GET", - "response_pointer": "/data", + "response_pointer": "/data/rules", "response_kind": "collection", "classification": "portable", "restore": { diff --git a/ops/signoz/organization-canonical-route-migration.yaml b/ops/signoz/organization-canonical-route-migration.yaml index d0c37435c..82422778a 100644 --- a/ops/signoz/organization-canonical-route-migration.yaml +++ b/ops/signoz/organization-canonical-route-migration.yaml @@ -202,7 +202,7 @@ runtime_observations: clickhouse_native_failed_artifact_retention_status: pending_bounded_retention_decision clickhouse_native_resume_inventory_status: pending_durable_submitted_inventory_contract signoz_sqlite_application_consistent_backup_status: pending - signoz_metadata_export_source_contract_status: deployed_inactive_verified + signoz_metadata_export_source_contract_status: source_candidate_pending_deployment signoz_metadata_export_policy: config/signoz/metadata-export-policy.json signoz_metadata_exporter: scripts/backup/signoz-metadata-export.py signoz_metadata_export_verifier: scripts/backup/verify-signoz-metadata-export.py diff --git a/ops/signoz/p0-obs-002-post-closure-regression.yaml b/ops/signoz/p0-obs-002-post-closure-regression.yaml index 865d4c5f8..044e742e5 100644 --- a/ops/signoz/p0-obs-002-post-closure-regression.yaml +++ b/ops/signoz/p0-obs-002-post-closure-regression.yaml @@ -760,7 +760,7 @@ pending_verifiers: status: pending_supported_non_raw_export_or_coordinated_snapshot raw_sqlite_read_or_sync_allowed: false sqlite_cli_present_in_runtime: false - source_contract_status: deployed_inactive_verified + source_contract_status: source_candidate_pending_deployment source_contract: policy: config/signoz/metadata-export-policy.json exporter: scripts/backup/signoz-metadata-export.py @@ -772,15 +772,25 @@ pending_verifiers: deployer_tests: scripts/ops/tests/test_signoz_metadata_toolchain_deploy.py expected_file_count: 8 hashes: - policy_sha256: df09dbf91d30bcf4d1a2119b1c301240a252fddd446c56ecba253d5399526533 + policy_sha256: 3ac9dbc1fb74bd7c6a15e99c54c8119e930df229e9d7cf8e3e323596bebe17fb exporter_sha256: b184d55bd5601377d25e78e422de172d01c3f8b8ea2de948b65c915201094956 shared_contract_sha256: f719d3f2ec86acfafe12be1883e57e6675b8b64caab0ee16d6054c8b383fa213 independent_export_verifier_sha256: cbab56dd3919866a96550c1dce46ec67783cd6f00473e4491861e17d1430048c isolated_restore_driver_sha256: 38ce70b3f891a3ce8edfaeb880ccf69d0bb6817c3f801a0aefa0f06341901997 - contract_tests_sha256: 474d9dfcfe170c62425356ea9f06ed0dc1629b39215ee80f395f721c7d43a923 - controlled_deployer_sha256: 2d0f7ae20ff8919dfc0f6caa1c3e346c0d8127d997ffa46f86a6e92df212e6f4 - deployer_tests_sha256: eb6acd4da593f75dbd26a0abfc6674a60221ef7eac91e2f679f8ac75e9eda43e - test_terminal: 21_passed + contract_tests_sha256: 427093334d006f1a5d45c7f3691a22e1679c3279caf5b9f6d1b4954db2142b79 + controlled_deployer_sha256: e4183d1461fdce66af351d8bdcfe0626c3a240edd81eaee1d179498719f1cde0 + deployer_tests_sha256: 4b4b8185fc456d6cbe4ab56affdfa919bf48b4cd6f330cdf7bf413451d331209 + test_terminal: 24_passed + candidate_deployment: + status: pending_exact_check_apply_postcheck + asset_id: host110-signoz-metadata-toolchain + target_host: 192.168.0.110 + target_root: /backup/toolchains/signoz-metadata + bundle_id: 337b5f86921d9b0af1a49e3bfe3e16c6a359f8fdfa1521c9dcbf230582ec286e + target_path: /backup/toolchains/signoz-metadata/337b5f86921d9b0af1a49e3bfe3e16c6a359f8fdfa1521c9dcbf230582ec286e + active_pointer_present: false + runtime_export_executed: false + completion_claim: false deployment: status: deployed_inactive_verified asset_id: host110-signoz-metadata-toolchain diff --git a/scripts/backup/tests/test_signoz_metadata_export_contract.py b/scripts/backup/tests/test_signoz_metadata_export_contract.py index f53283dbd..e61b6bb42 100644 --- a/scripts/backup/tests/test_signoz_metadata_export_contract.py +++ b/scripts/backup/tests/test_signoz_metadata_export_contract.py @@ -26,7 +26,10 @@ TOKEN = "test-only-signoz-token-reference" SOURCE_ASSETS: dict[str, Any] = { "/api/v1/dashboards": {"data": [{"id": "dash-1", "title": "Ops"}]}, - "/api/v1/rules": {"data": [{"id": "rule-1", "name": "Latency"}]}, + "/api/v1/rules": { + "status": "success", + "data": {"rules": [{"id": "rule-1", "name": "Latency"}]}, + }, "/api/v1/explorer/views": {"data": [{"id": "view-1", "name": "Errors"}]}, "/api/v1/roles": {"data": [{"id": "role-1", "name": "Viewer"}]}, "/api/v1/org/preferences": {"data": [{"name": "theme", "value": "dark"}]}, @@ -74,9 +77,16 @@ class MetadataHandler(BaseHTTPRequestHandler): self._send_json(401, {"error": "unauthorized"}) return path = self.path.split("?", 1)[0] - if path not in self.assets or not isinstance( - self.assets[path].get("data"), list - ): + if path not in self.assets: + self._send_json(404, {"error": "not found"}) + return + data = self.assets[path].get("data") + collection = ( + data.get("rules") + if path == "/api/v1/rules" and isinstance(data, dict) + else data + ) + if not isinstance(collection, list): self._send_json(404, {"error": "not found"}) return length = int(self.headers.get("Content-Length", "0")) @@ -86,7 +96,7 @@ class MetadataHandler(BaseHTTPRequestHandler): return type(self).post_count += 1 restored = {"id": f"server-{type(self).post_count}", **value} - self.assets[path]["data"].append(restored) + collection.append(restored) self._send_json(201, {"data": restored}) @@ -254,6 +264,9 @@ def test_policy_is_explicitly_non_raw_and_non_completion() -> None: "/api/v1/domains", "/api/v1/channels", } + alert_rules = next(item for item in policy["assets"] if item["id"] == "alert_rules") + assert alert_rules["endpoint"] == "/api/v1/rules" + assert alert_rules["response_pointer"] == "/data/rules" def test_check_mode_writes_nothing(tmp_path: Path) -> None: @@ -415,7 +428,7 @@ def test_restore_check_and_semantic_apply_are_bounded(tmp_path: Path) -> None: bundle, credential = create_bundle(tmp_path) target_assets = { "/api/v1/dashboards": {"data": []}, - "/api/v1/rules": {"data": []}, + "/api/v1/rules": {"status": "success", "data": {"rules": []}}, "/api/v1/explorer/views": {"data": []}, } with metadata_server(target_assets) as (target_base_url, handler): diff --git a/scripts/ops/deploy-signoz-metadata-toolchain.sh b/scripts/ops/deploy-signoz-metadata-toolchain.sh index b6519d42b..3c438f62b 100755 --- a/scripts/ops/deploy-signoz-metadata-toolchain.sh +++ b/scripts/ops/deploy-signoz-metadata-toolchain.sh @@ -17,6 +17,8 @@ SCP_TIMEOUT_SECONDS="${SIGNOZ_METADATA_SCP_TIMEOUT_SECONDS:-120}" REMOTE_TIMEOUT_SECONDS="${SIGNOZ_METADATA_REMOTE_TIMEOUT_SECONDS:-60}" KILL_AFTER_SECONDS="${SIGNOZ_METADATA_KILL_AFTER_SECONDS:-10}" LOCAL_PYTHON_BIN="${SIGNOZ_METADATA_LOCAL_PYTHON_BIN:-python3.11}" +SSH_IDENTITY_FILE="${SIGNOZ_METADATA_SSH_IDENTITY_FILE:-}" +SSH_KNOWN_HOSTS_FILE="${SIGNOZ_METADATA_KNOWN_HOSTS_FILE:-}" LABELS=( metadata-export-policy.json @@ -35,11 +37,27 @@ LOCAL_SOURCES=( MODES=(0644 0644 0755 0755 0755) SSH_OPTIONS=( -o BatchMode=yes + -o IdentitiesOnly=yes + -o StrictHostKeyChecking=yes -o ConnectTimeout=8 -o ConnectionAttempts=1 -o ServerAliveInterval=5 -o ServerAliveCountMax=2 ) +if [ -n "${SSH_IDENTITY_FILE}" ]; then + [ -f "${SSH_IDENTITY_FILE}" ] && [ ! -L "${SSH_IDENTITY_FILE}" ] || { + echo "invalid SSH identity file" >&2 + exit 64 + } + SSH_OPTIONS+=( -i "${SSH_IDENTITY_FILE}" ) +fi +if [ -n "${SSH_KNOWN_HOSTS_FILE}" ]; then + [ -f "${SSH_KNOWN_HOSTS_FILE}" ] && [ ! -L "${SSH_KNOWN_HOSTS_FILE}" ] || { + echo "invalid SSH known_hosts file" >&2 + exit 64 + } + SSH_OPTIONS+=( -o "UserKnownHostsFile=${SSH_KNOWN_HOSTS_FILE}" ) +fi usage() { cat <<'USAGE' @@ -50,6 +68,10 @@ Required environment: RUN_ID Unique path-safe execution identifier WORK_ITEM_ID Must be P0-OBS-002 +Optional fixed transport paths: + SIGNOZ_METADATA_SSH_IDENTITY_FILE + SIGNOZ_METADATA_KNOWN_HOSTS_FILE + --check validates the five local sources and performs a no-write host110 diff. --apply creates one immutable exact-hash directory. It does not create or change an active pointer and does not run an authenticated metadata export. diff --git a/scripts/ops/tests/test_agent99_signoz_metadata_executor_runtime_entrypoint.py b/scripts/ops/tests/test_agent99_signoz_metadata_executor_runtime_entrypoint.py index c7c87263e..4419cb803 100644 --- a/scripts/ops/tests/test_agent99_signoz_metadata_executor_runtime_entrypoint.py +++ b/scripts/ops/tests/test_agent99_signoz_metadata_executor_runtime_entrypoint.py @@ -31,7 +31,7 @@ def test_entrypoint_is_fixed_to_windows99_host110_and_p0_obs_002() -> None: assert "$result.entrypointHashMatched" in source assert '$CredentialFile = "/etc/awoooi/secrets/signoz-metadata-api-key"' in source assert ( - '$BundleId = "bb6d78b67f506072b2e45e92bc1d415364e25852282a9b068900665c578df011"' + '$BundleId = "337b5f86921d9b0af1a49e3bfe3e16c6a359f8fdfa1521c9dcbf230582ec286e"' in source ) assert '$BaseUrl = "http://127.0.0.1:8080"' in source diff --git a/scripts/ops/tests/test_signoz_metadata_toolchain_deploy.py b/scripts/ops/tests/test_signoz_metadata_toolchain_deploy.py index c2b2840eb..d49dc59bc 100644 --- a/scripts/ops/tests/test_signoz_metadata_toolchain_deploy.py +++ b/scripts/ops/tests/test_signoz_metadata_toolchain_deploy.py @@ -86,11 +86,18 @@ printf 'scp %s\n' "$*" >> "${TEST_EVENT_LOG:?}" def run_deployer( - tmp_path: Path, *, mode: str, apply: bool + tmp_path: Path, + *, + mode: str, + apply: bool, + env_overrides: dict[str, str] | None = None, ) -> subprocess.CompletedProcess[str]: + env = fake_environment(tmp_path, mode=mode) + if env_overrides: + env.update(env_overrides) return subprocess.run( ["bash", str(DEPLOYER), "--apply" if apply else "--check"], - env=fake_environment(tmp_path, mode=mode), + env=env, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, @@ -165,6 +172,65 @@ def test_deployer_has_no_active_pointer_or_destructive_fallback() -> None: assert "ln -s" not in source +def test_transport_propagates_strict_fixed_paths_with_spaces(tmp_path: Path) -> None: + transport_dir = tmp_path / "transport paths" + transport_dir.mkdir() + identity = transport_dir / "agent99 identity" + known_hosts = transport_dir / "known hosts" + identity.write_text("test-only-identity\n", encoding="utf-8") + known_hosts.write_text("test-only-known-host\n", encoding="utf-8") + + result = run_deployer( + tmp_path, + mode="check_absent", + apply=False, + env_overrides={ + "SIGNOZ_METADATA_SSH_IDENTITY_FILE": str(identity), + "SIGNOZ_METADATA_KNOWN_HOSTS_FILE": str(known_hosts), + }, + ) + + assert result.returncode == 0, result.stderr + event = (tmp_path / "events.log").read_text(encoding="utf-8") + assert "IdentitiesOnly=yes" in event + assert "StrictHostKeyChecking=yes" in event + assert str(identity) in event + assert f"UserKnownHostsFile={known_hosts}" in event + + +def test_transport_rejects_missing_fixed_path_before_remote_use(tmp_path: Path) -> None: + result = run_deployer( + tmp_path, + mode="check_absent", + apply=False, + env_overrides={ + "SIGNOZ_METADATA_SSH_IDENTITY_FILE": str(tmp_path / "missing identity") + }, + ) + + assert result.returncode == 64 + assert "invalid SSH identity file" in result.stderr + assert not (tmp_path / "events.log").read_text(encoding="utf-8") + + +def test_transport_rejects_symlink_fixed_path_before_remote_use(tmp_path: Path) -> None: + target = tmp_path / "known-hosts-target" + target.write_text("test-only-known-host\n", encoding="utf-8") + symlink = tmp_path / "known hosts link" + symlink.symlink_to(target) + + result = run_deployer( + tmp_path, + mode="check_absent", + apply=False, + env_overrides={"SIGNOZ_METADATA_KNOWN_HOSTS_FILE": str(symlink)}, + ) + + assert result.returncode == 64 + assert "invalid SSH known_hosts file" in result.stderr + assert not (tmp_path / "events.log").read_text(encoding="utf-8") + + def test_shared_operation_lock_is_existing_read_only_and_fail_closed() -> None: source = DEPLOYER.read_text(encoding="utf-8") assert "OPERATION_LOCK=/tmp/awoooi-signoz-backup-operation.lock" in source diff --git a/scripts/reboot-recovery/signoz-canonical-route-preflight.py b/scripts/reboot-recovery/signoz-canonical-route-preflight.py index 4ef9cf80f..7c17ec555 100644 --- a/scripts/reboot-recovery/signoz-canonical-route-preflight.py +++ b/scripts/reboot-recovery/signoz-canonical-route-preflight.py @@ -858,6 +858,9 @@ def evaluate(root: Path, contract_path: Path) -> dict[str, Any]: "signoz_sqlite_application_consistent_backup", {} ) metadata_export_source_contract = sqlite_backup.get("source_contract", {}) + metadata_candidate_deployment = metadata_export_source_contract.get( + "candidate_deployment", {} + ) metadata_deployment = metadata_export_source_contract.get("deployment", {}) declared_metadata_export_hashes = metadata_export_source_contract.get("hashes", {}) actual_metadata_export_hashes = _current_metadata_export_source_hashes(root) @@ -866,7 +869,7 @@ def evaluate(root: Path, contract_path: Path) -> dict[str, Any]: == len(METADATA_EXPORT_SOURCE_PATHS) and set(declared_metadata_export_hashes) == set(METADATA_EXPORT_SOURCE_PATHS) and declared_metadata_export_hashes == actual_metadata_export_hashes - and metadata_export_source_contract.get("test_terminal") == "21_passed" + and metadata_export_source_contract.get("test_terminal") == "24_passed" ) offsite_backup = pending_verifiers.get("signoz_backup_offsite_dr", {}) failed_artifacts = pending_verifiers.get( @@ -889,7 +892,8 @@ def evaluate(root: Path, contract_path: Path) -> dict[str, Any]: == "pending_supported_non_raw_export_or_coordinated_snapshot" and sqlite_backup.get("raw_sqlite_read_or_sync_allowed") is False and sqlite_backup.get("sqlite_cli_present_in_runtime") is False - and sqlite_backup.get("source_contract_status") == "deployed_inactive_verified" + and sqlite_backup.get("source_contract_status") + == "source_candidate_pending_deployment" and metadata_export_source_contract.get("policy") == "config/signoz/metadata-export-policy.json" and metadata_export_source_contract.get("exporter") @@ -902,6 +906,18 @@ def evaluate(root: Path, contract_path: Path) -> dict[str, Any]: and metadata_export_source_contract.get("raw_volume_read") is False and metadata_export_source_contract.get("secret_value_persistence") is False and metadata_export_source_contract.get("full_sqlite_completion_claim") is False + and metadata_candidate_deployment.get("status") + == "pending_exact_check_apply_postcheck" + and metadata_candidate_deployment.get("asset_id") + == "host110-signoz-metadata-toolchain" + and metadata_candidate_deployment.get("target_host") == "192.168.0.110" + and metadata_candidate_deployment.get("bundle_id") + == "337b5f86921d9b0af1a49e3bfe3e16c6a359f8fdfa1521c9dcbf230582ec286e" + and metadata_candidate_deployment.get("target_path") + == "/backup/toolchains/signoz-metadata/337b5f86921d9b0af1a49e3bfe3e16c6a359f8fdfa1521c9dcbf230582ec286e" + and metadata_candidate_deployment.get("active_pointer_present") is False + and metadata_candidate_deployment.get("runtime_export_executed") is False + and metadata_candidate_deployment.get("completion_claim") is False and metadata_deployment.get("status") == "deployed_inactive_verified" and metadata_deployment.get("asset_id") == "host110-signoz-metadata-toolchain" and metadata_deployment.get("target_host") == "192.168.0.110" diff --git a/scripts/reboot-recovery/tests/test_signoz_canonical_route_migration.py b/scripts/reboot-recovery/tests/test_signoz_canonical_route_migration.py index 3ee5d5ae0..141a7de5f 100644 --- a/scripts/reboot-recovery/tests/test_signoz_canonical_route_migration.py +++ b/scripts/reboot-recovery/tests/test_signoz_canonical_route_migration.py @@ -834,7 +834,9 @@ def test_post_release_verifier_and_native_backup_close_with_remaining_gaps() -> ] assert sqlite_pending["raw_sqlite_read_or_sync_allowed"] is False assert sqlite_pending["sqlite_cli_present_in_runtime"] is False - assert sqlite_pending["source_contract_status"] == ("deployed_inactive_verified") + assert sqlite_pending["source_contract_status"] == ( + "source_candidate_pending_deployment" + ) source_contract = sqlite_pending["source_contract"] assert source_contract["policy"] == "config/signoz/metadata-export-policy.json" assert source_contract["exporter"] == ("scripts/backup/signoz-metadata-export.py") @@ -875,7 +877,17 @@ def test_post_release_verifier_and_native_backup_close_with_remaining_gaps() -> field: hashlib.sha256((ROOT / relative).read_bytes()).hexdigest() for field, relative in metadata_paths.items() } - assert source_contract["test_terminal"] == "21_passed" + assert source_contract["test_terminal"] == "24_passed" + candidate = source_contract["candidate_deployment"] + assert candidate["status"] == "pending_exact_check_apply_postcheck" + assert candidate["asset_id"] == "host110-signoz-metadata-toolchain" + assert candidate["target_host"] == "192.168.0.110" + assert candidate["bundle_id"] == ( + "337b5f86921d9b0af1a49e3bfe3e16c6a359f8fdfa1521c9dcbf230582ec286e" + ) + assert candidate["active_pointer_present"] is False + assert candidate["runtime_export_executed"] is False + assert candidate["completion_claim"] is False deployment = source_contract["deployment"] assert deployment["status"] == "deployed_inactive_verified" assert deployment["asset_id"] == "host110-signoz-metadata-toolchain" @@ -1094,7 +1106,7 @@ def test_post_closure_contract_mirror_and_program_blockers_are_consistent() -> N post_closure["signoz_sqlite_application_consistent_backup_status"] == "pending" ) assert post_closure["signoz_metadata_export_source_contract_status"] == ( - "deployed_inactive_verified" + "source_candidate_pending_deployment" ) assert post_closure["signoz_metadata_export_policy"] == ( "config/signoz/metadata-export-policy.json"