@rulebricks/cli 2.1.5 → 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.
@@ -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
  }
@@ -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
  */
@@ -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
- name: cert.metadata.name,
263
- dnsNames: cert.spec.dnsNames ?? [],
264
- ready: cert.status.conditions?.some((c) => c.type === "Ready" && c.status === "True") ?? false,
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
  */
@@ -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/charts/stack";
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
  /**
@@ -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/charts/stack";
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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulebricks/cli",
3
- "version": "2.1.5",
3
+ "version": "2.1.6",
4
4
  "description": "CLI for deploying and managing private Rulebricks instances",
5
5
  "type": "module",
6
6
  "bin": {