@rulebricks/cli 2.1.7 → 2.3.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.
Files changed (117) hide show
  1. package/README.md +51 -16
  2. package/cluster-setup/aws/README.md +96 -47
  3. package/cluster-setup/aws/check-aws-access.sh +216 -52
  4. package/cluster-setup/aws/parameters.json +13 -0
  5. package/cluster-setup/aws/rulebricks-cluster.cfn.yaml +355 -0
  6. package/cluster-setup/azure/README.md +103 -55
  7. package/cluster-setup/azure/check-aks-prereqs.sh +236 -56
  8. package/cluster-setup/azure/parameters.json +30 -0
  9. package/cluster-setup/azure/rulebricks-cluster.bicep +546 -0
  10. package/cluster-setup/gcp/README.md +51 -34
  11. package/cluster-setup/gcp/check-gke-prereqs.sh +222 -60
  12. package/dist/commands/backup.d.ts +5 -0
  13. package/dist/commands/backup.js +104 -0
  14. package/dist/commands/deploy.d.ts +3 -1
  15. package/dist/commands/deploy.js +226 -326
  16. package/dist/commands/destroy.d.ts +1 -1
  17. package/dist/commands/destroy.js +73 -123
  18. package/dist/commands/init.d.ts +5 -1
  19. package/dist/commands/init.js +78 -54
  20. package/dist/commands/list.d.ts +1 -0
  21. package/dist/commands/list.js +74 -0
  22. package/dist/commands/open.d.ts +1 -1
  23. package/dist/commands/open.js +4 -12
  24. package/dist/commands/redeploy.d.ts +6 -0
  25. package/dist/commands/redeploy.js +310 -0
  26. package/dist/commands/restore.d.ts +5 -0
  27. package/dist/commands/restore.js +338 -0
  28. package/dist/commands/status.js +62 -49
  29. package/dist/commands/upgrade.js +74 -51
  30. package/dist/components/DNSWaitScreen.d.ts +5 -1
  31. package/dist/components/DNSWaitScreen.js +47 -41
  32. package/dist/components/Wizard/WizardContext.d.ts +157 -36
  33. package/dist/components/Wizard/WizardContext.js +872 -160
  34. package/dist/components/Wizard/steps/CloudProviderStep.js +192 -107
  35. package/dist/components/Wizard/steps/DomainStep.js +5 -24
  36. package/dist/components/Wizard/steps/ExternalServicesStep.d.ts +6 -0
  37. package/dist/components/Wizard/steps/ExternalServicesStep.js +645 -0
  38. package/dist/components/Wizard/steps/FeatureConfigStep.d.ts +2 -1
  39. package/dist/components/Wizard/steps/FeatureConfigStep.js +739 -425
  40. package/dist/components/Wizard/steps/FeaturesStep.js +31 -35
  41. package/dist/components/Wizard/steps/ObservabilityStep.d.ts +6 -0
  42. package/dist/components/Wizard/steps/ObservabilityStep.js +137 -0
  43. package/dist/components/Wizard/steps/ReviewStep.d.ts +2 -1
  44. package/dist/components/Wizard/steps/ReviewStep.js +56 -12
  45. package/dist/components/Wizard/steps/StorageStep.d.ts +9 -0
  46. package/dist/components/Wizard/steps/StorageStep.js +592 -0
  47. package/dist/components/Wizard/steps/SupabaseCredentialsStep.js +20 -21
  48. package/dist/components/Wizard/steps/VersionStep.js +45 -23
  49. package/dist/components/Wizard/steps/index.d.ts +3 -3
  50. package/dist/components/Wizard/steps/index.js +3 -3
  51. package/dist/components/common/CommandApproval.d.ts +12 -0
  52. package/dist/components/common/CommandApproval.js +91 -0
  53. package/dist/components/common/DeploymentPicker.d.ts +14 -0
  54. package/dist/components/common/DeploymentPicker.js +16 -0
  55. package/dist/components/common/index.d.ts +2 -0
  56. package/dist/components/common/index.js +2 -0
  57. package/dist/index.js +94 -62
  58. package/dist/lib/cloudCli.d.ts +134 -63
  59. package/dist/lib/cloudCli.js +512 -220
  60. package/dist/lib/clusterSetupDefaults.d.ts +30 -0
  61. package/dist/lib/clusterSetupDefaults.js +64 -0
  62. package/dist/lib/commandApproval.d.ts +26 -0
  63. package/dist/lib/commandApproval.js +114 -0
  64. package/dist/lib/config.d.ts +12 -10
  65. package/dist/lib/config.js +91 -33
  66. package/dist/lib/configFixtures.d.ts +5 -0
  67. package/dist/lib/configFixtures.js +513 -0
  68. package/dist/lib/deploymentHealth.d.ts +32 -0
  69. package/dist/lib/deploymentHealth.js +157 -0
  70. package/dist/lib/dns.d.ts +1 -1
  71. package/dist/lib/dns.js +19 -1
  72. package/dist/lib/dns.test.d.ts +1 -0
  73. package/dist/lib/dns.test.js +27 -0
  74. package/dist/lib/dockerHub.d.ts +12 -1
  75. package/dist/lib/dockerHub.js +18 -8
  76. package/dist/lib/helm.d.ts +4 -0
  77. package/dist/lib/helm.js +16 -0
  78. package/dist/lib/helmValues.d.ts +25 -0
  79. package/dist/lib/helmValues.js +1762 -289
  80. package/dist/lib/helmValues.test.d.ts +1 -0
  81. package/dist/lib/helmValues.test.js +966 -0
  82. package/dist/lib/htpasswd.d.ts +1 -0
  83. package/dist/lib/htpasswd.js +15 -0
  84. package/dist/lib/kubernetes.d.ts +124 -17
  85. package/dist/lib/kubernetes.js +576 -145
  86. package/dist/lib/secrets.d.ts +23 -0
  87. package/dist/lib/secrets.js +158 -0
  88. package/dist/lib/validateValues.d.ts +31 -0
  89. package/dist/lib/validateValues.js +253 -0
  90. package/dist/lib/versions.d.ts +82 -11
  91. package/dist/lib/versions.js +131 -31
  92. package/dist/lib/versions.test.d.ts +1 -0
  93. package/dist/lib/versions.test.js +81 -0
  94. package/dist/lib/wizardSteps.d.ts +14 -0
  95. package/dist/lib/wizardSteps.js +23 -0
  96. package/dist/lib/workloadIdentity.d.ts +26 -0
  97. package/dist/lib/workloadIdentity.js +323 -0
  98. package/dist/lib/workloadIdentity.test.d.ts +1 -0
  99. package/dist/lib/workloadIdentity.test.js +57 -0
  100. package/dist/types/index.d.ts +1860 -164
  101. package/dist/types/index.js +518 -295
  102. package/package.json +9 -4
  103. package/schema/values.schema.json +1934 -0
  104. package/cluster-setup/aws/cluster.yaml +0 -33
  105. package/cluster-setup/azure/main.bicep +0 -282
  106. package/cluster-setup/azure/main.parameters.json +0 -21
  107. package/dist/components/Wizard/steps/CredentialsStep.d.ts +0 -6
  108. package/dist/components/Wizard/steps/CredentialsStep.js +0 -22
  109. package/dist/components/Wizard/steps/DeploymentModeStep.d.ts +0 -5
  110. package/dist/components/Wizard/steps/DeploymentModeStep.js +0 -26
  111. package/dist/components/Wizard/steps/TierStep.d.ts +0 -6
  112. package/dist/components/Wizard/steps/TierStep.js +0 -29
  113. package/dist/lib/terraform.d.ts +0 -66
  114. package/dist/lib/terraform.js +0 -754
  115. package/terraform/aws/main.tf +0 -355
  116. package/terraform/azure/main.tf +0 -371
  117. package/terraform/gcp/main.tf +0 -407
