generated from coulomb/repo-seed
feat: add deployment zone overlays
This commit is contained in:
@@ -117,6 +117,7 @@ spec:
|
|||||||
default_data_classification: internal
|
default_data_classification: internal
|
||||||
expected_interface_types:
|
expected_interface_types:
|
||||||
- http-api
|
- http-api
|
||||||
|
- mcp-api
|
||||||
- event-stream
|
- event-stream
|
||||||
tags: [coordination, state-hub, planning]
|
tags: [coordination, state-hub, planning]
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,14 @@ spec:
|
|||||||
typical_auth_methods: [none, oidc, jwt, mtls, api_key]
|
typical_auth_methods: [none, oidc, jwt, mtls, api_key]
|
||||||
versioning: URL path, static asset version, and documented user-facing workflow compatibility.
|
versioning: URL path, static asset version, and documented user-facing workflow compatibility.
|
||||||
|
|
||||||
|
- id: mcp-api
|
||||||
|
name: MCP API
|
||||||
|
lifecycle: active
|
||||||
|
description: Model Context Protocol interface consumed by agents or agent runtimes.
|
||||||
|
category: api
|
||||||
|
typical_auth_methods: [none, oidc, jwt, mtls, api_key]
|
||||||
|
versioning: MCP protocol version, tool schema version, and documented capability compatibility.
|
||||||
|
|
||||||
- id: oidc-discovery
|
- id: oidc-discovery
|
||||||
name: OIDC discovery
|
name: OIDC discovery
|
||||||
lifecycle: active
|
lifecycle: active
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ Machine-readable catalog files:
|
|||||||
|------|-----------|----------|--------------|
|
|------|-----------|----------|--------------|
|
||||||
| `http-api` | active | api | `none`, `oidc`, `jwt`, `mtls`, `api_key` |
|
| `http-api` | active | api | `none`, `oidc`, `jwt`, `mtls`, `api_key` |
|
||||||
| `web-ui` | active | ui | `none`, `oidc`, `jwt`, `mtls`, `api_key` |
|
| `web-ui` | active | ui | `none`, `oidc`, `jwt`, `mtls`, `api_key` |
|
||||||
|
| `mcp-api` | active | api | `none`, `oidc`, `jwt`, `mtls`, `api_key` |
|
||||||
| `oidc-discovery` | active | identity | `none` |
|
| `oidc-discovery` | active | identity | `none` |
|
||||||
| `kubernetes-secret` | active | kubernetes | `kubernetes_service_account` |
|
| `kubernetes-secret` | active | kubernetes | `kubernetes_service_account` |
|
||||||
| `kubernetes-crd` | active | kubernetes | `kubernetes_service_account` |
|
| `kubernetes-crd` | active | kubernetes | `kubernetes_service_account` |
|
||||||
|
|||||||
@@ -0,0 +1,404 @@
|
|||||||
|
apiVersion: railiance.fabric/v1alpha1
|
||||||
|
kind: DeploymentZoneInventory
|
||||||
|
generated_at: "2026-05-24T00:00:00+02:00"
|
||||||
|
source:
|
||||||
|
repo: railiance-fabric
|
||||||
|
workplan: RAIL-FAB-WP-0020
|
||||||
|
method: source-search-and-declared-surfaces
|
||||||
|
scope:
|
||||||
|
note: >
|
||||||
|
This inventory captures deployment-zone overlay evidence. It does not
|
||||||
|
define fabric membership, port ownership, live health, or access policy.
|
||||||
|
deployment_environments:
|
||||||
|
- id: dev
|
||||||
|
scenario: bernd-laptop
|
||||||
|
intended_reachability: private operator workstation
|
||||||
|
- id: test
|
||||||
|
scenario: coulombcore
|
||||||
|
intended_reachability: shared collaborator and early-access test stage
|
||||||
|
- id: prod
|
||||||
|
scenario: railiance01
|
||||||
|
intended_reachability: production stage, currently alpha-accessible to developers
|
||||||
|
surfaces:
|
||||||
|
- id: dev.bernd-laptop.railiance-fabric.registry-api
|
||||||
|
name: Railiance Fabric registry HTTP API
|
||||||
|
repo: railiance-fabric
|
||||||
|
service_id: railiance-fabric.registry
|
||||||
|
deployment_environment: dev
|
||||||
|
deployment_scenario: bernd-laptop
|
||||||
|
access_zone: private-dev
|
||||||
|
exposure_class: local-only
|
||||||
|
routing_authority: local-loopback-binding
|
||||||
|
policy_authority: local-loopback-binding
|
||||||
|
route_evidence:
|
||||||
|
route: http://127.0.0.1:8765
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 8765
|
||||||
|
protocol: http
|
||||||
|
evidence:
|
||||||
|
- path: fabric/interfaces/railiance-fabric-registry-http-api.yaml
|
||||||
|
kind: fabric-interface-declaration
|
||||||
|
- id: dev.bernd-laptop.railiance-fabric.graph-explorer
|
||||||
|
name: Railiance Fabric graph explorer UI
|
||||||
|
repo: railiance-fabric
|
||||||
|
service_id: railiance-fabric.registry
|
||||||
|
deployment_environment: dev
|
||||||
|
deployment_scenario: bernd-laptop
|
||||||
|
access_zone: private-dev
|
||||||
|
exposure_class: local-only
|
||||||
|
routing_authority: local-loopback-binding
|
||||||
|
policy_authority: local-loopback-binding
|
||||||
|
route_evidence:
|
||||||
|
route: http://127.0.0.1:8765/ui/graph-explorer
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 8765
|
||||||
|
protocol: http
|
||||||
|
path: /ui/graph-explorer
|
||||||
|
evidence:
|
||||||
|
- path: fabric/interfaces/railiance-fabric-registry-graph-explorer-ui.yaml
|
||||||
|
kind: fabric-interface-declaration
|
||||||
|
- id: dev.bernd-laptop.state-hub.api
|
||||||
|
name: State Hub HTTP API
|
||||||
|
repo: the-custodian
|
||||||
|
service_id: the-custodian.state-hub
|
||||||
|
deployment_environment: dev
|
||||||
|
deployment_scenario: bernd-laptop
|
||||||
|
access_zone: private-dev
|
||||||
|
exposure_class: local-only
|
||||||
|
routing_authority: local-loopback-binding
|
||||||
|
policy_authority: local-loopback-binding
|
||||||
|
route_evidence:
|
||||||
|
route: http://127.0.0.1:8000
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 8000
|
||||||
|
protocol: http
|
||||||
|
evidence:
|
||||||
|
- path: fabric/interfaces/the-custodian-state-hub-http-api.yaml
|
||||||
|
kind: fabric-interface-declaration
|
||||||
|
- id: dev.bernd-laptop.state-hub.mcp
|
||||||
|
name: State Hub MCP API
|
||||||
|
repo: the-custodian
|
||||||
|
service_id: the-custodian.state-hub
|
||||||
|
deployment_environment: dev
|
||||||
|
deployment_scenario: bernd-laptop
|
||||||
|
access_zone: private-dev
|
||||||
|
exposure_class: local-only
|
||||||
|
routing_authority: local-loopback-binding
|
||||||
|
policy_authority: local-loopback-binding
|
||||||
|
route_evidence:
|
||||||
|
route: http://127.0.0.1:8001
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 8001
|
||||||
|
protocol: http
|
||||||
|
evidence:
|
||||||
|
- path: fabric/interfaces/the-custodian-state-hub-mcp-api.yaml
|
||||||
|
kind: fabric-interface-declaration
|
||||||
|
- id: dev.bernd-laptop.state-hub.dashboard
|
||||||
|
name: State Hub dashboard
|
||||||
|
repo: the-custodian
|
||||||
|
service_id: the-custodian.state-hub
|
||||||
|
deployment_environment: dev
|
||||||
|
deployment_scenario: bernd-laptop
|
||||||
|
access_zone: private-dev
|
||||||
|
exposure_class: local-only
|
||||||
|
routing_authority: local-loopback-binding
|
||||||
|
policy_authority: local-loopback-binding
|
||||||
|
route_evidence:
|
||||||
|
route: http://127.0.0.1:3000
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 3000
|
||||||
|
protocol: http
|
||||||
|
evidence:
|
||||||
|
- path: fabric/interfaces/the-custodian-state-hub-dashboard.yaml
|
||||||
|
kind: fabric-interface-declaration
|
||||||
|
- id: dev.bernd-laptop.net-kingdom.control-surface
|
||||||
|
name: NetKingdom control surface
|
||||||
|
repo: net-kingdom
|
||||||
|
service_id: net-kingdom.iam-profile
|
||||||
|
deployment_environment: dev
|
||||||
|
deployment_scenario: bernd-laptop
|
||||||
|
access_zone: private-dev
|
||||||
|
exposure_class: local-only
|
||||||
|
routing_authority: local-loopback-binding
|
||||||
|
policy_authority: local-loopback-binding
|
||||||
|
route_evidence:
|
||||||
|
route: http://127.0.0.1:8876
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 8876
|
||||||
|
protocol: http
|
||||||
|
evidence:
|
||||||
|
- path: fabric/interfaces/net-kingdom-control-surface-ui.yaml
|
||||||
|
kind: fabric-interface-declaration
|
||||||
|
- path: ../net-kingdom/sso-mfa/k8s/keycape/README.md
|
||||||
|
kind: source-search-hit
|
||||||
|
note: local OIDC callback lists localhost port 8876
|
||||||
|
- id: test.coulombcore.state-hub.http-tunnel
|
||||||
|
name: State Hub HTTP API tunnel to coulombcore
|
||||||
|
repo: railiance-infra
|
||||||
|
service_id: the-custodian.state-hub
|
||||||
|
deployment_environment: test
|
||||||
|
deployment_scenario: coulombcore
|
||||||
|
access_zone: collaborator-test
|
||||||
|
exposure_class: collaborator-test
|
||||||
|
routing_authority: ops-bridge
|
||||||
|
policy_authority: ops-bridge-ssh
|
||||||
|
route_evidence:
|
||||||
|
route: http://127.0.0.1:18000
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 18000
|
||||||
|
protocol: http
|
||||||
|
tunnel_target: coulombcore
|
||||||
|
evidence:
|
||||||
|
- path: ../railiance-infra/docs/deploy-stack.md
|
||||||
|
lines: "127"
|
||||||
|
kind: source-search-hit
|
||||||
|
- id: test.coulombcore.state-hub.mcp-tunnel
|
||||||
|
name: State Hub MCP tunnel to coulombcore
|
||||||
|
repo: railiance-infra
|
||||||
|
service_id: the-custodian.state-hub
|
||||||
|
deployment_environment: test
|
||||||
|
deployment_scenario: coulombcore
|
||||||
|
access_zone: collaborator-test
|
||||||
|
exposure_class: collaborator-test
|
||||||
|
routing_authority: ops-bridge
|
||||||
|
policy_authority: ops-bridge-ssh
|
||||||
|
route_evidence:
|
||||||
|
route: http://127.0.0.1:18001
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 18001
|
||||||
|
protocol: http
|
||||||
|
tunnel_target: coulombcore
|
||||||
|
evidence:
|
||||||
|
- path: ../railiance-infra/docs/deploy-stack.md
|
||||||
|
lines: "128"
|
||||||
|
kind: source-search-hit
|
||||||
|
- id: test.coulombcore.k3s-api-tunnel
|
||||||
|
name: k3s API tunnel to coulombcore
|
||||||
|
repo: railiance-infra
|
||||||
|
deployment_environment: test
|
||||||
|
deployment_scenario: coulombcore
|
||||||
|
access_zone: collaborator-test
|
||||||
|
exposure_class: collaborator-test
|
||||||
|
routing_authority: ops-bridge
|
||||||
|
policy_authority: ops-bridge-ssh
|
||||||
|
route_evidence:
|
||||||
|
route: https://127.0.0.1:16443
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 16443
|
||||||
|
protocol: https
|
||||||
|
tunnel_target: coulombcore
|
||||||
|
evidence:
|
||||||
|
- path: ../railiance-infra/docs/deploy-stack.md
|
||||||
|
lines: "129"
|
||||||
|
kind: source-search-hit
|
||||||
|
- path: ../railiance-cluster/SCOPE.md
|
||||||
|
lines: "127"
|
||||||
|
kind: source-search-hit
|
||||||
|
note: cluster scope states it runs on COULOMBCORE
|
||||||
|
- id: prod.railiance01.gitea
|
||||||
|
name: Gitea ingress
|
||||||
|
repo: railiance-apps
|
||||||
|
deployment_environment: prod
|
||||||
|
deployment_scenario: railiance01
|
||||||
|
access_zone: production-public
|
||||||
|
exposure_class: production-public
|
||||||
|
routing_authority: traefik
|
||||||
|
policy_authority: null
|
||||||
|
tls_authority: cert-manager:letsencrypt-prod
|
||||||
|
route_evidence:
|
||||||
|
route: https://gitea.coulomb.social
|
||||||
|
hostname: gitea.coulomb.social
|
||||||
|
port: 443
|
||||||
|
protocol: https
|
||||||
|
review:
|
||||||
|
status: candidate
|
||||||
|
note: access zone and policy authority require operator review
|
||||||
|
evidence:
|
||||||
|
- path: ../railiance-apps/manifests/gitea-ingress.yaml
|
||||||
|
lines: "2,12,14,16,27"
|
||||||
|
kind: kubernetes-ingress
|
||||||
|
- path: ../railiance-apps/workplans/railiance-apps-WP-0002-vergabe-teilnahme-on-railiance01.md
|
||||||
|
lines: "612,613"
|
||||||
|
kind: source-search-hit
|
||||||
|
note: places Gitea before vergabe-teilnahme on railiance01
|
||||||
|
- id: prod.railiance01.vergabe-teilnahme
|
||||||
|
name: Vergabe Teilnahme ingress
|
||||||
|
repo: railiance-apps
|
||||||
|
deployment_environment: prod
|
||||||
|
deployment_scenario: railiance01
|
||||||
|
access_zone: production-public
|
||||||
|
exposure_class: production-public
|
||||||
|
routing_authority: traefik
|
||||||
|
policy_authority: null
|
||||||
|
tls_authority: cert-manager:letsencrypt-prod
|
||||||
|
route_evidence:
|
||||||
|
route: https://vergabe-teilnahme.whywhynot.de
|
||||||
|
hostname: vergabe-teilnahme.whywhynot.de
|
||||||
|
port: 443
|
||||||
|
protocol: https
|
||||||
|
review:
|
||||||
|
status: candidate
|
||||||
|
note: production public classification is inferred from ingress host and workplan
|
||||||
|
evidence:
|
||||||
|
- path: ../railiance-apps/manifests/vergabe-teilnahme-ingress.yaml
|
||||||
|
lines: "2,11,13,15,26"
|
||||||
|
kind: kubernetes-ingress
|
||||||
|
- path: ../railiance-apps/workplans/railiance-apps-WP-0002-vergabe-teilnahme-on-railiance01.md
|
||||||
|
lines: "22,40,68,69,163,612,613"
|
||||||
|
kind: source-search-hit
|
||||||
|
- id: prod.railiance01.authelia
|
||||||
|
name: Authelia ingress
|
||||||
|
repo: net-kingdom
|
||||||
|
deployment_environment: prod
|
||||||
|
deployment_scenario: railiance01
|
||||||
|
access_zone: production-public
|
||||||
|
exposure_class: production-public
|
||||||
|
routing_authority: traefik
|
||||||
|
policy_authority: authelia
|
||||||
|
tls_authority: cert-manager:letsencrypt-prod
|
||||||
|
route_evidence:
|
||||||
|
route: https://auth.coulomb.social
|
||||||
|
hostname: auth.coulomb.social
|
||||||
|
port: 443
|
||||||
|
protocol: https
|
||||||
|
review:
|
||||||
|
status: candidate
|
||||||
|
note: railiance01 attribution comes from NetKingdom deployment workplan
|
||||||
|
evidence:
|
||||||
|
- path: ../net-kingdom/sso-mfa/k8s/authelia/ingress.yaml
|
||||||
|
lines: "13,22,24,26,38"
|
||||||
|
kind: kubernetes-ingress
|
||||||
|
- path: ../net-kingdom/workplans/NK-WP-0003-keycape-privacyidea-cluster-deployment.md
|
||||||
|
lines: "29,47,88,101"
|
||||||
|
kind: source-search-hit
|
||||||
|
- id: prod.railiance01.keycape
|
||||||
|
name: Keycape ingress
|
||||||
|
repo: net-kingdom
|
||||||
|
deployment_environment: prod
|
||||||
|
deployment_scenario: railiance01
|
||||||
|
access_zone: production-public
|
||||||
|
exposure_class: production-public
|
||||||
|
routing_authority: traefik
|
||||||
|
policy_authority: traefik-middleware
|
||||||
|
tls_authority: cert-manager:letsencrypt-prod
|
||||||
|
route_evidence:
|
||||||
|
route: https://kc.coulomb.social
|
||||||
|
hostname: kc.coulomb.social
|
||||||
|
port: 443
|
||||||
|
protocol: https
|
||||||
|
review:
|
||||||
|
status: candidate
|
||||||
|
note: middleware is present, but intended audience still needs operator review
|
||||||
|
evidence:
|
||||||
|
- path: ../net-kingdom/sso-mfa/k8s/keycape/ingress.yaml
|
||||||
|
lines: "13,22,23,27,29,41"
|
||||||
|
kind: kubernetes-ingress
|
||||||
|
- path: ../net-kingdom/sso-mfa/k8s/keycape/middleware.yaml
|
||||||
|
lines: "9,24"
|
||||||
|
kind: traefik-middleware
|
||||||
|
- id: prod.railiance01.privacyidea
|
||||||
|
name: privacyIDEA ingress
|
||||||
|
repo: net-kingdom
|
||||||
|
deployment_environment: prod
|
||||||
|
deployment_scenario: railiance01
|
||||||
|
access_zone: production-admin
|
||||||
|
exposure_class: production-admin
|
||||||
|
routing_authority: traefik
|
||||||
|
policy_authority: traefik-middleware
|
||||||
|
tls_authority: cert-manager:letsencrypt-prod
|
||||||
|
route_evidence:
|
||||||
|
route: https://pink.coulomb.social
|
||||||
|
hostname: pink.coulomb.social
|
||||||
|
port: 443
|
||||||
|
protocol: https
|
||||||
|
review:
|
||||||
|
status: candidate
|
||||||
|
note: admin classification inferred from privacyIDEA role and middleware
|
||||||
|
evidence:
|
||||||
|
- path: ../net-kingdom/sso-mfa/k8s/privacyidea/ingress.yaml
|
||||||
|
lines: "25,34,36,38,40,52,60,69,71,75,77,89"
|
||||||
|
kind: kubernetes-ingress
|
||||||
|
- path: ../net-kingdom/sso-mfa/k8s/privacyidea/middleware.yaml
|
||||||
|
lines: "19,41"
|
||||||
|
kind: traefik-middleware
|
||||||
|
- id: prod.railiance01.privacyidea-account
|
||||||
|
name: privacyIDEA account self-service ingress
|
||||||
|
repo: net-kingdom
|
||||||
|
deployment_environment: prod
|
||||||
|
deployment_scenario: railiance01
|
||||||
|
access_zone: production-public
|
||||||
|
exposure_class: production-public
|
||||||
|
routing_authority: traefik
|
||||||
|
policy_authority: traefik-middleware
|
||||||
|
tls_authority: cert-manager:letsencrypt-prod
|
||||||
|
route_evidence:
|
||||||
|
route: https://pink-account.coulomb.social
|
||||||
|
hostname: pink-account.coulomb.social
|
||||||
|
port: 443
|
||||||
|
protocol: https
|
||||||
|
review:
|
||||||
|
status: candidate
|
||||||
|
note: self-service classification inferred from host name and middleware
|
||||||
|
evidence:
|
||||||
|
- path: ../net-kingdom/sso-mfa/k8s/privacyidea/ingress.yaml
|
||||||
|
lines: "94,103,104,106,108,120"
|
||||||
|
kind: kubernetes-ingress
|
||||||
|
- id: prod.railiance01.lldap
|
||||||
|
name: LLDAP ingress
|
||||||
|
repo: net-kingdom
|
||||||
|
deployment_environment: prod
|
||||||
|
deployment_scenario: railiance01
|
||||||
|
access_zone: production-admin
|
||||||
|
exposure_class: production-admin
|
||||||
|
routing_authority: traefik
|
||||||
|
policy_authority: traefik-admin-allowlist
|
||||||
|
tls_authority: cert-manager:letsencrypt-prod
|
||||||
|
route_evidence:
|
||||||
|
route: https://lldap.coulomb.social
|
||||||
|
hostname: lldap.coulomb.social
|
||||||
|
port: 443
|
||||||
|
protocol: https
|
||||||
|
review:
|
||||||
|
status: candidate
|
||||||
|
note: admin allowlist middleware indicates intended restricted access
|
||||||
|
evidence:
|
||||||
|
- path: ../net-kingdom/sso-mfa/k8s/lldap/ingress.yaml
|
||||||
|
lines: "12,21,22,24,26,38"
|
||||||
|
kind: kubernetes-ingress
|
||||||
|
- path: ../net-kingdom/sso-mfa/k8s/lldap/middleware.yaml
|
||||||
|
lines: "11"
|
||||||
|
kind: traefik-middleware
|
||||||
|
ambiguities:
|
||||||
|
- id: railiance01-coulombcore-ip-conflict
|
||||||
|
severity: high
|
||||||
|
summary: Source documents disagree on which host owns 92.205.130.254.
|
||||||
|
evidence:
|
||||||
|
- path: ../railiance-apps/workplans/railiance-apps-WP-0002-vergabe-teilnahme-on-railiance01.md
|
||||||
|
lines: "22,163"
|
||||||
|
note: says railiance01 and Traefik LoadBalancer use 92.205.130.254
|
||||||
|
- path: ../railiance-infra/SCOPE.md
|
||||||
|
lines: "126"
|
||||||
|
note: says COULOMBCORE is 92.205.130.254 and Railiance01 is 92.205.62.239
|
||||||
|
next: reconcile host inventory before treating IP evidence as authoritative
|
||||||
|
- id: prod-access-zone-review
|
||||||
|
severity: medium
|
||||||
|
summary: Production access zones are candidate classifications.
|
||||||
|
evidence:
|
||||||
|
- path: ../railiance-apps/manifests
|
||||||
|
note: app ingress manifests show routing and TLS but not business audience
|
||||||
|
- path: ../net-kingdom/sso-mfa/k8s
|
||||||
|
note: middleware and network policy hint at access intent but do not replace operator review
|
||||||
|
next: confirm each production host as public, admin, or early-access
|
||||||
|
- id: test-reachability-is-tunneled
|
||||||
|
severity: medium
|
||||||
|
summary: Current coulombcore routes are ops-bridge tunnel evidence, not public ingress evidence.
|
||||||
|
evidence:
|
||||||
|
- path: ../railiance-infra/docs/deploy-stack.md
|
||||||
|
lines: "127,128,129"
|
||||||
|
note: state-hub and k3s API access are tunnel commands
|
||||||
|
next: add executable test-stage ingress/service discovery when coulombcore manifests exist
|
||||||
|
missing_policy_authority:
|
||||||
|
- surface_id: prod.railiance01.gitea
|
||||||
|
reason: route and TLS are discovered, but access policy authority is not evident in the ingress artifact
|
||||||
|
- surface_id: prod.railiance01.vergabe-teilnahme
|
||||||
|
reason: route and TLS are discovered, but access policy authority is not evident in the ingress artifact
|
||||||
35
fabric/interfaces/net-kingdom-control-surface-ui.yaml
Normal file
35
fabric/interfaces/net-kingdom-control-surface-ui.yaml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
apiVersion: railiance.fabric/v1alpha1
|
||||||
|
kind: InterfaceDeclaration
|
||||||
|
metadata:
|
||||||
|
id: net-kingdom.control-surface.ui
|
||||||
|
name: NetKingdom Control Surface
|
||||||
|
owner: net-kingdom
|
||||||
|
repo: net-kingdom
|
||||||
|
domain: railiance
|
||||||
|
spec:
|
||||||
|
lifecycle: active
|
||||||
|
environments: [dev]
|
||||||
|
description: Local NetKingdom control surface on the operator workstation.
|
||||||
|
interface_type: web-ui
|
||||||
|
version: v1
|
||||||
|
service_id: net-kingdom.iam-profile
|
||||||
|
capability_ids:
|
||||||
|
- net-kingdom.iam-profile.issuer
|
||||||
|
endpoint:
|
||||||
|
url: http://127.0.0.1:8876
|
||||||
|
notes: Local workstation endpoint after moving away from the Fabric registry port.
|
||||||
|
deployment_overlay:
|
||||||
|
deployment_environment: dev
|
||||||
|
deployment_scenario: bernd-laptop
|
||||||
|
routing_authority: net-kingdom-local-process
|
||||||
|
access_zone: private-dev
|
||||||
|
policy_authority: local-loopback-binding
|
||||||
|
exposure_class: local-only
|
||||||
|
route_evidence:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 8876
|
||||||
|
protocol: tcp
|
||||||
|
route: http://127.0.0.1:8876
|
||||||
|
auth:
|
||||||
|
method: none
|
||||||
|
data_classification: internal
|
||||||
@@ -23,6 +23,18 @@ spec:
|
|||||||
endpoint:
|
endpoint:
|
||||||
url: http://127.0.0.1:8765/ui/graph-explorer
|
url: http://127.0.0.1:8765/ui/graph-explorer
|
||||||
notes: Local workstation UI when the registry service is running.
|
notes: Local workstation UI when the registry service is running.
|
||||||
|
deployment_overlay:
|
||||||
|
deployment_environment: dev
|
||||||
|
deployment_scenario: bernd-laptop
|
||||||
|
routing_authority: railiance-fabric-registry
|
||||||
|
access_zone: private-dev
|
||||||
|
policy_authority: local-loopback-binding
|
||||||
|
exposure_class: local-only
|
||||||
|
route_evidence:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 8765
|
||||||
|
protocol: tcp
|
||||||
|
route: http://127.0.0.1:8765/ui/graph-explorer
|
||||||
auth:
|
auth:
|
||||||
method: none
|
method: none
|
||||||
data_classification: internal
|
data_classification: internal
|
||||||
|
|||||||
@@ -23,6 +23,18 @@ spec:
|
|||||||
endpoint:
|
endpoint:
|
||||||
url: http://127.0.0.1:8765
|
url: http://127.0.0.1:8765
|
||||||
notes: Local workstation endpoint when the registry service is running.
|
notes: Local workstation endpoint when the registry service is running.
|
||||||
|
deployment_overlay:
|
||||||
|
deployment_environment: dev
|
||||||
|
deployment_scenario: bernd-laptop
|
||||||
|
routing_authority: local-process
|
||||||
|
access_zone: private-dev
|
||||||
|
policy_authority: local-loopback-binding
|
||||||
|
exposure_class: local-only
|
||||||
|
route_evidence:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 8765
|
||||||
|
protocol: tcp
|
||||||
|
route: http://127.0.0.1:8765
|
||||||
auth:
|
auth:
|
||||||
method: none
|
method: none
|
||||||
data_classification: internal
|
data_classification: internal
|
||||||
|
|||||||
35
fabric/interfaces/the-custodian-state-hub-dashboard.yaml
Normal file
35
fabric/interfaces/the-custodian-state-hub-dashboard.yaml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
apiVersion: railiance.fabric/v1alpha1
|
||||||
|
kind: InterfaceDeclaration
|
||||||
|
metadata:
|
||||||
|
id: the-custodian.state-hub.dashboard
|
||||||
|
name: State Hub Dashboard
|
||||||
|
owner: the-custodian
|
||||||
|
repo: the-custodian
|
||||||
|
domain: custodian
|
||||||
|
spec:
|
||||||
|
lifecycle: active
|
||||||
|
environments: [dev]
|
||||||
|
description: Local browser dashboard for State Hub coordination views.
|
||||||
|
interface_type: web-ui
|
||||||
|
version: v1
|
||||||
|
service_id: the-custodian.state-hub
|
||||||
|
capability_ids:
|
||||||
|
- the-custodian.state-hub.coordination
|
||||||
|
endpoint:
|
||||||
|
url: http://127.0.0.1:3000
|
||||||
|
notes: Local workstation dashboard endpoint when the State Hub frontend is running.
|
||||||
|
deployment_overlay:
|
||||||
|
deployment_environment: dev
|
||||||
|
deployment_scenario: bernd-laptop
|
||||||
|
routing_authority: vite-dev-server
|
||||||
|
access_zone: private-dev
|
||||||
|
policy_authority: local-loopback-binding
|
||||||
|
exposure_class: local-only
|
||||||
|
route_evidence:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 3000
|
||||||
|
protocol: tcp
|
||||||
|
route: http://127.0.0.1:3000
|
||||||
|
auth:
|
||||||
|
method: none
|
||||||
|
data_classification: internal
|
||||||
@@ -15,6 +15,21 @@ spec:
|
|||||||
service_id: the-custodian.state-hub
|
service_id: the-custodian.state-hub
|
||||||
capability_ids:
|
capability_ids:
|
||||||
- the-custodian.state-hub.coordination
|
- the-custodian.state-hub.coordination
|
||||||
|
endpoint:
|
||||||
|
url: http://127.0.0.1:8000
|
||||||
|
notes: Local workstation State Hub REST API.
|
||||||
|
deployment_overlay:
|
||||||
|
deployment_environment: dev
|
||||||
|
deployment_scenario: bernd-laptop
|
||||||
|
routing_authority: state-hub-local-process
|
||||||
|
access_zone: private-dev
|
||||||
|
policy_authority: local-loopback-binding
|
||||||
|
exposure_class: local-only
|
||||||
|
route_evidence:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 8000
|
||||||
|
protocol: tcp
|
||||||
|
route: http://127.0.0.1:8000
|
||||||
auth:
|
auth:
|
||||||
method: none
|
method: none
|
||||||
data_classification: internal
|
data_classification: internal
|
||||||
|
|||||||
35
fabric/interfaces/the-custodian-state-hub-mcp-api.yaml
Normal file
35
fabric/interfaces/the-custodian-state-hub-mcp-api.yaml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
apiVersion: railiance.fabric/v1alpha1
|
||||||
|
kind: InterfaceDeclaration
|
||||||
|
metadata:
|
||||||
|
id: the-custodian.state-hub.mcp-api
|
||||||
|
name: State Hub MCP API
|
||||||
|
owner: the-custodian
|
||||||
|
repo: the-custodian
|
||||||
|
domain: custodian
|
||||||
|
spec:
|
||||||
|
lifecycle: active
|
||||||
|
environments: [dev]
|
||||||
|
description: Local MCP surface for State Hub coordination tools.
|
||||||
|
interface_type: mcp-api
|
||||||
|
version: v1
|
||||||
|
service_id: the-custodian.state-hub
|
||||||
|
capability_ids:
|
||||||
|
- the-custodian.state-hub.coordination
|
||||||
|
endpoint:
|
||||||
|
url: http://127.0.0.1:8001
|
||||||
|
notes: Local workstation MCP endpoint when State Hub is running.
|
||||||
|
deployment_overlay:
|
||||||
|
deployment_environment: dev
|
||||||
|
deployment_scenario: bernd-laptop
|
||||||
|
routing_authority: state-hub-local-process
|
||||||
|
access_zone: private-dev
|
||||||
|
policy_authority: local-loopback-binding
|
||||||
|
exposure_class: local-only
|
||||||
|
route_evidence:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 8001
|
||||||
|
protocol: tcp
|
||||||
|
route: http://127.0.0.1:8001
|
||||||
|
auth:
|
||||||
|
method: none
|
||||||
|
data_classification: internal
|
||||||
@@ -15,3 +15,4 @@ spec:
|
|||||||
- net-kingdom.iam-profile.issuer
|
- net-kingdom.iam-profile.issuer
|
||||||
exposes_interfaces:
|
exposes_interfaces:
|
||||||
- net-kingdom.iam-profile.oidc-discovery
|
- net-kingdom.iam-profile.oidc-discovery
|
||||||
|
- net-kingdom.control-surface.ui
|
||||||
|
|||||||
@@ -15,3 +15,5 @@ spec:
|
|||||||
- the-custodian.state-hub.coordination
|
- the-custodian.state-hub.coordination
|
||||||
exposes_interfaces:
|
exposes_interfaces:
|
||||||
- the-custodian.state-hub.http-api
|
- the-custodian.state-hub.http-api
|
||||||
|
- the-custodian.state-hub.mcp-api
|
||||||
|
- the-custodian.state-hub.dashboard
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from datetime import datetime, timezone
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from .deployment_overlay import normalize_deployment_overlay
|
||||||
from .discovery import normalize_identity_part, short_fingerprint
|
from .discovery import normalize_identity_part, short_fingerprint
|
||||||
from .loader import load_yaml, repo_root
|
from .loader import load_yaml, repo_root
|
||||||
from .schema_validation import draft202012_validator
|
from .schema_validation import draft202012_validator
|
||||||
@@ -619,7 +620,7 @@ def _identity_from_evidence(root: dict[str, Any], item: dict[str, Any]) -> dict[
|
|||||||
or declared_slug
|
or declared_slug
|
||||||
or Path(str(source.get("path") or "")).name
|
or Path(str(source.get("path") or "")).name
|
||||||
)
|
)
|
||||||
return {
|
candidate = {
|
||||||
"identity_type": "Repository",
|
"identity_type": "Repository",
|
||||||
"label": identity_slug,
|
"label": identity_slug,
|
||||||
"graph_id": identity_slug,
|
"graph_id": identity_slug,
|
||||||
@@ -635,6 +636,10 @@ def _identity_from_evidence(root: dict[str, Any], item: dict[str, Any]) -> dict[
|
|||||||
},
|
},
|
||||||
"confidence": 0.9 if evidence_type == "repository_checkout" else 0.85,
|
"confidence": 0.9 if evidence_type == "repository_checkout" else 0.85,
|
||||||
}
|
}
|
||||||
|
overlay = normalize_deployment_overlay(source, attributes)
|
||||||
|
if overlay:
|
||||||
|
candidate["deployment_overlay"] = overlay
|
||||||
|
return candidate
|
||||||
if evidence_type in {"deployment_automation", "infrastructure_manifest"}:
|
if evidence_type in {"deployment_automation", "infrastructure_manifest"}:
|
||||||
path = str(source.get("path") or "")
|
path = str(source.get("path") or "")
|
||||||
return {
|
return {
|
||||||
@@ -665,7 +670,7 @@ def _identity_from_evidence(root: dict[str, Any], item: dict[str, Any]) -> dict[
|
|||||||
}
|
}
|
||||||
if evidence_type == "endpoint_contract":
|
if evidence_type == "endpoint_contract":
|
||||||
path = str(source.get("path") or "")
|
path = str(source.get("path") or "")
|
||||||
return {
|
candidate = {
|
||||||
"identity_type": "Endpoint",
|
"identity_type": "Endpoint",
|
||||||
"label": Path(path).name or "endpoint-contract",
|
"label": Path(path).name or "endpoint-contract",
|
||||||
"graph_id": path,
|
"graph_id": path,
|
||||||
@@ -677,6 +682,10 @@ def _identity_from_evidence(root: dict[str, Any], item: dict[str, Any]) -> dict[
|
|||||||
"attributes": {**attributes, "source_evidence_type": evidence_type},
|
"attributes": {**attributes, "source_evidence_type": evidence_type},
|
||||||
"confidence": 0.75,
|
"confidence": 0.75,
|
||||||
}
|
}
|
||||||
|
overlay = normalize_deployment_overlay(source, attributes)
|
||||||
|
if overlay:
|
||||||
|
candidate["deployment_overlay"] = overlay
|
||||||
|
return candidate
|
||||||
if evidence_type == "host_path_match":
|
if evidence_type == "host_path_match":
|
||||||
path = str(source.get("path") or "")
|
path = str(source.get("path") or "")
|
||||||
return {
|
return {
|
||||||
@@ -901,6 +910,7 @@ def _add_identity_candidate(
|
|||||||
evidence_ids: list[str],
|
evidence_ids: list[str],
|
||||||
aliases: list[str],
|
aliases: list[str],
|
||||||
attributes: dict[str, Any],
|
attributes: dict[str, Any],
|
||||||
|
deployment_overlay: dict[str, Any] | None = None,
|
||||||
confidence: float,
|
confidence: float,
|
||||||
) -> None:
|
) -> None:
|
||||||
normalized_type = normalize_identity_part(identity_type)
|
normalized_type = normalize_identity_part(identity_type)
|
||||||
@@ -924,6 +934,9 @@ def _add_identity_candidate(
|
|||||||
incoming["subfabric_id"] = subfabric_id
|
incoming["subfabric_id"] = subfabric_id
|
||||||
if owner_actor_id:
|
if owner_actor_id:
|
||||||
incoming["owner_actor_id"] = owner_actor_id
|
incoming["owner_actor_id"] = owner_actor_id
|
||||||
|
overlay = normalize_deployment_overlay(deployment_overlay or {}, attributes)
|
||||||
|
if overlay:
|
||||||
|
incoming["deployment_overlay"] = overlay
|
||||||
|
|
||||||
existing = candidates.get(stable_key)
|
existing = candidates.get(stable_key)
|
||||||
if existing is None:
|
if existing is None:
|
||||||
@@ -933,6 +946,11 @@ def _add_identity_candidate(
|
|||||||
existing["aliases"] = _unique_strings([*existing.get("aliases", []), *incoming["aliases"]])
|
existing["aliases"] = _unique_strings([*existing.get("aliases", []), *incoming["aliases"]])
|
||||||
existing["evidence_ids"] = _unique_strings([*existing.get("evidence_ids", []), *incoming["evidence_ids"]])
|
existing["evidence_ids"] = _unique_strings([*existing.get("evidence_ids", []), *incoming["evidence_ids"]])
|
||||||
existing["attributes"] = {**existing.get("attributes", {}), **incoming["attributes"]}
|
existing["attributes"] = {**existing.get("attributes", {}), **incoming["attributes"]}
|
||||||
|
if incoming.get("deployment_overlay"):
|
||||||
|
existing["deployment_overlay"] = normalize_deployment_overlay(
|
||||||
|
existing.get("deployment_overlay") if isinstance(existing.get("deployment_overlay"), dict) else {},
|
||||||
|
incoming["deployment_overlay"],
|
||||||
|
)
|
||||||
if incoming.get("owner_actor_id") and existing.get("owner_actor_id") and incoming["owner_actor_id"] != existing["owner_actor_id"]:
|
if incoming.get("owner_actor_id") and existing.get("owner_actor_id") and incoming["owner_actor_id"] != existing["owner_actor_id"]:
|
||||||
existing["attributes"]["ambiguous_owner_actor_ids"] = _unique_strings(
|
existing["attributes"]["ambiguous_owner_actor_ids"] = _unique_strings(
|
||||||
[existing["owner_actor_id"], incoming["owner_actor_id"], *existing["attributes"].get("ambiguous_owner_actor_ids", [])]
|
[existing["owner_actor_id"], incoming["owner_actor_id"], *existing["attributes"].get("ambiguous_owner_actor_ids", [])]
|
||||||
@@ -973,6 +991,7 @@ def _candidate_graph(candidates: list[dict[str, Any]], manifest: dict[str, Any])
|
|||||||
"fabric_id": candidate.get("fabric_id", ""),
|
"fabric_id": candidate.get("fabric_id", ""),
|
||||||
"subfabric_id": candidate.get("subfabric_id", ""),
|
"subfabric_id": candidate.get("subfabric_id", ""),
|
||||||
"owner_actor_id": candidate.get("owner_actor_id", ""),
|
"owner_actor_id": candidate.get("owner_actor_id", ""),
|
||||||
|
"deployment_overlay": candidate.get("deployment_overlay", {}),
|
||||||
}
|
}
|
||||||
for candidate in sorted(candidates, key=lambda item: item["stable_key"])
|
for candidate in sorted(candidates, key=lambda item: item["stable_key"])
|
||||||
]
|
]
|
||||||
|
|||||||
107
railiance_fabric/deployment_overlay.py
Normal file
107
railiance_fabric/deployment_overlay.py
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
OVERLAY_FIELDS = (
|
||||||
|
"deployment_environment",
|
||||||
|
"deployment_scenario",
|
||||||
|
"routing_authority",
|
||||||
|
"access_zone",
|
||||||
|
"policy_authority",
|
||||||
|
"exposure_class",
|
||||||
|
)
|
||||||
|
|
||||||
|
ROUTE_EVIDENCE_FIELDS = (
|
||||||
|
"host",
|
||||||
|
"hostname",
|
||||||
|
"port",
|
||||||
|
"protocol",
|
||||||
|
"route",
|
||||||
|
"route_url",
|
||||||
|
"path",
|
||||||
|
"scheme",
|
||||||
|
)
|
||||||
|
|
||||||
|
_ALIASES = {
|
||||||
|
"deployment_environment": ("deployment_environment", "environment"),
|
||||||
|
"deployment_scenario": ("deployment_scenario", "deployment_scenario_id", "scenario"),
|
||||||
|
"routing_authority": ("routing_authority",),
|
||||||
|
"access_zone": ("access_zone",),
|
||||||
|
"policy_authority": ("policy_authority",),
|
||||||
|
"exposure_class": ("exposure_class", "exposure"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_deployment_overlay(*sources: object) -> dict[str, Any]:
|
||||||
|
"""Collect deployment-zone overlay fields without changing fabric membership."""
|
||||||
|
|
||||||
|
overlay: dict[str, Any] = {}
|
||||||
|
route_evidence: dict[str, Any] = {}
|
||||||
|
|
||||||
|
for source in sources:
|
||||||
|
if not isinstance(source, dict):
|
||||||
|
continue
|
||||||
|
nested = source.get("deployment_overlay")
|
||||||
|
if isinstance(nested, dict):
|
||||||
|
_merge_overlay_source(overlay, route_evidence, nested)
|
||||||
|
_merge_overlay_source(overlay, route_evidence, source)
|
||||||
|
|
||||||
|
if route_evidence:
|
||||||
|
overlay["route_evidence"] = route_evidence
|
||||||
|
return {key: value for key, value in overlay.items() if _has_value(value)}
|
||||||
|
|
||||||
|
|
||||||
|
def graph_explorer_overlay_fields(overlay: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
route = overlay.get("route_evidence") if isinstance(overlay.get("route_evidence"), dict) else {}
|
||||||
|
return {
|
||||||
|
"deploymentOverlay": overlay,
|
||||||
|
"deploymentEnvironment": str(overlay.get("deployment_environment") or ""),
|
||||||
|
"deploymentScenario": str(overlay.get("deployment_scenario") or ""),
|
||||||
|
"routingAuthority": str(overlay.get("routing_authority") or ""),
|
||||||
|
"accessZone": str(overlay.get("access_zone") or ""),
|
||||||
|
"policyAuthority": str(overlay.get("policy_authority") or ""),
|
||||||
|
"exposureClass": str(overlay.get("exposure_class") or ""),
|
||||||
|
"routeHost": str(route.get("host") or ""),
|
||||||
|
"routeHostname": str(route.get("hostname") or ""),
|
||||||
|
"routePort": route.get("port") if route.get("port") not in (None, "") else "",
|
||||||
|
"routeProtocol": str(route.get("protocol") or ""),
|
||||||
|
"route": str(route.get("route") or route.get("route_url") or ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_overlay_source(overlay: dict[str, Any], route_evidence: dict[str, Any], source: dict[str, Any]) -> None:
|
||||||
|
for field in OVERLAY_FIELDS:
|
||||||
|
if field in overlay:
|
||||||
|
continue
|
||||||
|
value = _first_value(source, _ALIASES[field])
|
||||||
|
if _has_value(value):
|
||||||
|
overlay[field] = value
|
||||||
|
|
||||||
|
nested_route = source.get("route_evidence")
|
||||||
|
if isinstance(nested_route, dict):
|
||||||
|
for field in ROUTE_EVIDENCE_FIELDS:
|
||||||
|
value = nested_route.get(field)
|
||||||
|
if _has_value(value) and field not in route_evidence:
|
||||||
|
route_evidence[field] = value
|
||||||
|
|
||||||
|
for field in ROUTE_EVIDENCE_FIELDS:
|
||||||
|
value = source.get(field)
|
||||||
|
if _has_value(value) and field not in route_evidence:
|
||||||
|
route_evidence[field] = value
|
||||||
|
|
||||||
|
|
||||||
|
def _first_value(source: dict[str, Any], keys: tuple[str, ...]) -> Any:
|
||||||
|
for key in keys:
|
||||||
|
value = source.get(key)
|
||||||
|
if _has_value(value):
|
||||||
|
return value
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _has_value(value: Any) -> bool:
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return any(_has_value(item) for item in value.values())
|
||||||
|
if isinstance(value, list):
|
||||||
|
return any(_has_value(item) for item in value)
|
||||||
|
return value not in (None, "")
|
||||||
@@ -3,6 +3,8 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from .deployment_overlay import normalize_deployment_overlay
|
||||||
|
|
||||||
FINANCIAL_API_VERSION = "railiance.fabric/v1alpha2"
|
FINANCIAL_API_VERSION = "railiance.fabric/v1alpha2"
|
||||||
FINANCIAL_SCHEMA_VERSION = "financial-fabric-v1"
|
FINANCIAL_SCHEMA_VERSION = "financial-fabric-v1"
|
||||||
|
|
||||||
@@ -19,7 +21,7 @@ RELATIONSHIP_CATEGORIES = {
|
|||||||
"evidence",
|
"evidence",
|
||||||
}
|
}
|
||||||
FABRIC_KINDS = {"Fabric", "Subfabric"}
|
FABRIC_KINDS = {"Fabric", "Subfabric"}
|
||||||
ENVIRONMENT_LABELS = {"local", "dev", "staging", "prod", "lab", "all"}
|
ENVIRONMENT_LABELS = {"local", "dev", "test", "staging", "prod", "lab", "all"}
|
||||||
|
|
||||||
|
|
||||||
def is_financial_graph_export(graph: dict[str, Any]) -> bool:
|
def is_financial_graph_export(graph: dict[str, Any]) -> bool:
|
||||||
@@ -125,6 +127,7 @@ def financial_graph_errors(graph: dict[str, Any]) -> list[str]:
|
|||||||
_validate_containment(errors, f"nodes[{index}]", node, netkingdom_id, fabric_kinds, accepted=review_state == "accepted")
|
_validate_containment(errors, f"nodes[{index}]", node, netkingdom_id, fabric_kinds, accepted=review_state == "accepted")
|
||||||
_validate_ownership(errors, f"nodes[{index}]", node, actor_roles, accepted=review_state == "accepted")
|
_validate_ownership(errors, f"nodes[{index}]", node, actor_roles, accepted=review_state == "accepted")
|
||||||
_validate_optional_object(errors, f"nodes[{index}].accounting", node, "accounting")
|
_validate_optional_object(errors, f"nodes[{index}].accounting", node, "accounting")
|
||||||
|
_validate_overlay(errors, f"nodes[{index}].deployment_overlay", node)
|
||||||
|
|
||||||
edges = _indexed_items(errors, graph, "edges")
|
edges = _indexed_items(errors, graph, "edges")
|
||||||
for index, edge in edges:
|
for index, edge in edges:
|
||||||
@@ -141,6 +144,7 @@ def financial_graph_errors(graph: dict[str, Any]) -> list[str]:
|
|||||||
errors.append(f"{path}.relationship_category {category!r} is not valid")
|
errors.append(f"{path}.relationship_category {category!r} is not valid")
|
||||||
_validate_evidence(errors, path, edge)
|
_validate_evidence(errors, path, edge)
|
||||||
_validate_optional_object(errors, f"{path}.accounting", edge, "accounting")
|
_validate_optional_object(errors, f"{path}.accounting", edge, "accounting")
|
||||||
|
_validate_overlay(errors, f"{path}.deployment_overlay", edge)
|
||||||
if edge_type == "provides_utility_to" and category != "utility":
|
if edge_type == "provides_utility_to" and category != "utility":
|
||||||
errors.append(f"{path}.relationship_category must be 'utility' for provides_utility_to edges")
|
errors.append(f"{path}.relationship_category must be 'utility' for provides_utility_to edges")
|
||||||
if category == "utility":
|
if category == "utility":
|
||||||
@@ -211,6 +215,13 @@ def _materialize_node(node: dict[str, Any]) -> None:
|
|||||||
for key in ("containment", "ownership", "accounting"):
|
for key in ("containment", "ownership", "accounting"):
|
||||||
if key not in node and isinstance(attrs.get(key), dict):
|
if key not in node and isinstance(attrs.get(key), dict):
|
||||||
node[key] = attrs[key]
|
node[key] = attrs[key]
|
||||||
|
overlay = normalize_deployment_overlay(
|
||||||
|
node.get("deployment_overlay") if isinstance(node.get("deployment_overlay"), dict) else {},
|
||||||
|
attrs,
|
||||||
|
node.get("containment") if isinstance(node.get("containment"), dict) else {},
|
||||||
|
)
|
||||||
|
if overlay:
|
||||||
|
node["deployment_overlay"] = overlay
|
||||||
node.setdefault("evidence", _legacy_evidence(node, attrs))
|
node.setdefault("evidence", _legacy_evidence(node, attrs))
|
||||||
|
|
||||||
|
|
||||||
@@ -218,6 +229,12 @@ def _materialize_edge(edge: dict[str, Any]) -> None:
|
|||||||
attrs = edge.get("attributes") if isinstance(edge.get("attributes"), dict) else {}
|
attrs = edge.get("attributes") if isinstance(edge.get("attributes"), dict) else {}
|
||||||
if "accounting" not in edge and isinstance(attrs.get("accounting"), dict):
|
if "accounting" not in edge and isinstance(attrs.get("accounting"), dict):
|
||||||
edge["accounting"] = attrs["accounting"]
|
edge["accounting"] = attrs["accounting"]
|
||||||
|
overlay = normalize_deployment_overlay(
|
||||||
|
edge.get("deployment_overlay") if isinstance(edge.get("deployment_overlay"), dict) else {},
|
||||||
|
attrs,
|
||||||
|
)
|
||||||
|
if overlay:
|
||||||
|
edge["deployment_overlay"] = overlay
|
||||||
edge.setdefault("relationship_category", _relationship_category(edge))
|
edge.setdefault("relationship_category", _relationship_category(edge))
|
||||||
edge.setdefault("evidence", _legacy_evidence(edge, attrs))
|
edge.setdefault("evidence", _legacy_evidence(edge, attrs))
|
||||||
if edge.get("relationship_category") == "utility":
|
if edge.get("relationship_category") == "utility":
|
||||||
@@ -362,6 +379,18 @@ def _validate_optional_object(errors: list[str], path: str, item: dict[str, Any]
|
|||||||
errors.append(f"{path} must be an object")
|
errors.append(f"{path} must be an object")
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_overlay(errors: list[str], path: str, item: dict[str, Any]) -> None:
|
||||||
|
overlay = item.get("deployment_overlay")
|
||||||
|
if overlay is None:
|
||||||
|
return
|
||||||
|
if not isinstance(overlay, dict):
|
||||||
|
errors.append(f"{path} must be an object")
|
||||||
|
return
|
||||||
|
route = overlay.get("route_evidence")
|
||||||
|
if route is not None and not isinstance(route, dict):
|
||||||
|
errors.append(f"{path}.route_evidence must be an object")
|
||||||
|
|
||||||
|
|
||||||
def _required_object(errors: list[str], path: str, item: dict[str, Any], key: str) -> dict[str, Any]:
|
def _required_object(errors: list[str], path: str, item: dict[str, Any], key: str) -> dict[str, Any]:
|
||||||
value = item.get(key)
|
value = item.get(key)
|
||||||
if not isinstance(value, dict):
|
if not isinstance(value, dict):
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from datetime import datetime, timezone
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from .deployment_overlay import normalize_deployment_overlay
|
||||||
from .financial import (
|
from .financial import (
|
||||||
FINANCIAL_API_VERSION,
|
FINANCIAL_API_VERSION,
|
||||||
FINANCIAL_SCHEMA_VERSION,
|
FINANCIAL_SCHEMA_VERSION,
|
||||||
@@ -97,6 +98,13 @@ def _financial_node_from_legacy(
|
|||||||
accounting = _object(result["attributes"].get("accounting")) or accounting_default
|
accounting = _object(result["attributes"].get("accounting")) or accounting_default
|
||||||
if _has_value(accounting):
|
if _has_value(accounting):
|
||||||
result["accounting"] = json.loads(json.dumps(accounting))
|
result["accounting"] = json.loads(json.dumps(accounting))
|
||||||
|
overlay = normalize_deployment_overlay(
|
||||||
|
node.get("deployment_overlay") if isinstance(node.get("deployment_overlay"), dict) else {},
|
||||||
|
result["attributes"],
|
||||||
|
result["containment"],
|
||||||
|
)
|
||||||
|
if _has_value(overlay):
|
||||||
|
result["deployment_overlay"] = overlay
|
||||||
for key in ("canon_category", "canon_anchor", "mapping_fit"):
|
for key in ("canon_category", "canon_anchor", "mapping_fit"):
|
||||||
if node.get(key):
|
if node.get(key):
|
||||||
result[key] = node[key]
|
result[key] = node[key]
|
||||||
@@ -113,6 +121,12 @@ def _financial_edge_from_legacy(edge: dict[str, Any]) -> dict[str, Any]:
|
|||||||
for key in ("canonical_type", "canon_anchor", "mapping_fit", "display_only", "evidence_state"):
|
for key in ("canonical_type", "canon_anchor", "mapping_fit", "display_only", "evidence_state"):
|
||||||
if key in edge:
|
if key in edge:
|
||||||
result[key] = edge[key]
|
result[key] = edge[key]
|
||||||
|
overlay = normalize_deployment_overlay(
|
||||||
|
edge.get("deployment_overlay") if isinstance(edge.get("deployment_overlay"), dict) else {},
|
||||||
|
result["attributes"],
|
||||||
|
)
|
||||||
|
if _has_value(overlay):
|
||||||
|
result["deployment_overlay"] = overlay
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -340,9 +340,12 @@ def _export_attributes(declaration: Declaration) -> dict[str, Any]:
|
|||||||
|
|
||||||
def _base_export_attributes(declaration: Declaration) -> dict[str, Any]:
|
def _base_export_attributes(declaration: Declaration) -> dict[str, Any]:
|
||||||
source_links = declaration.metadata.get("source_links", [])
|
source_links = declaration.metadata.get("source_links", [])
|
||||||
return {
|
attributes = {
|
||||||
"owner": declaration.metadata.get("owner", ""),
|
"owner": declaration.metadata.get("owner", ""),
|
||||||
"description": declaration.spec.get("description", ""),
|
"description": declaration.spec.get("description", ""),
|
||||||
"source_path": str(declaration.path),
|
"source_path": str(declaration.path),
|
||||||
"source_links": source_links if isinstance(source_links, list) else [],
|
"source_links": source_links if isinstance(source_links, list) else [],
|
||||||
}
|
}
|
||||||
|
if isinstance(declaration.spec.get("deployment_overlay"), dict):
|
||||||
|
attributes["deployment_overlay"] = declaration.spec["deployment_overlay"]
|
||||||
|
return attributes
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from typing import Any
|
|||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from .canon import edge_canon_mapping
|
from .canon import edge_canon_mapping
|
||||||
|
from .deployment_overlay import graph_explorer_overlay_fields, normalize_deployment_overlay
|
||||||
|
|
||||||
|
|
||||||
DISPLAY_STATES = ("show", "blur", "hide", "highlight", "remove")
|
DISPLAY_STATES = ("show", "blur", "hide", "highlight", "remove")
|
||||||
@@ -134,6 +135,12 @@ def fabric_graph_explorer_manifest(base_url: str = "") -> dict[str, Any]:
|
|||||||
"layer",
|
"layer",
|
||||||
"kind",
|
"kind",
|
||||||
"environment",
|
"environment",
|
||||||
|
"deploymentEnvironment",
|
||||||
|
"deploymentScenario",
|
||||||
|
"routingAuthority",
|
||||||
|
"accessZone",
|
||||||
|
"policyAuthority",
|
||||||
|
"exposureClass",
|
||||||
"serverHost",
|
"serverHost",
|
||||||
"lifecycle",
|
"lifecycle",
|
||||||
"unresolved",
|
"unresolved",
|
||||||
@@ -147,7 +154,17 @@ def fabric_graph_explorer_manifest(base_url: str = "") -> dict[str, Any]:
|
|||||||
"repo",
|
"repo",
|
||||||
"domain",
|
"domain",
|
||||||
"environment",
|
"environment",
|
||||||
|
"deploymentEnvironment",
|
||||||
|
"deploymentScenario",
|
||||||
|
"routingAuthority",
|
||||||
|
"accessZone",
|
||||||
|
"policyAuthority",
|
||||||
|
"exposureClass",
|
||||||
"serverHost",
|
"serverHost",
|
||||||
|
"routeHost",
|
||||||
|
"routeHostname",
|
||||||
|
"routePort",
|
||||||
|
"routeProtocol",
|
||||||
"kind",
|
"kind",
|
||||||
"layer",
|
"layer",
|
||||||
"edgeType",
|
"edgeType",
|
||||||
@@ -163,7 +180,15 @@ def fabric_graph_explorer_manifest(base_url: str = "") -> dict[str, Any]:
|
|||||||
{"id": "repo", "label": "Repo", "type": "string"},
|
{"id": "repo", "label": "Repo", "type": "string"},
|
||||||
{"id": "domain", "label": "Domain", "type": "string"},
|
{"id": "domain", "label": "Domain", "type": "string"},
|
||||||
{"id": "environment", "label": "Environment", "type": "string"},
|
{"id": "environment", "label": "Environment", "type": "string"},
|
||||||
|
{"id": "deploymentEnvironment", "label": "Deployment Environment", "type": "string"},
|
||||||
|
{"id": "deploymentScenario", "label": "Deployment Scenario", "type": "string"},
|
||||||
|
{"id": "routingAuthority", "label": "Routing Authority", "type": "string"},
|
||||||
|
{"id": "accessZone", "label": "Access Zone", "type": "string"},
|
||||||
|
{"id": "policyAuthority", "label": "Policy Authority", "type": "string"},
|
||||||
|
{"id": "exposureClass", "label": "Exposure Class", "type": "string"},
|
||||||
{"id": "serverHost", "label": "Server Host", "type": "string"},
|
{"id": "serverHost", "label": "Server Host", "type": "string"},
|
||||||
|
{"id": "routeHost", "label": "Route Host", "type": "string"},
|
||||||
|
{"id": "routePort", "label": "Route Port", "type": "number"},
|
||||||
{"id": "lifecycle", "label": "Lifecycle", "type": "string"},
|
{"id": "lifecycle", "label": "Lifecycle", "type": "string"},
|
||||||
{"id": "reviewState", "label": "Review State", "type": "string"},
|
{"id": "reviewState", "label": "Review State", "type": "string"},
|
||||||
{"id": "unresolved", "label": "Unresolved", "type": "boolean"},
|
{"id": "unresolved", "label": "Unresolved", "type": "boolean"},
|
||||||
@@ -193,6 +218,14 @@ def fabric_graph_explorer_manifest(base_url: str = "") -> dict[str, Any]:
|
|||||||
"repo",
|
"repo",
|
||||||
"domain",
|
"domain",
|
||||||
"lifecycle",
|
"lifecycle",
|
||||||
|
"deploymentEnvironment",
|
||||||
|
"deploymentScenario",
|
||||||
|
"routingAuthority",
|
||||||
|
"accessZone",
|
||||||
|
"policyAuthority",
|
||||||
|
"exposureClass",
|
||||||
|
"routeHost",
|
||||||
|
"routePort",
|
||||||
"canonCategory",
|
"canonCategory",
|
||||||
"evidenceState",
|
"evidenceState",
|
||||||
"unresolved",
|
"unresolved",
|
||||||
@@ -215,6 +248,31 @@ def fabric_graph_explorer_manifest(base_url: str = "") -> dict[str, Any]:
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
"modes": [
|
"modes": [
|
||||||
|
{
|
||||||
|
"id": "by-fabric",
|
||||||
|
"label": "Fabric",
|
||||||
|
"description": "Group and filter by fabric/accountability fields.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "by-deployment-environment",
|
||||||
|
"label": "Environment",
|
||||||
|
"description": "Group and filter by deployment environment.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "by-deployment-scenario",
|
||||||
|
"label": "Scenario",
|
||||||
|
"description": "Group and filter by deployment scenario.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "by-routing-authority",
|
||||||
|
"label": "Routing",
|
||||||
|
"description": "Group and filter by routing authority.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "by-access-zone",
|
||||||
|
"label": "Access Zone",
|
||||||
|
"description": "Group and filter by intended access zone.",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "full",
|
"id": "full",
|
||||||
"label": "Full",
|
"label": "Full",
|
||||||
@@ -335,6 +393,10 @@ def fabric_graph_explorer_payload(
|
|||||||
if source_kind == "Repository":
|
if source_kind == "Repository":
|
||||||
continue
|
continue
|
||||||
attributes = node.get("attributes") if isinstance(node.get("attributes"), dict) else {}
|
attributes = node.get("attributes") if isinstance(node.get("attributes"), dict) else {}
|
||||||
|
overlay_data = _overlay_data(
|
||||||
|
node.get("deployment_overlay") if isinstance(node.get("deployment_overlay"), dict) else {},
|
||||||
|
attributes,
|
||||||
|
)
|
||||||
kind = _presentation_kind(source_kind, attributes)
|
kind = _presentation_kind(source_kind, attributes)
|
||||||
layer = _layer_for_kind(kind)
|
layer = _layer_for_kind(kind)
|
||||||
is_unresolved = node_id in unresolved
|
is_unresolved = node_id in unresolved
|
||||||
@@ -353,6 +415,8 @@ def fabric_graph_explorer_payload(
|
|||||||
"repo": str(node.get("repo", "")),
|
"repo": str(node.get("repo", "")),
|
||||||
"domain": str(node.get("domain", "")),
|
"domain": str(node.get("domain", "")),
|
||||||
"lifecycle": str(node.get("lifecycle", "")),
|
"lifecycle": str(node.get("lifecycle", "")),
|
||||||
|
"environment": overlay_data["deploymentEnvironment"],
|
||||||
|
**overlay_data,
|
||||||
"canonCategory": str(
|
"canonCategory": str(
|
||||||
node.get("canon_category") or attributes.get("canon_category") or ""
|
node.get("canon_category") or attributes.get("canon_category") or ""
|
||||||
),
|
),
|
||||||
@@ -522,12 +586,17 @@ def _presentation_edge_type(edge_type: str, source: str, target: str, node_kinds
|
|||||||
|
|
||||||
def _edge_metadata(edge: dict[str, Any], edge_type: str) -> dict[str, Any]:
|
def _edge_metadata(edge: dict[str, Any], edge_type: str) -> dict[str, Any]:
|
||||||
canon_mapping = edge_canon_mapping(edge_type)
|
canon_mapping = edge_canon_mapping(edge_type)
|
||||||
|
attributes = edge.get("attributes") if isinstance(edge.get("attributes"), dict) else {}
|
||||||
return {
|
return {
|
||||||
"canonical_type": str(edge.get("canonical_type") or canon_mapping.canonical_type),
|
"canonical_type": str(edge.get("canonical_type") or canon_mapping.canonical_type),
|
||||||
"canon_anchor": str(edge.get("canon_anchor") or canon_mapping.canon_anchor),
|
"canon_anchor": str(edge.get("canon_anchor") or canon_mapping.canon_anchor),
|
||||||
"mapping_fit": str(edge.get("mapping_fit") or canon_mapping.fit),
|
"mapping_fit": str(edge.get("mapping_fit") or canon_mapping.fit),
|
||||||
"display_only": bool(edge.get("display_only", canon_mapping.display_only)),
|
"display_only": bool(edge.get("display_only", canon_mapping.display_only)),
|
||||||
"evidence_state": str(edge.get("evidence_state") or "declared"),
|
"evidence_state": str(edge.get("evidence_state") or "declared"),
|
||||||
|
"deployment_overlay": normalize_deployment_overlay(
|
||||||
|
edge.get("deployment_overlay") if isinstance(edge.get("deployment_overlay"), dict) else {},
|
||||||
|
attributes,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -630,6 +699,12 @@ def _append_infrastructure_elements(
|
|||||||
for endpoint in service_endpoints
|
for endpoint in service_endpoints
|
||||||
if _environment_matches(environment, endpoint["environments"])
|
if _environment_matches(environment, endpoint["environments"])
|
||||||
]
|
]
|
||||||
|
deployment_overlay = normalize_deployment_overlay(
|
||||||
|
attributes,
|
||||||
|
{"deployment_environment": environment},
|
||||||
|
*[endpoint["deployment_overlay"] for endpoint in matching_endpoints],
|
||||||
|
)
|
||||||
|
overlay_data = _overlay_data(deployment_overlay)
|
||||||
server_hosts = sorted({endpoint["host"] for endpoint in matching_endpoints})
|
server_hosts = sorted({endpoint["host"] for endpoint in matching_endpoints})
|
||||||
deployment_data = {
|
deployment_data = {
|
||||||
"id": deployment_id,
|
"id": deployment_id,
|
||||||
@@ -643,6 +718,7 @@ def _append_infrastructure_elements(
|
|||||||
"domain": str(service.get("domain") or ""),
|
"domain": str(service.get("domain") or ""),
|
||||||
"lifecycle": str(service.get("lifecycle") or ""),
|
"lifecycle": str(service.get("lifecycle") or ""),
|
||||||
"environment": environment,
|
"environment": environment,
|
||||||
|
**overlay_data,
|
||||||
"serviceId": service_id,
|
"serviceId": service_id,
|
||||||
"serverHosts": server_hosts,
|
"serverHosts": server_hosts,
|
||||||
"reviewState": "accepted",
|
"reviewState": "accepted",
|
||||||
@@ -676,6 +752,10 @@ def _append_infrastructure_elements(
|
|||||||
host = endpoint["host"]
|
host = endpoint["host"]
|
||||||
port = endpoint["port"]
|
port = endpoint["port"]
|
||||||
protocol = endpoint["protocol"]
|
protocol = endpoint["protocol"]
|
||||||
|
endpoint_overlay_data = _overlay_data(
|
||||||
|
endpoint["deployment_overlay"],
|
||||||
|
{"deployment_environment": environment},
|
||||||
|
)
|
||||||
server_id = server_ids_by_host.get(host)
|
server_id = server_ids_by_host.get(host)
|
||||||
endpoint_key = _endpoint_key(host, port, protocol)
|
endpoint_key = _endpoint_key(host, port, protocol)
|
||||||
port_id = port_ids_by_endpoint.get(endpoint_key)
|
port_id = port_ids_by_endpoint.get(endpoint_key)
|
||||||
@@ -695,6 +775,7 @@ def _append_infrastructure_elements(
|
|||||||
"domain": str(service.get("domain") or ""),
|
"domain": str(service.get("domain") or ""),
|
||||||
"lifecycle": "active",
|
"lifecycle": "active",
|
||||||
"environment": environment,
|
"environment": environment,
|
||||||
|
**endpoint_overlay_data,
|
||||||
"serverHost": host,
|
"serverHost": host,
|
||||||
"reviewState": "accepted",
|
"reviewState": "accepted",
|
||||||
"freshnessState": "current",
|
"freshnessState": "current",
|
||||||
@@ -706,6 +787,7 @@ def _append_infrastructure_elements(
|
|||||||
"host": host,
|
"host": host,
|
||||||
"source_interface_id": endpoint["interface_id"],
|
"source_interface_id": endpoint["interface_id"],
|
||||||
"endpoint_url": endpoint["url"],
|
"endpoint_url": endpoint["url"],
|
||||||
|
"deployment_overlay": endpoint_overlay_data["deploymentOverlay"],
|
||||||
},
|
},
|
||||||
"displayState": "show",
|
"displayState": "show",
|
||||||
"visibilitySource": "default",
|
"visibilitySource": "default",
|
||||||
@@ -731,6 +813,7 @@ def _append_infrastructure_elements(
|
|||||||
"domain": str(service.get("domain") or ""),
|
"domain": str(service.get("domain") or ""),
|
||||||
"lifecycle": "active",
|
"lifecycle": "active",
|
||||||
"environment": environment,
|
"environment": environment,
|
||||||
|
**endpoint_overlay_data,
|
||||||
"serverHost": host,
|
"serverHost": host,
|
||||||
"reviewState": "accepted",
|
"reviewState": "accepted",
|
||||||
"freshnessState": "current",
|
"freshnessState": "current",
|
||||||
@@ -744,6 +827,7 @@ def _append_infrastructure_elements(
|
|||||||
"protocol": protocol,
|
"protocol": protocol,
|
||||||
"source_interface_id": endpoint["interface_id"],
|
"source_interface_id": endpoint["interface_id"],
|
||||||
"endpoint_url": endpoint["url"],
|
"endpoint_url": endpoint["url"],
|
||||||
|
"deployment_overlay": endpoint_overlay_data["deploymentOverlay"],
|
||||||
},
|
},
|
||||||
"displayState": "show",
|
"displayState": "show",
|
||||||
"visibilitySource": "default",
|
"visibilitySource": "default",
|
||||||
@@ -783,6 +867,16 @@ def _endpoints_by_service(source_nodes: list[dict[str, Any]]) -> dict[str, list[
|
|||||||
"url": url,
|
"url": url,
|
||||||
"interface_id": str(node.get("id") or ""),
|
"interface_id": str(node.get("id") or ""),
|
||||||
"environments": _environments(attributes),
|
"environments": _environments(attributes),
|
||||||
|
"deployment_overlay": normalize_deployment_overlay(
|
||||||
|
attributes,
|
||||||
|
endpoint,
|
||||||
|
{
|
||||||
|
"host": host,
|
||||||
|
"port": port,
|
||||||
|
"protocol": protocol,
|
||||||
|
"route": url,
|
||||||
|
},
|
||||||
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return endpoints
|
return endpoints
|
||||||
@@ -895,6 +989,7 @@ def _edge_element(
|
|||||||
mapping_fit: str = "",
|
mapping_fit: str = "",
|
||||||
display_only: bool = False,
|
display_only: bool = False,
|
||||||
evidence_state: str = "",
|
evidence_state: str = "",
|
||||||
|
deployment_overlay: dict[str, Any] | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
source_layer = node_layers.get(source, "unknown")
|
source_layer = node_layers.get(source, "unknown")
|
||||||
target_layer = node_layers.get(target, "unknown")
|
target_layer = node_layers.get(target, "unknown")
|
||||||
@@ -910,6 +1005,7 @@ def _edge_element(
|
|||||||
strength = _edge_strength(edge_type)
|
strength = _edge_strength(edge_type)
|
||||||
layout = _layout_hints(edge_type, source_layer, target_layer, same_repo)
|
layout = _layout_hints(edge_type, source_layer, target_layer, same_repo)
|
||||||
edge_id = f"edge:{edge_index}:{source}:{edge_type}:{target}"
|
edge_id = f"edge:{edge_index}:{source}:{edge_type}:{target}"
|
||||||
|
overlay_data = _overlay_data(deployment_overlay or {})
|
||||||
return {
|
return {
|
||||||
"data": {
|
"data": {
|
||||||
"id": edge_id,
|
"id": edge_id,
|
||||||
@@ -930,6 +1026,7 @@ def _edge_element(
|
|||||||
"mappingFit": mapping_fit,
|
"mappingFit": mapping_fit,
|
||||||
"displayOnly": display_only,
|
"displayOnly": display_only,
|
||||||
"evidenceState": evidence_state,
|
"evidenceState": evidence_state,
|
||||||
|
**overlay_data,
|
||||||
"strength": strength,
|
"strength": strength,
|
||||||
"edgeWidth": _edge_width(strength),
|
"edgeWidth": _edge_width(strength),
|
||||||
"sameLayer": source_layer == target_layer,
|
"sameLayer": source_layer == target_layer,
|
||||||
@@ -959,6 +1056,10 @@ def _edge_element(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _overlay_data(*sources: object) -> dict[str, Any]:
|
||||||
|
return graph_explorer_overlay_fields(normalize_deployment_overlay(*sources))
|
||||||
|
|
||||||
|
|
||||||
def _layout_hints(
|
def _layout_hints(
|
||||||
edge_type: str,
|
edge_type: str,
|
||||||
source_layer: str,
|
source_layer: str,
|
||||||
|
|||||||
@@ -574,7 +574,10 @@ def graph_explorer_page() -> str:
|
|||||||
|
|
||||||
const elementText = (data) => [
|
const elementText = (data) => [
|
||||||
data.id, data.stableKey, data.label, data.name, data.description,
|
data.id, data.stableKey, data.label, data.name, data.description,
|
||||||
data.repo, data.domain, data.kind, data.layer, data.edgeType
|
data.repo, data.domain, data.kind, data.layer, data.edgeType,
|
||||||
|
data.deploymentEnvironment, data.deploymentScenario, data.routingAuthority,
|
||||||
|
data.accessZone, data.policyAuthority, data.exposureClass,
|
||||||
|
data.routeHost, data.routeHostname, data.routePort, data.routeProtocol, data.route
|
||||||
].join(" ").toLowerCase();
|
].join(" ").toLowerCase();
|
||||||
|
|
||||||
const overrideKey = (element) => element.data("stableKey") || element.id();
|
const overrideKey = (element) => element.data("stableKey") || element.id();
|
||||||
@@ -614,6 +617,37 @@ def graph_explorer_page() -> str:
|
|||||||
|
|
||||||
const selectedEdgeTypes = () => checkedValues(edgeTypeFilter);
|
const selectedEdgeTypes = () => checkedValues(edgeTypeFilter);
|
||||||
|
|
||||||
|
const hasValue = (value) => value !== undefined && value !== null && value !== "";
|
||||||
|
|
||||||
|
const overlayRuleAttributes = [
|
||||||
|
"deploymentEnvironment",
|
||||||
|
"deploymentScenario",
|
||||||
|
"routingAuthority",
|
||||||
|
"accessZone",
|
||||||
|
"policyAuthority",
|
||||||
|
"exposureClass",
|
||||||
|
"routeHost",
|
||||||
|
"routeHostname",
|
||||||
|
"routePort",
|
||||||
|
"routeProtocol",
|
||||||
|
];
|
||||||
|
|
||||||
|
const zoneModeFields = {
|
||||||
|
"by-fabric": ["domain", "repo", "kind"],
|
||||||
|
"by-deployment-environment": ["deploymentEnvironment"],
|
||||||
|
"by-deployment-scenario": ["deploymentScenario"],
|
||||||
|
"by-routing-authority": ["routingAuthority"],
|
||||||
|
"by-access-zone": ["accessZone"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const zoneModeLabels = {
|
||||||
|
"by-fabric": "Fabric",
|
||||||
|
"by-deployment-environment": "Deployment Environment",
|
||||||
|
"by-deployment-scenario": "Deployment Scenario",
|
||||||
|
"by-routing-authority": "Routing Authority",
|
||||||
|
"by-access-zone": "Access Zone",
|
||||||
|
};
|
||||||
|
|
||||||
const summarizeSelection = (selected, allValues, labels, noun) => {
|
const summarizeSelection = (selected, allValues, labels, noun) => {
|
||||||
if (selected.size === 0) return `No ${noun}`;
|
if (selected.size === 0) return `No ${noun}`;
|
||||||
if (selected.size === allValues.length) return `All ${noun}`;
|
if (selected.size === allValues.length) return `All ${noun}`;
|
||||||
@@ -657,11 +691,21 @@ def graph_explorer_page() -> str:
|
|||||||
strength: "Strength",
|
strength: "Strength",
|
||||||
sameRepo: "Same repo",
|
sameRepo: "Same repo",
|
||||||
layoutAffinity: "Layout affinity",
|
layoutAffinity: "Layout affinity",
|
||||||
|
deploymentEnvironment: "Deployment Environment",
|
||||||
|
deploymentScenario: "Deployment Scenario",
|
||||||
|
routingAuthority: "Routing Authority",
|
||||||
|
accessZone: "Access Zone",
|
||||||
|
policyAuthority: "Policy Authority",
|
||||||
|
exposureClass: "Exposure Class",
|
||||||
|
routeHost: "Route Host",
|
||||||
|
routeHostname: "Route Hostname",
|
||||||
|
routePort: "Route Port",
|
||||||
|
routeProtocol: "Route Protocol",
|
||||||
};
|
};
|
||||||
|
|
||||||
const ruleAttributeCandidates = {
|
const ruleAttributeCandidates = {
|
||||||
node: ["any", "repo", "kind", "reviewState", "unresolved", "lifecycle"],
|
node: ["any", "repo", "kind", "reviewState", "unresolved", "lifecycle", ...overlayRuleAttributes],
|
||||||
edge: ["any", "repo", "kind", "reviewState", "unresolved", "strength", "sameRepo", "layoutAffinity"],
|
edge: ["any", "repo", "kind", "reviewState", "unresolved", "strength", "sameRepo", "layoutAffinity", ...overlayRuleAttributes],
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderOptions = (select, options, current = "") => {
|
const renderOptions = (select, options, current = "") => {
|
||||||
@@ -835,6 +879,79 @@ def graph_explorer_page() -> str:
|
|||||||
(!type || edge.data("edgeType") === type)
|
(!type || edge.data("edgeType") === type)
|
||||||
) : [];
|
) : [];
|
||||||
|
|
||||||
|
const routeLabel = (data) => {
|
||||||
|
if (hasValue(data.route)) return data.route;
|
||||||
|
const host = data.routeHost || data.routeHostname || "";
|
||||||
|
const protocol = data.routeProtocol || "";
|
||||||
|
const port = data.routePort || "";
|
||||||
|
if (!hasValue(host) && !hasValue(port)) return "";
|
||||||
|
const authority = hasValue(port) ? `${host}:${port}` : host;
|
||||||
|
return [protocol, authority].filter(hasValue).join(" ");
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasRouteEvidence = (data) =>
|
||||||
|
hasValue(data.route) ||
|
||||||
|
hasValue(data.routeHost) ||
|
||||||
|
hasValue(data.routeHostname) ||
|
||||||
|
hasValue(data.routePort);
|
||||||
|
|
||||||
|
const zoneWarningsForData = (data) => {
|
||||||
|
const warnings = [];
|
||||||
|
if (hasRouteEvidence(data) && !hasValue(data.policyAuthority)) {
|
||||||
|
warnings.push("route without policy authority");
|
||||||
|
}
|
||||||
|
return warnings;
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupCounts = (elements, field) => {
|
||||||
|
const counts = new Map();
|
||||||
|
elements.forEach((element) => {
|
||||||
|
const value = element.data(field);
|
||||||
|
if (hasValue(value)) counts.set(String(value), (counts.get(String(value)) || 0) + 1);
|
||||||
|
});
|
||||||
|
return Array.from(counts.entries())
|
||||||
|
.sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]));
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderMapOverview = () => {
|
||||||
|
const visibleElements = cy
|
||||||
|
? cy.elements().filter((element) => element.style("display") !== "none").toArray()
|
||||||
|
: [];
|
||||||
|
const visibleNodes = visibleElements.filter((element) => element.isNode()).length;
|
||||||
|
const visibleEdges = visibleElements.filter((element) => element.isEdge()).length;
|
||||||
|
const fields = zoneModeFields[activeMode] || [];
|
||||||
|
const title = zoneModeLabels[activeMode] || "Full";
|
||||||
|
detailTitle.textContent = activeMode === "full" ? "Fabric Map" : `${title} View`;
|
||||||
|
detailSummary.textContent = `${visibleNodes} nodes / ${visibleEdges} edges visible`;
|
||||||
|
detailPills.innerHTML = activeMode === "full" ? "" : `<span class="pill">${escapeHtml(activeMode)}</span>`;
|
||||||
|
|
||||||
|
const rows = [{label: "visible", value: `${visibleNodes} nodes / ${visibleEdges} edges`}];
|
||||||
|
fields.forEach((field) => {
|
||||||
|
const counts = groupCounts(visibleElements, field);
|
||||||
|
rows.push({
|
||||||
|
label: ruleAttributeLabels[field] || humanize(field),
|
||||||
|
value: counts.length
|
||||||
|
? counts.slice(0, 8).map(([value, count]) => `${value} (${count})`).join(", ")
|
||||||
|
: "no annotated visible entities",
|
||||||
|
state: counts.length ? "" : "warning",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const warnings = visibleElements
|
||||||
|
.flatMap((element) => zoneWarningsForData(element.data()).map((warning) =>
|
||||||
|
`${elementLabel(element)}: ${warning}`
|
||||||
|
));
|
||||||
|
warnings.slice(0, 6).forEach((warning) => rows.push({label: "warning", value: warning, state: "warning"}));
|
||||||
|
if (warnings.length > 6) {
|
||||||
|
rows.push({label: "warning", value: `${warnings.length - 6} additional route warnings`, state: "warning"});
|
||||||
|
}
|
||||||
|
|
||||||
|
detailList.innerHTML = rows
|
||||||
|
.filter((row) => row.value)
|
||||||
|
.map((row) => `<li class="${orientationStateClass(row.state)}"><strong>${escapeHtml(row.label)}</strong> ${escapeHtml(row.value)}</li>`)
|
||||||
|
.join("");
|
||||||
|
};
|
||||||
|
|
||||||
const collectionArray = (collection) => Array.from(collection || []);
|
const collectionArray = (collection) => Array.from(collection || []);
|
||||||
|
|
||||||
const orientationStateClass = (state) =>
|
const orientationStateClass = (state) =>
|
||||||
@@ -1145,6 +1262,12 @@ def graph_explorer_page() -> str:
|
|||||||
if (activeMode === "unresolved") {
|
if (activeMode === "unresolved") {
|
||||||
return new Set(cy.elements().filter((element) => element.data("unresolved") === true).map((element) => element.id()));
|
return new Set(cy.elements().filter((element) => element.data("unresolved") === true).map((element) => element.id()));
|
||||||
}
|
}
|
||||||
|
const zoneFields = zoneModeFields[activeMode] || [];
|
||||||
|
if (zoneFields.length && activeMode !== "by-fabric") {
|
||||||
|
return new Set(cy.elements().filter((element) =>
|
||||||
|
zoneFields.some((field) => hasValue(element.data(field)))
|
||||||
|
).map((element) => element.id()));
|
||||||
|
}
|
||||||
if (!selected) return null;
|
if (!selected) return null;
|
||||||
if (activeMode === "selected-path") {
|
if (activeMode === "selected-path") {
|
||||||
const collection = selected.union(selected.predecessors()).union(selected.successors());
|
const collection = selected.union(selected.predecessors()).union(selected.successors());
|
||||||
@@ -1204,6 +1327,7 @@ def graph_explorer_page() -> str:
|
|||||||
hiddenSummary.textContent = removed ? `Hidden ${hidden} / Removed ${removed}` : `Hidden ${hidden}`;
|
hiddenSummary.textContent = removed ? `Hidden ${hidden} / Removed ${removed}` : `Hidden ${hidden}`;
|
||||||
updateLabelVisibility();
|
updateLabelVisibility();
|
||||||
updateSelectionAnchor();
|
updateSelectionAnchor();
|
||||||
|
if (!selected) renderMapOverview();
|
||||||
if (options.redrawOnRemove && previousRemoved !== ruleRemovalSignature()) runLayout();
|
if (options.redrawOnRemove && previousRemoved !== ruleRemovalSignature()) runLayout();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1211,10 +1335,7 @@ def graph_explorer_page() -> str:
|
|||||||
selected = element || null;
|
selected = element || null;
|
||||||
if (!element) {
|
if (!element) {
|
||||||
orientationContext = null;
|
orientationContext = null;
|
||||||
detailTitle.textContent = "Fabric Map";
|
renderMapOverview();
|
||||||
detailSummary.textContent = "No selection";
|
|
||||||
detailPills.innerHTML = "";
|
|
||||||
detailList.innerHTML = "";
|
|
||||||
orientationTitle.textContent = "Select a service, interface, dependency, or registered-only repo.";
|
orientationTitle.textContent = "Select a service, interface, dependency, or registered-only repo.";
|
||||||
orientationList.innerHTML = "";
|
orientationList.innerHTML = "";
|
||||||
orientationActions.innerHTML = "";
|
orientationActions.innerHTML = "";
|
||||||
@@ -1241,6 +1362,14 @@ def graph_explorer_page() -> str:
|
|||||||
["mapping", data.mappingFit],
|
["mapping", data.mappingFit],
|
||||||
["display only", data.displayOnly === true ? "yes" : ""],
|
["display only", data.displayOnly === true ? "yes" : ""],
|
||||||
["strength", data.strength],
|
["strength", data.strength],
|
||||||
|
["deployment environment", data.deploymentEnvironment],
|
||||||
|
["deployment scenario", data.deploymentScenario],
|
||||||
|
["routing authority", data.routingAuthority],
|
||||||
|
["access zone", data.accessZone],
|
||||||
|
["policy authority", data.policyAuthority],
|
||||||
|
["exposure", data.exposureClass],
|
||||||
|
["route", routeLabel(data)],
|
||||||
|
...zoneWarningsForData(data).map((warning) => ["warning", warning]),
|
||||||
...Object.entries(links),
|
...Object.entries(links),
|
||||||
...refs.map((ref) => [ref.label || ref.kind || "source", ref.path || ref.url || ref.ref || ""])
|
...refs.map((ref) => [ref.label || ref.kind || "source", ref.path || ref.url || ref.ref || ""])
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import yaml
|
|||||||
|
|
||||||
from .canon import edge_canon_mapping, evidence_state_for, node_canon_mapping, source_kind_from_anchor
|
from .canon import edge_canon_mapping, evidence_state_for, node_canon_mapping, source_kind_from_anchor
|
||||||
from .connectors import ConnectorConfig, apply_connectors
|
from .connectors import ConnectorConfig, apply_connectors
|
||||||
|
from .deployment_overlay import normalize_deployment_overlay
|
||||||
from .discovery import (
|
from .discovery import (
|
||||||
attribute_stable_key,
|
attribute_stable_key,
|
||||||
discovery_stable_key,
|
discovery_stable_key,
|
||||||
@@ -1146,6 +1147,22 @@ def _add_runtime_endpoint(
|
|||||||
if not host or port_number is None:
|
if not host or port_number is None:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
runtime_attributes = {
|
||||||
|
"host": host,
|
||||||
|
"runtime_target_type": server_type,
|
||||||
|
**(attributes or {}),
|
||||||
|
}
|
||||||
|
overlay = _runtime_deployment_overlay(
|
||||||
|
host=host,
|
||||||
|
port=port_number,
|
||||||
|
protocol=protocol_value,
|
||||||
|
domain=domain,
|
||||||
|
server_type=server_type,
|
||||||
|
attributes=runtime_attributes,
|
||||||
|
)
|
||||||
|
if overlay:
|
||||||
|
runtime_attributes["deployment_overlay"] = overlay
|
||||||
|
|
||||||
target_kind = _runtime_target_kind(host, server_type)
|
target_kind = _runtime_target_kind(host, server_type)
|
||||||
target_key = discovery_stable_key(context.repo_slug, target_kind, host)
|
target_key = discovery_stable_key(context.repo_slug, target_kind, host)
|
||||||
context.accumulator.add_node(
|
context.accumulator.add_node(
|
||||||
@@ -1156,11 +1173,7 @@ def _add_runtime_endpoint(
|
|||||||
provenance=provenance,
|
provenance=provenance,
|
||||||
source_anchor=anchor,
|
source_anchor=anchor,
|
||||||
aliases=[host],
|
aliases=[host],
|
||||||
attributes={
|
attributes=runtime_attributes,
|
||||||
"host": host,
|
|
||||||
"runtime_target_type": server_type,
|
|
||||||
**(attributes or {}),
|
|
||||||
},
|
|
||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1175,10 +1188,9 @@ def _add_runtime_endpoint(
|
|||||||
source_anchor=anchor,
|
source_anchor=anchor,
|
||||||
aliases=[port_label],
|
aliases=[port_label],
|
||||||
attributes={
|
attributes={
|
||||||
"host": host,
|
|
||||||
"port": port_number,
|
"port": port_number,
|
||||||
"protocol": protocol_value,
|
"protocol": protocol_value,
|
||||||
**(attributes or {}),
|
**runtime_attributes,
|
||||||
},
|
},
|
||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
)
|
)
|
||||||
@@ -1333,6 +1345,79 @@ def _add_domain_route(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _runtime_deployment_overlay(
|
||||||
|
*,
|
||||||
|
host: str,
|
||||||
|
port: int,
|
||||||
|
protocol: str,
|
||||||
|
domain: str,
|
||||||
|
server_type: str,
|
||||||
|
attributes: dict[str, object],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
source = str(attributes.get("source") or server_type or "").strip()
|
||||||
|
overlay: dict[str, Any] = {
|
||||||
|
"routing_authority": _routing_authority(source, server_type),
|
||||||
|
"route_evidence": {
|
||||||
|
"host": host,
|
||||||
|
"hostname": _normalize_domain(domain) or host,
|
||||||
|
"port": port,
|
||||||
|
"protocol": protocol,
|
||||||
|
"route": str(attributes.get("endpoint_url") or domain or host),
|
||||||
|
"scheme": attributes.get("scheme", ""),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if _is_loopback_host(host):
|
||||||
|
overlay.update(
|
||||||
|
{
|
||||||
|
"deployment_environment": "dev",
|
||||||
|
"deployment_scenario": "bernd-laptop",
|
||||||
|
"access_zone": "private-dev",
|
||||||
|
"policy_authority": "local-loopback-binding",
|
||||||
|
"exposure_class": "local-only",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
elif _mentions_scenario(host, domain, "coulombcore"):
|
||||||
|
overlay.update(
|
||||||
|
{
|
||||||
|
"deployment_environment": "test",
|
||||||
|
"deployment_scenario": "coulombcore",
|
||||||
|
"access_zone": "collaborator-test",
|
||||||
|
"exposure_class": "collaborator-test",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
elif _mentions_scenario(host, domain, "railiance01"):
|
||||||
|
overlay.update(
|
||||||
|
{
|
||||||
|
"deployment_environment": "prod",
|
||||||
|
"deployment_scenario": "railiance01",
|
||||||
|
"access_zone": "production-public" if _looks_like_domain(domain or host) else "production-admin",
|
||||||
|
"exposure_class": "production-public" if _looks_like_domain(domain or host) else "production-admin",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return normalize_deployment_overlay(overlay)
|
||||||
|
|
||||||
|
|
||||||
|
def _routing_authority(source: str, server_type: str) -> str:
|
||||||
|
source_value = source.strip().lower()
|
||||||
|
if source_value == "docker-compose":
|
||||||
|
return "docker-compose"
|
||||||
|
if source_value.startswith("kubernetes-") or server_type == "kubernetes-service-dns":
|
||||||
|
return "kubernetes"
|
||||||
|
if server_type == "declared-endpoint":
|
||||||
|
return "declared-endpoint"
|
||||||
|
return source_value or server_type
|
||||||
|
|
||||||
|
|
||||||
|
def _is_loopback_host(host: str) -> bool:
|
||||||
|
value = _normalize_host(host)
|
||||||
|
return value in {"localhost", "127.0.0.1", "::1"}
|
||||||
|
|
||||||
|
|
||||||
|
def _mentions_scenario(host: str, domain: str, scenario: str) -> bool:
|
||||||
|
needle = scenario.strip().lower()
|
||||||
|
return needle in _normalize_host(host) or needle in _normalize_domain(domain)
|
||||||
|
|
||||||
|
|
||||||
def _compose_port_bindings(service: dict[str, Any]) -> list[dict[str, object]]:
|
def _compose_port_bindings(service: dict[str, Any]) -> list[dict[str, object]]:
|
||||||
ports = service.get("ports")
|
ports = service.get("ports")
|
||||||
if not isinstance(ports, list):
|
if not isinstance(ports, list):
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ $defs:
|
|||||||
type: string
|
type: string
|
||||||
owner_actor_id:
|
owner_actor_id:
|
||||||
type: string
|
type: string
|
||||||
|
deployment_overlay:
|
||||||
|
$ref: "#/$defs/deploymentOverlay"
|
||||||
review_state:
|
review_state:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
@@ -108,3 +110,29 @@ $defs:
|
|||||||
attributes:
|
attributes:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: true
|
additionalProperties: true
|
||||||
|
|
||||||
|
deploymentOverlay:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
properties:
|
||||||
|
deployment_environment:
|
||||||
|
type: string
|
||||||
|
deployment_scenario:
|
||||||
|
type: string
|
||||||
|
routing_authority:
|
||||||
|
type: string
|
||||||
|
access_zone:
|
||||||
|
type: string
|
||||||
|
policy_authority:
|
||||||
|
type: string
|
||||||
|
exposure_class:
|
||||||
|
type: string
|
||||||
|
route_evidence:
|
||||||
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
anyOf:
|
||||||
|
- type: string
|
||||||
|
- type: integer
|
||||||
|
- type: number
|
||||||
|
- type: boolean
|
||||||
|
- type: "null"
|
||||||
|
|||||||
@@ -61,9 +61,38 @@ properties:
|
|||||||
notes:
|
notes:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
|
deployment_overlay:
|
||||||
|
$ref: "#/$defs/deploymentOverlay"
|
||||||
auth:
|
auth:
|
||||||
$ref: "./common.schema.yaml#/$defs/auth"
|
$ref: "./common.schema.yaml#/$defs/auth"
|
||||||
data_classification:
|
data_classification:
|
||||||
$ref: "./common.schema.yaml#/$defs/dataClassification"
|
$ref: "./common.schema.yaml#/$defs/dataClassification"
|
||||||
compatibility:
|
compatibility:
|
||||||
$ref: "./common.schema.yaml#/$defs/compatibility"
|
$ref: "./common.schema.yaml#/$defs/compatibility"
|
||||||
|
|
||||||
|
$defs:
|
||||||
|
deploymentOverlay:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
properties:
|
||||||
|
deployment_environment:
|
||||||
|
type: string
|
||||||
|
deployment_scenario:
|
||||||
|
type: string
|
||||||
|
routing_authority:
|
||||||
|
type: string
|
||||||
|
access_zone:
|
||||||
|
type: string
|
||||||
|
policy_authority:
|
||||||
|
type: string
|
||||||
|
exposure_class:
|
||||||
|
type: string
|
||||||
|
route_evidence:
|
||||||
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
anyOf:
|
||||||
|
- type: string
|
||||||
|
- type: integer
|
||||||
|
- type: number
|
||||||
|
- type: boolean
|
||||||
|
- type: "null"
|
||||||
|
|||||||
@@ -273,6 +273,8 @@ $defs:
|
|||||||
$ref: "#/$defs/ownership"
|
$ref: "#/$defs/ownership"
|
||||||
accounting:
|
accounting:
|
||||||
$ref: "#/$defs/accounting"
|
$ref: "#/$defs/accounting"
|
||||||
|
deployment_overlay:
|
||||||
|
$ref: "#/$defs/deploymentOverlay"
|
||||||
evidence:
|
evidence:
|
||||||
$ref: "#/$defs/evidence"
|
$ref: "#/$defs/evidence"
|
||||||
canon_category:
|
canon_category:
|
||||||
@@ -336,6 +338,8 @@ $defs:
|
|||||||
$ref: "#/$defs/utility"
|
$ref: "#/$defs/utility"
|
||||||
accounting:
|
accounting:
|
||||||
$ref: "#/$defs/accounting"
|
$ref: "#/$defs/accounting"
|
||||||
|
deployment_overlay:
|
||||||
|
$ref: "#/$defs/deploymentOverlay"
|
||||||
evidence:
|
evidence:
|
||||||
$ref: "#/$defs/evidence"
|
$ref: "#/$defs/evidence"
|
||||||
attributes:
|
attributes:
|
||||||
@@ -484,6 +488,32 @@ $defs:
|
|||||||
- type: string
|
- type: string
|
||||||
- type: "null"
|
- type: "null"
|
||||||
|
|
||||||
|
deploymentOverlay:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
properties:
|
||||||
|
deployment_environment:
|
||||||
|
type: string
|
||||||
|
deployment_scenario:
|
||||||
|
type: string
|
||||||
|
routing_authority:
|
||||||
|
type: string
|
||||||
|
access_zone:
|
||||||
|
type: string
|
||||||
|
policy_authority:
|
||||||
|
type: string
|
||||||
|
exposure_class:
|
||||||
|
type: string
|
||||||
|
route_evidence:
|
||||||
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
anyOf:
|
||||||
|
- type: string
|
||||||
|
- type: integer
|
||||||
|
- type: number
|
||||||
|
- type: boolean
|
||||||
|
- type: "null"
|
||||||
|
|
||||||
evidence:
|
evidence:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
|||||||
47
tests/test_deployment_zone_inventory.py
Normal file
47
tests/test_deployment_zone_inventory.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
def test_deployment_zone_inventory_covers_current_scenarios() -> None:
|
||||||
|
inventory = yaml.safe_load(
|
||||||
|
Path("fabric/discovery/snapshots/2026-05-24-deployment-zone-inventory.yaml").read_text(encoding="utf-8")
|
||||||
|
)
|
||||||
|
|
||||||
|
surfaces = inventory["surfaces"]
|
||||||
|
scenarios = {surface["deployment_scenario"] for surface in surfaces}
|
||||||
|
environments = {surface["deployment_environment"] for surface in surfaces}
|
||||||
|
|
||||||
|
assert {"bernd-laptop", "coulombcore", "railiance01"} <= scenarios
|
||||||
|
assert {"dev", "test", "prod"} <= environments
|
||||||
|
|
||||||
|
dev_routes = [
|
||||||
|
surface["route_evidence"]
|
||||||
|
for surface in surfaces
|
||||||
|
if surface["deployment_environment"] == "dev"
|
||||||
|
]
|
||||||
|
assert {route["port"] for route in dev_routes} >= {3000, 8000, 8001, 8765, 8876}
|
||||||
|
|
||||||
|
test_routes = [
|
||||||
|
surface
|
||||||
|
for surface in surfaces
|
||||||
|
if surface["deployment_scenario"] == "coulombcore"
|
||||||
|
]
|
||||||
|
assert all(surface["routing_authority"] == "ops-bridge" for surface in test_routes)
|
||||||
|
assert all(surface["policy_authority"] == "ops-bridge-ssh" for surface in test_routes)
|
||||||
|
|
||||||
|
prod_hosts = {
|
||||||
|
surface["route_evidence"]["hostname"]
|
||||||
|
for surface in surfaces
|
||||||
|
if surface["deployment_scenario"] == "railiance01"
|
||||||
|
}
|
||||||
|
assert {"gitea.coulomb.social", "vergabe-teilnahme.whywhynot.de", "auth.coulomb.social"} <= prod_hosts
|
||||||
|
|
||||||
|
ambiguity_ids = {item["id"] for item in inventory["ambiguities"]}
|
||||||
|
assert "railiance01-coulombcore-ip-conflict" in ambiguity_ids
|
||||||
|
assert {item["surface_id"] for item in inventory["missing_policy_authority"]} >= {
|
||||||
|
"prod.railiance01.gitea",
|
||||||
|
"prod.railiance01.vergabe-teilnahme",
|
||||||
|
}
|
||||||
@@ -52,6 +52,11 @@ def test_graph_explorer_manifest_and_payload_validate() -> None:
|
|||||||
}
|
}
|
||||||
filter_labels = {field["id"]: field["label"] for field in manifest["filter"]["fields"]}
|
filter_labels = {field["id"]: field["label"] for field in manifest["filter"]["fields"]}
|
||||||
assert filter_labels["layer"] == "Node Type"
|
assert filter_labels["layer"] == "Node Type"
|
||||||
|
assert filter_labels["deploymentEnvironment"] == "Deployment Environment"
|
||||||
|
assert filter_labels["accessZone"] == "Access Zone"
|
||||||
|
assert {"by-deployment-environment", "by-deployment-scenario", "by-routing-authority", "by-access-zone"} <= {
|
||||||
|
mode["id"] for mode in manifest["modes"]
|
||||||
|
}
|
||||||
nodes = [element for element in payload["elements"] if "source" not in element["data"]]
|
nodes = [element for element in payload["elements"] if "source" not in element["data"]]
|
||||||
edges = [element for element in payload["elements"] if "source" in element["data"]]
|
edges = [element for element in payload["elements"] if "source" in element["data"]]
|
||||||
registered_only = next(
|
registered_only = next(
|
||||||
@@ -164,7 +169,19 @@ def test_graph_explorer_presents_legacy_server_nodes_as_runtime_entities() -> No
|
|||||||
"repo": "fixture-repo",
|
"repo": "fixture-repo",
|
||||||
"domain": "testing",
|
"domain": "testing",
|
||||||
"lifecycle": "active",
|
"lifecycle": "active",
|
||||||
"attributes": {"host": "gitea.example.test", "server_type": "ingress-host"},
|
"attributes": {
|
||||||
|
"host": "gitea.example.test",
|
||||||
|
"server_type": "ingress-host",
|
||||||
|
"deployment_overlay": {
|
||||||
|
"deployment_environment": "test",
|
||||||
|
"deployment_scenario": "coulombcore",
|
||||||
|
"routing_authority": "kubernetes",
|
||||||
|
"access_zone": "early-access",
|
||||||
|
"policy_authority": "netkingdom-iam",
|
||||||
|
"exposure_class": "early-access",
|
||||||
|
"route_evidence": {"hostname": "gitea.example.test", "port": 443, "protocol": "tcp"},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "fixture.server.gitea.default.svc.cluster.local",
|
"id": "fixture.server.gitea.default.svc.cluster.local",
|
||||||
@@ -217,6 +234,12 @@ def test_graph_explorer_presents_legacy_server_nodes_as_runtime_entities() -> No
|
|||||||
|
|
||||||
assert nodes_by_id["fixture.server.gitea.example.test"]["kind"] == "ApplicationEndpoint"
|
assert nodes_by_id["fixture.server.gitea.example.test"]["kind"] == "ApplicationEndpoint"
|
||||||
assert nodes_by_id["fixture.server.gitea.example.test"]["layer"] == "application"
|
assert nodes_by_id["fixture.server.gitea.example.test"]["layer"] == "application"
|
||||||
|
assert nodes_by_id["fixture.server.gitea.example.test"]["deploymentEnvironment"] == "test"
|
||||||
|
assert nodes_by_id["fixture.server.gitea.example.test"]["deploymentScenario"] == "coulombcore"
|
||||||
|
assert nodes_by_id["fixture.server.gitea.example.test"]["accessZone"] == "early-access"
|
||||||
|
assert nodes_by_id["fixture.server.gitea.example.test"]["policyAuthority"] == "netkingdom-iam"
|
||||||
|
assert nodes_by_id["fixture.server.gitea.example.test"]["routeHostname"] == "gitea.example.test"
|
||||||
|
assert nodes_by_id["fixture.server.gitea.example.test"]["routePort"] == 443
|
||||||
assert nodes_by_id["fixture.server.gitea.default.svc.cluster.local"]["kind"] == "RuntimeService"
|
assert nodes_by_id["fixture.server.gitea.default.svc.cluster.local"]["kind"] == "RuntimeService"
|
||||||
assert nodes_by_id["fixture.server.gitea.default.svc.cluster.local"]["layer"] == "runtime_service"
|
assert nodes_by_id["fixture.server.gitea.default.svc.cluster.local"]["layer"] == "runtime_service"
|
||||||
edge_types = {
|
edge_types = {
|
||||||
@@ -387,6 +410,12 @@ def test_registry_serves_graph_explorer_exports(tmp_path: Path) -> None:
|
|||||||
assert "updateLabelVisibility" in page
|
assert "updateLabelVisibility" in page
|
||||||
assert "ruleActionFor" in page
|
assert "ruleActionFor" in page
|
||||||
assert "ruleRemovalSignature" in page
|
assert "ruleRemovalSignature" in page
|
||||||
|
assert "zoneModeFields" in page
|
||||||
|
assert "renderMapOverview" in page
|
||||||
|
assert "route without policy authority" in page
|
||||||
|
assert "deploymentEnvironment" in page
|
||||||
|
assert "routingAuthority" in page
|
||||||
|
assert "accessZone" in page
|
||||||
assert "Remove and redraw" in page
|
assert "Remove and redraw" in page
|
||||||
assert "Rules are applied top to bottom" in page
|
assert "Rules are applied top to bottom" in page
|
||||||
assert "showHelp" in page
|
assert "showHelp" in page
|
||||||
|
|||||||
@@ -309,6 +309,8 @@ def test_registry_accepts_financial_graph_and_materializes_vnext_fields(tmp_path
|
|||||||
assert graph["apiVersion"] == "railiance.fabric/v1alpha2"
|
assert graph["apiVersion"] == "railiance.fabric/v1alpha2"
|
||||||
assert graph["schema_version"] == "financial-fabric-v1"
|
assert graph["schema_version"] == "financial-fabric-v1"
|
||||||
assert graph["nodes"][0]["evidence"]["review_state"] == "accepted"
|
assert graph["nodes"][0]["evidence"]["review_state"] == "accepted"
|
||||||
|
assert graph["nodes"][0]["deployment_overlay"]["deployment_environment"] == "dev"
|
||||||
|
assert graph["nodes"][0]["deployment_overlay"]["deployment_scenario"] == "bernd-laptop"
|
||||||
assert edge["relationship_category"] == "utility"
|
assert edge["relationship_category"] == "utility"
|
||||||
assert edge["boundary"]["crosses_fabric_boundary"] is False
|
assert edge["boundary"]["crosses_fabric_boundary"] is False
|
||||||
assert edge["boundary"]["crosses_subfabric_boundary"] is True
|
assert edge["boundary"]["crosses_subfabric_boundary"] is True
|
||||||
@@ -558,6 +560,15 @@ def _financial_graph() -> dict:
|
|||||||
"subfabric_id": None,
|
"subfabric_id": None,
|
||||||
"environment": "local",
|
"environment": "local",
|
||||||
},
|
},
|
||||||
|
"deployment_overlay": {
|
||||||
|
"deployment_environment": "dev",
|
||||||
|
"deployment_scenario": "bernd-laptop",
|
||||||
|
"routing_authority": "loopback",
|
||||||
|
"access_zone": "private-dev",
|
||||||
|
"policy_authority": "local-loopback-binding",
|
||||||
|
"exposure_class": "local-only",
|
||||||
|
"route_evidence": {"host": "127.0.0.1", "port": 8000, "protocol": "tcp"},
|
||||||
|
},
|
||||||
"ownership": {
|
"ownership": {
|
||||||
"owner_actor_id": "actor.railiance.primary-lord",
|
"owner_actor_id": "actor.railiance.primary-lord",
|
||||||
"owner_role": "lord",
|
"owner_role": "lord",
|
||||||
|
|||||||
@@ -44,6 +44,14 @@ def test_scan_repo_emits_schema_valid_deterministic_snapshot(tmp_path: Path) ->
|
|||||||
assert nodes_by_label[("Lockfile", "package-lock.json")]["attributes"]["path"] == "package-lock.json"
|
assert nodes_by_label[("Lockfile", "package-lock.json")]["attributes"]["path"] == "package-lock.json"
|
||||||
assert nodes_by_label[("ServiceConfig", "application.yaml")]["attributes"]["format"] == "yaml"
|
assert nodes_by_label[("ServiceConfig", "application.yaml")]["attributes"]["format"] == "yaml"
|
||||||
assert nodes_by_label[("Server", "127.0.0.1")]["attributes"]["runtime_target_type"] == "compose-host"
|
assert nodes_by_label[("Server", "127.0.0.1")]["attributes"]["runtime_target_type"] == "compose-host"
|
||||||
|
dev_overlay = nodes_by_label[("Server", "127.0.0.1")]["attributes"]["deployment_overlay"]
|
||||||
|
assert dev_overlay["deployment_environment"] == "dev"
|
||||||
|
assert dev_overlay["deployment_scenario"] == "bernd-laptop"
|
||||||
|
assert dev_overlay["access_zone"] == "private-dev"
|
||||||
|
assert dev_overlay["policy_authority"] == "local-loopback-binding"
|
||||||
|
assert dev_overlay["exposure_class"] == "local-only"
|
||||||
|
assert dev_overlay["routing_authority"] == "docker-compose"
|
||||||
|
assert dev_overlay["route_evidence"]["port"] == 8080
|
||||||
assert (
|
assert (
|
||||||
nodes_by_label[("RuntimeService", "fixture-api.testing.svc.cluster.local")]["attributes"]["runtime_target_type"]
|
nodes_by_label[("RuntimeService", "fixture-api.testing.svc.cluster.local")]["attributes"]["runtime_target_type"]
|
||||||
== "kubernetes-service-dns"
|
== "kubernetes-service-dns"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ type: workplan
|
|||||||
title: "Deployment Zone Discovery And Visualization"
|
title: "Deployment Zone Discovery And Visualization"
|
||||||
domain: railiance
|
domain: railiance
|
||||||
repo: railiance-fabric
|
repo: railiance-fabric
|
||||||
status: ready
|
status: finished
|
||||||
owner: codex
|
owner: codex
|
||||||
topic_slug: railiance
|
topic_slug: railiance
|
||||||
created: "2026-05-24"
|
created: "2026-05-24"
|
||||||
@@ -49,7 +49,7 @@ Railiance currently treats:
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: RAIL-FAB-WP-0020-T01
|
id: RAIL-FAB-WP-0020-T01
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "b8cf7d91-7743-4e58-9b13-ce99f2d9eef1"
|
state_hub_task_id: "b8cf7d91-7743-4e58-9b13-ce99f2d9eef1"
|
||||||
```
|
```
|
||||||
@@ -70,11 +70,15 @@ Fields should cover:
|
|||||||
Done when identity projection, financial export, and graph-explorer payloads
|
Done when identity projection, financial export, and graph-explorer payloads
|
||||||
have a clear place to carry these fields without changing fabric membership.
|
have a clear place to carry these fields without changing fabric membership.
|
||||||
|
|
||||||
|
Result: added a normalized `deployment_overlay` object and threaded it through
|
||||||
|
identity candidates, financial exports, State Hub export schema validation, and
|
||||||
|
graph-explorer payload fields/modes.
|
||||||
|
|
||||||
## T02 - Discover Local Dev Routing Evidence
|
## T02 - Discover Local Dev Routing Evidence
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: RAIL-FAB-WP-0020-T02
|
id: RAIL-FAB-WP-0020-T02
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "b072e11b-08b5-426f-9f98-001abf8afd70"
|
state_hub_task_id: "b072e11b-08b5-426f-9f98-001abf8afd70"
|
||||||
```
|
```
|
||||||
@@ -95,11 +99,16 @@ Done when local-only surfaces are marked as `deployment_environment: dev`,
|
|||||||
`deployment_scenario: bernd-laptop`, and `access_zone: private-dev` with
|
`deployment_scenario: bernd-laptop`, and `access_zone: private-dev` with
|
||||||
provenance.
|
provenance.
|
||||||
|
|
||||||
|
Result: known local surfaces are declared or discovered with `dev`,
|
||||||
|
`bernd-laptop`, `private-dev`, local-loopback policy authority, and route
|
||||||
|
evidence: Fabric registry/explorer `8765`, NetKingdom control surface `8876`,
|
||||||
|
State Hub API `8000`, State Hub MCP `8001`, and State Hub dashboard `3000`.
|
||||||
|
|
||||||
## T03 - Discover Test And Production Routing Authorities
|
## T03 - Discover Test And Production Routing Authorities
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: RAIL-FAB-WP-0020-T03
|
id: RAIL-FAB-WP-0020-T03
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "91fc3f28-fbb9-43d2-bb46-44d179f4b485"
|
state_hub_task_id: "91fc3f28-fbb9-43d2-bb46-44d179f4b485"
|
||||||
```
|
```
|
||||||
@@ -118,11 +127,16 @@ Done when test-stage routes can be attributed to `coulombcore` and production
|
|||||||
routes can be attributed to `railiance01`, with access zones flagged as
|
routes can be attributed to `railiance01`, with access zones flagged as
|
||||||
candidate values for operator review.
|
candidate values for operator review.
|
||||||
|
|
||||||
|
Result: published `fabric/discovery/snapshots/2026-05-24-deployment-zone-inventory.yaml`
|
||||||
|
with `coulombcore` test tunnel evidence, `railiance01` Traefik ingress evidence,
|
||||||
|
candidate access zones, and explicit ambiguity flags for host/IP conflicts and
|
||||||
|
operator review.
|
||||||
|
|
||||||
## T04 - Add Zone Overlay Graph Explorer Modes
|
## T04 - Add Zone Overlay Graph Explorer Modes
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: RAIL-FAB-WP-0020-T04
|
id: RAIL-FAB-WP-0020-T04
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "664c2688-f45b-47bf-90ff-b17096a326fb"
|
state_hub_task_id: "664c2688-f45b-47bf-90ff-b17096a326fb"
|
||||||
```
|
```
|
||||||
@@ -146,11 +160,15 @@ The UI should make it easy to answer:
|
|||||||
Done when the graph explorer can group/filter by overlay fields and surface the
|
Done when the graph explorer can group/filter by overlay fields and surface the
|
||||||
basic warnings without making policy decisions.
|
basic warnings without making policy decisions.
|
||||||
|
|
||||||
|
Result: graph-explorer manifests and payloads expose deployment overlay fields;
|
||||||
|
the UI includes zone modes, overlay search/rule filtering, visible zone summaries,
|
||||||
|
and route-without-policy-authority warnings.
|
||||||
|
|
||||||
## T05 - Preserve State Hub Read-Model Compatibility
|
## T05 - Preserve State Hub Read-Model Compatibility
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: RAIL-FAB-WP-0020-T05
|
id: RAIL-FAB-WP-0020-T05
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "1a5ef6f9-357f-4803-a1f8-ebd1ff5443fb"
|
state_hub_task_id: "1a5ef6f9-357f-4803-a1f8-ebd1ff5443fb"
|
||||||
```
|
```
|
||||||
@@ -161,11 +179,15 @@ Done when Fabric exports remain backward compatible, State Hub keeps importing
|
|||||||
valid v1alpha2 exports, and overlay fields are visible enough for dashboard or
|
valid v1alpha2 exports, and overlay fields are visible enough for dashboard or
|
||||||
search views.
|
search views.
|
||||||
|
|
||||||
|
Result: `schemas/state-hub-export.schema.yaml` accepts optional
|
||||||
|
`deployment_overlay` objects on financial nodes and edges while preserving the
|
||||||
|
legacy export shape. Focused compatibility tests pass.
|
||||||
|
|
||||||
## T06 - Publish Current Zone Inventory
|
## T06 - Publish Current Zone Inventory
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: RAIL-FAB-WP-0020-T06
|
id: RAIL-FAB-WP-0020-T06
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "a1b208e3-3321-4792-ba44-d32aba682183"
|
state_hub_task_id: "a1b208e3-3321-4792-ba44-d32aba682183"
|
||||||
```
|
```
|
||||||
@@ -180,3 +202,7 @@ Done when there is a saved artifact answering:
|
|||||||
- which production services are visible on `railiance01`;
|
- which production services are visible on `railiance01`;
|
||||||
- which routes or ports are ambiguous, conflicting, or missing a policy
|
- which routes or ports are ambiguous, conflicting, or missing a policy
|
||||||
authority.
|
authority.
|
||||||
|
|
||||||
|
Result: saved the current inventory at
|
||||||
|
`fabric/discovery/snapshots/2026-05-24-deployment-zone-inventory.yaml` and added
|
||||||
|
focused test coverage to keep the dev/test/prod scenario answers present.
|
||||||
|
|||||||
Reference in New Issue
Block a user