@opensoha/contracts 0.1.26 → 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 (213) hide show
  1. package/COMPATIBILITY.md +14 -3
  2. package/README.md +8 -9
  3. package/auth/catalog_generated.go +2 -2
  4. package/auth/permission-catalog.json +481 -1
  5. package/compat/consumer-revisions.json +1 -1
  6. package/compat/json-schema-ecosystem-0.1.0.json +1 -1
  7. package/compat/openapi-loose-boundaries-0.1.0.json +1 -1
  8. package/endpointcontrol/decode.go +64 -0
  9. package/endpointcontrol/models.go +138 -0
  10. package/endpointcontrol/models_test.go +51 -0
  11. package/endpointcontrol/protocol.schema.json +397 -0
  12. package/fixtures/json-schema/endpoint-controls/invalid/controls.json +16 -0
  13. package/fixtures/json-schema/endpoint-controls/invalid/missing-channel-decision.json +15 -0
  14. package/fixtures/json-schema/endpoint-controls/invalid/receipt-content.json +36 -0
  15. package/fixtures/json-schema/endpoint-controls/valid/controls.json +16 -0
  16. package/fixtures/json-schema/endpoint-controls/valid/receipt.json +35 -0
  17. package/fixtures/json-schema/network-runtime-protocol/invalid/eap-activate-bad-fingerprint.json +14 -0
  18. package/fixtures/json-schema/network-runtime-protocol/invalid/eap-gateway-request.json +14 -0
  19. package/fixtures/json-schema/network-runtime-protocol/invalid/eap-private-key-request.json +15 -0
  20. package/fixtures/json-schema/network-runtime-protocol/invalid/eap-profile-without-eap-credential.json +22 -0
  21. package/fixtures/json-schema/network-runtime-protocol/invalid/endpoint-dns-observation.json +25 -0
  22. package/fixtures/json-schema/network-runtime-protocol/invalid/gateway-presence.json +22 -0
  23. package/fixtures/json-schema/network-runtime-protocol/invalid/mihomo-protection-zero-version.json +45 -0
  24. package/fixtures/json-schema/network-runtime-protocol/invalid/presence-raw-key.json +23 -0
  25. package/fixtures/json-schema/network-runtime-protocol/invalid/presence-zero-sequence.json +22 -0
  26. package/fixtures/json-schema/network-runtime-protocol/invalid/workload-wide-vpn.json +17 -0
  27. package/fixtures/json-schema/network-runtime-protocol/valid/eap-certificate-activate.json +14 -0
  28. package/fixtures/json-schema/network-runtime-protocol/valid/eap-certificate-request.json +14 -0
  29. package/fixtures/json-schema/network-runtime-protocol/valid/eap-profile-enrollment.json +24 -0
  30. package/fixtures/json-schema/network-runtime-protocol/valid/endpoint-presence.json +22 -0
  31. package/fixtures/json-schema/network-runtime-protocol/valid/ipv6-configuration.json +102 -0
  32. package/fixtures/json-schema/network-runtime-protocol/valid/mihomo-protected-policy.json +45 -0
  33. package/fixtures/json-schema/network-runtime-protocol/valid/site-dns-ipv6.json +25 -0
  34. package/fixtures/json-schema/network-runtime-protocol/valid/workload-direct-ztna.json +20 -0
  35. package/fixtures/openapi/DLPShadowDecision/invalid/step-up-type.json +14 -0
  36. package/fixtures/openapi/DLPShadowDecision/valid/archive-v2.json +14 -0
  37. package/fixtures/openapi/DLPShadowDecision/valid/step-up.json +14 -0
  38. package/fixtures/openapi/DLPShadowInput/invalid/self-reported-result.json +7 -0
  39. package/fixtures/openapi/DLPShadowInput/invalid/too-many-samples.json +14 -0
  40. package/fixtures/openapi/DLPShadowInput/valid/bounded.json +7 -0
  41. package/fixtures/openapi/DockerVolumeDLPAppealInput/invalid/p4-response.json +3 -0
  42. package/fixtures/openapi/DockerVolumeDLPAppealInput/valid/p4-response.json +3 -0
  43. package/fixtures/openapi/DockerVolumeDLPAppealReviewInput/invalid/p4-response.json +5 -0
  44. package/fixtures/openapi/DockerVolumeDLPAppealReviewInput/valid/p4-response.json +5 -0
  45. package/fixtures/openapi/DockerVolumeDLPAuthorizationInput/invalid/p4-response.json +3 -0
  46. package/fixtures/openapi/DockerVolumeDLPAuthorizationInput/valid/p4-response.json +3 -0
  47. package/fixtures/openapi/DockerVolumeDLPContent/invalid/archive-unsupported-channel.json +9 -0
  48. package/fixtures/openapi/DockerVolumeDLPContent/invalid/p4.json +6 -0
  49. package/fixtures/openapi/DockerVolumeDLPContent/invalid/step-up-audit.json +9 -0
  50. package/fixtures/openapi/DockerVolumeDLPContent/invalid/unavailable-engine.json +9 -0
  51. package/fixtures/openapi/DockerVolumeDLPContent/invalid/unknown-data-level.json +12 -0
  52. package/fixtures/openapi/DockerVolumeDLPContent/valid/classified-rule.json +16 -0
  53. package/fixtures/openapi/DockerVolumeDLPContent/valid/p4.json +8 -0
  54. package/fixtures/openapi/DockerVolumeDLPContent/valid/step-up.json +9 -0
  55. package/fixtures/openapi/DockerVolumeDLPDecision/invalid/p4.json +9 -0
  56. package/fixtures/openapi/DockerVolumeDLPDecision/invalid/step-up-type.json +12 -0
  57. package/fixtures/openapi/DockerVolumeDLPDecision/valid/archive-v2.json +12 -0
  58. package/fixtures/openapi/DockerVolumeDLPDecision/valid/p4.json +11 -0
  59. package/fixtures/openapi/DockerVolumeDLPDecision/valid/step-up.json +12 -0
  60. package/fixtures/openapi/DockerVolumeDLPSaveInput/invalid/p4.json +11 -0
  61. package/fixtures/openapi/DockerVolumeDLPSaveInput/valid/p4.json +11 -0
  62. package/fixtures/openapi/DockerVolumeFileReadInput/invalid/p4-response.json +5 -0
  63. package/fixtures/openapi/DockerVolumeFileReadInput/valid/p4-response.json +4 -0
  64. package/fixtures/openapi/EndpointDeviceInput/invalid/reported-management.json +5 -0
  65. package/fixtures/openapi/EndpointDeviceInput/valid/management-state.json +5 -0
  66. package/fixtures/openapi/EndpointDeviceRegistrationInput/invalid/self-promote-management.json +5 -0
  67. package/fixtures/openapi/KubernetesClusterRegistrationRequest/invalid/metric-cluster-value-length.json +3 -0
  68. package/fixtures/openapi/KubernetesClusterRegistrationRequest/invalid/metric-cluster-value-type.json +3 -0
  69. package/fixtures/openapi/KubernetesClusterRegistrationRequest/valid/metric-cluster-alias.json +5 -0
  70. package/fixtures/openapi/KubernetesClusterRegistrationRequest/valid/metric-cluster-reset.json +3 -0
  71. package/fixtures/openapi/KubernetesHelmReleaseValuesUpdateInput/invalid/revision-overflow.json +4 -0
  72. package/fixtures/openapi/KubernetesHelmReleaseValuesUpdateInput/valid/legacy.json +3 -0
  73. package/fixtures/openapi/KubernetesHelmReleaseValuesUpdateInput/valid/observed-revision.json +4 -0
  74. package/fixtures/openapi/KubernetesJobDetail/invalid/execution-observation.json +23 -0
  75. package/fixtures/openapi/KubernetesJobDetail/valid/execution-observation.json +23 -0
  76. package/fixtures/openapi/KubernetesNode/invalid/labels.json +10 -0
  77. package/fixtures/openapi/KubernetesNode/valid/labels.json +11 -0
  78. package/fixtures/openapi/KubernetesPodListEnvelope/invalid/empty-uid.json +13 -0
  79. package/fixtures/openapi/KubernetesPodListEnvelope/invalid/invalid-container-state.json +20 -0
  80. package/fixtures/openapi/KubernetesPodListEnvelope/invalid/missing-container-ready.json +19 -0
  81. package/fixtures/openapi/KubernetesPodListEnvelope/valid/container-statuses.json +67 -0
  82. package/fixtures/openapi/KubernetesPodListEnvelope/valid/running-pod.json +1 -0
  83. package/fixtures/openapi/KubernetesPrometheusDetail/invalid/metric-cluster-value-type.json +4 -0
  84. package/fixtures/openapi/KubernetesPrometheusDetail/valid/metric-cluster-alias.json +5 -0
  85. package/fixtures/openapi/KubernetesResourceGraphEnvelope/invalid/network-edge-source-payload.json +73 -0
  86. package/fixtures/openapi/KubernetesResourceGraphEnvelope/valid/network-edge-facts.json +67 -0
  87. package/fixtures/openapi/KubernetesSecretCertificate/invalid/secret-data.json +1 -0
  88. package/fixtures/openapi/KubernetesSecretCertificate/invalid/status.json +1 -0
  89. package/fixtures/openapi/KubernetesSecretCertificate/valid/available.json +1 -0
  90. package/fixtures/openapi/KubernetesSecretCertificate/valid/missing.json +1 -0
  91. package/fixtures/openapi/ManagedContentPasteInput/invalid/boundary.json +5 -0
  92. package/fixtures/openapi/ManagedContentPasteInput/valid/boundary.json +6 -0
  93. package/fixtures/openapi/ManagedContentPolicy/invalid/self-reported-allow.json +8 -0
  94. package/fixtures/openapi/ManagedContentPolicy/invalid/step-up-audit.json +8 -0
  95. package/fixtures/openapi/ManagedContentPolicy/invalid/unknown-extractor.json +8 -0
  96. package/fixtures/openapi/ManagedContentPolicy/valid/archive-v2.json +8 -0
  97. package/fixtures/openapi/ManagedContentPolicy/valid/network-and-level.json +10 -0
  98. package/fixtures/openapi/ManagedContentPolicy/valid/step-up.json +8 -0
  99. package/fixtures/openapi/ManagedContentReviewInput/invalid/boundary.json +5 -0
  100. package/fixtures/openapi/ManagedContentReviewInput/valid/boundary.json +5 -0
  101. package/fixtures/openapi/ManagedContentSaveInput/invalid/boundary.json +14 -0
  102. package/fixtures/openapi/ManagedContentSaveInput/valid/boundary.json +16 -0
  103. package/fixtures/openapi/ManagedContentUploadInput/invalid/boundary.json +7 -0
  104. package/fixtures/openapi/ManagedContentUploadInput/valid/boundary.json +6 -0
  105. package/fixtures/openapi/NetworkAccessPolicyInput/invalid/unknown-management-state.json +35 -0
  106. package/fixtures/openapi/NetworkAccessPolicyInput/valid/managed-device.json +35 -0
  107. package/fixtures/openapi/NetworkAdmissionPreviewRequest/invalid/deviceId-characters.json +4 -0
  108. package/fixtures/openapi/NetworkAdmissionPreviewRequest/invalid/nasBindingId-characters.json +4 -0
  109. package/fixtures/openapi/NetworkAdmissionPreviewRequest/invalid/subject-override.json +1 -0
  110. package/fixtures/openapi/NetworkAdmissionPreviewRequest/valid/device-nas.json +1 -0
  111. package/fixtures/openapi/NetworkAdmissionPreviewResult/invalid/missing-attributeVersion.json +13 -0
  112. package/fixtures/openapi/NetworkAdmissionPreviewResult/invalid/missing-evaluatedAt.json +13 -0
  113. package/fixtures/openapi/NetworkAdmissionPreviewResult/valid/versioned.json +14 -0
  114. package/fixtures/openapi/NetworkConflictAnalysisRequest/invalid/missing-runtime-ranges.json +1 -0
  115. package/fixtures/openapi/NetworkConflictAnalysisRequest/invalid/unknown-source-type.json +9 -0
  116. package/fixtures/openapi/NetworkConflictAnalysisRequest/valid/empty-runtime-ranges.json +3 -0
  117. package/fixtures/openapi/NetworkConflictAnalysisRequest/{invalid → valid}/ipv6-range.json +1 -1
  118. package/fixtures/openapi/NetworkEAPServerTrust/invalid/no-server-validation.json +5 -0
  119. package/fixtures/openapi/NetworkEAPServerTrust/valid/named-server.json +9 -0
  120. package/fixtures/openapi/NetworkEndpointResponsePolicyInput/invalid/all-sites-implicit.json +45 -0
  121. package/fixtures/openapi/NetworkEndpointResponsePolicyInput/invalid/unbounded-stages.json +54 -0
  122. package/fixtures/openapi/NetworkEndpointResponsePolicyInput/invalid/zero-heartbeat.json +47 -0
  123. package/fixtures/openapi/NetworkEndpointResponsePolicyInput/valid/staged-protection.json +47 -0
  124. package/fixtures/openapi/NetworkEndpointResponseRestoreInput/invalid/missing-reason.json +3 -0
  125. package/fixtures/openapi/NetworkEndpointResponseRestoreInput/valid/expected-current.json +4 -0
  126. package/fixtures/openapi/NetworkPolicyPreviewResult/invalid/attribute-version.json +1 -0
  127. package/fixtures/openapi/NetworkPolicyPreviewResult/valid/admission-version.json +1 -0
  128. package/fixtures/openapi/NetworkPolicySubjects/invalid/duplicate-role.json +1 -0
  129. package/fixtures/openapi/NetworkPolicySubjects/invalid/role-characters.json +5 -0
  130. package/fixtures/openapi/NetworkPolicySubjects/valid/role-bound.json +1 -0
  131. package/fixtures/openapi/NetworkProxyConfigurationInput/invalid/missing-content.json +1 -0
  132. package/fixtures/openapi/NetworkProxyConfigurationPatchInput/invalid/bad-binding-id.json +9 -0
  133. package/fixtures/openapi/NetworkProxyConfigurationPatchInput/invalid/empty-content.json +1 -0
  134. package/fixtures/openapi/NetworkProxyConfigurationPatchInput/valid/binding-only.json +11 -0
  135. package/fixtures/openapi/NetworkProxyConfigurationPatchInput/valid/clear-binding-disabled.json +5 -0
  136. package/fixtures/openapi/NetworkProxyConfigurationPatchInput/valid/enabled.json +1 -0
  137. package/fixtures/openapi/NetworkSiteInput/invalid/unknown-governance.json +6 -0
  138. package/fixtures/openapi/NetworkSiteInput/valid/restricted-unmanaged.json +6 -0
  139. package/gen/go/sohaapi/client.go +1 -1
  140. package/gen/go/sohaapi/client_test.go +13 -0
  141. package/gen/go/sohaapi/kubernetes_workload_optional_test.go +40 -0
  142. package/gen/go/sohaapi/pod_status_compat_test.go +36 -0
  143. package/gen/go/sohaapi/proxy_configuration_test.go +38 -0
  144. package/gen/go/sohaapi/types.go +3222 -260
  145. package/gen/ts/sohaapi/index.ts +15506 -10104
  146. package/helmrelease/runtime/values_upgrade.go +78 -0
  147. package/helmrelease/runtime/values_upgrade_test.go +123 -0
  148. package/helmrelease/values_update.go +31 -0
  149. package/network/README.md +34 -0
  150. package/network/network-radius-accounting.schema.json +4 -1
  151. package/network/network-runtime-protocol.schema.json +2166 -317
  152. package/network/proxy-runtime.schema.json +482 -67
  153. package/openapi/network_control_test.go +64 -0
  154. package/openapi/soha-api.json +50412 -41310
  155. package/openapi/soha-api.yaml +16859 -10699
  156. package/openapi/spec_generated.go +3 -3
  157. package/package.json +6 -3
  158. package/proxytransport/handler.go +271 -0
  159. package/proxytransport/probe.go +30 -0
  160. package/proxytransport/scope.go +89 -0
  161. package/proxytransport/transport_test.go +60 -0
  162. package/resource/agent_profiles.go +9 -15
  163. package/resource/agent_profiles_test.go +9 -3
  164. package/resource/custom_graph_page.go +18 -0
  165. package/resource/models.go +102 -57
  166. package/resource/network_topology.go +22 -0
  167. package/resource/network_topology_projection.go +36 -22
  168. package/resource/network_topology_projection_test.go +4 -0
  169. package/resource/network_topology_test.go +20 -0
  170. package/resource/network_topology_validation.go +5 -0
  171. package/resource/observed_workload.go +18 -0
  172. package/resource/pod_status_test.go +22 -0
  173. package/resource/rbac_graph.go +4 -0
  174. package/resource/rollout_optional_test.go +33 -0
  175. package/resource/runtime/network_relationship_primitives.go +66 -0
  176. package/resource/runtime/network_relationship_primitives_test.go +147 -0
  177. package/resource/runtime/network_topology.go +17 -5
  178. package/resource/runtime/network_topology_builder.go +405 -0
  179. package/resource/runtime/network_topology_builder_backends.go +195 -0
  180. package/resource/runtime/network_topology_builder_gateway_l4_test.go +86 -0
  181. package/resource/runtime/network_topology_builder_ingress.go +302 -0
  182. package/resource/runtime/network_topology_builder_legacy.go +66 -0
  183. package/resource/runtime/network_topology_builder_providers.go +242 -0
  184. package/resource/runtime/network_topology_builder_test.go +186 -0
  185. package/resource/runtime/network_topology_declarations.go +329 -0
  186. package/resource/runtime/resource_edit.go +22 -0
  187. package/resource/runtime/resource_edit_test.go +32 -0
  188. package/resource/runtime/resource_graph_collect.go +325 -0
  189. package/resource/runtime/resource_graph_configuration.go +59 -0
  190. package/resource/runtime/resource_graph_configuration_collect.go +198 -0
  191. package/resource/runtime/resource_graph_configuration_test.go +470 -0
  192. package/resource/runtime/resource_graph_custom_page.go +159 -0
  193. package/resource/runtime/resource_graph_custom_root.go +132 -0
  194. package/resource/runtime/resource_graph_custom_root_test.go +197 -0
  195. package/resource/runtime/resource_graph_enrichment.go +417 -0
  196. package/resource/runtime/resource_graph_enrichment_hpa_test.go +117 -0
  197. package/resource/runtime/resource_graph_enrichment_test.go +201 -0
  198. package/resource/runtime/resource_graph_metadata.go +125 -0
  199. package/resource/runtime/resource_graph_metadata_test.go +204 -0
  200. package/resource/runtime/resource_graph_network.go +224 -0
  201. package/resource/runtime/resource_graph_network_collect.go +174 -0
  202. package/resource/runtime/resource_graph_network_collect_test.go +147 -0
  203. package/resource/runtime/resource_graph_network_test.go +342 -0
  204. package/resource/runtime/resource_graph_rbac.go +278 -0
  205. package/resource/runtime/resource_graph_rbac_test.go +372 -0
  206. package/resource/runtime/resource_graph_rules.go +834 -0
  207. package/resource/runtime/resource_graph_storage.go +144 -0
  208. package/resource/runtime/resource_graph_storage_collect.go +163 -0
  209. package/resource/runtime/resource_graph_storage_test.go +397 -0
  210. package/resource/runtime/resource_graph_test.go +627 -0
  211. package/resource/runtime/secret_certificate.go +44 -0
  212. package/resource/runtime/secret_certificate_test.go +87 -0
  213. package/resource/storage_graph.go +4 -0
