@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.
- package/COMPATIBILITY.md +4 -3
- package/README.md +8 -9
- package/compat/consumer-revisions.json +1 -1
- package/compat/json-schema-ecosystem-0.1.0.json +1 -1
- package/compat/openapi-loose-boundaries-0.1.0.json +1 -1
- package/fixtures/openapi/KubernetesClusterRegistrationRequest/invalid/metric-cluster-value-length.json +3 -0
- package/fixtures/openapi/KubernetesClusterRegistrationRequest/invalid/metric-cluster-value-type.json +3 -0
- package/fixtures/openapi/KubernetesClusterRegistrationRequest/valid/metric-cluster-alias.json +5 -0
- package/fixtures/openapi/KubernetesClusterRegistrationRequest/valid/metric-cluster-reset.json +3 -0
- package/fixtures/openapi/KubernetesHelmReleaseValuesUpdateInput/invalid/revision-overflow.json +4 -0
- package/fixtures/openapi/KubernetesHelmReleaseValuesUpdateInput/valid/legacy.json +3 -0
- package/fixtures/openapi/KubernetesHelmReleaseValuesUpdateInput/valid/observed-revision.json +4 -0
- package/fixtures/openapi/KubernetesJobDetail/invalid/execution-observation.json +23 -0
- package/fixtures/openapi/KubernetesJobDetail/valid/execution-observation.json +23 -0
- package/fixtures/openapi/KubernetesNode/invalid/labels.json +10 -0
- package/fixtures/openapi/KubernetesNode/valid/labels.json +11 -0
- package/fixtures/openapi/KubernetesPodListEnvelope/invalid/empty-uid.json +13 -0
- package/fixtures/openapi/KubernetesPodListEnvelope/invalid/invalid-container-state.json +20 -0
- package/fixtures/openapi/KubernetesPodListEnvelope/invalid/missing-container-ready.json +19 -0
- package/fixtures/openapi/KubernetesPodListEnvelope/valid/container-statuses.json +67 -0
- package/fixtures/openapi/KubernetesPodListEnvelope/valid/running-pod.json +1 -0
- package/fixtures/openapi/KubernetesPrometheusDetail/invalid/metric-cluster-value-type.json +4 -0
- package/fixtures/openapi/KubernetesPrometheusDetail/valid/metric-cluster-alias.json +5 -0
- package/fixtures/openapi/KubernetesResourceGraphEnvelope/invalid/network-edge-source-payload.json +73 -0
- package/fixtures/openapi/KubernetesResourceGraphEnvelope/valid/network-edge-facts.json +67 -0
- package/fixtures/openapi/KubernetesSecretCertificate/invalid/secret-data.json +1 -0
- package/fixtures/openapi/KubernetesSecretCertificate/invalid/status.json +1 -0
- package/fixtures/openapi/KubernetesSecretCertificate/valid/available.json +1 -0
- package/fixtures/openapi/KubernetesSecretCertificate/valid/missing.json +1 -0
- package/gen/go/sohaapi/client.go +1 -1
- package/gen/go/sohaapi/client_test.go +13 -0
- package/gen/go/sohaapi/kubernetes_workload_optional_test.go +40 -0
- package/gen/go/sohaapi/pod_status_compat_test.go +36 -0
- package/gen/go/sohaapi/types.go +421 -88
- package/gen/ts/sohaapi/index.ts +756 -4
- package/helmrelease/runtime/values_upgrade.go +78 -0
- package/helmrelease/runtime/values_upgrade_test.go +123 -0
- package/helmrelease/values_update.go +31 -0
- package/openapi/soha-api.json +1191 -14
- package/openapi/soha-api.yaml +588 -28
- package/openapi/spec_generated.go +3 -3
- package/package.json +1 -1
- package/resource/agent_profiles.go +9 -15
- package/resource/agent_profiles_test.go +9 -3
- package/resource/custom_graph_page.go +18 -0
- package/resource/models.go +102 -57
- package/resource/network_topology.go +22 -0
- package/resource/network_topology_projection.go +36 -22
- package/resource/network_topology_projection_test.go +4 -0
- package/resource/network_topology_test.go +20 -0
- package/resource/network_topology_validation.go +5 -0
- package/resource/observed_workload.go +18 -0
- package/resource/pod_status_test.go +22 -0
- package/resource/rbac_graph.go +4 -0
- package/resource/rollout_optional_test.go +33 -0
- package/resource/runtime/network_relationship_primitives.go +66 -0
- package/resource/runtime/network_relationship_primitives_test.go +147 -0
- package/resource/runtime/network_topology.go +17 -5
- package/resource/runtime/network_topology_builder.go +405 -0
- package/resource/runtime/network_topology_builder_backends.go +195 -0
- package/resource/runtime/network_topology_builder_gateway_l4_test.go +86 -0
- package/resource/runtime/network_topology_builder_ingress.go +302 -0
- package/resource/runtime/network_topology_builder_legacy.go +66 -0
- package/resource/runtime/network_topology_builder_providers.go +242 -0
- package/resource/runtime/network_topology_builder_test.go +186 -0
- package/resource/runtime/network_topology_declarations.go +329 -0
- package/resource/runtime/resource_edit.go +22 -0
- package/resource/runtime/resource_edit_test.go +32 -0
- package/resource/runtime/resource_graph_collect.go +325 -0
- package/resource/runtime/resource_graph_configuration.go +59 -0
- package/resource/runtime/resource_graph_configuration_collect.go +198 -0
- package/resource/runtime/resource_graph_configuration_test.go +470 -0
- package/resource/runtime/resource_graph_custom_page.go +159 -0
- package/resource/runtime/resource_graph_custom_root.go +132 -0
- package/resource/runtime/resource_graph_custom_root_test.go +197 -0
- package/resource/runtime/resource_graph_enrichment.go +417 -0
- package/resource/runtime/resource_graph_enrichment_hpa_test.go +117 -0
- package/resource/runtime/resource_graph_enrichment_test.go +201 -0
- package/resource/runtime/resource_graph_metadata.go +125 -0
- package/resource/runtime/resource_graph_metadata_test.go +204 -0
- package/resource/runtime/resource_graph_network.go +224 -0
- package/resource/runtime/resource_graph_network_collect.go +174 -0
- package/resource/runtime/resource_graph_network_collect_test.go +147 -0
- package/resource/runtime/resource_graph_network_test.go +342 -0
- package/resource/runtime/resource_graph_rbac.go +278 -0
- package/resource/runtime/resource_graph_rbac_test.go +372 -0
- package/resource/runtime/resource_graph_rules.go +834 -0
- package/resource/runtime/resource_graph_storage.go +144 -0
- package/resource/runtime/resource_graph_storage_collect.go +163 -0
- package/resource/runtime/resource_graph_storage_test.go +397 -0
- package/resource/runtime/resource_graph_test.go +627 -0
- package/resource/runtime/secret_certificate.go +44 -0
- package/resource/runtime/secret_certificate_test.go +87 -0
- package/resource/storage_graph.go +4 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
package runtime
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"fmt"
|
|
6
|
+
"strings"
|
|
7
|
+
|
|
8
|
+
cr "github.com/opensoha/soha-contracts/resource"
|
|
9
|
+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
10
|
+
"k8s.io/apimachinery/pkg/labels"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
// declarations adds only structured Kubernetes references. It never interprets
|
|
14
|
+
// arbitrary provider spec, certificate bytes, or policy as live connectivity.
|
|
15
|
+
func (g *topologyGraph) declarations(prioritized string) {
|
|
16
|
+
for _, o := range g.ordered {
|
|
17
|
+
if g.budgetReached() || g.ctx.Err() != nil {
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
if o.id() != prioritized {
|
|
21
|
+
g.objectDeclarations(o)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func (g *topologyGraph) objectDeclarations(o topologyObject) {
|
|
27
|
+
switch o.ref.Kind {
|
|
28
|
+
case "Ingress":
|
|
29
|
+
g.declaredRef(o, "networking.k8s.io/v1", "IngressClass", "", text(at(o.body, "spec", "ingressClassName")), "uses-class", "spec.ingressClassName", "")
|
|
30
|
+
for i, tls := range items(at(o.body, "spec", "tls")) {
|
|
31
|
+
g.declaredRef(o, "v1", "Secret", o.ref.Namespace, text(tls["secretName"]), "uses-secret", fmt.Sprintf("spec.tls[%d].secretName", i), "")
|
|
32
|
+
}
|
|
33
|
+
case "Gateway":
|
|
34
|
+
g.declaredRef(o, "gateway.networking.k8s.io/v1", "GatewayClass", "", text(at(o.body, "spec", "gatewayClassName")), "uses-class", "spec.gatewayClassName", "")
|
|
35
|
+
g.gatewayCertificates(o)
|
|
36
|
+
case "IngressClass":
|
|
37
|
+
g.classParameters(o, object(at(o.body, "spec", "parameters")), "spec.parameters")
|
|
38
|
+
case "GatewayClass":
|
|
39
|
+
g.classParameters(o, object(at(o.body, "spec", "parametersRef")), "spec.parametersRef")
|
|
40
|
+
case "BackendTLSPolicy":
|
|
41
|
+
g.backendTLSPolicy(o)
|
|
42
|
+
case "NetworkPolicy":
|
|
43
|
+
g.networkPolicy(o)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func topologySourceFor(kind, api string) (cr.NetworkTopologySourceDefinition, bool) {
|
|
48
|
+
for _, d := range cr.NetworkTopologySources() {
|
|
49
|
+
if d.Kind == kind && topologyDefinitionAPI(d, api) {
|
|
50
|
+
return d, true
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return cr.NetworkTopologySourceDefinition{}, false
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
func (g *topologyGraph) declaredRef(o topologyObject, api, kind, namespace, name, relation, path, section string) *cr.NetworkTopologyGraphEdge {
|
|
57
|
+
if name == "" {
|
|
58
|
+
return &cr.NetworkTopologyGraphEdge{}
|
|
59
|
+
}
|
|
60
|
+
source := g.objectNode(o, "backend-group", topologyProvider(o.source), "")
|
|
61
|
+
ref := cr.ResourceRef{ClusterID: g.view.ClusterID, APIVersion: api, Kind: kind, Namespace: namespace, Name: name, ScopeMode: cr.ResourceScopeModeNamespace}
|
|
62
|
+
if namespace == "" {
|
|
63
|
+
ref.ScopeMode = cr.ResourceScopeModeCluster
|
|
64
|
+
}
|
|
65
|
+
target := topologyObject{ref: ref}
|
|
66
|
+
state := "unknown"
|
|
67
|
+
if api == "" {
|
|
68
|
+
state = "unsupported_fragment"
|
|
69
|
+
}
|
|
70
|
+
if d, ok := topologySourceFor(kind, api); ok {
|
|
71
|
+
if g.completeSource(d.ID, namespace) {
|
|
72
|
+
state = "not_found"
|
|
73
|
+
}
|
|
74
|
+
if actual, ok := g.objects[topologyKey(kind, namespace, name)]; ok && actual.ref.APIVersion == api {
|
|
75
|
+
target = actual
|
|
76
|
+
state = "resolved"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Secret existence is never inferred from an ordinary object projection.
|
|
80
|
+
if kind == "Secret" && api == "v1" {
|
|
81
|
+
state = "unresolved"
|
|
82
|
+
}
|
|
83
|
+
id := ""
|
|
84
|
+
if state == "unsupported_fragment" {
|
|
85
|
+
id = g.unknown(source, kind, namespace, name, state)
|
|
86
|
+
} else if state == "unknown" || kind == "Secret" && api == "v1" {
|
|
87
|
+
// The public topology has no per-node product permission filter. Keep a
|
|
88
|
+
// declaration identity private for Graph's later authorization pass only.
|
|
89
|
+
id = g.unknown(source, kind, namespace, name, state)
|
|
90
|
+
g.hiddenRefs[id] = target.ref
|
|
91
|
+
} else {
|
|
92
|
+
id = g.objectNode(target, "backend-group", topologyProvider(o.source), "")
|
|
93
|
+
if state != "resolved" {
|
|
94
|
+
g.state(id, state)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
edge := g.link(source, id, relation, "", "")
|
|
98
|
+
edge.SourceRef = &o.ref
|
|
99
|
+
edge.FieldPath = path
|
|
100
|
+
edge.SectionName = section
|
|
101
|
+
edge.ResolutionState = state
|
|
102
|
+
return edge
|
|
103
|
+
}
|
|
104
|
+
func topologyProvider(source string) string {
|
|
105
|
+
d, _ := cr.LookupNetworkTopologySource(source)
|
|
106
|
+
return firstTopology(d.Provider, "kubernetes")
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
func (g *topologyGraph) classParameters(o topologyObject, ref map[string]any, path string) {
|
|
110
|
+
kind, name := text(ref["kind"]), text(ref["name"])
|
|
111
|
+
group := firstTopology(text(ref["group"]), text(ref["apiGroup"]))
|
|
112
|
+
if kind == "" || name == "" {
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
namespace := text(ref["namespace"])
|
|
116
|
+
if text(ref["scope"]) == "Namespace" && namespace == "" {
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
api := topologyReferenceAPI(group, kind)
|
|
120
|
+
g.declaredRef(o, api, kind, namespace, name, "parameters", path, "")
|
|
121
|
+
}
|
|
122
|
+
func topologyReferenceAPI(group, kind string) string {
|
|
123
|
+
for _, d := range cr.NetworkTopologySources() {
|
|
124
|
+
if d.Group == group && d.Kind == kind {
|
|
125
|
+
return strings.TrimPrefix(d.Group+"/"+d.Version, "/")
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if group == "" && kind == "Secret" {
|
|
129
|
+
return "v1"
|
|
130
|
+
}
|
|
131
|
+
// No version is guessed for an unsupported parameter kind.
|
|
132
|
+
return ""
|
|
133
|
+
}
|
|
134
|
+
func (g *topologyGraph) gatewayCertificates(o topologyObject) {
|
|
135
|
+
for i, l := range items(at(o.body, "spec", "listeners")) {
|
|
136
|
+
for j, r := range items(at(l, "tls", "certificateRefs")) {
|
|
137
|
+
group, kind := text(r["group"]), firstTopology(text(r["kind"]), "Secret")
|
|
138
|
+
ns := firstTopology(text(r["namespace"]), o.ref.Namespace)
|
|
139
|
+
authorized := ns == o.ref.Namespace || g.referenceAuthorized(o, group, kind, ns, text(r["name"]))
|
|
140
|
+
edge := g.declaredRef(o, topologyReferenceAPI(group, kind), kind, ns, text(r["name"]), "uses-secret", fmt.Sprintf("spec.listeners[%d].tls.certificateRefs[%d]", i, j), text(l["name"]))
|
|
141
|
+
edge.Protocol = text(l["protocol"])
|
|
142
|
+
edge.Port = text(l["port"])
|
|
143
|
+
if !authorized {
|
|
144
|
+
edge.ResolutionState = g.grantResolution(ns)
|
|
145
|
+
}
|
|
146
|
+
if group != "" || kind != "Secret" {
|
|
147
|
+
edge.ResolutionState = "unsupported_fragment"
|
|
148
|
+
}
|
|
149
|
+
edge.Note = "Certificate reference only; TLS activation is unverified"
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
func (g *topologyGraph) backendTLSPolicy(o topologyObject) {
|
|
154
|
+
refs := items(at(o.body, "spec", "targetRefs"))
|
|
155
|
+
path := "spec.targetRefs"
|
|
156
|
+
if len(refs) == 0 {
|
|
157
|
+
if ref := object(at(o.body, "spec", "targetRef")); len(ref) > 0 {
|
|
158
|
+
refs = append(refs, ref)
|
|
159
|
+
path = "spec.targetRef"
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
for i, r := range refs {
|
|
163
|
+
targetPath := path
|
|
164
|
+
if path == "spec.targetRefs" {
|
|
165
|
+
targetPath = fmt.Sprintf("%s[%d]", path, i)
|
|
166
|
+
}
|
|
167
|
+
kind, name, section := text(r["kind"]), text(r["name"]), text(r["sectionName"])
|
|
168
|
+
api := topologyReferenceAPI(text(r["group"]), kind)
|
|
169
|
+
edge := g.declaredRef(o, api, kind, o.ref.Namespace, name, "targets", targetPath, section)
|
|
170
|
+
if api == "v1" && kind == "Service" && section != "" && edge.ResolutionState == "resolved" {
|
|
171
|
+
service := g.objects[topologyKey(kind, o.ref.Namespace, name)]
|
|
172
|
+
resolveBackendTLSServiceSection(edge, service, section)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
for i, r := range items(at(o.body, "spec", "validation", "caCertificateRefs")) {
|
|
176
|
+
group, kind := text(r["group"]), firstTopology(text(r["kind"]), "ConfigMap")
|
|
177
|
+
edge := g.declaredRef(o, topologyReferenceAPI(group, kind), kind, o.ref.Namespace, text(r["name"]), "uses-config", fmt.Sprintf("spec.validation.caCertificateRefs[%d]", i), "")
|
|
178
|
+
if group != "" || kind != "ConfigMap" {
|
|
179
|
+
edge.ResolutionState = "unsupported_fragment"
|
|
180
|
+
}
|
|
181
|
+
edge.Note = "CA reference only; trust and certificate validity are unverified"
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
func resolveBackendTLSServiceSection(edge *cr.NetworkTopologyGraphEdge, service topologyObject, section string) {
|
|
186
|
+
for _, port := range items(at(service.body, "spec", "ports")) {
|
|
187
|
+
if text(port["name"]) == section {
|
|
188
|
+
edge.Port = text(port["port"])
|
|
189
|
+
edge.Protocol = firstTopology(text(port["protocol"]), "TCP")
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
edge.ResolutionState = "unresolved"
|
|
194
|
+
edge.Note = "Declared Service port name was not observed"
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// A grant is an OR of from entries AND an OR of to entries, scoped to
|
|
198
|
+
// the target namespace. Partial/unavailable grant coverage never confirms it.
|
|
199
|
+
func (g *topologyGraph) referenceAuthorized(o topologyObject, group, kind, namespace, name string) bool {
|
|
200
|
+
if namespace == o.ref.Namespace {
|
|
201
|
+
return true
|
|
202
|
+
}
|
|
203
|
+
if !g.completeSource("gateway.referencegrants", namespace) {
|
|
204
|
+
return false
|
|
205
|
+
}
|
|
206
|
+
fromGroup := strings.Split(o.ref.APIVersion, "/")[0]
|
|
207
|
+
for _, grant := range g.ordered {
|
|
208
|
+
if g.ctx.Err() != nil || g.budgetReached() {
|
|
209
|
+
return false
|
|
210
|
+
}
|
|
211
|
+
if grant.source != "gateway.referencegrants" || grant.ref.Namespace != namespace {
|
|
212
|
+
continue
|
|
213
|
+
}
|
|
214
|
+
from, to := false, false
|
|
215
|
+
for _, r := range items(at(grant.body, "spec", "from")) {
|
|
216
|
+
if text(r["namespace"]) == o.ref.Namespace && text(r["kind"]) == o.ref.Kind && text(r["group"]) == fromGroup {
|
|
217
|
+
from = true
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
for _, r := range items(at(grant.body, "spec", "to")) {
|
|
221
|
+
if text(r["group"]) == group && text(r["kind"]) == kind && (text(r["name"]) == "" || text(r["name"]) == name) {
|
|
222
|
+
to = true
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if from && to {
|
|
226
|
+
edge := g.link(g.objectNode(grant, "backend-group", "gateway", ""), g.objectNode(o, "entry", "gateway", ""), "authorizes-reference", "", "")
|
|
227
|
+
edge.SourceRef = &grant.ref
|
|
228
|
+
edge.FieldPath = "spec.from+spec.to"
|
|
229
|
+
edge.Note = "Authorizes a declared reference; does not carry traffic"
|
|
230
|
+
return true
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return false
|
|
234
|
+
}
|
|
235
|
+
func (g *topologyGraph) grantResolution(namespace string) string {
|
|
236
|
+
if g.completeSource("gateway.referencegrants", namespace) {
|
|
237
|
+
return "unresolved"
|
|
238
|
+
}
|
|
239
|
+
return "unknown"
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
func topologySelectorMatches(selector, values map[string]any) bool {
|
|
243
|
+
if selector == nil {
|
|
244
|
+
return false
|
|
245
|
+
}
|
|
246
|
+
raw, err := json.Marshal(selector)
|
|
247
|
+
if err != nil {
|
|
248
|
+
return false
|
|
249
|
+
}
|
|
250
|
+
var s metav1.LabelSelector
|
|
251
|
+
if json.Unmarshal(raw, &s) != nil {
|
|
252
|
+
return false
|
|
253
|
+
}
|
|
254
|
+
compiled, err := metav1.LabelSelectorAsSelector(&s)
|
|
255
|
+
if err != nil {
|
|
256
|
+
return false
|
|
257
|
+
}
|
|
258
|
+
set := labels.Set{}
|
|
259
|
+
for k, v := range values {
|
|
260
|
+
if value, ok := v.(string); ok {
|
|
261
|
+
set[k] = value
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return compiled.Matches(set)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
func (g *topologyGraph) networkPolicy(o topologyObject) {
|
|
268
|
+
id := g.objectNode(o, "backend-group", "kubernetes", "")
|
|
269
|
+
selected := object(at(o.body, "spec", "podSelector"))
|
|
270
|
+
for _, pod := range g.ordered {
|
|
271
|
+
if g.budgetReached() || g.ctx.Err() != nil {
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
if pod.source == "kubernetes.pods" && pod.ref.Namespace == o.ref.Namespace && topologySelectorMatches(selected, object(at(pod.body, "metadata", "labels"))) {
|
|
275
|
+
edge := g.link(id, g.objectNode(pod, "pod", "kubernetes", ""), "selects", "", "")
|
|
276
|
+
edge.FieldPath = "spec.podSelector"
|
|
277
|
+
edge.Note = "Selected Pod only; policy enforcement and connectivity are unverified"
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
for _, direction := range []string{"ingress", "egress"} {
|
|
281
|
+
peerField := "from"
|
|
282
|
+
if direction == "egress" {
|
|
283
|
+
peerField = "to"
|
|
284
|
+
}
|
|
285
|
+
for i, rule := range items(at(o.body, "spec", direction)) {
|
|
286
|
+
if len(items(rule[peerField])) == 0 {
|
|
287
|
+
g.properties(id, cr.NetworkTopologyProperty{Name: fmt.Sprintf("spec.%s[%d].%s", direction, i, peerField), Value: "All peers declared; connectivity is unverified"})
|
|
288
|
+
}
|
|
289
|
+
for j, peer := range items(rule[peerField]) {
|
|
290
|
+
path := fmt.Sprintf("spec.%s[%d].%s[%d]", direction, i, peerField, j)
|
|
291
|
+
if block := object(peer["ipBlock"]); len(block) > 0 {
|
|
292
|
+
g.properties(id, cr.NetworkTopologyProperty{Name: path + ".ipBlock", Value: text(block["cidr"]) + " except " + strings.Join(texts(block["except"]), ",") + "; declared CIDRs only"})
|
|
293
|
+
continue
|
|
294
|
+
}
|
|
295
|
+
g.policyPeers(o, id, peer, path)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
func (g *topologyGraph) policyPeers(o topologyObject, id string, peer map[string]any, path string) {
|
|
301
|
+
ps, hasPod := peer["podSelector"]
|
|
302
|
+
ns, hasNS := peer["namespaceSelector"]
|
|
303
|
+
if !hasPod && !hasNS {
|
|
304
|
+
g.properties(id, cr.NetworkTopologyProperty{Name: path, Value: "All peers declared; connectivity is unverified"})
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
for _, pod := range g.ordered {
|
|
308
|
+
if g.budgetReached() || g.ctx.Err() != nil {
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
if pod.source != "kubernetes.pods" {
|
|
312
|
+
continue
|
|
313
|
+
}
|
|
314
|
+
if hasNS {
|
|
315
|
+
namespace, ok := g.objects[topologyKey("Namespace", "", pod.ref.Namespace)]
|
|
316
|
+
if !ok || !g.completeSource("kubernetes.namespaces", "") || !topologySelectorMatches(object(ns), object(at(namespace.body, "metadata", "labels"))) {
|
|
317
|
+
continue
|
|
318
|
+
}
|
|
319
|
+
} else if pod.ref.Namespace != o.ref.Namespace {
|
|
320
|
+
continue
|
|
321
|
+
}
|
|
322
|
+
if hasPod && !topologySelectorMatches(object(ps), object(at(pod.body, "metadata", "labels"))) {
|
|
323
|
+
continue
|
|
324
|
+
}
|
|
325
|
+
edge := g.link(id, g.objectNode(pod, "pod", "kubernetes", ""), "policy-peer", "", "")
|
|
326
|
+
edge.FieldPath = path
|
|
327
|
+
edge.Note = "Peer selector match (namespace AND pod within this entry; entries OR); no live connectivity inferred"
|
|
328
|
+
}
|
|
329
|
+
}
|
|
@@ -10,6 +10,7 @@ import (
|
|
|
10
10
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
11
11
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
12
12
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
13
|
+
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
13
14
|
"k8s.io/apimachinery/pkg/types"
|
|
14
15
|
"k8s.io/apimachinery/pkg/util/sets"
|
|
15
16
|
"k8s.io/client-go/dynamic"
|
|
@@ -17,6 +18,27 @@ import (
|
|
|
17
18
|
"k8s.io/utils/ptr"
|
|
18
19
|
)
|
|
19
20
|
|
|
21
|
+
// BindResourceEditIdentity keeps legacy name-based edits and validates observed edits
|
|
22
|
+
// before attaching the live object's optimistic concurrency preconditions.
|
|
23
|
+
func BindResourceEditIdentity(current, desired *unstructured.Unstructured, resource schema.GroupResource, requireObserved bool) error {
|
|
24
|
+
if requireObserved {
|
|
25
|
+
if desired.GetUID() == "" || desired.GetResourceVersion() == "" {
|
|
26
|
+
return apierrors.NewBadRequest("observed YAML must include metadata.uid and metadata.resourceVersion")
|
|
27
|
+
}
|
|
28
|
+
if desired.GetUID() != current.GetUID() || desired.GetResourceVersion() != current.GetResourceVersion() {
|
|
29
|
+
return apierrors.NewConflict(resource, current.GetName(), errors.New("resource changed since the YAML was read"))
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
desired.SetUID(current.GetUID())
|
|
33
|
+
desired.SetResourceVersion(current.GetResourceVersion())
|
|
34
|
+
return nil
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// IsResourceEditOwnershipConflict separates a dry-run ownership report from stale identity.
|
|
38
|
+
func IsResourceEditOwnershipConflict(err error) bool {
|
|
39
|
+
return apierrors.HasStatusCause(err, metav1.CauseTypeFieldManagerConflict)
|
|
40
|
+
}
|
|
41
|
+
|
|
20
42
|
// ApplyResourceEdit preserves SSA conflicts while migrating this editor's own Create/Update fields.
|
|
21
43
|
// Dry-run migration validates a complete manifest with Update; it never changes live ownership.
|
|
22
44
|
func ApplyResourceEdit(ctx context.Context, resource dynamic.ResourceInterface, current, desired *unstructured.Unstructured, manager string, dryRun bool) (*unstructured.Unstructured, error) {
|
|
@@ -30,6 +30,38 @@ func editConflict(manager string) error {
|
|
|
30
30
|
return apierrors.NewApplyConflict([]metav1.StatusCause{{Type: metav1.CauseTypeFieldManagerConflict, Field: ".data.phase", Message: fmt.Sprintf("conflict with %q using v1", manager)}}, "field conflict")
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
func TestBindResourceEditIdentity(t *testing.T) {
|
|
34
|
+
resource := schema.GroupResource{Resource: "configmaps"}
|
|
35
|
+
current := editFixture()
|
|
36
|
+
for _, tc := range []struct {
|
|
37
|
+
name, uid, version string
|
|
38
|
+
strict bool
|
|
39
|
+
wantError func(error) bool
|
|
40
|
+
}{
|
|
41
|
+
{name: "legacy name only"},
|
|
42
|
+
{name: "observed current object", uid: "original", version: "1", strict: true},
|
|
43
|
+
{name: "missing observed identity", uid: "original", strict: true, wantError: apierrors.IsBadRequest},
|
|
44
|
+
{name: "same name replacement", uid: "deleted", version: "1", strict: true, wantError: apierrors.IsConflict},
|
|
45
|
+
{name: "same UID newer version", uid: "original", version: "0", strict: true, wantError: apierrors.IsConflict},
|
|
46
|
+
} {
|
|
47
|
+
t.Run(tc.name, func(t *testing.T) {
|
|
48
|
+
desired := current.DeepCopy()
|
|
49
|
+
desired.SetUID(types.UID(tc.uid))
|
|
50
|
+
desired.SetResourceVersion(tc.version)
|
|
51
|
+
err := BindResourceEditIdentity(current, desired, resource, tc.strict)
|
|
52
|
+
if tc.wantError != nil {
|
|
53
|
+
if !tc.wantError(err) || string(desired.GetUID()) != tc.uid || desired.GetResourceVersion() != tc.version {
|
|
54
|
+
t.Fatalf("stale request unexpectedly rebound: uid=%q version=%q err=%v", desired.GetUID(), desired.GetResourceVersion(), err)
|
|
55
|
+
}
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
if err != nil || desired.GetUID() != current.GetUID() || desired.GetResourceVersion() != current.GetResourceVersion() {
|
|
59
|
+
t.Fatalf("current identity not preserved: uid=%q version=%q err=%v", desired.GetUID(), desired.GetResourceVersion(), err)
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
33
65
|
func TestApplyResourceEditMigratesOnlyItsOwnCreationFields(t *testing.T) {
|
|
34
66
|
for _, test := range []struct {
|
|
35
67
|
name string
|