typekro 0.26.0 → 0.27.1
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/dist/.tsbuildinfo +1 -1
- package/dist/alchemy/kro-delete.d.ts +8 -0
- package/dist/alchemy/kro-delete.d.ts.map +1 -1
- package/dist/alchemy/kro-delete.js +34 -35
- package/dist/alchemy/kro-delete.js.map +1 -1
- package/dist/alchemy/resource-registration.d.ts +60 -0
- package/dist/alchemy/resource-registration.d.ts.map +1 -1
- package/dist/alchemy/resource-registration.js +290 -11
- package/dist/alchemy/resource-registration.js.map +1 -1
- package/dist/alchemy/types.d.ts +44 -0
- package/dist/alchemy/types.d.ts.map +1 -1
- package/dist/core/deployment/k8s-helpers.d.ts +12 -0
- package/dist/core/deployment/k8s-helpers.d.ts.map +1 -1
- package/dist/core/deployment/k8s-helpers.js +26 -1
- package/dist/core/deployment/k8s-helpers.js.map +1 -1
- package/dist/core/deployment/kro-factory.d.ts +248 -1
- package/dist/core/deployment/kro-factory.d.ts.map +1 -1
- package/dist/core/deployment/kro-factory.js +1060 -180
- package/dist/core/deployment/kro-factory.js.map +1 -1
- package/dist/core/deployment/kro-instance-safety.d.ts +209 -0
- package/dist/core/deployment/kro-instance-safety.d.ts.map +1 -1
- package/dist/core/deployment/kro-instance-safety.js +506 -24
- package/dist/core/deployment/kro-instance-safety.js.map +1 -1
- package/dist/core/deployment/kro-namespace-teardown.d.ts +248 -0
- package/dist/core/deployment/kro-namespace-teardown.d.ts.map +1 -0
- package/dist/core/deployment/kro-namespace-teardown.js +459 -0
- package/dist/core/deployment/kro-namespace-teardown.js.map +1 -0
- package/dist/core/deployment/rollback-manager.d.ts +32 -0
- package/dist/core/deployment/rollback-manager.d.ts.map +1 -1
- package/dist/core/deployment/rollback-manager.js +69 -21
- package/dist/core/deployment/rollback-manager.js.map +1 -1
- package/dist/core/serialization/core.d.ts.map +1 -1
- package/dist/core/serialization/core.js +82 -8
- package/dist/core/serialization/core.js.map +1 -1
- package/dist/core/types/deployment.d.ts +20 -0
- package/dist/core/types/deployment.d.ts.map +1 -1
- package/dist/factories/apisix/types.js +11 -11
- package/dist/factories/apisix/types.js.map +1 -1
- package/dist/factories/caddy/types.js +2 -2
- package/dist/factories/caddy/types.js.map +1 -1
- package/dist/factories/cert-manager/types.js +6 -6
- package/dist/factories/cert-manager/types.js.map +1 -1
- package/dist/factories/cilium/compositions/cilium-bootstrap.js +6 -6
- package/dist/factories/cilium/compositions/cilium-bootstrap.js.map +1 -1
- package/dist/factories/clickhouse/compositions/clickhouse-cluster.js +1 -1
- package/dist/factories/clickhouse/compositions/clickhouse-cluster.js.map +1 -1
- package/dist/factories/clickhouse/types.js +8 -8
- package/dist/factories/clickhouse/types.js.map +1 -1
- package/dist/factories/clickstack/types.js +5 -5
- package/dist/factories/clickstack/types.js.map +1 -1
- package/dist/factories/cnpg/types.js +6 -6
- package/dist/factories/cnpg/types.js.map +1 -1
- package/dist/factories/dagster/types.js +14 -14
- package/dist/factories/dagster/types.js.map +1 -1
- package/dist/factories/external-dns/types.js +3 -3
- package/dist/factories/external-dns/types.js.map +1 -1
- package/dist/factories/inngest/types.js +8 -8
- package/dist/factories/inngest/types.js.map +1 -1
- package/dist/factories/ory/types.js +40 -40
- package/dist/factories/ory/types.js.map +1 -1
- package/dist/factories/rook/types.js +7 -7
- package/dist/factories/rook/types.js.map +1 -1
- package/dist/factories/searxng/types.js +2 -2
- package/dist/factories/searxng/types.js.map +1 -1
- package/dist/factories/valkey/types.js +4 -4
- package/dist/factories/valkey/types.js.map +1 -1
- package/dist/factories/webapp/types.js +8 -8
- package/dist/factories/webapp/types.js.map +1 -1
- package/dist/utils/string.d.ts +14 -0
- package/dist/utils/string.d.ts.map +1 -1
- package/dist/utils/string.js +22 -0
- package/dist/utils/string.js.map +1 -1
- package/package.json +1 -1
|
@@ -32,6 +32,14 @@ export interface KubernetesObjectCleanupApi {
|
|
|
32
32
|
namespace?: string;
|
|
33
33
|
};
|
|
34
34
|
}): Promise<unknown>;
|
|
35
|
+
read(resource: {
|
|
36
|
+
apiVersion: string;
|
|
37
|
+
kind: string;
|
|
38
|
+
metadata: {
|
|
39
|
+
name: string;
|
|
40
|
+
namespace?: string;
|
|
41
|
+
};
|
|
42
|
+
}): Promise<unknown>;
|
|
35
43
|
}
|
|
36
44
|
export interface KubernetesObjectInstanceApi extends KubernetesObjectCleanupApi {
|
|
37
45
|
read(resource: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kro-delete.d.ts","sourceRoot":"","sources":["../../src/alchemy/kro-delete.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,
|
|
1
|
+
{"version":3,"file":"kro-delete.d.ts","sourceRoot":"","sources":["../../src/alchemy/kro-delete.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CRDInstanceError, ensureError } from '../core/errors.js';
|
|
2
|
+
import { createRollbackManager } from '../core/deployment/rollback-manager.js';
|
|
2
3
|
import { createBunCompatibleCustomObjectsApi, createBunCompatibleKubernetesObjectApi } from '../core/kubernetes/bun-api-client.js';
|
|
3
4
|
import { getComponentLogger } from '../core/logging/index.js';
|
|
4
5
|
function getSchemaVersion(apiVersion) {
|
|
@@ -60,47 +61,45 @@ export async function hasKroInstances(kubeConfig, options) {
|
|
|
60
61
|
}
|
|
61
62
|
export async function deleteKroDefinition(kubeConfig, options, k8sApi = createBunCompatibleKubernetesObjectApi(kubeConfig)) {
|
|
62
63
|
const logger = getComponentLogger('alchemy-kro-delete');
|
|
63
|
-
|
|
64
|
+
// ONE gating mechanism: the engine's rollback manager deletes then polls to a REAL 404
|
|
65
|
+
// and THROWS on timeout — the SAME primitive the imperative KRO teardown uses. A
|
|
66
|
+
// pre-existing 404 is treated as already-gone.
|
|
67
|
+
const rollback = createRollbackManager(k8sApi);
|
|
68
|
+
const timeout = options.timeout ?? 300000;
|
|
69
|
+
// The RGD delete is a HARD gate (throws on timeout).
|
|
64
70
|
try {
|
|
65
|
-
await
|
|
66
|
-
apiVersion: 'kro.run/v1alpha1',
|
|
67
|
-
kind: 'ResourceGraphDefinition',
|
|
68
|
-
metadata: { name: options.rgdName },
|
|
69
|
-
});
|
|
71
|
+
await rollback.deleteResourceAndWait({ apiVersion: 'kro.run/v1alpha1', kind: 'ResourceGraphDefinition', name: options.rgdName }, { timeout });
|
|
70
72
|
}
|
|
71
73
|
catch (error) {
|
|
72
|
-
|
|
73
|
-
logger.error('Alchemy KRO RGD cleanup failed', ensureError(error), {
|
|
74
|
-
rgdName: options.rgdName,
|
|
75
|
-
error: ensureError(error).message,
|
|
76
|
-
});
|
|
77
|
-
throw error;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
if (!crdPlural) {
|
|
81
|
-
logger.debug('Alchemy KRO CRD already absent during RGD cleanup', {
|
|
82
|
-
kind: options.kind,
|
|
74
|
+
logger.error('Alchemy KRO RGD cleanup failed', ensureError(error), {
|
|
83
75
|
rgdName: options.rgdName,
|
|
76
|
+
error: ensureError(error).message,
|
|
84
77
|
});
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
const crdName = `${crdPlural}.${getSchemaGroup(options)}`;
|
|
88
|
-
try {
|
|
89
|
-
await k8sApi.delete({
|
|
90
|
-
apiVersion: 'apiextensions.k8s.io/v1',
|
|
91
|
-
kind: 'CustomResourceDefinition',
|
|
92
|
-
metadata: { name: crdName },
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
catch (error) {
|
|
96
|
-
if (getKubernetesErrorCode(error) !== 404) {
|
|
97
|
-
logger.error('Alchemy KRO CRD cleanup failed', ensureError(error), {
|
|
98
|
-
crdName,
|
|
99
|
-
error: ensureError(error).message,
|
|
100
|
-
});
|
|
101
|
-
throw error;
|
|
102
|
-
}
|
|
78
|
+
throw error;
|
|
103
79
|
}
|
|
80
|
+
// FINDING #1 — DO NOT delete the generated CRD here (leave it for out-of-band GC).
|
|
81
|
+
//
|
|
82
|
+
// Under alchemy's reverse-topo teardown the hoisted workload Namespace is a SEPARATE
|
|
83
|
+
// resource deleted AFTER the RGD/instance (both `dependsOn` it): the order is
|
|
84
|
+
// instance → RGD → namespace. `deleteKroDefinition` runs during the instance/RGD
|
|
85
|
+
// delete, which is BEFORE the namespace resource's own delete step. If we deleted the
|
|
86
|
+
// CRD here, it would be Terminating while the namespace later drains — and a namespace
|
|
87
|
+
// controller stalls indefinitely enumerating a *Terminating* CRD's type (the
|
|
88
|
+
// apiextensions `customresourcecleanup` finalizer is slow/fragile even with zero
|
|
89
|
+
// instances, upstream kro #1171). That is the EXACT hang the imperative path avoids by
|
|
90
|
+
// deleting the CRD strictly AFTER the namespace.
|
|
91
|
+
//
|
|
92
|
+
// Alchemy cannot cheaply reorder the CRD delete to run after the (separate, later,
|
|
93
|
+
// possibly SHARED/deduped) namespace resource, so we choose the other invariant-safe
|
|
94
|
+
// option: NEVER delete the CRD in the alchemy path. It is never Terminating during
|
|
95
|
+
// namespace teardown, so a namespace can never hang against it. A leftover Active CRD
|
|
96
|
+
// with zero instances is harmless (KRO's default allowCRDDeletion=false already leaves
|
|
97
|
+
// it), cluster-scoped, and can be GC'd out-of-band. This is the accepted residual of the
|
|
98
|
+
// alchemy #1 fix — documented, not silent.
|
|
99
|
+
logger.debug('Alchemy KRO teardown: leaving the generated CRD for out-of-band GC (finding #1)', {
|
|
100
|
+
kind: options.kind,
|
|
101
|
+
rgdName: options.rgdName,
|
|
102
|
+
});
|
|
104
103
|
}
|
|
105
104
|
export async function deleteKroInstanceFinalizerSafe(kubeConfig, name, options) {
|
|
106
105
|
return deleteKroInstanceFinalizerSafeWithApis(kubeConfig, name, options, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kro-delete.js","sourceRoot":"","sources":["../../src/alchemy/kro-delete.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAAE,mCAAmC,EAAE,sCAAsC,EAAE,MAAM,sCAAsC,CAAC;AACnI,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAY9D,SAAS,gBAAgB,CAAC,UAAkB;IAC1C,OAAO,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC;AACxF,CAAC;AAED,SAAS,cAAc,CAAC,OAA2B;IACjD,IAAI,OAAO,CAAC,KAAK;QAAE,OAAO,OAAO,CAAC,KAAK,CAAC;IACxC,OAAO,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;AACtG,CAAC;AAED,SAAS,qBAAqB,CAAC,OAA2B;IACxD,OAAO,GAAG,cAAc,CAAC,OAAO,CAAC,IAAI,gBAAgB,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;AAC9E,CAAC;AAED,SAAS,sBAAsB,CAAC,KAAc;IAC5C,MAAM,QAAQ,GAAG,KAAyE,CAAC;IAC3F,OAAO,QAAQ,CAAC,UAAU,IAAI,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;AACrE,CAAC;AAED,SAAS,iBAAiB,CACxB,SAAgF,EAChF,UAAkB,EAClB,eAAwB,EACxB,eAAuB;IAEvB,MAAM,SAAS,GAAG,eAAe;QAC/B,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE;YAC5B,IAAI,QAAQ,CAAC,QAAQ,EAAE,IAAI,KAAK,UAAU;gBAAE,OAAO,IAAI,CAAC;YACxD,OAAO,QAAQ,CAAC,QAAQ,EAAE,SAAS,KAAK,eAAe,CAAC;QAC1D,CAAC,CAAC;QACJ,CAAC,CAAC,SAAS,CAAC;IACd,OAAO,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC;AAC9B,CAAC;
|
|
1
|
+
{"version":3,"file":"kro-delete.js","sourceRoot":"","sources":["../../src/alchemy/kro-delete.ts"],"names":[],"mappings":"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"}
|
|
@@ -13,11 +13,14 @@
|
|
|
13
13
|
* The Kubernetes machinery (`deployers`, `kro-delete`, client/deployer construction) is
|
|
14
14
|
* alchemy-version-agnostic and reused verbatim; only the registration glue changed.
|
|
15
15
|
*/
|
|
16
|
+
import type { KubeConfig } from '@kubernetes/client-node';
|
|
16
17
|
import { Effect } from 'effect';
|
|
17
18
|
import * as ProviderMod from 'alchemy/Provider';
|
|
18
19
|
import * as ResourceMod from 'alchemy/Resource';
|
|
19
20
|
import type { Resource as ResourceT } from 'alchemy/Resource';
|
|
21
|
+
import { type TypeKroLogger } from '../core/logging/index.js';
|
|
20
22
|
import { type ResourceScope } from '../core/metadata/index.js';
|
|
23
|
+
import { listNamespacesOwnedByRgd } from '../core/deployment/kro-namespace-teardown.js';
|
|
21
24
|
import type { DeploymentOptions } from '../core/types/deployment.js';
|
|
22
25
|
import type { Enhanced } from '../core/types/kubernetes.js';
|
|
23
26
|
import type { KroDeletionOptions } from './kro-delete.js';
|
|
@@ -83,6 +86,63 @@ export declare function singletonDriftVerdict(expectedFingerprint: string, deplo
|
|
|
83
86
|
drift: true;
|
|
84
87
|
reason: string;
|
|
85
88
|
};
|
|
89
|
+
/**
|
|
90
|
+
* The namespaces of EVERY existing instance of the shared RGD (finding #7, alchemy).
|
|
91
|
+
* Discovers the generated CRD from {@link TypeKroResourceProps.namespacePreHoistQuery},
|
|
92
|
+
* lists instances cluster-wide, and returns each instance's namespace.
|
|
93
|
+
*
|
|
94
|
+
* FAIL CLOSED: a CRD-discovery or instance-list FAILURE (RBAC/connectivity) is NOT proof
|
|
95
|
+
* of absence — it THROWS. Only a successful CRD list with no matching CRD (fresh cluster),
|
|
96
|
+
* or a definitive 404 listing instances, yields an empty set.
|
|
97
|
+
*
|
|
98
|
+
* NAMESPACE RESOLUTION (finding #1, v7 — per-instance exact or fail closed): each existing
|
|
99
|
+
* instance's namespaces are resolved EXACTLY and PER INSTANCE from that instance's OWN CR
|
|
100
|
+
* `typekro.io/hoisted-namespaces` record — the only per-instance proof. An instance that
|
|
101
|
+
* lacks its own record (a genuinely-legacy deployment) FAILS CLOSED (throws): the RGD-wide
|
|
102
|
+
* set of Namespaces carrying `typekro.io/created-by-rgd == props.namespaceOwnerRgd` is NOT
|
|
103
|
+
* per-instance proof — a DIFFERENT (modern) instance could have stamped it, so a non-empty
|
|
104
|
+
* RGD-wide set would MASK a legacy instance whose own — possibly different — namespace is
|
|
105
|
+
* then invisible and pruned. We never approximate from `metadata.namespace` / `spec.namespace`.
|
|
106
|
+
* The RGD-wide set IS unioned into the returned protected set (a superset catching leaked
|
|
107
|
+
* namespaces) — it just never SATISFIES the per-instance check. The incoming namespace is
|
|
108
|
+
* always checked directly by the caller regardless.
|
|
109
|
+
*/
|
|
110
|
+
/**
|
|
111
|
+
* Injectable API surface for {@link _existingInstanceNamespacesAlchemy} — lets the
|
|
112
|
+
* deterministic unit test supply mocks instead of a live cluster. Production passes
|
|
113
|
+
* nothing and the Bun-compatible clients are constructed from the KubeConfig.
|
|
114
|
+
*/
|
|
115
|
+
interface AlchemyPreHoistDeps {
|
|
116
|
+
objectApi?: {
|
|
117
|
+
list(apiVersion: string, kind: string): Promise<unknown>;
|
|
118
|
+
};
|
|
119
|
+
customApi?: {
|
|
120
|
+
listClusterCustomObject(request: {
|
|
121
|
+
group: string;
|
|
122
|
+
version: string;
|
|
123
|
+
plural: string;
|
|
124
|
+
}): Promise<{
|
|
125
|
+
items?: Array<{
|
|
126
|
+
metadata?: {
|
|
127
|
+
namespace?: unknown;
|
|
128
|
+
annotations?: Record<string, string>;
|
|
129
|
+
};
|
|
130
|
+
spec?: unknown;
|
|
131
|
+
}>;
|
|
132
|
+
}>;
|
|
133
|
+
};
|
|
134
|
+
/** The NamespaceListApi passed to {@link listNamespacesOwnedByRgd}. */
|
|
135
|
+
ownedNamespaceListApi?: Parameters<typeof listNamespacesOwnedByRgd>[2] extends infer O ? O extends {
|
|
136
|
+
k8sApi?: infer A;
|
|
137
|
+
} ? A : never : never;
|
|
138
|
+
}
|
|
139
|
+
declare function _existingInstanceNamespacesAlchemy<T extends Enhanced<unknown, unknown>>(props: TypeKroResourceProps<T>, kc: KubeConfig, logger: TypeKroLogger, deps?: AlchemyPreHoistDeps): Promise<Set<string>>;
|
|
140
|
+
/**
|
|
141
|
+
* Test-only export of the alchemy pre-hoist namespace resolver (finding #1, v7) so the
|
|
142
|
+
* deterministic unit test can prove the per-instance-exact-or-fail-closed behavior with
|
|
143
|
+
* injected API mocks (no cluster).
|
|
144
|
+
*/
|
|
145
|
+
export declare const existingInstanceNamespacesAlchemyForTest: typeof _existingInstanceNamespacesAlchemy;
|
|
86
146
|
declare function inferKroDeletionOptions<T extends Enhanced<unknown, unknown>>(props: TypeKroResourceProps<T>): KroDeletionOptions | undefined;
|
|
87
147
|
/** Internal test hook for legacy Alchemy KRO state rehydration. */
|
|
88
148
|
export declare const inferKroDeletionOptionsForTest: typeof inferKroDeletionOptions;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource-registration.d.ts","sourceRoot":"","sources":["../../src/alchemy/resource-registration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;
|
|
1
|
+
{"version":3,"file":"resource-registration.d.ts","sourceRoot":"","sources":["../../src/alchemy/resource-registration.ts"],"names":[],"mappings":"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"}
|
|
@@ -22,8 +22,9 @@ import { ensureError } from '../core/errors.js';
|
|
|
22
22
|
import { createKubernetesClientProvider } from '../core/kubernetes/client-provider.js';
|
|
23
23
|
import { getComponentLogger } from '../core/logging/index.js';
|
|
24
24
|
import { CEL_EXPRESSION_BRAND } from '../core/constants/brands.js';
|
|
25
|
-
import { createBunCompatibleKubernetesObjectApi } from '../core/kubernetes/index.js';
|
|
25
|
+
import { createBunCompatibleCustomObjectsApi, createBunCompatibleKubernetesObjectApi, } from '../core/kubernetes/index.js';
|
|
26
26
|
import { getResourceScope } from '../core/metadata/index.js';
|
|
27
|
+
import { decideNamespaceOwnershipCreateFirst, deleteNamespaceIfEmpty, HOISTED_NAMESPACES_ANNOTATION, listNamespacesOwnedByRgd, NAMESPACE_OWNER_ANNOTATION, readHoistedNamespacesRecord, } from '../core/deployment/kro-namespace-teardown.js';
|
|
27
28
|
import { SINGLETON_SPEC_FINGERPRINT_ANNOTATION } from '../core/deployment/resource-tagging.js';
|
|
28
29
|
import { stableSerialize } from '../core/singleton/singleton.js';
|
|
29
30
|
import { DirectTypeKroDeployer, KroTypeKroDeployer, ResourceGraphDefinitionDeletionDeferredError, } from './deployers.js';
|
|
@@ -120,27 +121,37 @@ export function materializeAlchemyResources(kroResource, declarations) {
|
|
|
120
121
|
*/
|
|
121
122
|
async function deployKroResource(props) {
|
|
122
123
|
const logger = getComponentLogger('alchemy-deployment').child({ alchemyType: KRO_RESOURCE_TYPE });
|
|
124
|
+
// Fail-closed PRE-HOIST guard on the ALCHEMY path (finding #7): before applying a
|
|
125
|
+
// typekro-hoisted workload Namespace, refuse to proceed if it is still a KRO ApplySet
|
|
126
|
+
// member from a pre-hoist RGD — rolling the new hoisted RGD over the old one would let
|
|
127
|
+
// KRO's prune delete it. Cluster-checked here since `toAlchemyResources` is cluster-free.
|
|
128
|
+
await _assertNoPreHoistNamespaceConflictAlchemy(props, logger);
|
|
123
129
|
// Singleton-owner spec-drift protection (the declarative analog of `assertNoDeployedSingletonSpecDrift`
|
|
124
130
|
// in the imperative deploy path): refuse to clobber a shared singleton that already exists with a
|
|
125
131
|
// different spec. Cluster-checked here since `toAlchemyResources` is intentionally cluster-free.
|
|
126
132
|
await _assertNoSingletonDrift(props, logger);
|
|
127
|
-
|
|
133
|
+
// finding #2 (adopted-namespace): the hoisted workload Namespace is stamped
|
|
134
|
+
// owned-by-this-RGD at BUILD time (cluster-free). READ the live namespace here and
|
|
135
|
+
// KEEP that stamp ONLY when typekro actually creates it (404) or already owns it;
|
|
136
|
+
// otherwise strip it so teardown never deletes a namespace typekro merely adopted.
|
|
137
|
+
const effectiveProps = await _preserveHoistedNamespaceAdoption(props, logger);
|
|
138
|
+
const { deployer, dispose } = await _resolveDeployer(effectiveProps, 'deployment');
|
|
128
139
|
try {
|
|
129
140
|
// Direct mode: hand the deployer the live state of this resource's dependencies so the engine
|
|
130
141
|
// resolves its cross-resource references + CEL expressions against them (the deps deployed
|
|
131
142
|
// first via alchemy ordering). KRO docs are self-contained, so the seed is irrelevant there.
|
|
132
|
-
const seedResources = _seedFromDependencies(
|
|
143
|
+
const seedResources = _seedFromDependencies(effectiveProps);
|
|
133
144
|
// alchemy serializes props to state, flattening this resource's CEL refs to `${…}` STRINGS.
|
|
134
145
|
// The engine resolver only evaluates CEL OBJECTS, so (direct mode, with dependency state to
|
|
135
146
|
// resolve against) re-hydrate those strings into template CEL objects before deploying — but
|
|
136
147
|
// ONLY strings that reference a known dependency id, so genuine `${…}` literals (e.g. a shell
|
|
137
148
|
// `${HOME}` in an env var) are left untouched.
|
|
138
|
-
let resourceForDeploy =
|
|
139
|
-
if (seedResources &&
|
|
140
|
-
resourceForDeploy = _rehydrateCelStrings(
|
|
149
|
+
let resourceForDeploy = effectiveProps.resource;
|
|
150
|
+
if (seedResources && effectiveProps.deploymentStrategy === 'direct') {
|
|
151
|
+
resourceForDeploy = _rehydrateCelStrings(effectiveProps.resource, new Set(seedResources.map((s) => s.id)));
|
|
141
152
|
}
|
|
142
|
-
else if (
|
|
143
|
-
|
|
153
|
+
else if (effectiveProps.deploymentStrategy === 'kro' &&
|
|
154
|
+
effectiveProps.resource.kind !== 'ResourceGraphDefinition') {
|
|
144
155
|
// KRO CR instance (the custom resource an RGD defines — not the RGD itself). After alchemy
|
|
145
156
|
// serializes props to state, the resource is a plain manifest with no readiness evaluator, so a
|
|
146
157
|
// `waitForReady` deploy throws "No readiness evaluator found for <Kind>". Re-wrap it with
|
|
@@ -148,7 +159,7 @@ async function deployKroResource(props) {
|
|
|
148
159
|
// InstanceSynced) — the same KRO-instance readiness the imperative deploy path waits on. The RGD
|
|
149
160
|
// is excluded (it carries its own evaluator via the resourceGraphDefinition factory).
|
|
150
161
|
const { kroCustomResource } = await import('../factories/kro/kro-custom-resource.js');
|
|
151
|
-
const r =
|
|
162
|
+
const r = effectiveProps.resource;
|
|
152
163
|
resourceForDeploy = kroCustomResource({
|
|
153
164
|
apiVersion: r.apiVersion ?? '',
|
|
154
165
|
kind: r.kind ?? '',
|
|
@@ -156,9 +167,11 @@ async function deployKroResource(props) {
|
|
|
156
167
|
spec: (r.spec ?? {}),
|
|
157
168
|
});
|
|
158
169
|
}
|
|
159
|
-
const deployProps = resourceForDeploy ===
|
|
170
|
+
const deployProps = resourceForDeploy === effectiveProps.resource
|
|
171
|
+
? effectiveProps
|
|
172
|
+
: { ...effectiveProps, resource: resourceForDeploy };
|
|
160
173
|
const { resourceProperties } = await _deployAndCreateResult(deployProps, deployer, seedResources);
|
|
161
|
-
_logDeploymentSuccess(logger, KRO_RESOURCE_TYPE,
|
|
174
|
+
_logDeploymentSuccess(logger, KRO_RESOURCE_TYPE, effectiveProps, resourceProperties);
|
|
162
175
|
return resourceProperties;
|
|
163
176
|
}
|
|
164
177
|
catch (error) {
|
|
@@ -200,6 +213,226 @@ export function singletonDriftVerdict(expectedFingerprint, deployingSpec, live)
|
|
|
200
213
|
* fires for resources carrying the singleton spec-fingerprint annotation (i.e. singleton instances);
|
|
201
214
|
* a missing instance / absent CRD / unreachable cluster is treated as "nothing to drift from".
|
|
202
215
|
*/
|
|
216
|
+
/**
|
|
217
|
+
* Fail-closed PRE-HOIST detection for the ALCHEMY deploy path (finding #7). Runs ONLY
|
|
218
|
+
* for a typekro-hoisted workload Namespace (`namespaceEmptyGate`). If the live namespace
|
|
219
|
+
* is a KRO ApplySet member (carries `applyset.kubernetes.io/part-of` or any `kro.run/*`
|
|
220
|
+
* label from a pre-hoist RGD), applying the new hoisted RGD would let KRO's prune delete
|
|
221
|
+
* it — so THROW. A 404 (fresh) is safe; any OTHER read error FAILS CLOSED (throws).
|
|
222
|
+
*/
|
|
223
|
+
async function _assertNoPreHoistNamespaceConflictAlchemy(props, logger) {
|
|
224
|
+
if (props.namespaceEmptyGate !== true)
|
|
225
|
+
return;
|
|
226
|
+
const incoming = props.resource.metadata?.name;
|
|
227
|
+
if (typeof incoming !== 'string' || incoming.length === 0)
|
|
228
|
+
return;
|
|
229
|
+
const kc = _createClientProvider(props, 'pre-hoist-check');
|
|
230
|
+
const api = createBunCompatibleKubernetesObjectApi(kc);
|
|
231
|
+
// The set of namespaces to check: the incoming one PLUS every EXISTING instance's
|
|
232
|
+
// namespace (finding #7). An upgrade prunes the ApplySet for ALL instances of the
|
|
233
|
+
// shared RGD at once, so missing one would let KRO delete it.
|
|
234
|
+
const namespacesToCheck = new Set([incoming]);
|
|
235
|
+
for (const ns of await _existingInstanceNamespacesAlchemy(props, kc, logger)) {
|
|
236
|
+
namespacesToCheck.add(ns);
|
|
237
|
+
}
|
|
238
|
+
for (const name of namespacesToCheck) {
|
|
239
|
+
let labels = {};
|
|
240
|
+
try {
|
|
241
|
+
const live = (await api.read({
|
|
242
|
+
apiVersion: 'v1',
|
|
243
|
+
kind: 'Namespace',
|
|
244
|
+
metadata: { name },
|
|
245
|
+
}));
|
|
246
|
+
labels = live.metadata?.labels ?? {};
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
const k8sErr = error;
|
|
250
|
+
const code = k8sErr.statusCode ?? k8sErr.code ?? k8sErr.body?.code;
|
|
251
|
+
if (code === 404)
|
|
252
|
+
continue; // fresh namespace — nothing to migrate
|
|
253
|
+
// FAIL CLOSED: a non-404 read error means we cannot prove the namespace is safe.
|
|
254
|
+
throw new Error(`Pre-hoist safety check could not read Namespace "${name}" (${ensureError(error).message}). ` +
|
|
255
|
+
`Refusing to deploy: a hoist over an unreadable namespace could let KRO's ApplySet prune delete it.`);
|
|
256
|
+
}
|
|
257
|
+
const partOfKro = typeof labels['applyset.kubernetes.io/part-of'] === 'string' ||
|
|
258
|
+
Object.keys(labels).some((key) => key.startsWith('kro.run/'));
|
|
259
|
+
if (partOfKro) {
|
|
260
|
+
throw new Error(`Pre-hoist deployment detected: the live Namespace "${name}" is a KRO ApplySet member ` +
|
|
261
|
+
`(carries applyset.kubernetes.io/part-of and/or kro.run/* labels from a pre-hoist RGD). ` +
|
|
262
|
+
`typekro now HOISTS every owned Namespace out of the RGD, so applying the new RGD in place ` +
|
|
263
|
+
`would drop "${name}" from KRO's ApplySet and KRO's prune would DELETE it and its workloads. ` +
|
|
264
|
+
`This upgrade is not auto-migrated — see docs/advanced/migration.md.`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
logger.debug('Pre-hoist namespace check passed (alchemy)', {
|
|
268
|
+
namespaces: [...namespacesToCheck],
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
async function _existingInstanceNamespacesAlchemy(props, kc, logger, deps = {}) {
|
|
272
|
+
const result = new Set();
|
|
273
|
+
const query = props.namespacePreHoistQuery;
|
|
274
|
+
if (!query)
|
|
275
|
+
return result;
|
|
276
|
+
// STRICT CRD discovery — a list failure FAILS CLOSED (throws).
|
|
277
|
+
const objectApi = deps.objectApi ?? createBunCompatibleKubernetesObjectApi(kc);
|
|
278
|
+
const crds = (await objectApi.list('apiextensions.k8s.io/v1', 'CustomResourceDefinition'));
|
|
279
|
+
const match = crds?.items?.find((crd) => crd.spec?.group === query.group && crd.spec?.names?.kind === query.kind);
|
|
280
|
+
const plural = match?.spec?.names?.plural;
|
|
281
|
+
if (!plural)
|
|
282
|
+
return result; // definitively fresh — no CRD, so no existing instances
|
|
283
|
+
let items;
|
|
284
|
+
try {
|
|
285
|
+
const customApi = deps.customApi ??
|
|
286
|
+
createBunCompatibleCustomObjectsApi(kc);
|
|
287
|
+
const listResponse = await customApi.listClusterCustomObject({
|
|
288
|
+
group: query.group,
|
|
289
|
+
version: query.version,
|
|
290
|
+
plural,
|
|
291
|
+
});
|
|
292
|
+
items = listResponse.items ?? [];
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
const k8sErr = error;
|
|
296
|
+
const status = k8sErr.statusCode ?? k8sErr.code ?? k8sErr.body?.code;
|
|
297
|
+
if (status === 404)
|
|
298
|
+
return result; // CRD/instances vanished — fresh
|
|
299
|
+
throw new Error(`Pre-hoist safety check could not list existing instances of the RGD's CRD ` +
|
|
300
|
+
`(${ensureError(error).message}). Refusing to deploy: another instance's namespace could ` +
|
|
301
|
+
`be pruned by the hoist without being checked.`);
|
|
302
|
+
}
|
|
303
|
+
if (items.length === 0)
|
|
304
|
+
return result;
|
|
305
|
+
// The RGD-wide record of every namespace this RGD created: namespaces carrying
|
|
306
|
+
// `typekro.io/created-by-rgd == props.namespaceOwnerRgd`. We union it into the PROTECTED set
|
|
307
|
+
// as a superset (it catches namespaces leaked by an interrupted teardown), but it is NOT
|
|
308
|
+
// used to satisfy the per-instance check below (finding #1, v7) — being RGD-wide it cannot
|
|
309
|
+
// prove a SPECIFIC instance's namespaces. A list error FAILS CLOSED.
|
|
310
|
+
const ownerRgd = props.namespaceOwnerRgd;
|
|
311
|
+
const ownedNamespaces = new Set();
|
|
312
|
+
if (typeof ownerRgd === 'string' && ownerRgd.length > 0) {
|
|
313
|
+
let owned;
|
|
314
|
+
try {
|
|
315
|
+
owned = await listNamespacesOwnedByRgd(kc, ownerRgd, {
|
|
316
|
+
logger,
|
|
317
|
+
...(deps.ownedNamespaceListApi ? { k8sApi: deps.ownedNamespaceListApi } : {}),
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
throw new Error(`Pre-hoist safety check could not list namespaces owned by RGD "${ownerRgd}" ` +
|
|
322
|
+
`(${ensureError(error).message}). Refusing to deploy: without the durable ownership ` +
|
|
323
|
+
`record we cannot prove the hoist won't prune an existing instance's namespace.`);
|
|
324
|
+
}
|
|
325
|
+
for (const ns of owned)
|
|
326
|
+
ownedNamespaces.add(ns);
|
|
327
|
+
for (const ns of ownedNamespaces)
|
|
328
|
+
result.add(ns);
|
|
329
|
+
}
|
|
330
|
+
for (const item of items) {
|
|
331
|
+
// Resolve THIS instance's namespaces from its OWN exact PER-INSTANCE record — the CR
|
|
332
|
+
// `typekro.io/hoisted-namespaces` annotation (finding #1, v7). It carries the instance's
|
|
333
|
+
// EXACT hoisted names (a name derived from an arbitrary spec field round-trips) and is the
|
|
334
|
+
// ONLY per-instance proof.
|
|
335
|
+
const record = readHoistedNamespacesRecord(item.metadata?.annotations?.[HOISTED_NAMESPACES_ANNOTATION]);
|
|
336
|
+
if (record.status === 'present') {
|
|
337
|
+
// A VALID record — including an explicit empty `[]` — is per-instance proof: this instance
|
|
338
|
+
// hoists exactly these namespaces (possibly none). Resolved; do NOT fail closed.
|
|
339
|
+
for (const ns of record.names)
|
|
340
|
+
result.add(ns);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
// MISSING or MALFORMED record → NO per-instance exact record. FAIL CLOSED (finding #1, v7). We do NOT
|
|
344
|
+
// fall back to the RGD-wide `created-by-rgd` set: being RGD-wide it is not per-instance
|
|
345
|
+
// proof — a DIFFERENT (modern) instance could have stamped it, so a non-empty set would
|
|
346
|
+
// MASK this legacy instance whose own — possibly different — owned namespace is then
|
|
347
|
+
// invisible and pruned. Nor do we approximate from metadata.namespace / spec.namespace.
|
|
348
|
+
throw new Error(`Pre-hoist safety check found an existing instance with no per-instance namespace record — ` +
|
|
349
|
+
`its CR carries no "${HOISTED_NAMESPACES_ANNOTATION}" annotation. The RGD-wide ` +
|
|
350
|
+
`"${NAMESPACE_OWNER_ANNOTATION}${ownerRgd ? `=${ownerRgd}` : ''}" set is NOT per-instance ` +
|
|
351
|
+
`proof (another instance could have stamped it), so this instance's own owned namespace ` +
|
|
352
|
+
`could be pruned by the hoist unseen. Refusing to deploy: migrate it (redeploy with a ` +
|
|
353
|
+
`current TypeKro so its namespaces are recorded) and retry.`);
|
|
354
|
+
}
|
|
355
|
+
logger.debug('Enumerated existing-instance namespaces for the alchemy pre-hoist check', {
|
|
356
|
+
count: result.size,
|
|
357
|
+
});
|
|
358
|
+
return result;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Test-only export of the alchemy pre-hoist namespace resolver (finding #1, v7) so the
|
|
362
|
+
* deterministic unit test can prove the per-instance-exact-or-fail-closed behavior with
|
|
363
|
+
* injected API mocks (no cluster).
|
|
364
|
+
*/
|
|
365
|
+
export const existingInstanceNamespacesAlchemyForTest = _existingInstanceNamespacesAlchemy;
|
|
366
|
+
/**
|
|
367
|
+
* finding #2 (alchemy adopted-namespace, mirrors the imperative `applyRetainedHoistedNamespace`
|
|
368
|
+
* create-vs-adopt logic): the hoisted workload Namespace carries a build-time
|
|
369
|
+
* `typekro.io/created-by-rgd` ownership stamp (added cluster-free in
|
|
370
|
+
* `buildHoistedNamespaceResource`). Applying it unconditionally would let the empty-gated
|
|
371
|
+
* teardown DELETE a namespace typekro merely ADOPTED. So READ the live namespace and keep
|
|
372
|
+
* the stamp ONLY when typekro truly creates it (404) or already owns it (stamp already ==
|
|
373
|
+
* this RGD); otherwise STRIP the stamp from the resource before apply so the live namespace
|
|
374
|
+
* never carries our ownership record and teardown retains it. `namespaceOwnerRgd` is KEPT on
|
|
375
|
+
* props either way — at delete time the empty-gate compares the LIVE annotation to it, and a
|
|
376
|
+
* stripped/adopted namespace (no matching annotation) is retained.
|
|
377
|
+
*
|
|
378
|
+
* Returns props unchanged for non-hoisted-namespace resources or when the stamp should
|
|
379
|
+
* stay; otherwise a shallow copy whose resource has the ownership annotation removed.
|
|
380
|
+
*/
|
|
381
|
+
async function _preserveHoistedNamespaceAdoption(props, logger) {
|
|
382
|
+
if (props.namespaceEmptyGate !== true || props.namespaceOwnerRgd === undefined)
|
|
383
|
+
return props;
|
|
384
|
+
const name = props.resource.metadata?.name;
|
|
385
|
+
if (typeof name !== 'string' || name.length === 0)
|
|
386
|
+
return props;
|
|
387
|
+
const kc = _createClientProvider(props, 'ownership-check');
|
|
388
|
+
const api = createBunCompatibleKubernetesObjectApi(kc);
|
|
389
|
+
// CREATE-FIRST ownership (finding #3), matching the imperative path: attempt to CREATE
|
|
390
|
+
// the namespace WITH the build-time stamp. A 201 is atomic proof typekro created it
|
|
391
|
+
// (owned). A 409 means it already exists — owned ONLY if a prior create by this RGD
|
|
392
|
+
// stamped it; otherwise adopted. This replaces the raceable read→(404)→own decision:
|
|
393
|
+
// there is no window in which another actor creates the namespace between our read and
|
|
394
|
+
// our claim. The generic deployer still SSA-applies the full config afterwards
|
|
395
|
+
// (idempotent), so a 201 here is not the final apply — it is the ownership PROBE.
|
|
396
|
+
let ownsNamespace;
|
|
397
|
+
try {
|
|
398
|
+
const decision = await decideNamespaceOwnershipCreateFirst(api, props.resource, props.namespaceOwnerRgd);
|
|
399
|
+
ownsNamespace = decision.owned;
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
402
|
+
// A non-conflict CREATE failure (or a failed conflict-read) — do NOT claim ownership
|
|
403
|
+
// (conservative: a namespace we cannot provably create/own must not be stampable, or
|
|
404
|
+
// teardown might delete an adopted one). The deployer's SSA apply below surfaces the
|
|
405
|
+
// real error if the cluster is genuinely broken.
|
|
406
|
+
logger.debug('Create-first ownership probe failed; treating as adopted (alchemy)', {
|
|
407
|
+
namespace: name,
|
|
408
|
+
rgd: props.namespaceOwnerRgd,
|
|
409
|
+
error: ensureError(error).message,
|
|
410
|
+
});
|
|
411
|
+
ownsNamespace = false;
|
|
412
|
+
}
|
|
413
|
+
if (ownsNamespace)
|
|
414
|
+
return props; // keep the build-time ownership stamp
|
|
415
|
+
logger.debug('Preserving namespace adoption — stripping build-time ownership stamp (alchemy)', {
|
|
416
|
+
namespace: name,
|
|
417
|
+
rgd: props.namespaceOwnerRgd,
|
|
418
|
+
});
|
|
419
|
+
return { ...props, resource: _stripNamespaceOwnerAnnotation(props.resource) };
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Return a shallow copy of a Namespace resource with the `typekro.io/created-by-rgd`
|
|
423
|
+
* ownership annotation removed. Used by {@link _preserveHoistedNamespaceAdoption} to avoid
|
|
424
|
+
* stamping ownership onto an adopted namespace. By reconcile time alchemy has JSON-round-
|
|
425
|
+
* tripped props to state (no symbols/brands), so a shallow object copy is safe here.
|
|
426
|
+
*/
|
|
427
|
+
function _stripNamespaceOwnerAnnotation(resource) {
|
|
428
|
+
const src = resource;
|
|
429
|
+
const annotations = { ...(src.metadata?.annotations ?? {}) };
|
|
430
|
+
delete annotations[NAMESPACE_OWNER_ANNOTATION];
|
|
431
|
+
return {
|
|
432
|
+
...resource,
|
|
433
|
+
metadata: { ...(src.metadata ?? {}), annotations },
|
|
434
|
+
};
|
|
435
|
+
}
|
|
203
436
|
async function _assertNoSingletonDrift(props, logger) {
|
|
204
437
|
const resource = props.resource;
|
|
205
438
|
const expected = resource.metadata?.annotations?.[SINGLETON_SPEC_FINGERPRINT_ANNOTATION];
|
|
@@ -308,6 +541,9 @@ function propsFromOutput(output) {
|
|
|
308
541
|
deploymentStrategy: output.deploymentStrategy ?? 'kro',
|
|
309
542
|
...(output.kubeConfigOptions !== undefined && { kubeConfigOptions: output.kubeConfigOptions }),
|
|
310
543
|
...(output.kroDeletion !== undefined && { kroDeletion: output.kroDeletion }),
|
|
544
|
+
...(output.retain === true && { retain: true }),
|
|
545
|
+
...(output.namespaceEmptyGate === true && { namespaceEmptyGate: true }),
|
|
546
|
+
...(output.namespaceOwnerRgd !== undefined && { namespaceOwnerRgd: output.namespaceOwnerRgd }),
|
|
311
547
|
};
|
|
312
548
|
}
|
|
313
549
|
/**
|
|
@@ -423,6 +659,46 @@ async function _resolveDeployer(props, phase) {
|
|
|
423
659
|
*/
|
|
424
660
|
async function deleteKroResource(props) {
|
|
425
661
|
const logger = getComponentLogger('alchemy-deployment').child({ alchemyType: KRO_RESOURCE_TYPE });
|
|
662
|
+
// Retained (shared) resources — e.g. the KRO instance control-plane Namespace —
|
|
663
|
+
// must survive a single stack's teardown/prune: another stack targeting the same
|
|
664
|
+
// workload namespace may still have KRO instances inside it. Drop only the state
|
|
665
|
+
// entry (like the deferred shared-RGD delete below), leaving the object on-cluster.
|
|
666
|
+
if (props.retain === true) {
|
|
667
|
+
logger.debug('Skipping delete: resource is retained (shared); dropping state entry only', {
|
|
668
|
+
resourceName: props.resource.metadata?.name,
|
|
669
|
+
namespace: props.namespace,
|
|
670
|
+
});
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
// Empty-gated Namespace teardown (findings #3 + #4): a typekro-hoisted workload
|
|
674
|
+
// Namespace is deleted ONLY if empty and RETAINED if another stack/user still has
|
|
675
|
+
// resources inside it. Alchemy's reverse-topo teardown runs this AFTER the RGD +
|
|
676
|
+
// instance that `dependsOn` it are gone; the instance is deleted FIRST and its delete
|
|
677
|
+
// waits for the CR's `kro.run/finalizer` to clear (KRO graph-deletes all children),
|
|
678
|
+
// so by the time this runs everything THIS instance owned has already drained from
|
|
679
|
+
// the namespace (finding #1's ordering, provided here by the dependency graph + the
|
|
680
|
+
// instance-drain wait rather than an in-line CRD-404 wait; the gate is fail-safe
|
|
681
|
+
// regardless). Replaces the old retain-by-name-equality distinction — runtime
|
|
682
|
+
// emptiness is the truth for both the instance's own namespace and a shared one.
|
|
683
|
+
if (props.namespaceEmptyGate === true) {
|
|
684
|
+
const namespaceName = props.resource.metadata?.name;
|
|
685
|
+
if (typeof namespaceName !== 'string' || namespaceName.length === 0) {
|
|
686
|
+
logger.warn('Empty-gated namespace teardown skipped: resource has no metadata.name', {
|
|
687
|
+
namespace: props.namespace,
|
|
688
|
+
});
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
const kubeConfig = _createClientProvider(props, 'delete');
|
|
692
|
+
await deleteNamespaceIfEmpty(kubeConfig, namespaceName, {
|
|
693
|
+
logger,
|
|
694
|
+
// Ownership record (finding #4) + gated delete (finding #1): only delete a
|
|
695
|
+
// namespace this composition's RGD created, and gate it to a real 404.
|
|
696
|
+
...(props.namespaceOwnerRgd !== undefined && { ownedByRgd: props.namespaceOwnerRgd }),
|
|
697
|
+
...(props.options?.timeout !== undefined && { timeoutMs: props.options.timeout }),
|
|
698
|
+
context: { alchemyType: KRO_RESOURCE_TYPE },
|
|
699
|
+
});
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
426
702
|
const { deployer, dispose } = await _resolveDeployer(props, 'delete');
|
|
427
703
|
try {
|
|
428
704
|
await deployer.delete(props.resource, {
|
|
@@ -473,6 +749,9 @@ async function _deployAndCreateResult(props, deployer, seedResources) {
|
|
|
473
749
|
deploymentStrategy: props.deploymentStrategy,
|
|
474
750
|
...(props.kubeConfigOptions !== undefined && { kubeConfigOptions: props.kubeConfigOptions }),
|
|
475
751
|
...(props.kroDeletion !== undefined && { kroDeletion: props.kroDeletion }),
|
|
752
|
+
...(props.retain === true && { retain: true }),
|
|
753
|
+
...(props.namespaceEmptyGate === true && { namespaceEmptyGate: true }),
|
|
754
|
+
...(props.namespaceOwnerRgd !== undefined && { namespaceOwnerRgd: props.namespaceOwnerRgd }),
|
|
476
755
|
deployedResource: cleanDeployedResource,
|
|
477
756
|
ready: true,
|
|
478
757
|
deployedAt: Date.now(),
|