@@ -0,0 +1,78 @@
1
+ package runtime
2
+
3
+ import (
4
+ "errors"
5
+ "fmt"
6
+
7
+ "github.com/opensoha/soha-contracts/helmrelease"
8
+ "helm.sh/helm/v4/pkg/action"
9
+ "helm.sh/helm/v4/pkg/release"
10
+ helmreleasev1 "helm.sh/helm/v4/pkg/release/v1"
11
+ "helm.sh/helm/v4/pkg/storage/driver"
12
+ )
13
+
14
+ // GuardValuesUpgradeRevision protects a single Values upgrade on a request-local Helm configuration.
15
+ func GuardValuesUpgradeRevision(cfg *action.Configuration, upgrade *action.Upgrade, current *helmreleasev1.Release, expected int) error {
16
+ if expected == 0 {
17
+ return nil
18
+ }
19
+ if !helmrelease.ValuesExpectedRevision(expected).Valid() || current.Version != expected ||
20
+ (current.Info != nil && current.Info.Status.IsPending()) {
21
+ return helmValuesRevisionConflict()
22
+ }
23
+ // Storage.Create otherwise prunes history before it attempts to reserve the revision.
24
+ upgrade.MaxHistory = 0
25
+ cfg.Releases.Driver = valuesRevisionDriver{Driver: cfg.Releases.Driver, name: current.Name, expected: expected}
26
+ return nil
27
+ }
28
+
29
+ type valuesRevisionDriver struct {
30
+ driver.Driver
31
+ name string
32
+ expected int
33
+ }
34
+
35
+ func (d valuesRevisionDriver) Query(labels map[string]string) ([]release.Releaser, error) {
36
+ items, err := d.Driver.Query(labels)
37
+ // Helm's preparation reads history again and can encounter a newer pending release.
38
+ if len(labels) != 2 || labels["name"] != d.name || labels["owner"] != "helm" {
39
+ return items, err
40
+ }
41
+ if errors.Is(err, driver.ErrReleaseNotFound) {
42
+ return nil, helmValuesRevisionConflict()
43
+ }
44
+ if err != nil {
45
+ return nil, err
46
+ }
47
+ latest := 0
48
+ for _, item := range items {
49
+ revision, err := release.NewAccessor(item)
50
+ if err != nil {
51
+ return nil, err
52
+ }
53
+ latest = max(latest, revision.Version())
54
+ }
55
+ if latest != d.expected {
56
+ return nil, helmValuesRevisionConflict()
57
+ }
58
+ return items, nil
59
+ }
60
+
61
+ func (d valuesRevisionDriver) Create(key string, item release.Releaser) error {
62
+ revision, err := release.NewAccessor(item)
63
+ if err != nil {
64
+ return err
65
+ }
66
+ if revision.Name() != d.name || revision.Version() != d.expected+1 {
67
+ return helmValuesRevisionConflict()
68
+ }
69
+ err = d.Driver.Create(key, item)
70
+ if errors.Is(err, driver.ErrReleaseExists) {
71
+ return helmValuesRevisionConflict()
72
+ }
73
+ return err
74
+ }
75
+
76
+ func helmValuesRevisionConflict() error {
77
+ return fmt.Errorf("%w: Helm release revision changed or is pending; refresh its values before saving", ErrConflict)
78
+ }
@@ -0,0 +1,123 @@
1
+ package runtime
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "io"
7
+ "testing"
8
+
9
+ "helm.sh/helm/v4/pkg/action"
10
+ "helm.sh/helm/v4/pkg/chart/common"
11
+ chartv2 "helm.sh/helm/v4/pkg/chart/v2"
12
+ "helm.sh/helm/v4/pkg/kube"
13
+ kubefake "helm.sh/helm/v4/pkg/kube/fake"
14
+ "helm.sh/helm/v4/pkg/release"
15
+ releasecommon "helm.sh/helm/v4/pkg/release/common"
16
+ releasev1 "helm.sh/helm/v4/pkg/release/v1"
17
+ "helm.sh/helm/v4/pkg/storage"
18
+ "helm.sh/helm/v4/pkg/storage/driver"
19
+ )
20
+
21
+ func TestGuardValuesUpgradeRevision(t *testing.T) {
22
+ for _, tc := range []struct {
23
+ name string
24
+ expected int
25
+ change bool
26
+ collision bool
27
+ wantRevisions int
28
+ wantWrites int
29
+ }{
30
+ {name: "stale before upgrade", expected: 1, wantRevisions: 2},
31
+ {name: "external pending revision after editor read", expected: 2, change: true, wantRevisions: 3},
32
+ {name: "same revision storage collision", expected: 2, collision: true, wantRevisions: 3},
33
+ {name: "current revision", expected: 2, wantRevisions: 3, wantWrites: 1},
34
+ } {
35
+ t.Run(tc.name, func(t *testing.T) {
36
+ chart := &chartv2.Chart{Metadata: &chartv2.Metadata{APIVersion: "v2", Name: "app", Version: "1.0.0"}}
37
+ first := &releasev1.Release{Name: "app", Namespace: "test", Version: 1, Chart: chart, Info: &releasev1.Info{Status: releasecommon.StatusSuperseded}}
38
+ current := &releasev1.Release{Name: "app", Namespace: "test", Version: 2, Chart: chart, Info: &releasev1.Info{Status: releasecommon.StatusDeployed}}
39
+ winner := &releasev1.Release{Name: "app", Namespace: "test", Version: 3, Chart: chart, Info: &releasev1.Info{Status: releasecommon.StatusPendingUpgrade}, Config: map[string]any{"winner": true}}
40
+ history := storage.Init(driver.NewMemory())
41
+ for _, item := range []*releasev1.Release{first, current} {
42
+ if err := history.Create(item); err != nil {
43
+ t.Fatal(err)
44
+ }
45
+ }
46
+ kubeClient := &valuesUpgradeKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, history: history}
47
+ racingDriver := valuesUpgradeRaceDriver{Driver: history.Driver}
48
+ if tc.collision {
49
+ racingDriver.winner = winner
50
+ }
51
+ cfg := &action.Configuration{Releases: storage.Init(racingDriver), KubeClient: kubeClient, Capabilities: common.DefaultCapabilities}
52
+ upgrade := action.NewUpgrade(cfg)
53
+ upgrade.Namespace = current.Namespace
54
+ upgrade.MaxHistory = 1
55
+ err := GuardValuesUpgradeRevision(cfg, upgrade, current, tc.expected)
56
+ if tc.change {
57
+ if err := history.Create(winner); err != nil {
58
+ t.Fatal(err)
59
+ }
60
+ }
61
+ if err == nil {
62
+ _, err = upgrade.RunWithContext(context.Background(), current.Name, chart, map[string]any{"edited": true})
63
+ }
64
+ if tc.wantWrites == 0 && !errors.Is(err, ErrConflict) || tc.wantWrites > 0 && err != nil {
65
+ t.Fatalf("upgrade error = %v", err)
66
+ }
67
+ if kubeClient.writes != tc.wantWrites {
68
+ t.Fatalf("Kubernetes updates = %d, want %d", kubeClient.writes, tc.wantWrites)
69
+ }
70
+ if kubeClient.writes > 0 && !kubeClient.reservedBeforeUpdate {
71
+ t.Fatal("Kubernetes updated before revision 3 was reserved")
72
+ }
73
+ items, err := history.History(current.Name)
74
+ if err != nil || len(items) != tc.wantRevisions {
75
+ t.Fatalf("history contains %d revisions, want %d, err=%v", len(items), tc.wantRevisions, err)
76
+ }
77
+ if tc.change || tc.collision {
78
+ latest, err := history.Last(current.Name)
79
+ if err != nil {
80
+ t.Fatal(err)
81
+ }
82
+ latestRelease, ok := latest.(*releasev1.Release)
83
+ if !ok {
84
+ t.Fatalf("latest revision has unexpected type %T", latest)
85
+ }
86
+ if latestRelease.Config["winner"] != true {
87
+ t.Fatal("concurrent writer's Values were overwritten")
88
+ }
89
+ }
90
+ })
91
+ }
92
+ }
93
+
94
+ type valuesUpgradeRaceDriver struct {
95
+ driver.Driver
96
+ winner *releasev1.Release
97
+ }
98
+
99
+ func (d valuesUpgradeRaceDriver) Create(key string, item release.Releaser) error {
100
+ if d.winner != nil {
101
+ if err := d.Driver.Create(key, d.winner); err != nil {
102
+ return err
103
+ }
104
+ }
105
+ return d.Driver.Create(key, item)
106
+ }
107
+
108
+ type valuesUpgradeKubeClient struct {
109
+ kubefake.PrintingKubeClient
110
+ history *storage.Storage
111
+ writes int
112
+ reservedBeforeUpdate bool
113
+ }
114
+
115
+ func (c *valuesUpgradeKubeClient) Update(original, target kube.ResourceList, opts ...kube.ClientUpdateOption) (*kube.Result, error) {
116
+ c.writes++
117
+ latest, err := c.history.Last("app")
118
+ if err == nil {
119
+ latestRelease, ok := latest.(*releasev1.Release)
120
+ c.reservedBeforeUpdate = ok && latestRelease.Version == 3 && latestRelease.Info.Status == releasecommon.StatusPendingUpgrade
121
+ }
122
+ return c.PrintingKubeClient.Update(original, target, opts...)
123
+ }
@@ -0,0 +1,31 @@
1
+ package helmrelease
2
+
3
+ import (
4
+ "encoding/json"
5
+ "errors"
6
+ "math"
7
+ )
8
+
9
+ type ValuesUpdateRequest struct {
10
+ Content string `json:"content"`
11
+ ExpectedRevision ValuesExpectedRevision `json:"expectedRevision,omitempty"`
12
+ }
13
+
14
+ // ValuesExpectedRevision uses zero internally for legacy requests that omit the field.
15
+ type ValuesExpectedRevision int
16
+
17
+ func (r ValuesExpectedRevision) Valid() bool {
18
+ return r > 0 && r < math.MaxInt32
19
+ }
20
+
21
+ func (r *ValuesExpectedRevision) UnmarshalJSON(data []byte) error {
22
+ var value int
23
+ if err := json.Unmarshal(data, &value); err != nil {
24
+ return err
25
+ }
26
+ if !ValuesExpectedRevision(value).Valid() {
27
+ return errors.New("expectedRevision must be between 1 and 2147483646")
28
+ }
29
+ *r = ValuesExpectedRevision(value)
30
+ return nil
31
+ }
package/network/README.md CHANGED
@@ -57,6 +57,11 @@ for authorization, routing, firewall configuration or accounting.
57
57
  reply attribute and require NAS accounting to echo it as `sessionId` when
