@opensoha/contracts 0.1.27 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/COMPATIBILITY.md +4 -3
  2. package/README.md +8 -9
  3. package/compat/consumer-revisions.json +1 -1
  4. package/compat/json-schema-ecosystem-0.1.0.json +1 -1
  5. package/compat/openapi-loose-boundaries-0.1.0.json +1 -1
  6. package/fixtures/openapi/KubernetesClusterRegistrationRequest/invalid/metric-cluster-value-length.json +3 -0
  7. package/fixtures/openapi/KubernetesClusterRegistrationRequest/invalid/metric-cluster-value-type.json +3 -0
  8. package/fixtures/openapi/KubernetesClusterRegistrationRequest/valid/metric-cluster-alias.json +5 -0
  9. package/fixtures/openapi/KubernetesClusterRegistrationRequest/valid/metric-cluster-reset.json +3 -0
  10. package/fixtures/openapi/KubernetesHelmReleaseValuesUpdateInput/invalid/revision-overflow.json +4 -0
  11. package/fixtures/openapi/KubernetesHelmReleaseValuesUpdateInput/valid/legacy.json +3 -0
  12. package/fixtures/openapi/KubernetesHelmReleaseValuesUpdateInput/valid/observed-revision.json +4 -0
  13. package/fixtures/openapi/KubernetesJobDetail/invalid/execution-observation.json +23 -0
  14. package/fixtures/openapi/KubernetesJobDetail/valid/execution-observation.json +23 -0
  15. package/fixtures/openapi/KubernetesNode/invalid/labels.json +10 -0
  16. package/fixtures/openapi/KubernetesNode/valid/labels.json +11 -0
  17. package/fixtures/openapi/KubernetesPodListEnvelope/invalid/empty-uid.json +13 -0
  18. package/fixtures/openapi/KubernetesPodListEnvelope/invalid/invalid-container-state.json +20 -0
  19. package/fixtures/openapi/KubernetesPodListEnvelope/invalid/missing-container-ready.json +19 -0
  20. package/fixtures/openapi/KubernetesPodListEnvelope/valid/container-statuses.json +67 -0
  21. package/fixtures/openapi/KubernetesPodListEnvelope/valid/running-pod.json +1 -0
  22. package/fixtures/openapi/KubernetesPrometheusDetail/invalid/metric-cluster-value-type.json +4 -0
  23. package/fixtures/openapi/KubernetesPrometheusDetail/valid/metric-cluster-alias.json +5 -0
  24. package/fixtures/openapi/KubernetesResourceGraphEnvelope/invalid/network-edge-source-payload.json +73 -0
  25. package/fixtures/openapi/KubernetesResourceGraphEnvelope/valid/network-edge-facts.json +67 -0
  26. package/fixtures/openapi/KubernetesSecretCertificate/invalid/secret-data.json +1 -0
  27. package/fixtures/openapi/KubernetesSecretCertificate/invalid/status.json +1 -0
  28. package/fixtures/openapi/KubernetesSecretCertificate/valid/available.json +1 -0
  29. package/fixtures/openapi/KubernetesSecretCertificate/valid/missing.json +1 -0
  30. package/gen/go/sohaapi/client.go +1 -1
  31. package/gen/go/sohaapi/client_test.go +13 -0
  32. package/gen/go/sohaapi/kubernetes_workload_optional_test.go +40 -0
  33. package/gen/go/sohaapi/pod_status_compat_test.go +36 -0
  34. package/gen/go/sohaapi/types.go +421 -88
  35. package/gen/ts/sohaapi/index.ts +756 -4
  36. package/helmrelease/runtime/values_upgrade.go +78 -0
  37. package/helmrelease/runtime/values_upgrade_test.go +123 -0
  38. package/helmrelease/values_update.go +31 -0
  39. package/openapi/soha-api.json +1191 -14
  40. package/openapi/soha-api.yaml +588 -28
  41. package/openapi/spec_generated.go +3 -3
  42. package/package.json +1 -1
  43. package/resource/agent_profiles.go +9 -15
  44. package/resource/agent_profiles_test.go +9 -3
  45. package/resource/custom_graph_page.go +18 -0
  46. package/resource/models.go +102 -57
  47. package/resource/network_topology.go +22 -0
  48. package/resource/network_topology_projection.go +36 -22
  49. package/resource/network_topology_projection_test.go +4 -0
  50. package/resource/network_topology_test.go +20 -0
  51. package/resource/network_topology_validation.go +5 -0
  52. package/resource/observed_workload.go +18 -0
  53. package/resource/pod_status_test.go +22 -0
  54. package/resource/rbac_graph.go +4 -0
  55. package/resource/rollout_optional_test.go +33 -0
  56. package/resource/runtime/network_relationship_primitives.go +66 -0
  57. package/resource/runtime/network_relationship_primitives_test.go +147 -0
  58. package/resource/runtime/network_topology.go +17 -5
  59. package/resource/runtime/network_topology_builder.go +405 -0
  60. package/resource/runtime/network_topology_builder_backends.go +195 -0
  61. package/resource/runtime/network_topology_builder_gateway_l4_test.go +86 -0
  62. package/resource/runtime/network_topology_builder_ingress.go +302 -0
  63. package/resource/runtime/network_topology_builder_legacy.go +66 -0
  64. package/resource/runtime/network_topology_builder_providers.go +242 -0
  65. package/resource/runtime/network_topology_builder_test.go +186 -0
  66. package/resource/runtime/network_topology_declarations.go +329 -0
  67. package/resource/runtime/resource_edit.go +22 -0
  68. package/resource/runtime/resource_edit_test.go +32 -0
  69. package/resource/runtime/resource_graph_collect.go +325 -0
  70. package/resource/runtime/resource_graph_configuration.go +59 -0
  71. package/resource/runtime/resource_graph_configuration_collect.go +198 -0
  72. package/resource/runtime/resource_graph_configuration_test.go +470 -0
  73. package/resource/runtime/resource_graph_custom_page.go +159 -0
  74. package/resource/runtime/resource_graph_custom_root.go +132 -0
  75. package/resource/runtime/resource_graph_custom_root_test.go +197 -0
  76. package/resource/runtime/resource_graph_enrichment.go +417 -0
  77. package/resource/runtime/resource_graph_enrichment_hpa_test.go +117 -0
  78. package/resource/runtime/resource_graph_enrichment_test.go +201 -0
  79. package/resource/runtime/resource_graph_metadata.go +125 -0
  80. package/resource/runtime/resource_graph_metadata_test.go +204 -0
  81. package/resource/runtime/resource_graph_network.go +224 -0
  82. package/resource/runtime/resource_graph_network_collect.go +174 -0
  83. package/resource/runtime/resource_graph_network_collect_test.go +147 -0
  84. package/resource/runtime/resource_graph_network_test.go +342 -0
  85. package/resource/runtime/resource_graph_rbac.go +278 -0
  86. package/resource/runtime/resource_graph_rbac_test.go +372 -0
  87. package/resource/runtime/resource_graph_rules.go +834 -0
  88. package/resource/runtime/resource_graph_storage.go +144 -0
  89. package/resource/runtime/resource_graph_storage_collect.go +163 -0
  90. package/resource/runtime/resource_graph_storage_test.go +397 -0
  91. package/resource/runtime/resource_graph_test.go +627 -0
  92. package/resource/runtime/secret_certificate.go +44 -0
  93. package/resource/runtime/secret_certificate_test.go +87 -0
  94. package/resource/storage_graph.go +4 -0