@@ -0,0 +1,592 @@
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Box, Text } from "ink";
4
+ import SelectInput from "ink-select-input";
5
+ import TextInput from "ink-text-input";
6
+ import { useWizard } from "../WizardContext.js";
7
+ import { BorderBox, useGatedInput, useTheme } from "../../common/index.js";
8
+ import { Spinner } from "../../common/Spinner.js";
9
+ import { CLOUD_REGIONS, } from "../../../types/index.js";
10
+ import { listRegions, listBucketsInRegion, listAzureBlobContainers, listIamRoles, listAzureManagedIdentities, getAzureTenantId, listGcpServiceAccounts, } from "../../../lib/cloudCli.js";
11
+ import { findClusterSetupDefaultIndex } from "../../../lib/clusterSetupDefaults.js";
12
+ // Sentinel value used in select lists to drop into manual text entry.
13
+ const MANUAL = "__manual__";
14
+ const PROVIDERS = [
15
+ { label: "AWS S3", value: "s3" },
16
+ { label: "Azure Blob Storage", value: "azure-blob" },
17
+ { label: "Google Cloud Storage", value: "gcs" },
18
+ ];
19
+ const AZURE_AUTH = [
20
+ { label: "Workload identity (recommended)", value: "workload-identity" },
21
+ { label: "Connection string Secret (fallback)", value: "secret" },
22
+ ];
23
+ // Healthy cron presets so users don't hand-write cron for DB backups.
24
+ const BACKUP_FREQUENCY_PRESETS = [
25
+ { label: "Every 30 minutes", value: "*/30 * * * *" },
26
+ { label: "Hourly", value: "0 * * * *" },
27
+ { label: "Every 6 hours", value: "0 */6 * * *" },
28
+ { label: "Every 12 hours", value: "0 */12 * * *" },
29
+ { label: "Daily (02:00 UTC)", value: "0 2 * * *" },
30
+ { label: "Weekly (Sun 02:00 UTC)", value: "0 2 * * 0" },
31
+ { label: "Custom cron…", value: "__custom__" },
32
+ ];
33
+ const CUSTOM_CRON = "__custom__";
34
+ function providerToCloud(provider) {
35
+ if (provider === "azure-blob")
36
+ return "azure";
37
+ if (provider === "gcs")
38
+ return "gcp";
39
+ return "aws";
40
+ }
41
+ export function storageProviderForCloud(provider) {
42
+ if (provider === "azure")
43
+ return "azure-blob";
44
+ if (provider === "gcp")
45
+ return "gcs";
46
+ return "s3";
47
+ }
48
+ // GKE zonal clusters report a zone (e.g. us-central1-a) as their location, but
49
+ // object storage wants the region (us-central1). GCP zones always end in a
50
+ // single-letter suffix; AWS/Azure regions never do, so this is GCS-only.
51
+ function gcpZoneToRegion(location) {
52
+ return location.replace(/-[a-z]$/, "");
53
+ }
54
+ export function storageRegionForCloud(cloudProvider, clusterRegion, savedStorageProvider, savedStorageRegion) {
55
+ const provider = storageProviderForCloud(cloudProvider);
56
+ if (savedStorageProvider === provider && savedStorageRegion) {
57
+ return savedStorageRegion;
58
+ }
59
+ return provider === "gcs" ? gcpZoneToRegion(clusterRegion) : clusterRegion;
60
+ }
61
+ export function StorageStep({ onComplete, onBack }) {
62
+ const { state, dispatch } = useWizard();
63
+ const { colors } = useTheme();
64
+ // Object storage always lives in the same cloud as the cluster, so derive the
65
+ // provider from the cluster's cloud instead of asking again. (No real scenario
66
+ // deploys to e.g. an AKS cluster and writes to S3.)
67
+ const provider = storageProviderForCloud(state.provider);
68
+ // The storage region is likewise assumed to be the cluster's region, so the
69
+ // region question is skipped whenever one is known (Esc from the bucket list
70
+ // still drops back to region selection for the rare cross-region setup).
71
+ // Only the manual-cluster path (no region captured) asks up front.
72
+ const savedStorageMatchesProvider = state.storageProvider === provider;
73
+ const initialRegion = storageRegionForCloud(state.provider, state.region, state.storageProvider, state.storageRegion) || "";
74
+ const [field, setField] = useState(initialRegion ? "bucket-loading" : "region-loading");
75
+ const [region, setRegion] = useState(initialRegion);
76
+ const [bucket, setBucket] = useState(savedStorageMatchesProvider ? state.storageBucket || "" : "");
77
+ const [roleArn, setRoleArn] = useState(savedStorageMatchesProvider && provider === "s3"
78
+ ? state.storageAwsIamRoleArn || ""
79
+ : "");
80
+ const [azureContainer, setAzureContainer] = useState(savedStorageMatchesProvider && provider === "azure-blob"
81
+ ? state.storageAzureBlobContainer || "rulebricks"
82
+ : "rulebricks");
83
+ const [authMode, setAuthMode] = useState(savedStorageMatchesProvider && provider === "azure-blob"
84
+ ? state.storageCloudAuthMode || "workload-identity"
85
+ : "workload-identity");
86
+ const [azureClientId, setAzureClientId] = useState(savedStorageMatchesProvider && provider === "azure-blob"
87
+ ? state.storageAzureBlobClientId || ""
88
+ : "");
89
+ const [azureTenantId, setAzureTenantId] = useState(savedStorageMatchesProvider && provider === "azure-blob"
90
+ ? state.storageAzureBlobTenantId || ""
91
+ : "");
92
+ const [tenantAutoDetected, setTenantAutoDetected] = useState(false);
93
+ const [azureSecretRef, setAzureSecretRef] = useState(savedStorageMatchesProvider && provider === "azure-blob"
94
+ ? state.storageAzureBlobConnectionStringSecretRef || ""
95
+ : "");
96
+ const [gcpServiceAccount, setGcpServiceAccount] = useState(savedStorageMatchesProvider && provider === "gcs"
97
+ ? state.storageGcpServiceAccountEmail || ""
98
+ : "");
99
+ // Database backups are configured in this same step only when the chart owns
100
+ // the in-cluster Postgres. Managed/external databases own their own backups.
101
+ const usesInClusterPostgres = state.databaseType === "self-hosted" && state.postgresMode !== "external";
102
+ const [backupEnabled, setBackupEnabled] = useState(state.backupEnabled);
103
+ const [backupSchedule, setBackupSchedule] = useState(state.backupSchedule || "0 2 * * *");
104
+ const [backupRetentionDays, setBackupRetentionDays] = useState(String(state.backupRetentionDays || 7));
105
+ // Dynamic resource lists (empty => manual entry fallback).
106
+ const [availableRegions, setAvailableRegions] = useState([]);
107
+ const [availableBuckets, setAvailableBuckets] = useState([]);
108
+ const [availableContainers, setAvailableContainers] = useState([]);
109
+ const [availableRoles, setAvailableRoles] = useState([]);
110
+ const [availableIdentities, setAvailableIdentities] = useState([]);
111
+ const [availableServiceAccounts, setAvailableServiceAccounts] = useState([]);
112
+ const [isRefreshing, setIsRefreshing] = useState(false);
113
+ const [error, setError] = useState(null);
114
+ const bucketLabel = provider === "azure-blob" ? "Storage account" : "Bucket";
115
+ // ===== Loaders =====
116
+ const loadRegions = async (selected) => {
117
+ const cloud = providerToCloud(selected);
118
+ setField("region-loading");
119
+ try {
120
+ const regions = await listRegions(cloud);
121
+ setAvailableRegions(regions.length > 0 ? regions : CLOUD_REGIONS[cloud]);
122
+ }
123
+ catch {
124
+ setAvailableRegions(CLOUD_REGIONS[cloud]);
125
+ }
126
+ setField("region");
127
+ };
128
+ // Provider (and usually region) are derived from the cluster, so jump
129
+ // straight to bucket discovery when the region is already known.
130
+ useEffect(() => {
131
+ if (initialRegion) {
132
+ loadBuckets(initialRegion);
133
+ }
134
+ else {
135
+ loadRegions(provider);
136
+ }
137
+ // eslint-disable-next-line react-hooks/exhaustive-deps
138
+ }, []);
139
+ const loadBuckets = async (selectedRegion) => {
140
+ const cloud = providerToCloud(provider);
141
+ setField("bucket-loading");
142
+ try {
143
+ const buckets = await listBucketsInRegion(cloud, selectedRegion);
144
+ setAvailableBuckets(buckets);
145
+ }
146
+ catch {
147
+ setAvailableBuckets([]);
148
+ }
149
+ setField("bucket");
150
+ };
151
+ const proceedAfterBucket = () => {
152
+ if (provider === "s3") {
153
+ loadRoles();
154
+ }
155
+ else if (provider === "gcs") {
156
+ loadServiceAccounts();
157
+ }
158
+ else {
159
+ loadContainers(bucket);
160
+ }
161
+ };
162
+ // Narrow huge account-wide lists to the Rulebricks workload identity from
163
+ // cluster-setup (by cluster name or the "rulebricks" marker). Falls back to the
164
+ // full list if nothing matches, and "Enter manually…" is always available.
165
+ const relevantToRulebricks = (name) => {
166
+ const n = name.toLowerCase();
167
+ const cluster = (state.clusterName || "").toLowerCase();
168
+ return n.includes("rulebricks") || (cluster !== "" && n.includes(cluster));
169
+ };
170
+ const loadRoles = async () => {
171
+ setField("s3-role-loading");
172
+ try {
173
+ const roles = await listIamRoles();
174
+ const narrowed = roles.filter((r) => relevantToRulebricks(r.name));
175
+ setAvailableRoles(narrowed.length > 0 ? narrowed : roles);
176
+ }
177
+ catch {
178
+ setAvailableRoles([]);
179
+ }
180
+ setField("s3-role");
181
+ };
182
+ const loadServiceAccounts = async () => {
183
+ setField("gcp-sa-loading");
184
+ try {
185
+ const accounts = await listGcpServiceAccounts();
186
+ const narrowed = accounts.filter((s) => relevantToRulebricks(s.email));
187
+ setAvailableServiceAccounts(narrowed.length > 0 ? narrowed : accounts);
188
+ }
189
+ catch {
190
+ setAvailableServiceAccounts([]);
191
+ }
192
+ setField("gcp-sa");
193
+ };
194
+ const loadContainers = async (account) => {
195
+ setField("azure-container-loading");
196
+ try {
197
+ setAvailableContainers(await listAzureBlobContainers(account));
198
+ }
199
+ catch {
200
+ setAvailableContainers([]);
201
+ }
202
+ setField("azure-container");
203
+ };
204
+ const loadAzureIdentities = async () => {
205
+ setField("azure-identity-loading");
206
+ try {
207
+ const [identities, tenant] = await Promise.all([
208
+ listAzureManagedIdentities(),
209
+ azureTenantId ? Promise.resolve(null) : getAzureTenantId(),
210
+ ]);
211
+ // Hide the identities AKS creates for itself (the kubelet "-agentpool" and
212
+ // the control-plane "<cluster>-identity") -- they are never the Rulebricks
213
+ // workload identity, and listing them just makes the choice confusing.
214
+ const cluster = (state.clusterName || "").toLowerCase();
215
+ const workloadIdentities = identities.filter((i) => {
216
+ const name = i.name.toLowerCase();
217
+ if (name.endsWith("-agentpool"))
218
+ return false;
219
+ if (cluster && name === `${cluster}-identity`)
220
+ return false;
221
+ return true;
222
+ });
223
+ setAvailableIdentities(workloadIdentities.length > 0 ? workloadIdentities : identities);
224
+ if (tenant) {
225
+ setAzureTenantId(tenant);
226
+ setTenantAutoDetected(true);
227
+ }
228
+ }
229
+ catch {
230
+ setAvailableIdentities([]);
231
+ }
232
+ setField("azure-client");
233
+ };
234
+ const refreshList = async () => {
235
+ if (isRefreshing)
236
+ return;
237
+ const cloud = providerToCloud(provider);
238
+ setIsRefreshing(true);
239
+ try {
240
+ if (field === "bucket") {
241
+ setAvailableBuckets(await listBucketsInRegion(cloud, region));
242
+ }
243
+ else if (field === "azure-container") {
244
+ setAvailableContainers(await listAzureBlobContainers(bucket));
245
+ }
246
+ }
247
+ catch {
248
+ // Keep existing list on error
249
+ }
250
+ setIsRefreshing(false);
251
+ };
252
+ // ===== Persistence =====
253
+ const persistStorage = () => {
254
+ dispatch({
255
+ type: "SET_STORAGE_CONFIG",
256
+ config: {
257
+ storageProvider: provider,
258
+ storageRegion: region,
259
+ storageBucket: bucket,
260
+ storageCloudAuthMode: provider === "azure-blob" ? authMode : "workload-identity",
261
+ storageAwsIamRoleArn: provider === "s3" ? roleArn : "",
262
+ storageAzureBlobContainer: provider === "azure-blob" ? azureContainer : "",
263
+ storageAzureBlobClientId: provider === "azure-blob" && authMode === "workload-identity"
264
+ ? azureClientId
265
+ : "",
266
+ storageAzureBlobTenantId: provider === "azure-blob" && authMode === "workload-identity"
267
+ ? azureTenantId
268
+ : "",
269
+ storageAzureBlobConnectionStringSecretRef: provider === "azure-blob" && authMode === "secret"
270
+ ? azureSecretRef
271
+ : "",
272
+ storageGcpServiceAccountEmail: provider === "gcs" ? gcpServiceAccount : "",
273
+ },
274
+ });
275
+ };
276
+ // After object storage is configured, deployments with bundled Postgres
277
+ // continue into the database backup policy (same bucket); external databases
278
+ // are done here.
279
+ const completeFromStorage = () => {
280
+ persistStorage();
281
+ if (usesInClusterPostgres) {
282
+ setField("backup-enabled");
283
+ }
284
+ else {
285
+ onComplete();
286
+ }
287
+ };
288
+ const finishBackups = () => {
289
+ const parsedRetention = Number.parseInt(backupRetentionDays, 10);
290
+ dispatch({ type: "SET_BACKUP_ENABLED", enabled: backupEnabled });
291
+ dispatch({
292
+ type: "SET_BACKUP_SCHEDULE",
293
+ schedule: backupSchedule || "0 2 * * *",
294
+ });
295
+ dispatch({
296
+ type: "SET_BACKUP_RETENTION_DAYS",
297
+ retentionDays: Number.isFinite(parsedRetention) ? parsedRetention : 7,
298
+ });
299
+ onComplete();
300
+ };
301
+ // ===== Back navigation =====
302
+ const handleBack = () => {
303
+ setError(null);
304
+ switch (field) {
305
+ case "region":
306
+ case "region-loading":
307
+ // Provider is derived from the cluster cloud (no provider step), so the
308
+ // first storage field returns to the previous wizard step.
309
+ onBack();
310
+ break;
311
+ case "bucket":
312
+ case "bucket-loading":
313
+ case "bucket-manual":
314
+ // The region list may not have been loaded yet when the region
315
+ // question was skipped (region derived from the cluster).
316
+ if (availableRegions.length === 0) {
317
+ loadRegions(provider);
318
+ }
319
+ else {
320
+ setField("region");
321
+ }
322
+ break;
323
+ case "s3-role":
324
+ case "s3-role-loading":
325
+ case "s3-role-manual":
326
+ case "azure-container":
327
+ case "azure-container-loading":
328
+ case "azure-container-manual":
329
+ case "gcp-sa":
330
+ case "gcp-sa-loading":
331
+ case "gcp-sa-manual":
332
+ setField("bucket");
333
+ break;
334
+ case "azure-auth":
335
+ setField("azure-container");
336
+ break;
337
+ case "azure-client":
338
+ case "azure-identity-loading":
339
+ case "azure-client-manual":
340
+ setField("azure-auth");
341
+ break;
342
+ case "azure-tenant":
343
+ setField("azure-client");
344
+ break;
345
+ case "azure-secret":
346
+ setField("azure-auth");
347
+ break;
348
+ case "done":
349
+ if (provider === "s3")
350
+ setField("s3-role");
351
+ else if (provider === "gcs")
352
+ setField("gcp-sa");
353
+ else
354
+ setField(authMode === "secret" ? "azure-secret" : "azure-tenant");
355
+ break;
356
+ case "backup-enabled":
357
+ setField("done");
358
+ break;
359
+ case "backup-frequency":
360
+ case "backup-frequency-custom":
361
+ setField("backup-enabled");
362
+ break;
363
+ case "backup-retention":
364
+ setField("backup-frequency");
365
+ break;
366
+ case "backup-done":
367
+ setField("backup-retention");
368
+ break;
369
+ }
370
+ };
371
+ useGatedInput((input, key) => {
372
+ if (key.escape) {
373
+ handleBack();
374
+ return;
375
+ }
376
+ if (input.toLowerCase() === "r" &&
377
+ (field === "bucket" || field === "azure-container")) {
378
+ refreshList();
379
+ return;
380
+ }
381
+ if (field === "done" && key.return) {
382
+ completeFromStorage();
383
+ return;
384
+ }
385
+ if (field === "backup-enabled") {
386
+ if (input === " " || input.toLowerCase() === "x") {
387
+ setBackupEnabled((value) => !value);
388
+ }
389
+ else if (key.return) {
390
+ if (backupEnabled) {
391
+ setField("backup-frequency");
392
+ }
393
+ else {
394
+ finishBackups();
395
+ }
396
+ }
397
+ return;
398
+ }
399
+ if (field === "backup-done" && key.return) {
400
+ finishBackups();
401
+ }
402
+ });
403
+ // ===== Selection handlers =====
404
+ const handleRegionSelect = (item) => {
405
+ setRegion(item.value);
406
+ loadBuckets(item.value);
407
+ };
408
+ const handleBucketSelect = (item) => {
409
+ if (item.value === MANUAL) {
410
+ setField("bucket-manual");
411
+ return;
412
+ }
413
+ setBucket(item.value);
414
+ proceedAfterBucket();
415
+ };
416
+ const handleBucketManualSubmit = () => {
417
+ if (!bucket.trim()) {
418
+ setError(`${bucketLabel} name is required`);
419
+ return;
420
+ }
421
+ setError(null);
422
+ proceedAfterBucket();
423
+ };
424
+ const handleRoleSelect = (item) => {
425
+ if (item.value === MANUAL) {
426
+ setField("s3-role-manual");
427
+ return;
428
+ }
429
+ setRoleArn(item.value);
430
+ setField("done");
431
+ };
432
+ const handleRoleManualSubmit = () => {
433
+ if (!roleArn.startsWith("arn:")) {
434
+ setError("Enter a valid IAM role ARN (arn:aws:iam::...)");
435
+ return;
436
+ }
437
+ setError(null);
438
+ setField("done");
439
+ };
440
+ const handleServiceAccountSelect = (item) => {
441
+ if (item.value === MANUAL) {
442
+ setField("gcp-sa-manual");
443
+ return;
444
+ }
445
+ setGcpServiceAccount(item.value);
446
+ setField("done");
447
+ };
448
+ const handleServiceAccountManualSubmit = () => {
449
+ if (!gcpServiceAccount.includes("@")) {
450
+ setError("Enter a valid service account email");
451
+ return;
452
+ }
453
+ setError(null);
454
+ setField("done");
455
+ };
456
+ const handleContainerSelect = (item) => {
457
+ if (item.value === MANUAL) {
458
+ setField("azure-container-manual");
459
+ return;
460
+ }
461
+ setAzureContainer(item.value);
462
+ setField("azure-auth");
463
+ };
464
+ const handleContainerManualSubmit = () => {
465
+ if (!azureContainer.trim()) {
466
+ setError("Container name is required");
467
+ return;
468
+ }
469
+ setError(null);
470
+ setField("azure-auth");
471
+ };
472
+ const handleAuthSelect = (item) => {
473
+ const mode = item.value;
474
+ setAuthMode(mode);
475
+ if (mode === "workload-identity") {
476
+ loadAzureIdentities();
477
+ }
478
+ else {
479
+ setField("azure-secret");
480
+ }
481
+ };
482
+ const handleIdentitySelect = (item) => {
483
+ if (item.value === MANUAL) {
484
+ setField("azure-client-manual");
485
+ return;
486
+ }
487
+ setAzureClientId(item.value);
488
+ setField("azure-tenant");
489
+ };
490
+ const handleIdentityManualSubmit = () => {
491
+ if (!azureClientId.trim()) {
492
+ setError("Managed identity client ID is required");
493
+ return;
494
+ }
495
+ setError(null);
496
+ setField("azure-tenant");
497
+ };
498
+ const handleTenantSubmit = () => {
499
+ if (!azureTenantId.trim()) {
500
+ setError("Azure tenant ID is required");
501
+ return;
502
+ }
503
+ setError(null);
504
+ setField("done");
505
+ };
506
+ const handleSecretSubmit = () => {
507
+ if (!azureSecretRef.includes(":")) {
508
+ setError("Use secret-name:key format");
509
+ return;
510
+ }
511
+ setError(null);
512
+ setField("done");
513
+ };
514
+ // ===== Backup handlers =====
515
+ const handleBackupFrequencySelect = (item) => {
516
+ if (item.value === CUSTOM_CRON) {
517
+ setField("backup-frequency-custom");
518
+ return;
519
+ }
520
+ setBackupSchedule(item.value);
521
+ setField("backup-retention");
522
+ };
523
+ const handleBackupFrequencyCustomSubmit = () => {
524
+ if (!backupSchedule.trim()) {
525
+ setError("Enter a cron expression or go back to pick a preset");
526
+ return;
527
+ }
528
+ setError(null);
529
+ setField("backup-retention");
530
+ };
531
+ const handleBackupRetentionSubmit = () => {
532
+ const parsed = Number.parseInt(backupRetentionDays, 10);
533
+ if (!Number.isFinite(parsed) || parsed < 2) {
534
+ setError("Retention must be greater than 1 (at least 2 days)");
535
+ return;
536
+ }
537
+ setError(null);
538
+ setField("backup-done");
539
+ };
540
+ // ===== Item builders =====
541
+ const withManual = (items) => [
542
+ ...items,
543
+ { label: "Enter manually…", value: MANUAL },
544
+ ];
545
+ const renderSelect = (items, onSelect, initialIndex = 0) => (_jsx(Box, { marginTop: 1, height: 10, flexDirection: "column", overflowY: "hidden", children: _jsx(SelectInput, { items: items, onSelect: onSelect, limit: 8, initialIndex: initialIndex, indicatorComponent: () => null, itemComponent: ({ isSelected, label }) => (_jsxs(Text, { color: isSelected ? colors.accent : undefined, children: [isSelected ? "❯ " : " ", label] })) }) }));
546
+ // Summary of what's chosen so far (shown on identity sub-steps).
547
+ const ChosenSummary = () => (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: colors.success, children: "\u2713" }), _jsxs(Text, { color: "gray", children: [" ", PROVIDERS.find((p) => p.value === provider)?.label] })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.success, children: "\u2713" }), _jsxs(Text, { color: "gray", children: [" Region: ", region] })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.success, children: "\u2713" }), _jsxs(Text, { color: "gray", children: [" ", bucketLabel, ": ", bucket] })] })] }));
548
+ const storageTitle = usesInClusterPostgres
549
+ ? "Storage & Backups"
550
+ : "Object Storage";
551
+ const storagePurpose = usesInClusterPostgres
552
+ ? "Decision logs and database backups are stored as prefixes within it."
553
+ : "Decision logs are stored as a prefix within it.";
554
+ const cloudAccessPurpose = usesInClusterPostgres
555
+ ? "decision logs, database backups, and metrics"
556
+ : "decision logs and metrics";
557
+ return (_jsxs(BorderBox, { title: storageTitle, children: [_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { children: "Configure one bucket/container for all Rulebricks data." }), _jsx(Text, { color: "gray", dimColor: true, children: storagePurpose })] }), field === "region-loading" && (_jsx(Spinner, { label: "Loading available regions..." })), field === "region" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Select Region" }), _jsx(Text, { color: "gray", dimColor: true, children: "Region where decision logs will be stored." }), renderSelect(availableRegions.map((r) => ({ label: r, value: r })), handleRegionSelect, Math.max(0, availableRegions.indexOf(region)))] })), field === "bucket-loading" && (_jsx(Spinner, { label: `Loading ${bucketLabel.toLowerCase()}s in ${region}...` })), field === "bucket" && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: ["Select ", provider === "azure-blob" ? "Storage Account" : "Bucket"] }), _jsxs(Text, { color: "gray", dimColor: true, children: ["Existing ", bucketLabel.toLowerCase(), "s in ", region, "."] }), isRefreshing ? (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: "Refreshing list..." }) })) : (_jsxs(_Fragment, { children: [availableBuckets.length === 0 && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "yellow", children: ["None found in ", region, ". Press R to refresh or enter manually."] }) })), renderSelect(withManual(availableBuckets.map((b) => ({ label: b, value: b }))), handleBucketSelect, Math.max(0, findClusterSetupDefaultIndex(availableBuckets, "decision-logs-bucket", {
558
+ provider: providerToCloud(provider),
559
+ clusterName: state.clusterName,
560
+ })))] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "R to refresh \u2022 \u2191/\u2193 to navigate \u2022 Enter to select \u2022 Esc to change region" }) })] })), field === "bucket-manual" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: provider === "azure-blob"
561
+ ? "Storage Account Name"
562
+ : "Bucket Name" }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: bucket, onChange: setBucket, onSubmit: handleBucketManualSubmit, placeholder: provider === "azure-blob" ? "mystorageaccount" : "my-bucket" }) })] })), field === "s3-role-loading" && (_jsx(Spinner, { label: "Loading IAM roles..." })), field === "s3-role" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Select the Rulebricks IAM role" }), _jsxs(Text, { color: "gray", dimColor: true, children: ["The single role from cluster-setup (", `${state.clusterName || "<cluster>"}-rulebricks`, "), used for all cloud access: ", cloudAccessPurpose, "."] }), (() => {
563
+ const recommendedIndex = Math.max(0, findClusterSetupDefaultIndex(availableRoles.map((r) => r.name), "decision-logs-identity", { provider: "aws", clusterName: state.clusterName }));
564
+ return renderSelect(withManual(availableRoles.map((r, idx) => ({
565
+ label: idx === recommendedIndex
566
+ ? `${r.name} - recommended`
567
+ : r.name,
568
+ value: r.arn,
569
+ }))), handleRoleSelect, recommendedIndex);
570
+ })(), _jsx(ChosenSummary, {})] })), field === "s3-role-manual" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "S3 IRSA Role ARN" }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: roleArn, onChange: setRoleArn, onSubmit: handleRoleManualSubmit, placeholder: "arn:aws:iam::123456789012:role/rulebricks-vector" }) }), _jsx(ChosenSummary, {})] })), field === "gcp-sa-loading" && (_jsx(Spinner, { label: "Loading service accounts..." })), field === "gcp-sa" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Select the Rulebricks Google service account" }), _jsxs(Text, { color: "gray", dimColor: true, children: ["The single service account from cluster-setup, used for all cloud access: ", cloudAccessPurpose, "."] }), (() => {
571
+ const recommendedIndex = Math.max(0, findClusterSetupDefaultIndex(availableServiceAccounts.map((s) => s.email), "decision-logs-identity", { provider: "gcp", clusterName: state.clusterName }));
572
+ return renderSelect(withManual(availableServiceAccounts.map((s, idx) => ({
573
+ label: idx === recommendedIndex
574
+ ? `${s.email} - recommended`
575
+ : s.email,
576
+ value: s.email,
577
+ }))), handleServiceAccountSelect, recommendedIndex);
578
+ })(), _jsx(ChosenSummary, {})] })), field === "gcp-sa-manual" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Google Service Account Email" }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: gcpServiceAccount, onChange: setGcpServiceAccount, onSubmit: handleServiceAccountManualSubmit, placeholder: "rulebricks-vector@project.iam.gserviceaccount.com" }) }), _jsx(ChosenSummary, {})] })), field === "azure-container-loading" && (_jsx(Spinner, { label: "Loading blob containers..." })), field === "azure-container" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Select Blob Container" }), _jsxs(Text, { color: "gray", dimColor: true, children: ["Container in ", bucket, " where decision logs are written."] }), isRefreshing ? (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: "Refreshing list..." }) })) : (_jsxs(_Fragment, { children: [availableContainers.length === 0 && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: "None found. Press R to refresh or enter manually." }) })), renderSelect(withManual(availableContainers.map((c) => ({ label: c, value: c }))), handleContainerSelect, Math.max(0, findClusterSetupDefaultIndex(availableContainers, "decision-logs-container", { provider: "azure", clusterName: state.clusterName })))] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "R to refresh \u2022 \u2191/\u2193 to navigate \u2022 Enter to select" }) })] })), field === "azure-container-manual" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Azure Blob Container" }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: azureContainer, onChange: setAzureContainer, onSubmit: handleContainerManualSubmit, placeholder: "rulebricks-logs" }) })] })), field === "azure-auth" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Azure Blob Authentication" }), _jsx(Text, { color: "gray", dimColor: true, children: "Workload identity is recommended; connection-string Secret is a fallback for clusters without Azure Workload Identity." }), renderSelect(AZURE_AUTH, handleAuthSelect)] })), field === "azure-identity-loading" && (_jsx(Spinner, { label: "Loading managed identities..." })), field === "azure-client" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Select the Rulebricks workload identity" }), _jsxs(Text, { color: "gray", dimColor: true, children: ["The single identity from cluster-setup (", `${state.clusterName || "<cluster>"}-rulebricks`, "), used for all cloud access: ", cloudAccessPurpose, "."] }), (() => {
579
+ const recommendedIndex = Math.max(0, findClusterSetupDefaultIndex(availableIdentities.map((i) => i.name), "decision-logs-identity", { provider: "azure", clusterName: state.clusterName }));
580
+ return renderSelect(withManual(availableIdentities.map((i, idx) => ({
581
+ label: idx === recommendedIndex
582
+ ? `${i.name} (${i.clientId}) - recommended`
583
+ : `${i.name} (${i.clientId})`,
584
+ value: i.clientId,
585
+ }))), handleIdentitySelect, recommendedIndex);
586
+ })()] })), field === "azure-client-manual" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Managed Identity Client ID" }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: azureClientId, onChange: setAzureClientId, onSubmit: handleIdentityManualSubmit, placeholder: "00000000-0000-0000-0000-000000000000" }) })] })), field === "azure-tenant" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Azure Tenant ID" }), _jsx(Text, { color: "gray", dimColor: true, children: tenantAutoDetected
587
+ ? "Auto-detected from your Azure CLI session - edit if needed."
588
+ : "Tenant ID used by Azure Workload Identity." }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: azureTenantId, onChange: setAzureTenantId, onSubmit: handleTenantSubmit, placeholder: "00000000-0000-0000-0000-000000000000" }) })] })), field === "azure-secret" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Azure Connection String Secret" }), _jsx(Text, { color: "gray", dimColor: true, children: "Existing Kubernetes Secret key in the format name:key." }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: azureSecretRef, onChange: setAzureSecretRef, onSubmit: handleSecretSubmit, placeholder: "azure-blob-logs:connection-string" }) })] })), field === "done" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: colors.success, children: "Storage backend configured." }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: ["Provider: ", PROVIDERS.find((p) => p.value === provider)?.label] }), _jsxs(Text, { children: [bucketLabel, ": ", bucket] }), _jsxs(Text, { children: ["Region: ", region] }), provider === "azure-blob" && (_jsxs(Text, { children: ["Container: ", azureContainer] }))] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, children: usesInClusterPostgres
589
+ ? "Press Enter to configure database backups"
590
+ : "Press Enter to continue" }) })] })), field === "backup-enabled" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Database Backups" }), _jsxs(Text, { color: "gray", dimColor: true, children: ["Logical pg_dump backups of the in-cluster Postgres are written to the same bucket under the db-backups/ prefix. Restore any time with `rulebricks restore ", state.name, "`."] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.accent, children: [backupEnabled ? "[x]" : "[ ]", " Enable database backups"] }) }), _jsx(Text, { color: colors.muted, children: "Space toggles backups. Enter continues." })] })), field === "backup-frequency" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Backup frequency" }), _jsx(Text, { color: "gray", dimColor: true, children: "How often a backup is taken (UTC cron)." }), renderSelect(BACKUP_FREQUENCY_PRESETS, handleBackupFrequencySelect, Math.max(0, BACKUP_FREQUENCY_PRESETS.findIndex((p) => p.value === backupSchedule)))] })), field === "backup-frequency-custom" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Custom cron schedule" }), _jsx(Text, { color: "gray", dimColor: true, children: "Standard cron format (UTC)." }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: backupSchedule, onChange: setBackupSchedule, onSubmit: handleBackupFrequencyCustomSubmit, placeholder: "0 2 * * *" }) })] })), field === "backup-retention" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Retention days" }), _jsx(Text, { color: "gray", dimColor: true, children: "Backups older than this are pruned from object storage (must be greater than 1)." }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: backupRetentionDays, onChange: setBackupRetentionDays, onSubmit: handleBackupRetentionSubmit, placeholder: "7" }) })] })), field === "backup-done" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: colors.success, children: "Database backups configured." }), _jsxs(Text, { children: ["Frequency:", " ", BACKUP_FREQUENCY_PRESETS.find((p) => p.value === backupSchedule)
591
+ ?.label || backupSchedule] }), _jsxs(Text, { children: ["Retention: ", backupRetentionDays, " days"] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, children: "Press Enter to continue" }) })] })), error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: ["\u2717 ", error] }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Esc to go back \u2022 Enter to continue" }) })] }));
592
+ }