58
58
  available. Session commands repeat the bound `subjectId` and `deviceId` so
59
59
  the adapter can target CoA/Disconnect without querying management APIs.
60
+ - Capability `nas-session-accounting-v1` enables the optional command target
61
+ `stationId`, `accountingSessionId` and `nasPortId`. For EAP-TLS, station identity
62
+ comes from saved authorization/accounting, never the opaque Soha device ID.
63
+ A runtime that cannot receive the required target binding must report
64
+ unsupported rather than send a command to a guessed station.
60
65
  - If a requested CoA is unsupported, issue a disconnect command when the NAS
61
66
  binding supports it; otherwise fail closed and record the unsupported
62
67
  enforcement result.
@@ -166,3 +171,32 @@ for authorization, routing, firewall configuration or accounting.
166
171
  Consumers must cover these rules with deterministic tests. Failure is
167
172
  fail-closed for control messages and bounded reject/drop-with-counter behavior
168
173
  for telemetry.
174
+
175
+ ## Endpoint presence and RADIUS response
176
+
177
+ - `endpoint.presence.request` goes to
178
+ `POST /api/network-control/v1/runtimes/{runtimeId}/endpoint:presence` using the
179
+ enrolled endpoint's control identity and `endpoint-presence-v1` capability.
180
+ `bootId` identifies the protected service incarnation; a new incarnation
181
+ starts a new sequence and retires the previous one. A duplicate must not
182
+ refresh server-side liveness. UI presence never implies a healthy service.
183
+ - `POST /api/network-control/v1/runtimes/{runtimeId}/nas:accounting` uses
184
+ the enrolled NAS control identity and the existing RADIUS Accounting schema.
185
+ The control plane checks NAS, subject, station and accounting-session binding
186
+ against the saved authorization before updating session evidence. The
187
+ separate ingest submission remains telemetry and cannot change admission.
188
+ - A Stop remains terminal for that Core session. A reused accounting ID,
189
+ conflicting binding, restart after Start, or missing Start remains unknown.
190
+ Received time alone cannot refresh an old observation. Accounting evidence
191
+ cannot authorize access or prove that a VLAN/ACL was applied.
192
+ - Persist response restrictions before delivering NAS commands. A new
193
+ authentication must observe every active restriction. Policy retirement
194
+ never removes an existing restriction, and restoration requires current
195
+ health plus authorization rather than replaying a previous allow decision.
196
+
197
+ ## Dedicated EAP certificates and native profiles
198
+
199
+ - `eap.certificate.request` uses `POST /api/network-control/v1/runtimes/{runtimeId}/eap:certificate`; `eap.certificate.activate.request` uses the same prefix with `eap:activate`. Both require the endpoint control identity and `eap-profile-v1` plus `radius-eap-tls-v1`. CSR private keys stay on the endpoint. Core binds the subject and device from enrollment and ignores CSR identity extensions.
200
+ - A dedicated EAP issuer signs only client authentication certificates with a `network-eap` URI. EAP certificates cannot authenticate control or ingest requests. Without an issuer, issuance is unavailable. Existing runtime certificates are accepted for legacy EAP only when enrollment explicitly declares `radius-eap-tls-v1`; ordinary control credentials do not imply EAP permission.
201
+ - Issuance is idempotent per runtime/request ID and CSR. Certificates start pending and cannot grant network access. After native installation and readback, activation atomically replaces the old EAP certificate under the parent runtime credential lock. A retired or expired parent invalidates every child. Replaying an older activation cannot revive it.
202
+ - `admission.profiles` is delivered only to `eap-profile-v1` endpoints, with exact EAP server names, CA certificates, subject and the dedicated active EAP certificate fingerprint. Configuration export, installation, 802.1X connection and actual NAS enforcement are separate results.
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://contracts.opensoha.dev/network/network-radius-accounting.schema.json",
4
4
  "title": "OpenSoha FreeRADIUS Accounting Adapter Request",
5
- "description": "A strict credential-free request converted by soha-ingest into one canonical radius.accounting event.",
5
+ "description": "A strict credential-free request converted by soha-ingest into telemetry. When submitted to network-control with an enrolled NAS identity, its NAS and session binding are independently verified before updating admission evidence.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
8
  "required": [
@@ -20,6 +20,9 @@
20
20
  "accountingSessionId": { "$ref": "#/$defs/Identifier" },
21
21
  "sessionId": { "$ref": "#/$defs/Identifier" },
22
22
  "nasId": { "$ref": "#/$defs/Identifier" },
23
+ "accountingDelaySeconds": { "type": "integer", "minimum": 0, "maximum": 86400 },
24
+ "observedVlanId": { "type": "integer", "minimum": 1, "maximum": 4094 },
25
+ "observedFilterId": { "type": "string", "minLength": 1, "maxLength": 128 },
23
26
  "nasPortId": { "type": "string", "minLength": 1, "maxLength": 128 },
24
27
  "userId": { "$ref": "#/$defs/Identifier" },
25
28
  "deviceId": { "$ref": "#/$defs/Identifier" },