@@ -5,9 +5,9 @@ package openapi
5
5
  import _ "embed"
6
6
 
7
7
  const (
8
- Version = "0.1.27"
9
- YAMLSHA256 = "7ec8930963123f5b79dff6dfaddf7200dd0fce51ada6212d47f415d4e7539727"
10
- JSONSHA256 = "6c1d5c0d4871c5ff6ea82241a02ff8ee4147792c6026a474a9d10a3ef3ca051b"
8
+ Version = "0.1.29"
9
+ YAMLSHA256 = "95ddf7a050831e3a9bae045952877b32f52d879a70e482c9c446a84f8cde4596"
10
+ JSONSHA256 = "4db09842ef24e3485e59e1d617f5027365eb7474de068425a50bf8a13222ad49"
11
11
  )
12
12
 
13
13
  //go:embed soha-api.yaml
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opensoha/contracts",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "private": false,
5
5
  "description": "Shared OpenSoha API, agent, MCP, skill, event, and auth contracts.",
6
6
  "license": "Apache-2.0",
@@ -8,22 +8,14 @@ func KubernetesAgentProfileActions(profile string) ([]string, error) {
8
8
  switch profile {
9
9
  case "read-only":
10
10
  return []string{"platform.custom_resources.list"}, nil
11
- case "workbench":
11
+ case "workbench", "legacy":
12
12
  return append([]string(nil), kubernetesWorkbenchActions[:]...), nil
13
- case "legacy":
14
- actions := make([]string, 0, len(kubernetesWorkbenchActions))
15
- for _, action := range kubernetesWorkbenchActions {
16
- if action != "platform.helm_releases.install" && action != "platform.helm_releases.values_update" && action != "platform.helm_releases.delete" && action != "platform.helm_releases.rollback" {
17
- actions = append(actions, action)
18
- }
19
- }
20
- return actions, nil
21
13
  default:
22
14
  return nil, fmt.Errorf("unknown Kubernetes Agent action profile")
23
15
  }
24
16
  }
25
17
 
26
- // KubernetesAgentActions lists implemented platform actions, including separately opted-in CRD deletion.
18
+ // KubernetesAgentActions lists implemented platform actions, including separately opted-in mutations.
27
19
  func KubernetesAgentActions() []string {
28
20
  actions := make([]string, 0, len(kubernetesWorkbenchActions))
29
21
  for _, action := range kubernetesWorkbenchActions {
@@ -31,7 +23,13 @@ func KubernetesAgentActions() []string {
31
23
  actions = append(actions, action)
32
24
  }
33
25
  }
34
- return append(actions, "platform.crds.delete")
26
+ return append(actions,
27
+ "platform.helm_releases.install",
28
+ "platform.helm_releases.values_update",
29
+ "platform.helm_releases.delete",
30
+ "platform.helm_releases.rollback",
31
+ "platform.crds.delete",
32
+ )
35
33
  }
36
34
 
