@rulebricks/cli 2.1.4 → 2.1.6
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/commands/deploy.js +31 -4
- package/dist/lib/kubernetes.d.ts +24 -0
- package/dist/lib/kubernetes.js +98 -5
- package/dist/lib/terraform.js +164 -4
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.js +1 -1
- package/package.json +1 -1
package/dist/commands/deploy.js
CHANGED
|
@@ -8,7 +8,7 @@ import { loadDeploymentConfig, loadDeploymentState, saveDeploymentState, updateD
|
|
|
8
8
|
import { setupTerraformWorkspace, terraformInit, terraformPlan, terraformApply, terraformDestroy, cleanupOrphanedResources, updateKubeconfig, hasTerraformState, isTerraformInstalled, generateTerraformVars, } from "../lib/terraform.js";
|
|
9
9
|
import { checkGcpApplicationDefaultCredentials, checkAzureResourceProviders, checkAzureVmQuota, AZURE_TIER_CORES, } from "../lib/cloudCli.js";
|
|
10
10
|
import { installOrUpgradeChart, upgradeChart, isHelmInstalled, } from "../lib/helm.js";
|
|
11
|
-
import { isKubectlInstalled, checkClusterAccessible, } from "../lib/kubernetes.js";
|
|
11
|
+
import { isKubectlInstalled, checkClusterAccessible, waitForCertificatesReady, } from "../lib/kubernetes.js";
|
|
12
12
|
import { generateHelmValues, updateHelmValuesForTLS, } from "../lib/helmValues.js";
|
|
13
13
|
import { isSupportedDnsProvider, getNamespace, getReleaseName, } from "../types/index.js";
|
|
14
14
|
function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
|
|
@@ -20,11 +20,13 @@ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
|
|
|
20
20
|
const [useExternalDns, setUseExternalDns] = useState(false);
|
|
21
21
|
const infraStartedRef = useRef(false); // Track if we started infra provisioning (ref for sync access)
|
|
22
22
|
const [cleanupError, setCleanupError] = useState(null);
|
|
23
|
+
const [tlsWarning, setTlsWarning] = useState(null);
|
|
23
24
|
const [status, setStatus] = useState({
|
|
24
25
|
preflight: "pending",
|
|
25
26
|
infrastructure: "pending",
|
|
26
27
|
kubeconfig: "pending",
|
|
27
28
|
helmInstall: "pending",
|
|
29
|
+
certCheck: "pending",
|
|
28
30
|
dnsConfig: "pending",
|
|
29
31
|
helmUpgradeTls: "pending",
|
|
30
32
|
});
|
|
@@ -83,7 +85,17 @@ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
|
|
|
83
85
|
const releaseName = getReleaseName(config.name);
|
|
84
86
|
// Upgrade the chart with TLS enabled
|
|
85
87
|
await upgradeChart(name, { releaseName, namespace, version, wait: true });
|
|
86
|
-
setStatus((s) => ({ ...s, helmUpgradeTls: "success" }));
|
|
88
|
+
setStatus((s) => ({ ...s, helmUpgradeTls: "success", certCheck: "running" }));
|
|
89
|
+
setStep("cert-check");
|
|
90
|
+
try {
|
|
91
|
+
await waitForCertificatesReady(namespace);
|
|
92
|
+
setStatus((s) => ({ ...s, certCheck: "success" }));
|
|
93
|
+
}
|
|
94
|
+
catch (certErr) {
|
|
95
|
+
setStatus((s) => ({ ...s, certCheck: "error" }));
|
|
96
|
+
setTlsWarning("TLS certificates are still being issued. " +
|
|
97
|
+
"HTTPS may not be available yet.");
|
|
98
|
+
}
|
|
87
99
|
// Update state
|
|
88
100
|
await updateDeploymentStatus(name, "running", {
|
|
89
101
|
application: {
|
|
@@ -113,6 +125,7 @@ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
|
|
|
113
125
|
...s,
|
|
114
126
|
dnsConfig: "skipped",
|
|
115
127
|
helmUpgradeTls: "skipped",
|
|
128
|
+
certCheck: "skipped",
|
|
116
129
|
}));
|
|
117
130
|
const namespace = getNamespace(config.name);
|
|
118
131
|
// Mark as running without TLS upgrade
|
|
@@ -223,7 +236,18 @@ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
|
|
|
223
236
|
helmInstall: "success",
|
|
224
237
|
dnsConfig: "skipped", // External DNS handles this
|
|
225
238
|
helmUpgradeTls: "skipped", // TLS enabled from start
|
|
239
|
+
certCheck: "running",
|
|
226
240
|
}));
|
|
241
|
+
setStep("cert-check");
|
|
242
|
+
try {
|
|
243
|
+
await waitForCertificatesReady(namespace);
|
|
244
|
+
setStatus((s) => ({ ...s, certCheck: "success" }));
|
|
245
|
+
}
|
|
246
|
+
catch (certErr) {
|
|
247
|
+
setStatus((s) => ({ ...s, certCheck: "error" }));
|
|
248
|
+
setTlsWarning("TLS certificates are still being issued. " +
|
|
249
|
+
"HTTPS may not be available yet.");
|
|
250
|
+
}
|
|
227
251
|
// Update state to running
|
|
228
252
|
await updateDeploymentStatus(name, "running", {
|
|
229
253
|
application: {
|
|
@@ -254,6 +278,7 @@ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
|
|
|
254
278
|
...s,
|
|
255
279
|
dnsConfig: "skipped",
|
|
256
280
|
helmUpgradeTls: "skipped",
|
|
281
|
+
certCheck: "skipped",
|
|
257
282
|
}));
|
|
258
283
|
await updateDeploymentStatus(name, "waiting-dns", {
|
|
259
284
|
application: {
|
|
@@ -421,7 +446,7 @@ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
|
|
|
421
446
|
// Complete screen
|
|
422
447
|
if (step === "complete") {
|
|
423
448
|
const tlsSkipped = status.helmUpgradeTls === "skipped" && !useExternalDns;
|
|
424
|
-
return (_jsx(BorderBox, { title: "Deployment Complete", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: colors.success, bold: true, children: "\u2713 Rulebricks deployed successfully!" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: ["URL:", " ", _jsxs(Text, { color: colors.accent, children: ["https://", config?.domain, "/auth/signup"] })] }), useExternalDns && (_jsx(Text, { color: colors.muted, children: "DNS records will be created automatically by external-dns" })), tlsSkipped && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.warning, children: ["\u26A0 TLS not configured. Run `rulebricks deploy ", name, "` again after DNS setup."] }) }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Next steps:" }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 Visit the URL to complete initial setup"] }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 Run `rulebricks status ", name, "` to check deployment health"] }), tlsSkipped && (_jsxs(Text, { color: colors.muted, children: [" ", "\u2022 Configure DNS and re-run deploy for TLS"] }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.muted, dimColor: true, children: "Tip: If the URL isn't accessible yet, your local DNS may need time to propagate." }), _jsxs(Text, { color: colors.muted, dimColor: true, children: ["Flush DNS cache: ", getDnsFlushCommand()] })] })] }) }));
|
|
449
|
+
return (_jsx(BorderBox, { title: "Deployment Complete", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: colors.success, bold: true, children: "\u2713 Rulebricks deployed successfully!" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: ["URL:", " ", _jsxs(Text, { color: colors.accent, children: ["https://", config?.domain, "/auth/signup"] })] }), useExternalDns && (_jsx(Text, { color: colors.muted, children: "DNS records will be created automatically by external-dns" })), tlsSkipped && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.warning, children: ["\u26A0 TLS not configured. Run `rulebricks deploy ", name, "` again after DNS setup."] }) })), tlsWarning && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.warning, children: ["\u26A0 ", tlsWarning] }) }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Next steps:" }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 Visit the URL to complete initial setup"] }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 Run `rulebricks status ", name, "` to check deployment health"] }), tlsSkipped && (_jsxs(Text, { color: colors.muted, children: [" ", "\u2022 Configure DNS and re-run deploy for TLS"] })), tlsWarning && (_jsxs(Text, { color: colors.muted, children: [" ", "\u2022 Run `rulebricks status ", name, "` to check TLS certificate status"] }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.muted, dimColor: true, children: "Tip: If the URL isn't accessible yet, your local DNS may need time to propagate." }), _jsxs(Text, { color: colors.muted, dimColor: true, children: ["Flush DNS cache: ", getDnsFlushCommand()] })] })] }) }));
|
|
425
450
|
}
|
|
426
451
|
// Progress screen
|
|
427
452
|
const helmInstallLabel = useExternalDns
|
|
@@ -435,7 +460,7 @@ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
|
|
|
435
460
|
? "Planning changes"
|
|
436
461
|
: step === "infra-apply"
|
|
437
462
|
? "Applying infrastructure"
|
|
438
|
-
: undefined }), _jsx(StatusLine, { status: status.kubeconfig, label: "Kubernetes configuration" }), _jsx(StatusLine, { status: status.helmInstall, label: helmInstallLabel }), !useExternalDns && (_jsxs(_Fragment, { children: [_jsx(StatusLine, { status: status.dnsConfig, label: "DNS configuration" }), _jsx(StatusLine, { status: status.helmUpgradeTls, label: "TLS configuration" })] })), step !== "dns-wait" && (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: getStepLabel(step, useExternalDns) }) }))] }) }));
|
|
463
|
+
: undefined }), _jsx(StatusLine, { status: status.kubeconfig, label: "Kubernetes configuration" }), _jsx(StatusLine, { status: status.helmInstall, label: helmInstallLabel }), !useExternalDns && (_jsxs(_Fragment, { children: [_jsx(StatusLine, { status: status.dnsConfig, label: "DNS configuration" }), _jsx(StatusLine, { status: status.helmUpgradeTls, label: "TLS configuration" })] })), _jsx(StatusLine, { status: status.certCheck, label: "TLS certificate verification" }), step !== "dns-wait" && (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: getStepLabel(step, useExternalDns) }) }))] }) }));
|
|
439
464
|
}
|
|
440
465
|
function getDnsFlushCommand() {
|
|
441
466
|
switch (platform()) {
|
|
@@ -471,6 +496,8 @@ function getStepLabel(step, useExternalDns) {
|
|
|
471
496
|
return "Waiting for DNS configuration...";
|
|
472
497
|
case "helm-upgrade-tls":
|
|
473
498
|
return "Enabling TLS certificates...";
|
|
499
|
+
case "cert-check":
|
|
500
|
+
return "Verifying TLS certificates...";
|
|
474
501
|
default:
|
|
475
502
|
return "Processing...";
|
|
476
503
|
}
|
package/dist/lib/kubernetes.d.ts
CHANGED
|
@@ -58,7 +58,31 @@ export interface CertificateStatus {
|
|
|
58
58
|
name: string;
|
|
59
59
|
dnsNames: string[];
|
|
60
60
|
ready: boolean;
|
|
61
|
+
failed: boolean;
|
|
62
|
+
message?: string;
|
|
61
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Deletes a failed cert-manager Certificate and recreates it from its spec,
|
|
66
|
+
* bypassing cert-manager's exponential backoff on failed issuance attempts.
|
|
67
|
+
* The delete cascades to the failed CertificateRequest and ACME Order via
|
|
68
|
+
* owner references, so the recreated Certificate starts with a clean slate.
|
|
69
|
+
*/
|
|
70
|
+
export declare function recreateFailedCertificate(namespace: string, certName: string): Promise<boolean>;
|
|
71
|
+
/**
|
|
72
|
+
* Polls cert-manager Certificates until all are Ready, with automatic retry
|
|
73
|
+
* for transient ACME failures (e.g. order finalization race conditions).
|
|
74
|
+
*
|
|
75
|
+
* On failure detection: deletes and recreates the Certificate resource to
|
|
76
|
+
* bypass cert-manager's 1-hour exponential backoff, then continues polling.
|
|
77
|
+
*
|
|
78
|
+
* Throws on timeout with details about which certs are not ready.
|
|
79
|
+
* Returns silently if no Certificate resources exist in the namespace.
|
|
80
|
+
*/
|
|
81
|
+
export declare function waitForCertificatesReady(namespace: string, options?: {
|
|
82
|
+
timeoutMs?: number;
|
|
83
|
+
pollIntervalMs?: number;
|
|
84
|
+
maxRetries?: number;
|
|
85
|
+
}): Promise<void>;
|
|
62
86
|
/**
|
|
63
87
|
* Streams logs from a pod
|
|
64
88
|
*/
|
package/dist/lib/kubernetes.js
CHANGED
|
@@ -258,16 +258,109 @@ export async function getCertificateStatus(namespace = DEFAULT_NAMESPACE) {
|
|
|
258
258
|
"json",
|
|
259
259
|
]);
|
|
260
260
|
const data = JSON.parse(stdout);
|
|
261
|
-
return data.items.map((cert) =>
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
ready
|
|
265
|
-
|
|
261
|
+
return data.items.map((cert) => {
|
|
262
|
+
const readyCond = cert.status.conditions?.find((c) => c.type === "Ready");
|
|
263
|
+
const issuingCond = cert.status.conditions?.find((c) => c.type === "Issuing");
|
|
264
|
+
const ready = readyCond?.status === "True";
|
|
265
|
+
const failed = !ready &&
|
|
266
|
+
issuingCond?.status === "False" &&
|
|
267
|
+
issuingCond?.reason === "Failed";
|
|
268
|
+
return {
|
|
269
|
+
name: cert.metadata.name,
|
|
270
|
+
dnsNames: cert.spec.dnsNames ?? [],
|
|
271
|
+
ready,
|
|
272
|
+
failed: failed ?? false,
|
|
273
|
+
message: failed ? issuingCond?.message : readyCond?.message,
|
|
274
|
+
};
|
|
275
|
+
});
|
|
266
276
|
}
|
|
267
277
|
catch {
|
|
268
278
|
return [];
|
|
269
279
|
}
|
|
270
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* Deletes a failed cert-manager Certificate and recreates it from its spec,
|
|
283
|
+
* bypassing cert-manager's exponential backoff on failed issuance attempts.
|
|
284
|
+
* The delete cascades to the failed CertificateRequest and ACME Order via
|
|
285
|
+
* owner references, so the recreated Certificate starts with a clean slate.
|
|
286
|
+
*/
|
|
287
|
+
export async function recreateFailedCertificate(namespace, certName) {
|
|
288
|
+
try {
|
|
289
|
+
const { stdout } = await execa("kubectl", [
|
|
290
|
+
"get",
|
|
291
|
+
"certificate",
|
|
292
|
+
certName,
|
|
293
|
+
"-n",
|
|
294
|
+
namespace,
|
|
295
|
+
"-o",
|
|
296
|
+
"json",
|
|
297
|
+
]);
|
|
298
|
+
const cert = JSON.parse(stdout);
|
|
299
|
+
const recreated = {
|
|
300
|
+
apiVersion: "cert-manager.io/v1",
|
|
301
|
+
kind: "Certificate",
|
|
302
|
+
metadata: {
|
|
303
|
+
name: cert.metadata.name,
|
|
304
|
+
namespace: cert.metadata.namespace,
|
|
305
|
+
...(cert.metadata.labels ? { labels: cert.metadata.labels } : {}),
|
|
306
|
+
...(cert.metadata.annotations
|
|
307
|
+
? { annotations: cert.metadata.annotations }
|
|
308
|
+
: {}),
|
|
309
|
+
},
|
|
310
|
+
spec: cert.spec,
|
|
311
|
+
};
|
|
312
|
+
await execa("kubectl", ["delete", "certificate", certName, "-n", namespace]);
|
|
313
|
+
await execa("kubectl", ["apply", "-f", "-"], {
|
|
314
|
+
input: JSON.stringify(recreated),
|
|
315
|
+
});
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Polls cert-manager Certificates until all are Ready, with automatic retry
|
|
324
|
+
* for transient ACME failures (e.g. order finalization race conditions).
|
|
325
|
+
*
|
|
326
|
+
* On failure detection: deletes and recreates the Certificate resource to
|
|
327
|
+
* bypass cert-manager's 1-hour exponential backoff, then continues polling.
|
|
328
|
+
*
|
|
329
|
+
* Throws on timeout with details about which certs are not ready.
|
|
330
|
+
* Returns silently if no Certificate resources exist in the namespace.
|
|
331
|
+
*/
|
|
332
|
+
export async function waitForCertificatesReady(namespace, options) {
|
|
333
|
+
const { timeoutMs = 120_000, pollIntervalMs = 5_000, maxRetries = 1, } = options ?? {};
|
|
334
|
+
let retriesUsed = 0;
|
|
335
|
+
const deadline = Date.now() + timeoutMs;
|
|
336
|
+
while (Date.now() < deadline) {
|
|
337
|
+
const certs = await getCertificateStatus(namespace);
|
|
338
|
+
if (certs.length === 0)
|
|
339
|
+
return;
|
|
340
|
+
if (certs.every((c) => c.ready))
|
|
341
|
+
return;
|
|
342
|
+
const failed = certs.filter((c) => c.failed);
|
|
343
|
+
if (failed.length > 0 && retriesUsed < maxRetries) {
|
|
344
|
+
for (const cert of failed) {
|
|
345
|
+
await recreateFailedCertificate(namespace, cert.name);
|
|
346
|
+
}
|
|
347
|
+
retriesUsed++;
|
|
348
|
+
}
|
|
349
|
+
await sleep(pollIntervalMs);
|
|
350
|
+
}
|
|
351
|
+
// Final check after timeout
|
|
352
|
+
const certs = await getCertificateStatus(namespace);
|
|
353
|
+
if (certs.length > 0 && certs.every((c) => c.ready))
|
|
354
|
+
return;
|
|
355
|
+
const notReady = certs.filter((c) => !c.ready);
|
|
356
|
+
if (notReady.length > 0) {
|
|
357
|
+
const details = notReady
|
|
358
|
+
.map((c) => ` ${c.name}: ${c.message || "not ready"}`)
|
|
359
|
+
.join("\n");
|
|
360
|
+
throw new Error(`TLS certificates not ready after ${timeoutMs / 1000}s:\n${details}\n\n` +
|
|
361
|
+
`Run 'rulebricks status' to check certificate status.`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
271
364
|
/**
|
|
272
365
|
* Streams logs from a pod
|
|
273
366
|
*/
|
package/dist/lib/terraform.js
CHANGED
|
@@ -265,7 +265,32 @@ async function deleteAwsCloudWatchLogGroup(clusterName, region) {
|
|
|
265
265
|
}
|
|
266
266
|
catch { /* may not exist */ }
|
|
267
267
|
}
|
|
268
|
-
|
|
268
|
+
/**
|
|
269
|
+
* Captures the OIDC issuer URL from an EKS cluster before it's deleted.
|
|
270
|
+
* The URL uses a random cluster ID (not the cluster name), so we must
|
|
271
|
+
* grab it while the cluster still exists to identify the OIDC provider later.
|
|
272
|
+
*/
|
|
273
|
+
async function getEksOidcIssuer(clusterName, region) {
|
|
274
|
+
try {
|
|
275
|
+
const { stdout } = await execa('aws', [
|
|
276
|
+
'eks', 'describe-cluster',
|
|
277
|
+
'--name', clusterName,
|
|
278
|
+
'--region', region,
|
|
279
|
+
'--query', 'cluster.identity.oidc.issuer',
|
|
280
|
+
'--output', 'text',
|
|
281
|
+
]);
|
|
282
|
+
const url = stdout.trim();
|
|
283
|
+
return url && url !== 'None' ? url : undefined;
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
async function deleteAwsOidcProvider(oidcIssuerUrl) {
|
|
290
|
+
if (!oidcIssuerUrl)
|
|
291
|
+
return;
|
|
292
|
+
// Strip the https:// prefix to match how IAM stores the URL
|
|
293
|
+
const issuerHost = oidcIssuerUrl.replace('https://', '');
|
|
269
294
|
let providerArns;
|
|
270
295
|
try {
|
|
271
296
|
const { stdout } = await execa('aws', [
|
|
@@ -286,7 +311,7 @@ async function deleteAwsOidcProvider(clusterName) {
|
|
|
286
311
|
'--output', 'json',
|
|
287
312
|
]);
|
|
288
313
|
const parsed = JSON.parse(stdout);
|
|
289
|
-
if (parsed.Url && parsed.Url
|
|
314
|
+
if (parsed.Url && issuerHost.includes(parsed.Url)) {
|
|
290
315
|
await execa('aws', [
|
|
291
316
|
'iam', 'delete-open-id-connect-provider',
|
|
292
317
|
'--open-id-connect-provider-arn', arn,
|
|
@@ -296,6 +321,29 @@ async function deleteAwsOidcProvider(clusterName) {
|
|
|
296
321
|
catch { /* skip */ }
|
|
297
322
|
}
|
|
298
323
|
}
|
|
324
|
+
async function releaseAwsElasticIps(clusterName, region) {
|
|
325
|
+
try {
|
|
326
|
+
const { stdout } = await execa('aws', [
|
|
327
|
+
'ec2', 'describe-addresses',
|
|
328
|
+
'--filters', `Name=tag:Name,Values=*${clusterName}*`,
|
|
329
|
+
'--region', region,
|
|
330
|
+
'--query', 'Addresses[?AssociationId==null].AllocationId',
|
|
331
|
+
'--output', 'json',
|
|
332
|
+
]);
|
|
333
|
+
const allocationIds = JSON.parse(stdout);
|
|
334
|
+
for (const id of allocationIds) {
|
|
335
|
+
try {
|
|
336
|
+
await execa('aws', [
|
|
337
|
+
'ec2', 'release-address',
|
|
338
|
+
'--allocation-id', id,
|
|
339
|
+
'--region', region,
|
|
340
|
+
]);
|
|
341
|
+
}
|
|
342
|
+
catch { /* may already be released */ }
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
catch { /* skip */ }
|
|
346
|
+
}
|
|
299
347
|
async function deleteAwsIamRole(roleName) {
|
|
300
348
|
// Detach all managed policies
|
|
301
349
|
try {
|
|
@@ -343,6 +391,107 @@ async function deleteAwsIamRole(roleName) {
|
|
|
343
391
|
}
|
|
344
392
|
catch { /* may not exist */ }
|
|
345
393
|
}
|
|
394
|
+
async function deleteAwsKmsAlias(clusterName, region) {
|
|
395
|
+
const aliasName = `alias/eks/${clusterName}`;
|
|
396
|
+
let keyId;
|
|
397
|
+
// Find the KMS key behind the alias so we can schedule it for deletion
|
|
398
|
+
try {
|
|
399
|
+
const { stdout } = await execa('aws', [
|
|
400
|
+
'kms', 'list-aliases',
|
|
401
|
+
'--query', `Aliases[?AliasName=='${aliasName}'].TargetKeyId | [0]`,
|
|
402
|
+
'--output', 'text',
|
|
403
|
+
'--region', region,
|
|
404
|
+
]);
|
|
405
|
+
const id = stdout.trim();
|
|
406
|
+
if (id && id !== 'None') {
|
|
407
|
+
keyId = id;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
catch { /* skip */ }
|
|
411
|
+
// Delete the alias (unique name constraint -- blocks re-deploy if left behind)
|
|
412
|
+
try {
|
|
413
|
+
await execa('aws', [
|
|
414
|
+
'kms', 'delete-alias',
|
|
415
|
+
'--alias-name', aliasName,
|
|
416
|
+
'--region', region,
|
|
417
|
+
]);
|
|
418
|
+
}
|
|
419
|
+
catch { /* may not exist */ }
|
|
420
|
+
// Schedule the underlying key for deletion (7-day mandatory minimum)
|
|
421
|
+
if (keyId) {
|
|
422
|
+
try {
|
|
423
|
+
await execa('aws', [
|
|
424
|
+
'kms', 'schedule-key-deletion',
|
|
425
|
+
'--key-id', keyId,
|
|
426
|
+
'--pending-window-in-days', '7',
|
|
427
|
+
'--region', region,
|
|
428
|
+
]);
|
|
429
|
+
}
|
|
430
|
+
catch { /* key may already be pending deletion or not exist */ }
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Finds KMS keys by the description the EKS module uses, and schedules them for
|
|
435
|
+
* deletion. Catches keys that survive after their alias is already deleted.
|
|
436
|
+
*/
|
|
437
|
+
async function scheduleAwsOrphanedKmsKeys(clusterName, region) {
|
|
438
|
+
try {
|
|
439
|
+
const { stdout } = await execa('aws', [
|
|
440
|
+
'kms', 'list-keys',
|
|
441
|
+
'--region', region,
|
|
442
|
+
'--query', 'Keys[].KeyId',
|
|
443
|
+
'--output', 'json',
|
|
444
|
+
]);
|
|
445
|
+
const keyIds = JSON.parse(stdout);
|
|
446
|
+
for (const keyId of keyIds) {
|
|
447
|
+
try {
|
|
448
|
+
const { stdout: meta } = await execa('aws', [
|
|
449
|
+
'kms', 'describe-key',
|
|
450
|
+
'--key-id', keyId,
|
|
451
|
+
'--region', region,
|
|
452
|
+
'--query', 'KeyMetadata.{State:KeyState,Desc:Description,Manager:KeyManager}',
|
|
453
|
+
'--output', 'json',
|
|
454
|
+
]);
|
|
455
|
+
const info = JSON.parse(meta);
|
|
456
|
+
if (info.Manager === 'CUSTOMER' &&
|
|
457
|
+
info.State === 'Enabled' &&
|
|
458
|
+
info.Desc.includes(clusterName)) {
|
|
459
|
+
await execa('aws', [
|
|
460
|
+
'kms', 'schedule-key-deletion',
|
|
461
|
+
'--key-id', keyId,
|
|
462
|
+
'--pending-window-in-days', '7',
|
|
463
|
+
'--region', region,
|
|
464
|
+
]);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
catch { /* skip individual key */ }
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
catch { /* skip */ }
|
|
471
|
+
}
|
|
472
|
+
async function deleteAwsLaunchTemplates(clusterName, region) {
|
|
473
|
+
try {
|
|
474
|
+
const { stdout } = await execa('aws', [
|
|
475
|
+
'ec2', 'describe-launch-templates',
|
|
476
|
+
'--filters', `Name=tag:Environment,Values=rulebricks`,
|
|
477
|
+
'--region', region,
|
|
478
|
+
'--query', 'LaunchTemplates[].LaunchTemplateId',
|
|
479
|
+
'--output', 'json',
|
|
480
|
+
]);
|
|
481
|
+
const ids = JSON.parse(stdout);
|
|
482
|
+
for (const id of ids) {
|
|
483
|
+
try {
|
|
484
|
+
await execa('aws', [
|
|
485
|
+
'ec2', 'delete-launch-template',
|
|
486
|
+
'--launch-template-id', id,
|
|
487
|
+
'--region', region,
|
|
488
|
+
]);
|
|
489
|
+
}
|
|
490
|
+
catch { /* may not exist or in use */ }
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
catch { /* skip */ }
|
|
494
|
+
}
|
|
346
495
|
async function deleteAwsIamPolicy(policyName) {
|
|
347
496
|
try {
|
|
348
497
|
const { stdout } = await execa('aws', [
|
|
@@ -366,20 +515,31 @@ async function deleteAwsIamPolicy(policyName) {
|
|
|
366
515
|
* Entirely best-effort: every step silently swallows errors.
|
|
367
516
|
*/
|
|
368
517
|
async function cleanupAwsResources(clusterName, region) {
|
|
518
|
+
// Capture the OIDC issuer URL BEFORE deleting the cluster -- the URL uses a
|
|
519
|
+
// random cluster ID (not the cluster name) so we can't find it after deletion.
|
|
520
|
+
const oidcIssuerUrl = await getEksOidcIssuer(clusterName, region);
|
|
369
521
|
// 1. EKS node groups (must be deleted before cluster)
|
|
370
522
|
await deleteAwsEksNodeGroups(clusterName, region);
|
|
371
523
|
// 2. EKS cluster
|
|
372
524
|
await deleteAwsEksCluster(clusterName, region);
|
|
373
525
|
// 3. CloudWatch log group (now safe -- cluster is gone, won't be recreated)
|
|
374
526
|
await deleteAwsCloudWatchLogGroup(clusterName, region);
|
|
375
|
-
// 4. OIDC provider (
|
|
376
|
-
await deleteAwsOidcProvider(
|
|
527
|
+
// 4. OIDC provider (matched by issuer URL captured above)
|
|
528
|
+
await deleteAwsOidcProvider(oidcIssuerUrl);
|
|
377
529
|
// 5. IAM roles created by terraform modules
|
|
378
530
|
await deleteAwsIamRole(`${clusterName}-ebs-csi`);
|
|
379
531
|
await deleteAwsIamRole(`${clusterName}-external-dns`);
|
|
380
532
|
await deleteAwsIamRole(`${clusterName}-vector`);
|
|
381
533
|
// 6. Customer-managed IAM policies
|
|
382
534
|
await deleteAwsIamPolicy(`${clusterName}-vector-s3`);
|
|
535
|
+
// 7. KMS key + alias (created by EKS module for envelope encryption)
|
|
536
|
+
await deleteAwsKmsAlias(clusterName, region);
|
|
537
|
+
// 8. KMS keys that lost their alias but are still Enabled (matched by description)
|
|
538
|
+
await scheduleAwsOrphanedKmsKeys(clusterName, region);
|
|
539
|
+
// 9. Launch templates (created by EKS managed node groups)
|
|
540
|
+
await deleteAwsLaunchTemplates(clusterName, region);
|
|
541
|
+
// 10. Elastic IPs (created by VPC module for NAT gateways, cost money if leaked)
|
|
542
|
+
await releaseAwsElasticIps(clusterName, region);
|
|
383
543
|
}
|
|
384
544
|
/**
|
|
385
545
|
* Destroys Terraform infrastructure, then sweeps remaining cloud resources.
|
package/dist/types/index.d.ts
CHANGED
|
@@ -701,7 +701,7 @@ export declare const ProfileConfigSchema: z.ZodObject<{
|
|
|
701
701
|
}>;
|
|
702
702
|
export type ProfileConfig = z.infer<typeof ProfileConfigSchema>;
|
|
703
703
|
export declare const CHANGELOG_URL = "https://rulebricks.com/docs/changelog";
|
|
704
|
-
export declare const HELM_CHART_OCI = "oci://ghcr.io/rulebricks/
|
|
704
|
+
export declare const HELM_CHART_OCI = "oci://ghcr.io/rulebricks/helm/stack";
|
|
705
705
|
export declare const DEFAULT_NAMESPACE = "rulebricks";
|
|
706
706
|
export declare const LEGACY_RELEASE_NAME = "rulebricks";
|
|
707
707
|
/**
|
package/dist/types/index.js
CHANGED
|
@@ -603,7 +603,7 @@ export const ProfileConfigSchema = z.object({
|
|
|
603
603
|
});
|
|
604
604
|
// Constants
|
|
605
605
|
export const CHANGELOG_URL = "https://rulebricks.com/docs/changelog";
|
|
606
|
-
export const HELM_CHART_OCI = "oci://ghcr.io/rulebricks/
|
|
606
|
+
export const HELM_CHART_OCI = "oci://ghcr.io/rulebricks/helm/stack";
|
|
607
607
|
// Legacy namespace/release name - kept for backwards compatibility with existing deployments
|
|
608
608
|
export const DEFAULT_NAMESPACE = "rulebricks";
|
|
609
609
|
export const LEGACY_RELEASE_NAME = "rulebricks";
|