@opensoha/contracts 0.1.24 → 0.1.26
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 +24 -0
- package/README.md +1 -1
- package/agent/environment/export.go +1 -0
- package/agent/environment/protocol_test.go +1 -1
- package/agent/guest/protocol_test.go +2 -2
- package/auth/catalog_generated.go +2 -2
- package/auth/gateway_permissions_test.go +48 -0
- package/auth/permission-catalog.json +300 -1
- 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/delivery/document.schema.json +127 -9
- package/examples/relations_compat_test.go +18 -0
- package/fixtures/openapi/DeliveryDocument/invalid/migration-missing-guide-build-template.json +30 -0
- package/fixtures/openapi/DeliveryDocument/invalid/migration-missing-guide-deployment-template.json +30 -0
- package/fixtures/openapi/DeliveryDocument/invalid/migration-missing-guide-workflow-template.json +27 -0
- package/fixtures/openapi/DeliveryDocument/valid/migration-build-template.json +31 -0
- package/fixtures/openapi/DeliveryDocument/valid/migration-deployment-template.json +31 -0
- package/fixtures/openapi/DeliveryDocument/valid/migration-workflow-template.json +28 -0
- package/fixtures/openapi/DeliveryPlanInput/invalid/manual-plan.json +7 -0
- package/fixtures/openapi/DeliveryPlanInput/valid/manual-plan.json +6 -0
- package/fixtures/openapi/DeliveryTargetInput/invalid/candidate-on-runtime-deploy.json +9 -0
- package/fixtures/openapi/DeliveryTargetInput/invalid/config-update-missing-service.json +7 -0
- package/fixtures/openapi/DeliveryTargetInput/valid/template-configuration-only.json +8 -0
- package/fixtures/openapi/DeliveryTargetInput/valid/template-configuration-without-runtime-environment.json +1 -0
- package/fixtures/openapi/KubernetesAgentActionCheck/invalid/unproven-state.json +6 -0
- package/fixtures/openapi/KubernetesAgentActionCheck/valid/configured-denied.json +9 -0
- package/fixtures/openapi/KubernetesAgentActionProfile/invalid/wildcard.json +1 -0
- package/fixtures/openapi/KubernetesAgentActionProfile/valid/workbench.json +1 -0
- package/fixtures/openapi/KubernetesClusterRegistrationRequest/invalid/action-profile.json +3 -0
- package/fixtures/openapi/KubernetesClusterRegistrationRequest/valid/action-profile.json +5 -0
- package/fixtures/openapi/KubernetesImportCandidateListEnvelope/invalid/source-with-spec.json +33 -0
- package/fixtures/openapi/KubernetesImportCandidateListEnvelope/invalid/source-without-uid.json +29 -0
- package/fixtures/openapi/KubernetesImportCandidateListEnvelope/valid/stable-source.json +30 -0
- package/fixtures/openapi/KubernetesNodeListEnvelope/invalid/condition-status-boolean.json +17 -0
- package/fixtures/openapi/KubernetesNodeListEnvelope/invalid/taint-effect-missing.json +16 -0
- package/fixtures/openapi/KubernetesNodeListEnvelope/valid/legacy-fields-absent.json +11 -0
- package/fixtures/openapi/KubernetesNodeListEnvelope/valid/no-taints.json +13 -0
- package/fixtures/openapi/KubernetesNodeListEnvelope/valid/ready-cordoned-tainted.json +28 -0
- package/fixtures/openapi/KubernetesNodeListEnvelope/valid/unknown-fields-null.json +13 -0
- package/fixtures/openapi/KubernetesPod/invalid/pod-uid.json +9 -0
- package/fixtures/openapi/KubernetesPod/valid/pod-uid.json +9 -0
- package/fixtures/openapi/KubernetesPodDetail/invalid/pod-uid.json +6 -0
- package/fixtures/openapi/KubernetesPodDetail/valid/pod-uid.json +6 -0
- package/fixtures/openapi/KubernetesServiceImportRequest/invalid/empty-source-uid.json +48 -0
- package/fixtures/openapi/KubernetesServiceImportRequest/valid/pinned-workloads-and-cr.json +48 -0
- package/fixtures/openapi/KubernetesTopologyCoverage/invalid/empty-as-success.json +5 -0
- package/fixtures/openapi/KubernetesTopologyCoverage/valid/not-installed.json +6 -0
- package/fixtures/openapi/KubernetesTopologyGraphEdge/invalid/negative-weight.json +8 -0
- package/fixtures/openapi/KubernetesTopologyGraphEdge/valid/zero-weight.json +20 -0
- package/fixtures/openapi/KubernetesTopologyGraphNode/invalid/fake-flow-kind.json +42 -0
- package/fixtures/openapi/KubernetesTopologyGraphNode/valid/rule.json +42 -0
- package/fixtures/openapi/KubernetesTopologySnapshot/invalid/raw-source-field.json +33 -0
- package/fixtures/openapi/KubernetesTopologySnapshot/valid/projected-source.json +32 -0
- package/fixtures/openapi/KubernetesTopologySnapshotRequest/invalid/arbitrary-gvr.json +8 -0
- package/fixtures/openapi/KubernetesTopologySnapshotRequest/valid/bounded-sources.json +11 -0
- package/fixtures/openapi/KubernetesTopologySourceQuery/invalid/cluster-namespace.json +4 -0
- package/fixtures/openapi/KubernetesTopologySourceQuery/valid/all-namespaces.json +4 -0
- package/fixtures/openapi/ManifestExecutionTaskPayload/invalid/initialization.json +7 -0
- package/fixtures/openapi/ManifestExecutionTaskPayload/valid/initialization.json +7 -0
- package/fixtures/openapi/ManifestRootResource/invalid/root.json +1 -0
- package/fixtures/openapi/ManifestRootResource/valid/root.json +1 -0
- package/fixtures/openapi/ResourceMonitoringPanel/invalid/unknown-quality.json +7 -0
- package/fixtures/openapi/ResourceMonitoringPanel/valid/not-collected.json +7 -0
- package/fixtures/openapi/ResourceMonitoringPoint/invalid/string-zero.json +4 -0
- package/fixtures/openapi/ResourceMonitoringPoint/valid/missing.json +4 -0
- package/fixtures/openapi/ResourceMonitoringPoint/valid/zero.json +4 -0
- package/fixtures/openapi/ResourceMonitoringView/invalid/raw-query.json +12 -0
- package/fixtures/openapi/ResourceMonitoringView/valid/unconfigured.json +12 -0
- package/fixtures/openapi/ResourceRelationConfig/invalid/executable-rule.json +17 -0
- package/fixtures/openapi/ResourceRelationConfig/valid/unknown-kind.json +13 -0
- package/fixtures/openapi/ServiceDeploymentTemplateInput/invalid/migration-missing-guide-valid-yaml.json +38 -0
- package/fixtures/openapi/ServiceDeploymentTemplateInput/valid/migration-valid-yaml.json +39 -0
- package/fixtures/openapi/TemplateMigrationAssessment/invalid/initialization.json +9 -0
- package/fixtures/openapi/TemplateMigrationAssessment/valid/initialization.json +9 -0
- package/fixtures/openapi/TemplateMigrationPolicy/invalid/blank-guide.json +5 -0
- package/fixtures/openapi/TemplateMigrationPolicy/invalid/missing-guide.json +4 -0
- package/fixtures/openapi/TemplateMigrationPolicy/invalid/unknown-escape-hatch.json +6 -0
- package/fixtures/openapi/TemplateMigrationPolicy/valid/legacy-safe.json +4 -0
- package/fixtures/openapi/TemplateMigrationPolicy/valid/ordered-data-change.json +5 -0
- package/fixtures/openapi/TemplateUpdateBatchInput/invalid/duplicate-candidates.json +11 -0
- package/fixtures/openapi/TemplateUpdateBatchInput/invalid/empty-identities.json +10 -0
- package/fixtures/openapi/TemplateUpdateBatchInput/valid/staged-adoption.json +10 -0
- package/fixtures/openapi/TemplateUpdatePolicyInput/invalid/unknown-mode.json +8 -0
- package/fixtures/openapi/TemplateUpdatePolicyInput/valid/explicit-automatic.json +8 -0
- package/fixtures/openapi/TemplateUpdatePreviewInput/invalid/empty-identities.json +10 -0
- package/fixtures/openapi/TemplateUpdatePreviewInput/invalid/empty-targets.json +6 -0
- package/fixtures/openapi/TemplateUpdatePreviewInput/valid/cross-version-manual.json +14 -0
- package/gen/go/sohaapi/client.go +1 -1
- package/gen/go/sohaapi/client_test.go +2 -2
- package/gen/go/sohaapi/import_source_compat_test.go +12 -0
- package/gen/go/sohaapi/kubernetes_monitoring_test.go +67 -0
- package/gen/go/sohaapi/types.go +2210 -387
- package/gen/ts/sohaapi/index.ts +1862 -5
- package/go.mod +2 -2
- package/helmrelease/delivery.go +4 -1
- package/helmrelease/initialization.go +10 -0
- package/helmrelease/runtime/delivery.go +40 -10
- package/helmrelease/runtime/delivery_test.go +4 -0
- package/helmrelease/runtime/initialization.go +186 -0
- package/helmrelease/runtime/initialization_test.go +203 -0
- package/openapi/soha-api.json +5514 -1179
- package/openapi/soha-api.yaml +1080 -11
- package/openapi/spec_generated.go +3 -3
- package/openapi/spec_test.go +76 -0
- package/package.json +1 -1
- package/resource/agent_access.go +12 -0
- package/resource/agent_profiles.go +62 -0
- package/resource/agent_profiles_test.go +36 -0
- package/resource/kubernetes_kinds.go +73 -0
- package/resource/kubernetes_kinds_test.go +38 -0
- package/resource/models.go +25 -14
- package/resource/network_topology.go +166 -0
- package/resource/network_topology_projection.go +208 -0
- package/resource/network_topology_projection_test.go +53 -0
- package/resource/network_topology_test.go +51 -0
- package/resource/network_topology_validation.go +29 -0
- package/resource/node_drain.go +34 -0
- package/resource/relations.go +225 -0
- package/resource/relations_test.go +47 -0
- package/resource/resource_monitoring.go +99 -0
- package/resource/root_cr.go +38 -0
- package/resource/runtime/argocd_integration_test.go +1 -1
- package/resource/runtime/initialization.go +66 -0
- package/resource/runtime/initialization_test.go +80 -0
- package/resource/runtime/network_topology.go +232 -0
- package/resource/runtime/network_topology_test.go +62 -0
- package/resource/runtime/node_drain.go +124 -0
- package/resource/runtime/node_drain_test.go +95 -0
- package/resource/runtime/relations.go +161 -0
- package/resource/runtime/relations_test.go +100 -0
- package/resource/runtime/resource_edit.go +128 -0
- package/resource/runtime/resource_edit_test.go +183 -0
- package/resource/runtime/resource_monitoring.go +243 -0
- package/resource/runtime/resource_monitoring_test.go +88 -0
- package/resource/runtime/root_cr.go +79 -0
- package/resource/runtime/root_cr_test.go +118 -0
package/go.mod
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module github.com/opensoha/soha-contracts
|
|
2
2
|
|
|
3
|
-
go 1.27.
|
|
3
|
+
go 1.27.3
|
|
4
4
|
|
|
5
5
|
require (
|
|
6
6
|
github.com/google/uuid v1.6.0
|
|
@@ -12,6 +12,7 @@ require (
|
|
|
12
12
|
k8s.io/apimachinery v0.37.1
|
|
13
13
|
k8s.io/cli-runtime v0.37.1
|
|
14
14
|
k8s.io/client-go v0.37.1
|
|
15
|
+
k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3
|
|
15
16
|
sigs.k8s.io/yaml v1.6.0
|
|
16
17
|
)
|
|
17
18
|
|
|
@@ -121,7 +122,6 @@ require (
|
|
|
121
122
|
k8s.io/klog/v2 v2.140.0 // indirect
|
|
122
123
|
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad // indirect
|
|
123
124
|
k8s.io/kubectl v0.37.1 // indirect
|
|
124
|
-
k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3 // indirect
|
|
125
125
|
oras.land/oras-go/v2 v2.6.2 // indirect
|
|
126
126
|
sigs.k8s.io/controller-runtime v0.25.2 // indirect
|
|
127
127
|
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
package/helmrelease/delivery.go
CHANGED
|
@@ -241,6 +241,9 @@ func (r chartBudgetReader) Read(p []byte) (int, error) {
|
|
|
241
241
|
// Prepare accepts an unfinished render; Execute additionally verifies its digests.
|
|
242
242
|
func ValidateTaskIdentity(input sohaapi.HelmExecutionTaskPayload) error {
|
|
243
243
|
s := input.Snapshot
|
|
244
|
+
if IsInitialization(s) && (s.Operation != sohaapi.HelmDeliverySnapshotOperationInstall || s.ExpectedRevision != 0 || s.RollbackRevision != 0 || s.Migration.Blocked || !s.Migration.RequiresApproval || s.Migration.FromVersion != 0 || s.TemplateInputs == nil || s.TemplateInputs.TemplateVersion != s.Migration.ToVersion) {
|
|
245
|
+
return errors.New("initialization requires an approved create-only first deployment snapshot")
|
|
246
|
+
}
|
|
244
247
|
for _, value := range []string{s.DeliveryPlanID, s.TargetID, s.ApplicationID, s.ApplicationEnvironmentID, s.ServiceID, s.ClusterID} {
|
|
245
248
|
if strings.TrimSpace(value) == "" || len(value) > 512 {
|
|
246
249
|
return errors.New("helm task identity is incomplete")
|
|
@@ -249,7 +252,7 @@ func ValidateTaskIdentity(input sohaapi.HelmExecutionTaskPayload) error {
|
|
|
249
252
|
if !input.Action.Valid() || !helmName.MatchString(s.Namespace) || len(s.Namespace) > 63 || !helmName.MatchString(s.ReleaseName) || len(s.ReleaseName) > 53 || s.ExpectedRevision < 0 || s.RollbackRevision < 0 || s.TimeoutSeconds < 0 || s.TimeoutSeconds > 3600 {
|
|
250
253
|
return errors.New("helm task action, namespace, revision or timeout is invalid")
|
|
251
254
|
}
|
|
252
|
-
if len(s.Resources) > 300 {
|
|
255
|
+
if len(s.Resources) > 300 || len(input.InitializationResources) > 300 {
|
|
253
256
|
return errors.New("helm task exceeds 300 resources")
|
|
254
257
|
}
|
|
255
258
|
if input.Prepared != nil {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
package helmrelease
|
|
2
|
+
|
|
3
|
+
import "github.com/opensoha/soha-contracts/gen/go/sohaapi"
|
|
4
|
+
|
|
5
|
+
const InitializationCapability = "helm.initialize.v1"
|
|
6
|
+
const InitializationProviderPrefix = "helm_initialize_v1."
|
|
7
|
+
|
|
8
|
+
func IsInitialization(snapshot sohaapi.HelmDeliverySnapshot) bool {
|
|
9
|
+
return snapshot.Migration != nil && snapshot.Migration.Initialization
|
|
10
|
+
}
|
|
@@ -28,6 +28,7 @@ import (
|
|
|
28
28
|
"k8s.io/apimachinery/pkg/api/meta"
|
|
29
29
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
30
30
|
"k8s.io/apimachinery/pkg/runtime"
|
|
31
|
+
"k8s.io/apimachinery/pkg/types"
|
|
31
32
|
"k8s.io/client-go/dynamic"
|
|
32
33
|
)
|
|
33
34
|
|
|
@@ -39,6 +40,13 @@ func Prepare(ctx context.Context, cfg *action.Configuration, input sohaapi.HelmE
|
|
|
39
40
|
if err != nil {
|
|
40
41
|
return input, err
|
|
41
42
|
}
|
|
43
|
+
if helmrelease.IsInitialization(input.Snapshot) && helmDeliveryAlreadyApplied(current, input.Snapshot) {
|
|
44
|
+
if err := validateHelmPrepared(cfg, input); err != nil {
|
|
45
|
+
return input, err
|
|
46
|
+
}
|
|
47
|
+
_, err := observeInitializedHelm(ctx, cfg, input, current)
|
|
48
|
+
return input, err
|
|
49
|
+
}
|
|
42
50
|
input.Snapshot.ExpectedRevision = 0
|
|
43
51
|
input.Snapshot.Operation = sohaapi.HelmDeliverySnapshotOperationInstall
|
|
44
52
|
if current != nil {
|
|
@@ -105,17 +113,22 @@ func Execute(ctx context.Context, cfg *action.Configuration, input sohaapi.HelmE
|
|
|
105
113
|
return result, err
|
|
106
114
|
}
|
|
107
115
|
if input.Action == sohaapi.Observe {
|
|
108
|
-
return
|
|
116
|
+
return observeInitializedHelm(ctx, cfg, input, current)
|
|
109
117
|
}
|
|
110
118
|
if err := validateHelmPrepared(cfg, input); err != nil {
|
|
111
119
|
return result, err
|
|
112
120
|
}
|
|
113
121
|
if helmDeliveryAlreadyApplied(current, input.Snapshot) {
|
|
114
|
-
return
|
|
122
|
+
return observeInitializedHelm(ctx, cfg, input, current)
|
|
115
123
|
}
|
|
116
124
|
if err := helmDeliveryRevisionMatches(current, input.Snapshot); err != nil {
|
|
117
125
|
return result, err
|
|
118
126
|
}
|
|
127
|
+
if helmrelease.IsInitialization(input.Snapshot) {
|
|
128
|
+
if err := requireInitialHelmResourcesAbsent(cfg, input); err != nil {
|
|
129
|
+
return result, err
|
|
130
|
+
}
|
|
131
|
+
}
|
|
119
132
|
if input.Action == sohaapi.Preflight {
|
|
120
133
|
result.Ready, result.Status, result.Revision = true, "preflighted", input.Snapshot.ExpectedRevision
|
|
121
134
|
return result, nil
|
|
@@ -136,10 +149,21 @@ func Execute(ctx context.Context, cfg *action.Configuration, input sohaapi.HelmE
|
|
|
136
149
|
// Helm's RunWithContext may return while its mutation goroutine is still
|
|
137
150
|
// running. Once mutation begins, native Timeout bounds it; cancellation is
|
|
138
151
|
// acknowledged only after Run has returned and the release is inspected.
|
|
152
|
+
var initialization *initializingHelmClient
|
|
153
|
+
if helmrelease.IsInitialization(input.Snapshot) {
|
|
154
|
+
initialization = &initializingHelmClient{Interface: cfg.KubeClient, created: make(map[string]types.UID)}
|
|
155
|
+
cfg = helmConfigurationWithClient(cfg, initialization)
|
|
156
|
+
}
|
|
139
157
|
_, runErr := runHelmDeliveryAction(context.Background(), cfg, input, ch, false)
|
|
140
158
|
current, err = currentHelmDelivery(cfg, input.Snapshot.ReleaseName, 0)
|
|
141
159
|
if err == nil && current != nil {
|
|
142
160
|
result, err = observeHelmDelivery(ctx, cfg, input.Snapshot, current)
|
|
161
|
+
if err == nil && initialization != nil {
|
|
162
|
+
err = initialization.validateCreated(result)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if err != nil || runErr != nil {
|
|
166
|
+
result.Ready = false
|
|
143
167
|
}
|
|
144
168
|
if runErr != nil {
|
|
145
169
|
return result, fmt.Errorf("%w: Helm execution failed; inspect the recorded revision and resource state", ErrUnavailable)
|
|
@@ -150,17 +174,23 @@ func Execute(ctx context.Context, cfg *action.Configuration, input sohaapi.HelmE
|
|
|
150
174
|
return result, err
|
|
151
175
|
}
|
|
152
176
|
|
|
177
|
+
// Build a per-invocation configuration without copying Helm's internal mutex or
|
|
178
|
+
// replacing the caller's client. Dry-run template functions also remain local.
|
|
179
|
+
func helmConfigurationWithClient(original *action.Configuration, client kube.Interface) *action.Configuration {
|
|
180
|
+
cfg := &action.Configuration{
|
|
181
|
+
RESTClientGetter: original.RESTClientGetter, Releases: original.Releases,
|
|
182
|
+
KubeClient: client, RegistryClient: original.RegistryClient,
|
|
183
|
+
Capabilities: original.Capabilities, HookOutputFunc: original.HookOutputFunc,
|
|
184
|
+
CustomTemplateFuncs: maps.Clone(original.CustomTemplateFuncs),
|
|
185
|
+
}
|
|
186
|
+
cfg.SetLogger(original.Logger().Handler())
|
|
187
|
+
return cfg
|
|
188
|
+
}
|
|
189
|
+
|
|
153
190
|
func runHelmDeliveryAction(ctx context.Context, cfg *action.Configuration, input sohaapi.HelmExecutionTaskPayload, ch chart.Charter, dryRun bool) (*helmreleasev1.Release, error) {
|
|
154
191
|
if dryRun {
|
|
155
192
|
// Discover real cluster capabilities, but keep template lookup disabled.
|
|
156
|
-
|
|
157
|
-
cfg = &action.Configuration{
|
|
158
|
-
RESTClientGetter: original.RESTClientGetter, Releases: original.Releases,
|
|
159
|
-
KubeClient: original.KubeClient, RegistryClient: original.RegistryClient,
|
|
160
|
-
Capabilities: original.Capabilities, HookOutputFunc: original.HookOutputFunc,
|
|
161
|
-
CustomTemplateFuncs: maps.Clone(original.CustomTemplateFuncs),
|
|
162
|
-
}
|
|
163
|
-
cfg.SetLogger(original.Logger().Handler())
|
|
193
|
+
cfg = helmConfigurationWithClient(cfg, cfg.KubeClient)
|
|
164
194
|
if cfg.CustomTemplateFuncs == nil {
|
|
165
195
|
cfg.CustomTemplateFuncs = make(map[string]any)
|
|
166
196
|
}
|
|
@@ -39,6 +39,7 @@ type observedHelmKubeClient struct {
|
|
|
39
39
|
missing bool
|
|
40
40
|
namespace string
|
|
41
41
|
reads int
|
|
42
|
+
uid string
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
func (c *observedHelmKubeClient) Build(io.Reader, bool) (kube.ResourceList, error) {
|
|
@@ -49,6 +50,9 @@ func (c *observedHelmKubeClient) Build(io.Reader, bool) (kube.ResourceList, erro
|
|
|
49
50
|
return &http.Response{StatusCode: http.StatusNotFound, Header: http.Header{"Content-Type": {"application/json"}}, Body: io.NopCloser(strings.NewReader(`{"apiVersion":"v1","kind":"Status","status":"Failure","reason":"NotFound","code":404}`))}, nil
|
|
50
51
|
}
|
|
51
52
|
body := object
|
|
53
|
+
if c.uid != "" {
|
|
54
|
+
body = strings.Replace(body, `"name":"app"`, `"name":"app","uid":"`+c.uid+`"`, 1)
|
|
55
|
+
}
|
|
52
56
|
if c.deleting {
|
|
53
57
|
body = strings.Replace(body, `"name":"app"`, `"name":"app","deletionTimestamp":"2026-09-12T00:00:00Z"`, 1)
|
|
54
58
|
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
package runtime
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"strings"
|
|
7
|
+
"sync"
|
|
8
|
+
|
|
9
|
+
"github.com/opensoha/soha-contracts/gen/go/sohaapi"
|
|
10
|
+
"github.com/opensoha/soha-contracts/helmrelease"
|
|
11
|
+
"helm.sh/helm/v4/pkg/action"
|
|
12
|
+
"helm.sh/helm/v4/pkg/kube"
|
|
13
|
+
helmreleasev1 "helm.sh/helm/v4/pkg/release/v1"
|
|
14
|
+
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
15
|
+
"k8s.io/apimachinery/pkg/api/meta"
|
|
16
|
+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
17
|
+
"k8s.io/apimachinery/pkg/types"
|
|
18
|
+
kuberesource "k8s.io/cli-runtime/pkg/resource"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
func requireInitialHelmResourcesAbsent(cfg *action.Configuration, input sohaapi.HelmExecutionTaskPayload) error {
|
|
22
|
+
if input.Snapshot.Operation != sohaapi.HelmDeliverySnapshotOperationInstall || input.Snapshot.ExpectedRevision != 0 || input.Snapshot.RollbackRevision != 0 || input.Prepared == nil {
|
|
23
|
+
return fmt.Errorf("%w: initialization requires a new install", ErrConflict)
|
|
24
|
+
}
|
|
25
|
+
for _, document := range append([]string{input.Prepared.Manifest}, input.Prepared.Hooks...) {
|
|
26
|
+
resources, err := cfg.KubeClient.Build(strings.NewReader(document), false)
|
|
27
|
+
if err != nil {
|
|
28
|
+
return err
|
|
29
|
+
}
|
|
30
|
+
if err := requireHelmResourcesAbsent(resources); err != nil {
|
|
31
|
+
return err
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return nil
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func requireHelmResourcesAbsent(resources kube.ResourceList) error {
|
|
38
|
+
for _, resource := range resources {
|
|
39
|
+
err := resource.Get()
|
|
40
|
+
if !apierrors.IsNotFound(err) {
|
|
41
|
+
return fmt.Errorf("%w: initialization resource exists or its absence cannot be verified", ErrConflict)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return nil
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Helm's ordinary install may adopt correctly labelled existing objects. This
|
|
48
|
+
// client refuses that update path and forces native POSTs, including for hooks.
|
|
49
|
+
// Hook cleanup can delete only UIDs created by this exact native invocation.
|
|
50
|
+
type initializingHelmClient struct {
|
|
51
|
+
kube.Interface
|
|
52
|
+
mu sync.Mutex
|
|
53
|
+
created map[string]types.UID
|
|
54
|
+
hooks map[string]bool
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func (c *initializingHelmClient) Create(resources kube.ResourceList, options ...kube.ClientCreateOption) (*kube.Result, error) {
|
|
58
|
+
if err := requireHelmResourcesAbsent(resources); err != nil {
|
|
59
|
+
return nil, err
|
|
60
|
+
}
|
|
61
|
+
result, err := c.Interface.Create(resources, append(options, kube.ClientCreateOptionServerSideApply(false, false))...)
|
|
62
|
+
if result != nil {
|
|
63
|
+
c.mu.Lock()
|
|
64
|
+
defer c.mu.Unlock()
|
|
65
|
+
for _, resource := range result.Created {
|
|
66
|
+
object, accessErr := meta.Accessor(resource.Object)
|
|
67
|
+
if accessErr == nil && object.GetUID() != "" {
|
|
68
|
+
key := initialHelmResourceKey(resource)
|
|
69
|
+
c.created[key] = object.GetUID()
|
|
70
|
+
if object.GetAnnotations()["helm.sh/hook"] != "" {
|
|
71
|
+
if c.hooks == nil {
|
|
72
|
+
c.hooks = make(map[string]bool)
|
|
73
|
+
}
|
|
74
|
+
c.hooks[key] = true
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return result, err
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func (c *initializingHelmClient) Update(kube.ResourceList, kube.ResourceList, ...kube.ClientUpdateOption) (*kube.Result, error) {
|
|
83
|
+
return nil, fmt.Errorf("%w: initialization cannot update or adopt existing resources", ErrConflict)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
func (c *initializingHelmClient) Delete(resources kube.ResourceList, policy metav1.DeletionPropagation) (*kube.Result, []error) {
|
|
87
|
+
result := &kube.Result{}
|
|
88
|
+
for _, resource := range resources {
|
|
89
|
+
if err := c.deleteCreatedHook(resource, policy); err != nil {
|
|
90
|
+
return result, []error{err}
|
|
91
|
+
}
|
|
92
|
+
result.Deleted = append(result.Deleted, resource)
|
|
93
|
+
}
|
|
94
|
+
return result, nil
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
func (c *initializingHelmClient) deleteCreatedHook(resource *kuberesource.Info, policy metav1.DeletionPropagation) error {
|
|
98
|
+
if err := resource.Get(); err != nil {
|
|
99
|
+
if apierrors.IsNotFound(err) {
|
|
100
|
+
return nil
|
|
101
|
+
}
|
|
102
|
+
return fmt.Errorf("%w: hook identity cannot be verified", ErrConflict)
|
|
103
|
+
}
|
|
104
|
+
object, err := meta.Accessor(resource.Object)
|
|
105
|
+
if err != nil {
|
|
106
|
+
return err
|
|
107
|
+
}
|
|
108
|
+
c.mu.Lock()
|
|
109
|
+
uid := c.created[initialHelmResourceKey(resource)]
|
|
110
|
+
hook := c.hooks[initialHelmResourceKey(resource)]
|
|
111
|
+
c.mu.Unlock()
|
|
112
|
+
if !hook || uid == "" || object.GetUID() != uid || object.GetResourceVersion() == "" {
|
|
113
|
+
return fmt.Errorf("%w: initialization cannot delete a pre-existing or replaced hook", ErrConflict)
|
|
114
|
+
}
|
|
115
|
+
version := object.GetResourceVersion()
|
|
116
|
+
_, err = kuberesource.NewHelper(resource.Client, resource.Mapping).DeleteWithOptions(resource.Namespace, resource.Name, &metav1.DeleteOptions{PropagationPolicy: &policy, Preconditions: &metav1.Preconditions{UID: &uid, ResourceVersion: &version}})
|
|
117
|
+
return err
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
func initialInventoryKey(item sohaapi.ManifestResourceInventory) string {
|
|
121
|
+
group := ""
|
|
122
|
+
if parts := strings.SplitN(item.APIVersion, "/", 2); len(parts) == 2 {
|
|
123
|
+
group = parts[0]
|
|
124
|
+
}
|
|
125
|
+
return group + "/" + item.Kind + "/" + item.Namespace + "/" + item.Name
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ValidateInitializationResources binds a fresh observation to the durable
|
|
129
|
+
// original apply receipt, never to UIDs first read during recovery.
|
|
130
|
+
func ValidateInitializationResources(observed, original []sohaapi.ManifestResourceInventory) error {
|
|
131
|
+
if len(original) == 0 || len(observed) != len(original) {
|
|
132
|
+
return fmt.Errorf("%w: initialization creation receipt is missing or incomplete", ErrConflict)
|
|
133
|
+
}
|
|
134
|
+
uids := make(map[string]string, len(original))
|
|
135
|
+
for _, item := range original {
|
|
136
|
+
key := initialInventoryKey(item)
|
|
137
|
+
if item.UID == "" || uids[key] != "" {
|
|
138
|
+
return fmt.Errorf("%w: invalid initialization receipt", ErrConflict)
|
|
139
|
+
}
|
|
140
|
+
uids[key] = item.UID
|
|
141
|
+
}
|
|
142
|
+
for _, item := range observed {
|
|
143
|
+
key := initialInventoryKey(item)
|
|
144
|
+
if item.UID == "" || uids[key] != item.UID {
|
|
145
|
+
return fmt.Errorf("%w: initialized resource was replaced", ErrConflict)
|
|
146
|
+
}
|
|
147
|
+
delete(uids, key)
|
|
148
|
+
}
|
|
149
|
+
return nil
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
func (c *initializingHelmClient) validateCreated(result sohaapi.HelmExecutionTaskResult) error {
|
|
153
|
+
c.mu.Lock()
|
|
154
|
+
defer c.mu.Unlock()
|
|
155
|
+
if len(result.Resources) == 0 {
|
|
156
|
+
return fmt.Errorf("%w: initialization has no observable resource receipt", ErrConflict)
|
|
157
|
+
}
|
|
158
|
+
for _, item := range result.Resources {
|
|
159
|
+
if item.UID == "" || string(c.created[initialInventoryKey(item)]) != item.UID {
|
|
160
|
+
return fmt.Errorf("%w: initialized resource creation UID cannot be verified", ErrConflict)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return nil
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
func observeInitializedHelm(ctx context.Context, cfg *action.Configuration, input sohaapi.HelmExecutionTaskPayload, current *helmreleasev1.Release) (sohaapi.HelmExecutionTaskResult, error) {
|
|
167
|
+
result, err := observeHelmDelivery(ctx, cfg, input.Snapshot, current)
|
|
168
|
+
if err == nil && helmrelease.IsInitialization(input.Snapshot) {
|
|
169
|
+
err = ValidateInitializationResources(result.Resources, input.InitializationResources)
|
|
170
|
+
}
|
|
171
|
+
if err != nil {
|
|
172
|
+
result.Ready = false
|
|
173
|
+
}
|
|
174
|
+
return result, err
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
func initialHelmResourceKey(resource *kuberesource.Info) string {
|
|
178
|
+
return resource.Mapping.GroupVersionKind.Group + "/" + resource.Mapping.GroupVersionKind.Kind + "/" + resource.Namespace + "/" + resource.Name
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
func (c *initializingHelmClient) GetWaiterWithOptions(strategy kube.WaitStrategy, options ...kube.WaitOption) (kube.Waiter, error) {
|
|
182
|
+
if client, ok := c.Interface.(kube.InterfaceWaitOptions); ok {
|
|
183
|
+
return client.GetWaiterWithOptions(strategy, options...)
|
|
184
|
+
}
|
|
185
|
+
return c.GetWaiter(strategy)
|
|
186
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
package runtime
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/base64"
|
|
6
|
+
"encoding/json"
|
|
7
|
+
"io"
|
|
8
|
+
"log/slog"
|
|
9
|
+
"net/http"
|
|
10
|
+
"reflect"
|
|
11
|
+
"slices"
|
|
12
|
+
"strings"
|
|
13
|
+
"testing"
|
|
14
|
+
|
|
15
|
+
"github.com/opensoha/soha-contracts/gen/go/sohaapi"
|
|
16
|
+
"github.com/opensoha/soha-contracts/helmrelease"
|
|
17
|
+
"helm.sh/helm/v4/pkg/action"
|
|
18
|
+
"helm.sh/helm/v4/pkg/chart/common"
|
|
19
|
+
"helm.sh/helm/v4/pkg/kube"
|
|
20
|
+
releasecommon "helm.sh/helm/v4/pkg/release/common"
|
|
21
|
+
releasev1 "helm.sh/helm/v4/pkg/release/v1"
|
|
22
|
+
"helm.sh/helm/v4/pkg/storage"
|
|
23
|
+
"helm.sh/helm/v4/pkg/storage/driver"
|
|
24
|
+
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
25
|
+
"k8s.io/apimachinery/pkg/api/meta"
|
|
26
|
+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
27
|
+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
28
|
+
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
29
|
+
"k8s.io/apimachinery/pkg/types"
|
|
30
|
+
kuberesource "k8s.io/cli-runtime/pkg/resource"
|
|
31
|
+
restfake "k8s.io/client-go/rest/fake"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
func TestHelmInitializationLeavesCallerConfigurationUntouched(t *testing.T) {
|
|
35
|
+
client := &observedHelmKubeClient{}
|
|
36
|
+
original := &action.Configuration{KubeClient: client, Releases: storage.Init(driver.NewMemory()), CustomTemplateFuncs: map[string]any{"caller": "preserved"}}
|
|
37
|
+
protected := &initializingHelmClient{Interface: client, created: make(map[string]types.UID)}
|
|
38
|
+
local := helmConfigurationWithClient(original, protected)
|
|
39
|
+
local.CustomTemplateFuncs["caller"] = "changed"
|
|
40
|
+
if original.KubeClient != client || original.CustomTemplateFuncs["caller"] != "preserved" {
|
|
41
|
+
t.Fatal("initialization changed the caller's Kubernetes client or template functions")
|
|
42
|
+
}
|
|
43
|
+
if local.KubeClient != protected || local.Releases != original.Releases {
|
|
44
|
+
t.Fatal("per-invocation configuration lost the protected client or release storage")
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func TestHelmInitializationObserveAndPrepareRecoveryRequireOriginalReceipt(t *testing.T) {
|
|
49
|
+
client := &observedHelmKubeClient{missing: true}
|
|
50
|
+
cfg := &action.Configuration{KubeClient: client, Releases: storage.Init(driver.NewMemory()), Capabilities: common.DefaultCapabilities}
|
|
51
|
+
archive := helmSourceTestArchive(t)
|
|
52
|
+
input := sohaapi.HelmExecutionTaskPayload{Action: sohaapi.Preflight, Snapshot: sohaapi.HelmDeliverySnapshot{DeliveryPlanID: "first", TargetID: "target", ApplicationID: "app", ApplicationEnvironmentID: "dev", ServiceID: "svc", ClusterID: "cluster", Namespace: "dev", ReleaseName: "app", ChartDigest: helmrelease.SHA256(archive)}, Prepared: &sohaapi.HelmPreparedRelease{ChartArchive: base64.StdEncoding.EncodeToString(archive)}}
|
|
53
|
+
input, err := Prepare(context.Background(), cfg, input)
|
|
54
|
+
if err != nil {
|
|
55
|
+
t.Fatal(err)
|
|
56
|
+
}
|
|
57
|
+
input.Snapshot.TemplateInputs = &sohaapi.TemplateVersionReference{TemplateID: "template", TemplateVersion: 3, TemplateDigest: "immutable"}
|
|
58
|
+
input.Snapshot.Migration = &sohaapi.TemplateMigrationAssessment{Initialization: true, FromVersion: 0, ToVersion: 3, RequiresApproval: true}
|
|
59
|
+
release := &releasev1.Release{Name: "app", Namespace: "dev", Version: 1, Manifest: input.Prepared.Manifest, Labels: helmDeliveryLabels(input.Snapshot), Info: &releasev1.Info{Status: releasecommon.StatusDeployed}}
|
|
60
|
+
if err := cfg.Releases.Create(release); err != nil {
|
|
61
|
+
t.Fatal(err)
|
|
62
|
+
}
|
|
63
|
+
client.missing = false
|
|
64
|
+
client.uid = "created"
|
|
65
|
+
for _, mode := range []string{"missing", "original", "replaced"} {
|
|
66
|
+
t.Run(mode, func(t *testing.T) {
|
|
67
|
+
candidate := input
|
|
68
|
+
if mode != "missing" {
|
|
69
|
+
candidate.InitializationResources = []sohaapi.ManifestResourceInventory{{APIVersion: "v1", Kind: "ConfigMap", Namespace: "dev", Name: "app", UID: "created"}}
|
|
70
|
+
}
|
|
71
|
+
if mode == "replaced" {
|
|
72
|
+
client.uid = "replacement"
|
|
73
|
+
} else {
|
|
74
|
+
client.uid = "created"
|
|
75
|
+
}
|
|
76
|
+
candidate.Action = sohaapi.Observe
|
|
77
|
+
result, err := Execute(context.Background(), cfg, candidate)
|
|
78
|
+
if (err == nil) != (mode == "original") || result.Ready != (mode == "original") {
|
|
79
|
+
t.Fatalf("observation %s: %+v %v", mode, result, err)
|
|
80
|
+
}
|
|
81
|
+
candidate.Action = sohaapi.Preflight
|
|
82
|
+
prepared, err := Prepare(context.Background(), cfg, candidate)
|
|
83
|
+
if (err == nil) != (mode == "original") {
|
|
84
|
+
t.Fatalf("recovery %s: %v", mode, err)
|
|
85
|
+
}
|
|
86
|
+
if err == nil && (prepared.Snapshot.ExpectedRevision != 0 || prepared.Snapshot.Operation != sohaapi.HelmDeliverySnapshotOperationInstall) {
|
|
87
|
+
t.Fatal("recovery turned initialization into upgrade")
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
func TestHelmInitializationReceiptRejectsMissingReplacedAndDuplicateUID(t *testing.T) {
|
|
94
|
+
original := []sohaapi.ManifestResourceInventory{{APIVersion: "v1", Kind: "ConfigMap", Namespace: "dev", Name: "app", UID: "created"}}
|
|
95
|
+
if err := ValidateInitializationResources(original, original); err != nil {
|
|
96
|
+
t.Fatal(err)
|
|
97
|
+
}
|
|
98
|
+
for _, mode := range []string{"missing", "replaced", "empty", "duplicate"} {
|
|
99
|
+
observed, receipt := slices.Clone(original), slices.Clone(original)
|
|
100
|
+
switch mode {
|
|
101
|
+
case "missing":
|
|
102
|
+
receipt = nil
|
|
103
|
+
case "replaced":
|
|
104
|
+
observed[0].UID = "replacement"
|
|
105
|
+
case "empty":
|
|
106
|
+
receipt[0].UID = ""
|
|
107
|
+
case "duplicate":
|
|
108
|
+
receipt = append(receipt, receipt[0])
|
|
109
|
+
observed = append(observed, observed[0])
|
|
110
|
+
}
|
|
111
|
+
if ValidateInitializationResources(observed, receipt) == nil {
|
|
112
|
+
t.Fatalf("accepted %s receipt", mode)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
guard := &initializingHelmClient{created: map[string]types.UID{"/ConfigMap/dev/app": "created"}}
|
|
116
|
+
if err := guard.validateCreated(sohaapi.HelmExecutionTaskResult{Resources: original}); err != nil {
|
|
117
|
+
t.Fatal(err)
|
|
118
|
+
}
|
|
119
|
+
original[0].UID = "replacement"
|
|
120
|
+
if guard.validateCreated(sohaapi.HelmExecutionTaskResult{Resources: original}) == nil {
|
|
121
|
+
t.Fatal("replacement registered as original creation")
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
func TestHelmInitializationPreflightRequiresAbsentResources(t *testing.T) {
|
|
126
|
+
input := sohaapi.HelmExecutionTaskPayload{Snapshot: sohaapi.HelmDeliverySnapshot{Operation: sohaapi.HelmDeliverySnapshotOperationInstall}, Prepared: &sohaapi.HelmPreparedRelease{Manifest: "manifest"}}
|
|
127
|
+
for _, missing := range []bool{false, true} {
|
|
128
|
+
cfg := &action.Configuration{KubeClient: &observedHelmKubeClient{missing: missing}}
|
|
129
|
+
err := requireInitialHelmResourcesAbsent(cfg, input)
|
|
130
|
+
if (err == nil) != missing {
|
|
131
|
+
t.Fatalf("absence=%v: %v", missing, err)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
input.Snapshot.ExpectedRevision = 1
|
|
135
|
+
if requireInitialHelmResourcesAbsent(&action.Configuration{}, input) == nil {
|
|
136
|
+
t.Fatal("existing revision accepted as initialization")
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
func TestHelmInitializationForcesAtomicCreateAndUIDBoundHookCleanup(t *testing.T) {
|
|
141
|
+
for _, race := range []bool{false, true} {
|
|
142
|
+
t.Run(map[bool]string{false: "new", true: "create-race"}[race], func(t *testing.T) {
|
|
143
|
+
uid := ""
|
|
144
|
+
methods := []string{}
|
|
145
|
+
object := `{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"app","namespace":"dev","uid":"created","resourceVersion":"7","annotations":{"helm.sh/hook":"post-install"}}}`
|
|
146
|
+
client := &restfake.RESTClient{GroupVersion: schema.GroupVersion{Version: "v1"}, NegotiatedSerializer: kuberesource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: restfake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
|
147
|
+
methods = append(methods, req.Method)
|
|
148
|
+
code, body := http.StatusOK, object
|
|
149
|
+
switch req.Method {
|
|
150
|
+
case http.MethodGet:
|
|
151
|
+
if uid == "" {
|
|
152
|
+
code, body = http.StatusNotFound, `{"apiVersion":"v1","kind":"Status","status":"Failure","reason":"NotFound","code":404}`
|
|
153
|
+
} else {
|
|
154
|
+
body = strings.ReplaceAll(object, "created", uid)
|
|
155
|
+
}
|
|
156
|
+
case http.MethodPost:
|
|
157
|
+
if race {
|
|
158
|
+
uid = "foreign"
|
|
159
|
+
code, body = http.StatusConflict, `{"apiVersion":"v1","kind":"Status","status":"Failure","reason":"AlreadyExists","code":409}`
|
|
160
|
+
} else {
|
|
161
|
+
uid = "created"
|
|
162
|
+
}
|
|
163
|
+
case http.MethodDelete:
|
|
164
|
+
var options metav1.DeleteOptions
|
|
165
|
+
if err := json.NewDecoder(req.Body).Decode(&options); err != nil || options.Preconditions == nil || options.Preconditions.UID == nil || *options.Preconditions.UID != "created" || options.Preconditions.ResourceVersion == nil || *options.Preconditions.ResourceVersion != "7" {
|
|
166
|
+
t.Fatal("hook cleanup lost UID/resourceVersion preconditions", err)
|
|
167
|
+
}
|
|
168
|
+
uid = ""
|
|
169
|
+
default:
|
|
170
|
+
t.Fatalf("initialization issued unsafe HTTP %s", req.Method)
|
|
171
|
+
}
|
|
172
|
+
return &http.Response{StatusCode: code, Header: http.Header{"Content-Type": {"application/json"}}, Body: io.NopCloser(strings.NewReader(body))}, nil
|
|
173
|
+
})}
|
|
174
|
+
var desired unstructured.Unstructured
|
|
175
|
+
if err := json.Unmarshal([]byte(object), &desired); err != nil {
|
|
176
|
+
t.Fatal(err)
|
|
177
|
+
}
|
|
178
|
+
desired.SetUID("")
|
|
179
|
+
desired.SetResourceVersion("")
|
|
180
|
+
resources := kube.ResourceList{&kuberesource.Info{Client: client, Namespace: "dev", Name: "app", Object: &desired, Mapping: &meta.RESTMapping{Resource: schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}, GroupVersionKind: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}, Scope: meta.RESTScopeNamespace}}}
|
|
181
|
+
native := &kube.Client{}
|
|
182
|
+
native.SetLogger(slog.NewTextHandler(io.Discard, nil))
|
|
183
|
+
guard := &initializingHelmClient{Interface: native, created: map[string]types.UID{}}
|
|
184
|
+
_, err := guard.Create(resources, kube.ClientCreateOptionServerSideApply(true, true))
|
|
185
|
+
if race && !apierrors.IsAlreadyExists(err) || !race && err != nil {
|
|
186
|
+
t.Fatalf("atomic create race=%v: %v", race, err)
|
|
187
|
+
}
|
|
188
|
+
if !reflect.DeepEqual(methods, []string{http.MethodGet, http.MethodGet, http.MethodPost}) {
|
|
189
|
+
t.Fatalf("native initialization did not use GET/POST only: %v", methods)
|
|
190
|
+
}
|
|
191
|
+
if _, err := guard.Update(nil, resources); err == nil {
|
|
192
|
+
t.Fatal("initialization adopted existing resources through Update")
|
|
193
|
+
}
|
|
194
|
+
_, errs := guard.Delete(resources, metav1.DeletePropagationBackground)
|
|
195
|
+
if (len(errs) == 0) == race {
|
|
196
|
+
t.Fatalf("hook ownership race=%v: %v", race, errs)
|
|
197
|
+
}
|
|
198
|
+
if race && methods[len(methods)-1] != http.MethodGet || !race && methods[len(methods)-1] != http.MethodDelete {
|
|
199
|
+
t.Fatalf("hook deletion ignored ownership: %v", methods)
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
}
|