37
35
  var kubernetesWorkbenchActions = [...]string{
@@ -54,9 +52,5 @@ var kubernetesWorkbenchActions = [...]string{
54
52
  "platform.port_forwards.create",
55
53
  "platform.port_forwards.tunnel",
56
54
  "platform.port_forwards.delete",
57
- "platform.helm_releases.install",
58
- "platform.helm_releases.values_update",
59
- "platform.helm_releases.delete",
60
- "platform.helm_releases.rollback",
61
55
  "runtime.execution_tasks.cancel",
62
56
  }
@@ -10,7 +10,7 @@ func TestKubernetesAgentProfilesKeepPermissionsExplicit(t *testing.T) {
10
10
  if err != nil {
11
11
  t.Fatal(err)
12
12
  }
13
- for _, action := range []string{"platform.nodes.drain", "platform.resources.apply", "platform.resources.create", "platform.resources.delete", "platform.helm_releases.rollback", "platform.port_forwards.tunnel"} {
13
+ for _, action := range []string{"platform.nodes.drain", "platform.resources.apply", "platform.resources.create", "platform.resources.delete", "platform.port_forwards.tunnel"} {
14
14
  if !slices.Contains(workbench, action) {
15
15
  t.Fatalf("missing workbench action %s", action)
16
16
  }
@@ -19,8 +19,14 @@ func TestKubernetesAgentProfilesKeepPermissionsExplicit(t *testing.T) {
19
19
  t.Fatal("workbench must not opt into unrestricted or CRD deletion actions")
20
20
  }
21
21
  legacy, _ := KubernetesAgentProfileActions("legacy")
22
- if slices.Contains(legacy, "platform.helm_releases.install") {
23
- t.Fatal("legacy installation permissions expanded")
22
+ if !slices.Equal(workbench, legacy) {
23
+ t.Fatal("legacy action set changed")
24
+ }
25
+ implemented := KubernetesAgentActions()
26
+ for _, action := range []string{"platform.helm_releases.install", "platform.helm_releases.values_update", "platform.helm_releases.delete", "platform.helm_releases.rollback"} {
27
+ if slices.Contains(workbench, action) || !slices.Contains(implemented, action) {
28
+ t.Fatalf("Helm mutation %s must require explicit opt-in", action)
29
+ }
24
30
  }
25
31
  readOnly, _ := KubernetesAgentProfileActions("read-only")
26
32
  if !slices.Equal(readOnly, []string{"platform.custom_resources.list"}) {
@@ -0,0 +1,18 @@
1
+ package resource
2
+
3
+ import "encoding/json"
4
+
5
+ // CustomResourceGraphObjectPath is relative to the authenticated Agent platform API.
6
+ const CustomResourceGraphObjectPath = "/extensions/custom-resources/graph-object"
7
+
8
+ type CustomResourceGraphObjectRequest struct {
9
+ Definition CRDResourceDefinition `json:"definition"`
10
+ Namespace string `json:"namespace"`
11
+ Name string `json:"name"`
12
+ }
13
+
14
+ // CustomResourceGraphPage contains only observed GVK and relationship metadata.
15
+ type CustomResourceGraphPage struct {
16
+ Items []json.RawMessage `json:"items"`
17
+ Truncated bool `json:"truncated"`
18
+ }
@@ -54,10 +54,17 @@ type ResourceGraphNode struct {
54
54
  }
55
55
 
56
56
  type ResourceGraphEdge struct {
57
- ID string `json:"id"`
58
- SourceID string `json:"sourceId"`
59
- TargetID string `json:"targetId"`
60
- Relation string `json:"relation"`
57
+ ID string `json:"id"`
58
+ SourceID string `json:"sourceId"`
59
+ TargetID string `json:"targetId"`
60
+ Relation string `json:"relation"`
61
+ ResolutionState string `json:"resolutionState,omitempty"`
62
+ Protocol string `json:"protocol,omitempty"`
63
+ Port string `json:"port,omitempty"`
64
+ SectionName string `json:"sectionName,omitempty"`
65
+ FieldPath string `json:"fieldPath,omitempty"`
66
+ Note string `json:"note,omitempty"`
67
+ SourceRef *ResourceRef `json:"sourceRef,omitempty"`
61
68
  }
62
69
 
63
70
  type ResourceEvidence struct {
@@ -104,23 +111,34 @@ type NamespaceUpsertInput struct {
104
111
  }
105
112
 
106
113
  type PodView struct {
107
- UID string `json:"uid,omitempty"`
108
- Name string `json:"name"`
109
- Namespace string `json:"namespace"`
110
- Phase string `json:"phase"`
111
- NodeName string `json:"nodeName,omitempty"`
112
- PodIP string `json:"podIp,omitempty"`
113
- CreatedAt string `json:"createdAt,omitempty"`
114
- CPU string `json:"cpu,omitempty"`
115
- Memory string `json:"memory,omitempty"`
116
- Requests ResourceQuantityView `json:"requests,omitempty"`
117
- Limits ResourceQuantityView `json:"limits,omitempty"`
118
- Labels map[string]string `json:"labels,omitempty"`
119
- PersistentVolumeClaims []string `json:"persistentVolumeClaims,omitempty"`
120
- ReadyContainers string `json:"readyContainers"`
121
- Restarts int32 `json:"restarts"`
122
- AgeSeconds int64 `json:"ageSeconds"`
123
- AllowedActions []string `json:"allowedActions,omitempty"`
114
+ UID string `json:"uid,omitempty"`
115
+ Name string `json:"name"`
116
+ Namespace string `json:"namespace"`
117
+ Phase string `json:"phase"`
118
+ NodeName string `json:"nodeName,omitempty"`
119
+ PodIP string `json:"podIp,omitempty"`
120
+ CreatedAt string `json:"createdAt,omitempty"`
121
+ CPU string `json:"cpu,omitempty"`
122
+ Memory string `json:"memory,omitempty"`
123
+ Requests ResourceQuantityView `json:"requests,omitempty"`
124
+ Limits ResourceQuantityView `json:"limits,omitempty"`
125
+ Labels map[string]string `json:"labels,omitempty"`
126
+ PersistentVolumeClaims []string `json:"persistentVolumeClaims,omitempty"`
127
+ ReadyContainers string `json:"readyContainers"`
128
+ QOSClass string `json:"qosClass,omitempty"`
129
+ ContainerStatuses []PodContainerStatusView `json:"containerStatuses,omitempty"`
130
+ Restarts int32 `json:"restarts"`
131
+ AgeSeconds int64 `json:"ageSeconds"`
132
+ AllowedActions []string `json:"allowedActions,omitempty"`
133
+ }
134
+
135
+ type PodContainerStatusView struct {
136
+ Name string `json:"name"`
137
+ Kind string `json:"kind"`
138
+ Ready bool `json:"ready"`
139
+ State string `json:"state"`
140
+ Reason string `json:"reason,omitempty"`
141
+ ExitCode *int32 `json:"exitCode,omitempty"`
124
142
  }
125
143
 
126
144
  type WorkloadOverviewNamespaceView struct {
@@ -340,10 +358,11 @@ type ResourceYAMLView struct {
340
358
  }
341
359
 
342
360
  type ResourceUpdatePlanRequest struct {
343
- Namespace string `json:"namespace,omitempty"`
344
- Kind string `json:"kind"`
345
- Name string `json:"name"`
346
- Content string `json:"content"`
361
+ Namespace string `json:"namespace,omitempty"`
362
+ Kind string `json:"kind"`
363
+ Name string `json:"name"`
364
+ Content string `json:"content"`
365
+ RequireObservedIdentity bool `json:"requireObservedIdentity,omitempty"`
347
366
  }
348
367
 
349
368
  type ManagedFieldOwner struct {
@@ -434,6 +453,7 @@ type SubjectAccessReviewResult struct {
434
453
  type DeploymentView struct {
435
454
  Name string `json:"name"`
436
455
  Namespace string `json:"namespace"`
456
+ UID string `json:"uid,omitempty"`
437
457
  Labels map[string]string `json:"labels,omitempty"`
438
458
  DesiredReplicas int32 `json:"desiredReplicas"`
439
459
  ReadyReplicas int32 `json:"readyReplicas"`
@@ -446,6 +466,7 @@ type DeploymentView struct {
446
466
  type DeploymentDetailView struct {
447
467
  Name string `json:"name"`
448
468
  Namespace string `json:"namespace"`
469
+ UID string `json:"uid,omitempty"`
449
470
  DesiredReplicas int32 `json:"desiredReplicas"`
450
471
  ReadyReplicas int32 `json:"readyReplicas"`
451
472
  UpdatedReplicas int32 `json:"updatedReplicas"`
@@ -464,13 +485,15 @@ type DeploymentDetailView struct {
464
485
  }
465
486
 
466
487
  type RolloutHistoryView struct {
467
- Name string `json:"name"`
468
- Namespace string `json:"namespace"`
469
- Revision string `json:"revision"`
470
- Images []string `json:"images,omitempty"`
471
- Replicas int32 `json:"replicas"`
472
- ReadyReplicas int32 `json:"readyReplicas"`
473
- CreatedAt string `json:"createdAt,omitempty"`
488
+ Name string `json:"name"`
489
+ Namespace string `json:"namespace"`
490
+ Revision string `json:"revision"`
491
+ Images []string `json:"images,omitempty"`
492
+ Replicas int32 `json:"replicas"`
493
+ ReadyReplicas int32 `json:"readyReplicas"`
494
+ CreatedAt string `json:"createdAt,omitempty"`
495
+ PodRevisionHash string `json:"podRevisionHash,omitempty"`
496
+ MatchesCurrentTemplate *bool `json:"matchesCurrentTemplate,omitempty"`
474
497
  }
475
498
 
476
499
  type DeploymentRolloutStatusView struct {
@@ -556,34 +579,45 @@ type DaemonSetDetailView struct {
556
579
  }
557
580
 
558
581
  type JobView struct {
559
- Name string `json:"name"`
560
- Namespace string `json:"namespace"`
561
- Completions int32 `json:"completions"`
562
- Succeeded int32 `json:"succeeded"`
563
- Failed int32 `json:"failed"`
564
- Active int32 `json:"active"`
565
- CompletionMode string `json:"completionMode,omitempty"`
566
- AgeSeconds int64 `json:"ageSeconds"`
567
- AllowedActions []string `json:"allowedActions,omitempty"`
582
+ Name string `json:"name"`
583
+ Namespace string `json:"namespace"`
584
+ Completions int32 `json:"completions"`
585
+ Succeeded int32 `json:"succeeded"`
586
+ Failed int32 `json:"failed"`
587
+ Active int32 `json:"active"`
588
+ CompletionMode string `json:"completionMode,omitempty"`
589
+ AgeSeconds int64 `json:"ageSeconds"`
590
+ AllowedActions []string `json:"allowedActions,omitempty"`
591
+ CompletionTarget *int32 `json:"completionTarget,omitempty"`
592
+ Suspend *bool `json:"suspend,omitempty"`
593
+ Conditions []WorkloadConditionView `json:"conditions,omitempty"`
594
+ StartTime string `json:"startTime,omitempty"`
595
+ CompletionTime string `json:"completionTime,omitempty"`
568
596
  }
569
597
 
570
598
  type JobDetailView struct {
571
- Name string `json:"name"`
572
- Namespace string `json:"namespace"`
573
- Completions int32 `json:"completions"`
574
- Parallelism int32 `json:"parallelism"`
575
- Succeeded int32 `json:"succeeded"`
576
- Failed int32 `json:"failed"`
577
- Active int32 `json:"active"`
578
- CompletionMode string `json:"completionMode,omitempty"`
579
- CreatedAt string `json:"createdAt,omitempty"`
580
- StartTime string `json:"startTime,omitempty"`
581
- CompletionTime string `json:"completionTime,omitempty"`
582
- Labels map[string]string `json:"labels,omitempty"`
583
- Annotations map[string]string `json:"annotations,omitempty"`
584
- Pods []PodView `json:"pods,omitempty"`
585
- RelatedResources []WorkloadRelationView `json:"relatedResources,omitempty"`
586
- AllowedActions []string `json:"allowedActions,omitempty"`
599
+ Name string `json:"name"`
600
+ Namespace string `json:"namespace"`
601
+ Completions int32 `json:"completions"`
602
+ Parallelism int32 `json:"parallelism"`
603
+ Succeeded int32 `json:"succeeded"`
604
+ Failed int32 `json:"failed"`
605
+ Active int32 `json:"active"`
606
+ CompletionMode string `json:"completionMode,omitempty"`
607
+ CreatedAt string `json:"createdAt,omitempty"`
608
+ StartTime string `json:"startTime,omitempty"`
609
+ CompletionTime string `json:"completionTime,omitempty"`
610
+ Labels map[string]string `json:"labels,omitempty"`
611
+ Annotations map[string]string `json:"annotations,omitempty"`
612
+ Pods []PodView `json:"pods,omitempty"`
613
+ RelatedResources []WorkloadRelationView `json:"relatedResources,omitempty"`
614
+ AllowedActions []string `json:"allowedActions,omitempty"`
615
+ CompletionTarget *int32 `json:"completionTarget,omitempty"`
616
+ Suspend *bool `json:"suspend,omitempty"`
617
+ Conditions []WorkloadConditionView `json:"conditions,omitempty"`
618
+ BackoffLimit *int32 `json:"backoffLimit,omitempty"`
619
+ ActiveDeadlineSeconds *int64 `json:"activeDeadlineSeconds,omitempty"`
620
+ TTLSecondsAfterFinished *int32 `json:"ttlSecondsAfterFinished,omitempty"`
587
621
  }
588
622
 
589
623
  type CronJobView struct {
@@ -916,6 +950,7 @@ type NetworkTopologyView struct {
916
950
  }
917
951
 
918
952
  type NodeView struct {
953
+ Labels map[string]string `json:"labels,omitempty"`
919
954
  Name string `json:"name"`
920
955
  Status string `json:"status"`
921
956
  Unschedulable bool `json:"unschedulable"`
@@ -1379,6 +1414,16 @@ type SecretView struct {
1379
1414
  AllowedActions []string `json:"allowedActions,omitempty"`
1380
1415
  }
1381
1416
 
1417
+ // SecretCertificateView contains public facts only; it cannot carry Secret data or PEM.
1418
+ type SecretCertificateView struct {
1419
+ Name string `json:"name"`
1420
+ Namespace string `json:"namespace"`
1421
+ Status string `json:"status"`
1422
+ NotBefore string `json:"notBefore,omitempty"`
1423
+ NotAfter string `json:"notAfter,omitempty"`
1424
+ SelfSigned *bool `json:"selfSigned,omitempty"`
1425
+ }
1426
+
1382
1427
  type SecretDetailView struct {
1383
1428
  Name string `json:"name"`
1384
1429
  Namespace string `json:"namespace"`
@@ -1,6 +1,7 @@
1
1
  package resource
2
2
 
3
3
  const (
4
+ NetworkTopologyNamedRootFeature = "network.topology.named-root.v1"
4
5
  NetworkTopologySnapshotFeature = "network.topology.snapshot.v1"
5
6
  NetworkTopologyGatewayL4Feature = "network.topology.gateway-l4.v1"
6
7
  NetworkTopologyMaxQueries = 64
@@ -36,6 +37,8 @@ type NetworkTopologyGraphEdge struct {
36
37
  ResolutionState string `json:"resolutionState"`
37
38
  Protocol string `json:"protocol,omitempty"`
38
39
  Port string `json:"port,omitempty"`
40
+ SectionName string `json:"sectionName,omitempty"`
41
+ FieldPath string `json:"fieldPath,omitempty"`
39
42
  Weight *int64 `json:"weight,omitempty"`
40
43
  TotalWeight *int64 `json:"totalWeight,omitempty"`
41
44
  MirrorPercent *float64 `json:"mirrorPercent,omitempty"`
@@ -74,6 +77,7 @@ type NetworkTopologyCondition struct {
74
77
  }
75
78
 
76
79
  type NetworkTopologyCoverage struct {
80
+ Name string `json:"name,omitempty"`
77
81
  Provider string `json:"provider"`
78
82
  SourceID string `json:"sourceId"`
79
83
  State string `json:"state"`
@@ -87,6 +91,7 @@ type NetworkTopologyCoverage struct {
87
91
 
88
92
  // Snapshot queries name a fixed source, never a caller-supplied GVR or field path.
89
93
  type NetworkTopologySourceQuery struct {
94
+ Name string `json:"name,omitempty"`
90
95
  SourceID string `json:"sourceId"`
91
96
  Namespace string `json:"namespace,omitempty"`
92
97
  }
@@ -120,6 +125,10 @@ type NetworkTopologySourceDefinition struct {
120
125
  }
121
126
 
122
127
  var networkTopologySources = [...]NetworkTopologySourceDefinition{
128
+ {ID: "kubernetes.networkpolicies", Provider: "kubernetes", CRDResourceDefinition: CRDResourceDefinition{Kind: "NetworkPolicy", Group: "networking.k8s.io", Version: "v1", Resource: "networkpolicies", Namespaced: true}},
129
+ {ID: "kubernetes.namespaces", Provider: "kubernetes", CRDResourceDefinition: CRDResourceDefinition{Kind: "Namespace", Version: "v1", Resource: "namespaces"}},
130
+ {ID: "kubernetes.configmaps", Provider: "kubernetes", CRDResourceDefinition: CRDResourceDefinition{Kind: "ConfigMap", Version: "v1", Resource: "configmaps", Namespaced: true}},
131
+ {ID: "gateway.backendtlspolicies", Provider: "gateway", FallbackVersion: "v1alpha3", CRDResourceDefinition: CRDResourceDefinition{Kind: "BackendTLSPolicy", Group: "gateway.networking.k8s.io", Version: "v1", Resource: "backendtlspolicies", Namespaced: true}},
123
132
  {ID: "kubernetes.services", Provider: "kubernetes", CRDResourceDefinition: CRDResourceDefinition{Kind: "Service", Version: "v1", Resource: "services", Namespaced: true}},
124
133
  {ID: "kubernetes.pods", Provider: "kubernetes", CRDResourceDefinition: CRDResourceDefinition{Kind: "Pod", Version: "v1", Resource: "pods", Namespaced: true}},
125
134
  {ID: "kubernetes.endpointslices", Provider: "kubernetes", CRDResourceDefinition: CRDResourceDefinition{Kind: "EndpointSlice", Group: "discovery.k8s.io", Version: "v1", Resource: "endpointslices", Namespaced: true}},
@@ -164,3 +173,16 @@ func LookupNetworkTopologySource(id string) (NetworkTopologySourceDefinition, bo
164
173
  }
165
174
  return NetworkTopologySourceDefinition{}, false
166
175
  }
176
+
177
+ // NetworkTopologySourceVersions is the explicit served-version allowlist. Legacy
178
+ // BackendTLSPolicy v1alpha2 uses targetRef rather than targetRefs.
179
+ func NetworkTopologySourceVersions(d NetworkTopologySourceDefinition) []string {
180
+ versions := []string{d.Version}
181
+ if d.FallbackVersion != "" {
182
+ versions = append(versions, d.FallbackVersion)
183
+ }
184
+ if d.ID == "gateway.backendtlspolicies" {
185
+ versions = append(versions, "v1alpha2")
186
+ }
187
+ return versions
188
+ }
@@ -124,28 +124,32 @@ func redactTopologyMatches(value any, key string) {
124
124
  }
125
125
 
126
126
  var topologyProjectionPaths = map[string][]string{
127
- "kubernetes.services": strings.Fields("spec.type spec.externalName spec.clusterIP spec.selector.* spec.ports.name spec.ports.port spec.ports.targetPort spec.ports.protocol spec.publishNotReadyAddresses"),
128
- "kubernetes.pods": strings.Fields("metadata.labels.* status.phase status.conditions.type status.conditions.status"),
129
- "kubernetes.endpointslices": strings.Fields("metadata.labels.* addressType ports.name ports.port ports.protocol endpoints.addresses endpoints.conditions.ready endpoints.conditions.serving endpoints.conditions.terminating endpoints.targetRef.kind endpoints.targetRef.namespace endpoints.targetRef.name endpoints.targetRef.uid"),
130
- "kubernetes.ingresses": strings.Fields("spec.ingressClassName spec.defaultBackend.service.name spec.defaultBackend.service.port.name spec.defaultBackend.service.port.number spec.defaultBackend.resource.kind spec.rules.host spec.rules.http.paths.path spec.rules.http.paths.pathType spec.rules.http.paths.backend.service.name spec.rules.http.paths.backend.service.port.name spec.rules.http.paths.backend.service.port.number spec.rules.http.paths.backend.resource.kind spec.tls.hosts"),
131
- "kubernetes.ingressclasses": strings.Fields("spec.controller"),
132
- "gateway.gateways": strings.Fields("spec.gatewayClassName spec.listeners.name spec.listeners.port spec.listeners.protocol spec.listeners.hostname spec.listeners.allowedRoutes.namespaces.from spec.listeners.allowedRoutes.namespaces.selector.matchLabels.* spec.listeners.allowedRoutes.kinds.kind spec.listeners.allowedRoutes.kinds.group spec.listeners.tls.mode"),
133
- "gateway.gatewayclasses": strings.Fields("spec.controllerName"),
134
- "gateway.httproutes": gatewayTopologyPaths(),
135
- "gateway.grpcroutes": gatewayTopologyPaths(),
136
- "gateway.tcproutes": gatewayL4TopologyPaths(),
137
- "gateway.tlsroutes": gatewayL4TopologyPaths(),
138
- "gateway.udproutes": gatewayL4TopologyPaths(),
139
- "gateway.referencegrants": strings.Fields("spec.from.group spec.from.kind spec.from.namespace spec.to.group spec.to.kind spec.to.name"),
140
- "traefik.ingressroutes": traefikTopologyPaths(),
141
- "traefik.ingressroutetcps": traefikTopologyPaths(),
142
- "traefik.ingressrouteudps": traefikTopologyPaths(),
143
- "traefik.traefikservices": strings.Fields("spec.weighted.services.name spec.weighted.services.namespace spec.weighted.services.kind spec.weighted.services.port spec.weighted.services.weight spec.mirroring.name spec.mirroring.namespace spec.mirroring.kind spec.mirroring.port spec.mirroring.mirrors.name spec.mirroring.mirrors.namespace spec.mirroring.mirrors.kind spec.mirroring.mirrors.port spec.mirroring.mirrors.percent"),
144
- "traefik.middlewares": strings.Fields("spec.chain.middlewares.name spec.chain.middlewares.namespace"),
145
- "traefik.middlewaretcps": strings.Fields("spec.chain.middlewares.name spec.chain.middlewares.namespace"),
146
- "apisix.apisixroutes": apisixTopologyPaths(),
147
- "apisix.apisixupstreams": strings.Fields("spec.scheme spec.loadbalancer.type spec.externalNodes.type spec.externalNodes.name spec.externalNodes.port spec.externalNodes.weight spec.subsets.name spec.subsets.labels.*"),
148
- "apisix.apisixtls": strings.Fields("spec.hosts spec.secret.name spec.secret.namespace"),
127
+ "kubernetes.namespaces": strings.Fields("metadata.labels.*"),
128
+ "kubernetes.configmaps": {},
129
+ "kubernetes.networkpolicies": networkPolicyTopologyPaths(),
130
+ "gateway.backendtlspolicies": strings.Fields("spec.targetRefs.group spec.targetRefs.kind spec.targetRefs.name spec.targetRefs.sectionName spec.targetRef.group spec.targetRef.kind spec.targetRef.name spec.targetRef.sectionName spec.validation.caCertificateRefs.group spec.validation.caCertificateRefs.kind spec.validation.caCertificateRefs.name"),
131
+ "kubernetes.services": strings.Fields("spec.type spec.externalName spec.clusterIP spec.selector.* spec.ports.name spec.ports.port spec.ports.targetPort spec.ports.protocol spec.publishNotReadyAddresses"),
132
+ "kubernetes.pods": strings.Fields("metadata.labels.* status.phase status.conditions.type status.conditions.status"),
133
+ "kubernetes.endpointslices": strings.Fields("metadata.labels.* addressType ports.name ports.port ports.protocol endpoints.addresses endpoints.conditions.ready endpoints.conditions.serving endpoints.conditions.terminating endpoints.targetRef.apiVersion endpoints.targetRef.kind endpoints.targetRef.namespace endpoints.targetRef.name endpoints.targetRef.uid"),
134
+ "kubernetes.ingresses": strings.Fields("spec.ingressClassName spec.defaultBackend.service.name spec.defaultBackend.service.port.name spec.defaultBackend.service.port.number spec.defaultBackend.resource.kind spec.rules.host spec.rules.http.paths.path spec.rules.http.paths.pathType spec.rules.http.paths.backend.service.name spec.rules.http.paths.backend.service.port.name spec.rules.http.paths.backend.service.port.number spec.rules.http.paths.backend.resource.kind spec.tls.hosts spec.tls.secretName"),
135
+ "kubernetes.ingressclasses": strings.Fields("spec.controller spec.parameters.apiGroup spec.parameters.kind spec.parameters.name spec.parameters.namespace spec.parameters.scope"),
136
+ "gateway.gateways": strings.Fields("spec.gatewayClassName spec.listeners.name spec.listeners.port spec.listeners.protocol spec.listeners.hostname spec.listeners.allowedRoutes.namespaces.from spec.listeners.allowedRoutes.namespaces.selector.matchLabels.* spec.listeners.allowedRoutes.kinds.kind spec.listeners.allowedRoutes.kinds.group spec.listeners.tls.mode spec.listeners.tls.certificateRefs.group spec.listeners.tls.certificateRefs.kind spec.listeners.tls.certificateRefs.name spec.listeners.tls.certificateRefs.namespace spec.listeners.allowedRoutes.namespaces.selector.matchExpressions.key spec.listeners.allowedRoutes.namespaces.selector.matchExpressions.operator spec.listeners.allowedRoutes.namespaces.selector.matchExpressions.values"),
137
+ "gateway.gatewayclasses": strings.Fields("spec.controllerName spec.parametersRef.group spec.parametersRef.kind spec.parametersRef.name spec.parametersRef.namespace"),
138
+ "gateway.httproutes": gatewayTopologyPaths(),
139
+ "gateway.grpcroutes": gatewayTopologyPaths(),
140
+ "gateway.tcproutes": gatewayL4TopologyPaths(),
141
+ "gateway.tlsroutes": gatewayL4TopologyPaths(),
142
+ "gateway.udproutes": gatewayL4TopologyPaths(),
143
+ "gateway.referencegrants": strings.Fields("spec.from.group spec.from.kind spec.from.namespace spec.to.group spec.to.kind spec.to.name"),
144
+ "traefik.ingressroutes": traefikTopologyPaths(),
145
+ "traefik.ingressroutetcps": traefikTopologyPaths(),
146
+ "traefik.ingressrouteudps": traefikTopologyPaths(),
147
+ "traefik.traefikservices": strings.Fields("spec.weighted.services.name spec.weighted.services.namespace spec.weighted.services.kind spec.weighted.services.port spec.weighted.services.weight spec.mirroring.name spec.mirroring.namespace spec.mirroring.kind spec.mirroring.port spec.mirroring.mirrors.name spec.mirroring.mirrors.namespace spec.mirroring.mirrors.kind spec.mirroring.mirrors.port spec.mirroring.mirrors.percent"),
148
+ "traefik.middlewares": strings.Fields("spec.chain.middlewares.name spec.chain.middlewares.namespace"),
149
+ "traefik.middlewaretcps": strings.Fields("spec.chain.middlewares.name spec.chain.middlewares.namespace"),
150
+ "apisix.apisixroutes": apisixTopologyPaths(),
151
+ "apisix.apisixupstreams": strings.Fields("spec.scheme spec.loadbalancer.type spec.externalNodes.type spec.externalNodes.name spec.externalNodes.port spec.externalNodes.weight spec.subsets.name spec.subsets.labels.*"),
152
+ "apisix.apisixtls": strings.Fields("spec.hosts spec.secret.name spec.secret.namespace"),
149
153
  }
150
154
 
151
155
  func gatewayTopologyPaths() []string {
@@ -206,3 +210,13 @@ func topologyMiddlewareTypes(input map[string]any) []string {
206
210
  sort.Strings(result)
207
211
  return result
208
212
  }
213
+
214
+ func networkPolicyTopologyPaths() []string {
215
+ paths := strings.Fields("spec.podSelector.matchLabels.* spec.podSelector.matchExpressions.key spec.podSelector.matchExpressions.operator spec.podSelector.matchExpressions.values spec.policyTypes")
216
+ for _, direction := range []string{"ingress.from", "egress.to"} {
217
+ for _, field := range strings.Fields("podSelector.matchLabels.* podSelector.matchExpressions.key podSelector.matchExpressions.operator podSelector.matchExpressions.values namespaceSelector.matchLabels.* namespaceSelector.matchExpressions.key namespaceSelector.matchExpressions.operator namespaceSelector.matchExpressions.values ipBlock.cidr ipBlock.except") {
218
+ paths = append(paths, "spec."+direction+"."+field)
219
+ }
220
+ }
221
+ return paths
222
+ }
@@ -11,6 +11,10 @@ func TestTopologyProjectionExcludesCredentialsAndPreservesAlternatives(t *testin
11
11
  source, input string
12
12
  keep []string
13
13
  }{
14
+ {"kubernetes.configmaps", `{"metadata":{"name":"ca","uid":"ca-uid","annotations":{"key":"secret-value"}},"data":{"ca.crt":"secret-value"},"binaryData":{"key":"secret-value"}}`, []string{"ca-uid"}},
15
+ {"kubernetes.namespaces", `{"metadata":{"name":"team","labels":{"access":"permitted"},"annotations":{"token":"secret-value"}}}`, []string{"permitted"}},
16
+ {"kubernetes.networkpolicies", `{"spec":{"podSelector":{"matchLabels":{"app":"web"}},"ingress":[{"from":[{"namespaceSelector":{"matchLabels":{"role":"client"}},"podSelector":{"matchExpressions":[{"key":"tier","operator":"In","values":["frontend"]}]}},{"ipBlock":{"cidr":"10.0.0.0/8","except":["10.1.0.0/16"]}}]}]},"data":"secret-value"}`, []string{"frontend", "10.1.0.0/16"}},
17
+ {"gateway.backendtlspolicies", `{"spec":{"targetRefs":[{"group":"","kind":"Service","name":"web","sectionName":"https"}],"targetRef":{"kind":"Service","name":"legacy"},"validation":{"caCertificateRefs":[{"kind":"ConfigMap","name":"ca"}],"privateKey":"secret-value"}}}`, []string{"https", "legacy", "ca"}},
14
18
  {"kubernetes.pods", `{"metadata":{"name":"p","namespace":"n","annotations":{"token":"secret-value"},"labels":{"app":"web"}},"spec":{"containers":[{"env":[{"value":"secret-value"}]}]},"status":{"phase":"Running"}}`, []string{"Running", "web"}},
15
19
  {"gateway.httproutes", `{"metadata":{"name":"r"},"spec":{"rules":[{"matches":[{"method":"GET","headers":[{"name":"Authorization","value":"secret-value"}],"queryParams":[{"name":"token","value":"secret-value"}]}],"backendRefs":[{"name":"web","weight":0,"port":80}]}]}}`, []string{"GET", "[redacted]", "\"weight\":0"}},
16
20
  {"gateway.tcproutes", `{"spec":{"parentRefs":[{"name":"gw","sectionName":"tcp","port":9000}],"rules":[{"backendRefs":[{"name":"web","port":9000,"weight":0}],"filters":[{"secret":"secret-value"}]}]}}`, []string{"tcp", "9000", "\"weight\":0"}},
@@ -49,3 +49,23 @@ func TestTopologySourcesKeepSupportedVersionsAndDedicatedKindsExplicit(t *testin
49
49
  t.Fatal("source allowlist is mutable by callers")
50
50
  }
51
51
  }
52
+
53
+ func TestNetworkTopologyNamedQueriesRejectUnsafeIdentity(t *testing.T) {
54
+ for _, query := range []NetworkTopologySourceQuery{
55
+ {SourceID: "kubernetes.services", Namespace: "team", Name: "../secret"},
56
+ {SourceID: "traefik.ingressroutes", Namespace: "team", Name: "route"},
57
+ {SourceID: "gateway.tcproutes", Namespace: "team", Name: "route"},
58
+ {SourceID: "kubernetes.services", Name: "service"},
59
+ {SourceID: "kubernetes.services", Namespace: "team", Name: "service/name"},
60
+ {SourceID: "kubernetes.services", Namespace: "team", Name: "service?watch=true"},
61
+ {SourceID: "kubernetes.ingressclasses", Namespace: "team", Name: "class"},
62
+ } {
63
+ if err := ValidateNetworkTopologySnapshotRequest(NetworkTopologySnapshotRequest{Queries: []NetworkTopologySourceQuery{query}}); err == nil {
64
+ t.Fatalf("unsafe named query accepted: %+v", query)
65
+ }
66
+ }
67
+ valid := NetworkTopologySnapshotRequest{Queries: []NetworkTopologySourceQuery{{SourceID: "kubernetes.services", Namespace: "team", Name: "service"}, {SourceID: "kubernetes.services", Namespace: "team"}}}
68
+ if err := ValidateNetworkTopologySnapshotRequest(valid); err != nil {
69
+ t.Fatalf("named and list queries must coexist: %v", err)
70
+ }
71
+ }
@@ -6,6 +6,8 @@ import (
6
6
  "regexp"
7
7
  )
8
8
 
9
+ var topologyNamePattern = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`)
10
+
9
11
  var topologyNamespacePattern = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]*[a-z0-9])?$`)
10
12
 
11
13
  func ValidateNetworkTopologySnapshotRequest(request NetworkTopologySnapshotRequest) error {
@@ -19,6 +21,9 @@ func ValidateNetworkTopologySnapshotRequest(request NetworkTopologySnapshotReque
19
21
  return fmt.Errorf("unknown or duplicate topology source query")
20
22
  }
21
23
  seen[query] = true
24
+ if query.Name != "" && (definition.RequiresCustomResourceAccess() || len(query.Name) > 253 || !topologyNamePattern.MatchString(query.Name) || definition.Namespaced && query.Namespace == "") {
25
+ return fmt.Errorf("invalid named topology query")
26
+ }
22
27
  if query.Namespace != "" && (!definition.Namespaced || len(query.Namespace) > 63 || !topologyNamespacePattern.MatchString(query.Namespace)) {
23
28
  return fmt.Errorf("invalid topology source namespace")
24
29
  }
@@ -0,0 +1,18 @@
1
+ package resource
2
+
3
+ import "regexp"
4
+
5
+ var workloadUIDPattern = regexp.MustCompile(`^[A-Za-z0-9._-]{1,128}$`)
6
+
7
+ // ValidWorkloadUID validates an observed Kubernetes UID at the API boundary.
8
+ func ValidWorkloadUID(uid string) bool {
9
+ return workloadUIDPattern.MatchString(uid)
10
+ }
11
+
12
+ // ObservedWorkloadDeleteRequest is the Agent request for UID-guarded workload deletion.
13
+ type ObservedWorkloadDeleteRequest struct {
14
+ Namespace string `json:"namespace"`
15
+ Kind string `json:"kind"`
16
+ Name string `json:"name"`
17
+ ExpectedUID string `json:"expectedUid"`
18
+ }
@@ -0,0 +1,22 @@
1
+ package resource
2
+
3
+ import (
4
+ "encoding/json"
5
+ "strings"
6
+ "testing"
7
+ )
8
+
9
+ func TestPodContainerStatusKeepsSuccessfulExitAndOldPayloads(t *testing.T) {
10
+ exitCode := int32(0)
11
+ encoded, err := json.Marshal(PodView{ContainerStatuses: []PodContainerStatusView{{Name: "migrate", Kind: "init", State: "terminated", ExitCode: &exitCode}}})
12
+ if err != nil || !strings.Contains(string(encoded), `"exitCode":0`) {
13
+ t.Fatalf("successful termination was lost: %s, %v", encoded, err)
14
+ }
15
+ var old PodView
16
+ if err := json.Unmarshal([]byte(`{"name":"api","namespace":"apps","phase":"Running","readyContainers":"1/1","restarts":0,"ageSeconds":1}`), &old); err != nil {
17
+ t.Fatal(err)
18
+ }
19
+ if old.QOSClass != "" || len(old.ContainerStatuses) != 0 {
20
+ t.Fatal("old payload must not invent QoS or container states")
21
+ }
22
+ }
@@ -0,0 +1,4 @@
1
+ package resource
2
+
3
+ // RBACResourceGraphFeature identifies Agent support for the RBAC graph roots.
4
+ const RBACResourceGraphFeature = "resource.graph.rbac.v1"
@@ -0,0 +1,33 @@
1
+ package resource
2
+
3
+ import (
4
+ "encoding/json"
5
+ "testing"
6
+ )
7
+
8
+ func TestRolloutTemplateMatchDistinguishesUnknownFromMismatch(t *testing.T) {
9
+ var legacy RolloutHistoryView
10
+ if err := json.Unmarshal([]byte(`{"name":"revision-a","namespace":"team","revision":"1","replicas":0,"readyReplicas":0}`), &legacy); err != nil {
11
+ t.Fatal(err)
12
+ }
13
+ if legacy.MatchesCurrentTemplate != nil || legacy.PodRevisionHash != "" {
14
+ t.Fatal("legacy history must not invent template match or Pod hash")
15
+ }
16
+ mismatch := false
17
+ legacy.MatchesCurrentTemplate = &mismatch
18
+ legacy.PodRevisionHash = "observed-hash"
19
+ data, err := json.Marshal(legacy)
20
+ if err != nil {
21
+ t.Fatal(err)
22
+ }
23
+ var wire map[string]any
24
+ if err := json.Unmarshal(data, &wire); err != nil {
25
+ t.Fatal(err)
26
+ }
27
+ if value, ok := wire["matchesCurrentTemplate"]; !ok || value != false {
28
+ t.Fatal("confirmed mismatch must survive JSON encoding")
29
+ }
30
+ if wire["podRevisionHash"] != "observed-hash" {
31
+ t.Fatal("observed hash must survive JSON encoding")
32
+ }
33
+ }