@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
|
@@ -5,9 +5,9 @@ package openapi
|
|
|
5
5
|
import _ "embed"
|
|
6
6
|
|
|
7
7
|
const (
|
|
8
|
-
Version = "0.1.
|
|
9
|
-
YAMLSHA256 = "
|
|
10
|
-
JSONSHA256 = "
|
|
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
|
@@ -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
|
|
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,
|
|
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.
|
|
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.
|
|
23
|
-
t.Fatal("legacy
|
|
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
|
+
}
|
package/resource/models.go
CHANGED
|
@@ -54,10 +54,17 @@ type ResourceGraphNode struct {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
type ResourceGraphEdge struct {
|
|
57
|
-
ID
|
|
58
|
-
SourceID
|
|
59
|
-
TargetID
|
|
60
|
-
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
|
|
108
|
-
Name string
|
|
109
|
-
Namespace string
|
|
110
|
-
Phase string
|
|
111
|
-
NodeName string
|
|
112
|
-
PodIP string
|
|
113
|
-
CreatedAt string
|
|
114
|
-
CPU string
|
|
115
|
-
Memory string
|
|
116
|
-
Requests ResourceQuantityView
|
|
117
|
-
Limits ResourceQuantityView
|
|
118
|
-
Labels map[string]string
|
|
119
|
-
PersistentVolumeClaims []string
|
|
120
|
-
ReadyContainers string
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
344
|
-
Kind
|
|
345
|
-
Name
|
|
346
|
-
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
|
|
468
|
-
Namespace
|
|
469
|
-
Revision
|
|
470
|
-
Images
|
|
471
|
-
Replicas
|
|
472
|
-
ReadyReplicas
|
|
473
|
-
CreatedAt
|
|
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
|
|
560
|
-
Namespace
|
|
561
|
-
Completions
|
|
562
|
-
Succeeded
|
|
563
|
-
Failed
|
|
564
|
-
Active
|
|
565
|
-
CompletionMode
|
|
566
|
-
AgeSeconds
|
|
567
|
-
AllowedActions
|
|
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
|
|
572
|
-
Namespace
|
|
573
|
-
Completions
|
|
574
|
-
Parallelism
|
|
575
|
-
Succeeded
|
|
576
|
-
Failed
|
|
577
|
-
Active
|
|
578
|
-
CompletionMode
|
|
579
|
-
CreatedAt
|
|
580
|
-
StartTime
|
|
581
|
-
CompletionTime
|
|
582
|
-
Labels
|
|
583
|
-
Annotations
|
|
584
|
-
Pods
|
|
585
|
-
RelatedResources
|
|
586
|
-
AllowedActions
|
|
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.
|
|
128
|
-
"kubernetes.
|
|
129
|
-
"kubernetes.
|
|
130
|
-
"
|
|
131
|
-
"kubernetes.
|
|
132
|
-
"
|
|
133
|
-
"
|
|
134
|
-
"
|
|
135
|
-
"
|
|
136
|
-
"gateway.
|
|
137
|
-
"gateway.
|
|
138
|
-
"gateway.
|
|
139
|
-
"gateway.
|
|
140
|
-
"
|
|
141
|
-
"
|
|
142
|
-
"
|
|
143
|
-
"
|
|
144
|
-
"traefik.
|
|
145
|
-
"traefik.
|
|
146
|
-
"
|
|
147
|
-
"
|
|
148
|
-
"
|
|
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,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
|
+
}
|