@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
@@ -6,22 +6,10 @@ export const SUPPORTED_DNS_PROVIDERS = [
6
6
  "google",
7
7
  "azure",
8
8
  ];
9
- // Sink category mappings
10
- export const LOGGING_SINK_CATEGORIES = {
11
- s3: "cloud-storage",
12
- "azure-blob": "cloud-storage",
13
- gcs: "cloud-storage",
14
- datadog: "logging-platform",
15
- splunk: "logging-platform",
16
- elasticsearch: "logging-platform",
17
- loki: "logging-platform",
18
- newrelic: "logging-platform",
19
- axiom: "logging-platform",
20
- };
21
9
  // Region mappings
22
10
  export const CLOUD_REGIONS = {
23
11
  aws: [
24
- // US regions (c8g Graviton4 available)
12
+ // US regions
25
13
  "us-east-1",
26
14
  "us-east-2",
27
15
  "us-west-1",
@@ -29,7 +17,7 @@ export const CLOUD_REGIONS = {
29
17
  // Canada
30
18
  "ca-central-1",
31
19
  "ca-west-1",
32
- // Europe (c8g available)
20
+ // Europe
33
21
  "eu-west-1",
34
22
  "eu-west-2",
35
23
  "eu-west-3",
@@ -38,7 +26,7 @@ export const CLOUD_REGIONS = {
38
26
  "eu-north-1",
39
27
  "eu-south-1",
40
28
  "eu-south-2",
41
- // Asia Pacific (c8g available)
29
+ // Asia Pacific
42
30
  "ap-northeast-1",
43
31
  "ap-northeast-2",
44
32
  "ap-northeast-3",
@@ -60,31 +48,30 @@ export const CLOUD_REGIONS = {
60
48
  "il-central-1",
61
49
  ],
62
50
  gcp: [
63
- // Tier 1: Full C4A (Google Axion ARM64) availability - 3+ zones confirmed
64
51
  // US regions
65
- "us-central1", // C4A in zones a, b, c, f (best availability)
66
- "us-east1", // C4A in zones b, c, d
67
- "us-east4", // C4A in zones a, b, c
68
- "us-west1", // C4A in zones a, b, c
69
- "us-west4", // C4A in zones a, b, c
52
+ "us-central1",
53
+ "us-east1",
54
+ "us-east4",
55
+ "us-west1",
56
+ "us-west4",
70
57
  // North America
71
- "northamerica-south1", // C4A in zones a, b, c (Mexico)
58
+ "northamerica-south1",
72
59
  // Europe
73
- "europe-west1", // C4A in zones b, c, d
74
- "europe-west2", // C4A in zones a, b, c
75
- "europe-west3", // C4A in zones a, b, c
76
- "europe-west4", // C4A in zones a, b, c
77
- "europe-north1", // C4A in zones a, b
60
+ "europe-west1",
61
+ "europe-west2",
62
+ "europe-west3",
63
+ "europe-west4",
64
+ "europe-north1",
78
65
  // Asia Pacific
79
- "asia-east1", // C4A in zones a, b, c
80
- "asia-northeast1", // C4A in zones b, c
81
- "asia-south1", // C4A in zones a, b, c
82
- "asia-southeast1", // C4A in zones a, b, c
66
+ "asia-east1",
67
+ "asia-northeast1",
68
+ "asia-south1",
69
+ "asia-southeast1",
83
70
  // Australia
84
- "australia-southeast2", // C4A in zones a, b, c
71
+ "australia-southeast2",
85
72
  ],
86
73
  azure: [
87
- // US regions (Dpsv5 ARM64 available)
74
+ // US regions
88
75
  "eastus",
89
76
  "eastus2",
90
77
  "westus",
@@ -99,7 +86,7 @@ export const CLOUD_REGIONS = {
99
86
  "canadaeast",
100
87
  // South America
101
88
  "brazilsouth",
102
- // Europe (Dpsv5 available)
89
+ // Europe
103
90
  "northeurope",
104
91
  "westeurope",
105
92
  "uksouth",
@@ -137,156 +124,6 @@ export const CLOUD_REGIONS = {
137
124
  "israelcentral",
138
125
  ],
139
126
  };
140
- // Performance tier configurations
141
- export const TIER_CONFIGS = {
142
- small: {
143
- description: "Development & Testing",
144
- throughput: "<1,000 rules/sec",
145
- nodes: { min: 4, max: 4 },
146
- resources: "2 vCPU, 4GB RAM each",
147
- // HPS
148
- hpsReplicas: 2,
149
- hpsWorkerReplicas: { min: 4, max: 8 },
150
- hpsResources: {
151
- requests: { cpu: "500m", memory: "1Gi" },
152
- limits: { cpu: "1500m", memory: "1536Mi" },
153
- },
154
- hpsWorkerResources: {
155
- requests: { cpu: "100m", memory: "128Mi" },
156
- limits: { cpu: "500m", memory: "512Mi" },
157
- },
158
- // Kafka
159
- kafkaStorage: "10Gi",
160
- kafkaReplication: 1,
161
- kafkaResources: {
162
- requests: { cpu: "500m", memory: "2Gi" },
163
- limits: { cpu: "2000m", memory: "3Gi" },
164
- },
165
- kafkaHeapOpts: "-Xmx1g -Xms1g -XX:+UseZGC -XX:+AlwaysPreTouch",
166
- // Redis
167
- redisResources: {
168
- requests: { cpu: "200m", memory: "256Mi" },
169
- limits: { cpu: "500m", memory: "2Gi" },
170
- },
171
- redisPersistenceSize: "4Gi",
172
- // Vector
173
- vectorReplicas: 2,
174
- vectorResources: {
175
- requests: { cpu: "50m", memory: "128Mi" },
176
- limits: { cpu: "200m", memory: "256Mi" },
177
- },
178
- // Database
179
- dbResources: {
180
- requests: { cpu: "500m", memory: "1Gi" },
181
- limits: { cpu: "1000m", memory: "2Gi" },
182
- },
183
- dbPersistenceSize: "10Gi",
184
- // App
185
- appReplicas: 2,
186
- appResources: {
187
- requests: { cpu: "500m", memory: "512Mi" },
188
- limits: { cpu: "2000m", memory: "2Gi" },
189
- },
190
- },
191
- medium: {
192
- description: "Production",
193
- throughput: "1,000-10,000 rules/sec",
194
- nodes: { min: 4, max: 8 },
195
- resources: "2-4 vCPU, 4-8GB RAM each",
196
- // HPS
197
- hpsReplicas: 3,
198
- hpsWorkerReplicas: { min: 10, max: 24 },
199
- hpsResources: {
200
- requests: { cpu: "1000m", memory: "1Gi" },
201
- limits: { cpu: "4000m", memory: "4Gi" },
202
- },
203
- hpsWorkerResources: {
204
- requests: { cpu: "500m", memory: "512Mi" },
205
- limits: { cpu: "2000m", memory: "2Gi" },
206
- },
207
- // Kafka
208
- kafkaStorage: "50Gi",
209
- kafkaReplication: 2,
210
- kafkaResources: {
211
- requests: { cpu: "1000m", memory: "3Gi" },
212
- limits: { cpu: "2000m", memory: "4Gi" },
213
- },
214
- kafkaHeapOpts: "-Xmx2g -Xms2g -XX:+UseZGC -XX:+AlwaysPreTouch",
215
- // Redis
216
- redisResources: {
217
- requests: { cpu: "200m", memory: "512Mi" },
218
- limits: { cpu: "1000m", memory: "4Gi" },
219
- },
220
- redisPersistenceSize: "8Gi",
221
- // Vector
222
- vectorReplicas: 2,
223
- vectorResources: {
224
- requests: { cpu: "100m", memory: "256Mi" },
225
- limits: { cpu: "500m", memory: "512Mi" },
226
- },
227
- // Database
228
- dbResources: {
229
- requests: { cpu: "1000m", memory: "2Gi" },
230
- limits: { cpu: "2000m", memory: "4Gi" },
231
- },
232
- dbPersistenceSize: "50Gi",
233
- // App
234
- appReplicas: 2,
235
- appResources: {
236
- requests: { cpu: "500m", memory: "512Mi" },
237
- limits: { cpu: "2000m", memory: "2Gi" },
238
- },
239
- },
240
- large: {
241
- description: "High Performance",
242
- throughput: ">10,000 rules/sec",
243
- nodes: { min: 5, max: 16 },
244
- resources: "2-4 vCPU, 4-8GB RAM each",
245
- // HPS
246
- hpsReplicas: 4,
247
- hpsWorkerReplicas: { min: 10, max: 48 },
248
- hpsResources: {
249
- requests: { cpu: "2000m", memory: "2Gi" },
250
- limits: { cpu: "4000m", memory: "4Gi" },
251
- },
252
- hpsWorkerResources: {
253
- requests: { cpu: "1000m", memory: "1Gi" },
254
- limits: { cpu: "2000m", memory: "2Gi" },
255
- },
256
- // Kafka
257
- kafkaStorage: "100Gi",
258
- kafkaReplication: 3,
259
- kafkaResources: {
260
- requests: { cpu: "2000m", memory: "4Gi" },
261
- limits: { cpu: "4000m", memory: "6Gi" },
262
- },
263
- kafkaHeapOpts: "-Xmx3g -Xms3g -XX:+UseZGC -XX:+AlwaysPreTouch",
264
- // Redis
265
- redisResources: {
266
- requests: { cpu: "500m", memory: "1Gi" },
267
- limits: { cpu: "2000m", memory: "8Gi" },
268
- },
269
- redisPersistenceSize: "16Gi",
270
- // Vector
271
- vectorReplicas: 3,
272
- vectorResources: {
273
- requests: { cpu: "200m", memory: "512Mi" },
274
- limits: { cpu: "1000m", memory: "1Gi" },
275
- },
276
- // Database
277
- dbResources: {
278
- requests: { cpu: "2000m", memory: "4Gi" },
279
- limits: { cpu: "4000m", memory: "8Gi" },
280
- },
281
- dbPersistenceSize: "100Gi",
282
- // App
283
- appReplicas: 3,
284
- appResources: {
285
- requests: { cpu: "1000m", memory: "1Gi" },
286
- limits: { cpu: "2000m", memory: "2Gi" },
287
- },
288
- },
289
- };
290
127
  // Default SMTP providers
291
128
  export const SMTP_PROVIDERS = {
292
129
  "aws-ses": {
@@ -315,6 +152,13 @@ export const DNS_PROVIDER_NAMES = {
315
152
  azure: "Azure DNS",
316
153
  other: "Other / Not sure",
317
154
  };
155
+ // Cloud provider display names with proper casing for UI labels. Acronyms stay
156
+ // uppercase; Azure is title-cased (so it doesn't render as "AZURE").
157
+ export const CLOUD_PROVIDER_NAMES = {
158
+ aws: "AWS",
159
+ gcp: "GCP",
160
+ azure: "Azure",
161
+ };
318
162
  // Logging sink display info
319
163
  export const LOGGING_SINK_INFO = {
320
164
  console: {
@@ -325,19 +169,6 @@ export const LOGGING_SINK_INFO = {
325
169
  name: "External (not configured)",
326
170
  description: "External logging enabled but destination not selected",
327
171
  },
328
- // Cloud Storage
329
- s3: {
330
- name: "AWS S3",
331
- description: "Store logs in an S3 bucket",
332
- },
333
- "azure-blob": {
334
- name: "Azure Blob Storage",
335
- description: "Store logs in Azure Blob container",
336
- },
337
- gcs: {
338
- name: "Google Cloud Storage",
339
- description: "Store logs in a GCS bucket",
340
- },
341
172
  // Logging Platforms
342
173
  datadog: {
343
174
  name: "Datadog",
@@ -364,30 +195,81 @@ export const LOGGING_SINK_INFO = {
364
195
  description: "Send logs to Axiom dataset",
365
196
  },
366
197
  };
367
- // Logging destination labels for UI display (shown in the Rulebricks app)
368
- export const LOGGING_DESTINATION_LABELS = {
369
- console: "Console (stdout)",
370
- pending: "External (configuring...)",
371
- s3: "AWS S3",
372
- "azure-blob": "Azure Blob Storage",
373
- gcs: "Google Cloud Storage",
374
- datadog: "Datadog",
375
- splunk: "Splunk",
376
- elasticsearch: "Elasticsearch",
377
- loki: "Grafana Loki",
378
- newrelic: "New Relic",
379
- axiom: "Axiom",
380
- };
381
- // Helper to get logging destination label for Helm values
382
- export function getLoggingDestinationLabel(sink) {
383
- return LOGGING_DESTINATION_LABELS[sink] || "Console (stdout)";
384
- }
385
198
  const SecretKeyRefSchema = z.object({
386
199
  name: z.string().min(1),
387
200
  key: z.string().min(1),
388
201
  });
389
- const RemoteWriteConfigSchema = z.object({
390
- destination: z.enum(["aws-amp", "azure-monitor", "grafana-cloud", "generic"]),
202
+ /**
203
+ * Validates a Prometheus remote_write config the same way buildHelmValues and
204
+ * the Helm chart do, returning human-readable errors. Centralized so the wizard
205
+ * gate, the Zod schema (load time), and Helm value generation all enforce the
206
+ * exact same per-destination/auth requirements. This is what prevents the CLI
207
+ * from ever persisting a monitoring config that throws at deploy time (e.g.
208
+ * "Azure Monitor remote_write managed identity requires client ID").
209
+ */
210
+ export function validateRemoteWriteConfig(rw) {
211
+ const errors = [];
212
+ switch (rw.destination) {
213
+ case "aws-amp":
214
+ if (!rw.awsRegion) {
215
+ errors.push("AWS Managed Prometheus remote write requires a region.");
216
+ }
217
+ break;
218
+ case "azure-monitor": {
219
+ // The remote_write URL must be the full DCE metrics-ingestion path, not the
220
+ // bare DCE host. Azure Monitor expects:
221
+ // https://<dce>.<region>.metrics.ingest.monitor.azure.com/dataCollectionRules/<dcrImmutableId>/streams/Microsoft-PrometheusMetrics/api/v1/write?api-version=2023-04-24
222
+ // A bare host silently 404s, so catch the common copy-paste mistake here.
223
+ if (rw.url &&
224
+ !(rw.url.includes("/dataCollectionRules/") &&
225
+ rw.url.includes("/streams/") &&
226
+ rw.url.includes("/api/v1/write"))) {
227
+ errors.push("Azure Monitor remote write URL must be the full DCE metrics-ingestion path " +
228
+ "(https://<dce>.<region>.metrics.ingest.monitor.azure.com/dataCollectionRules/<dcrImmutableId>/streams/Microsoft-PrometheusMetrics/api/v1/write?api-version=2023-04-24), " +
229
+ "not just the data collection endpoint host.");
230
+ }
231
+ // An unset authType is treated as managed identity (the chart default).
232
+ const authType = rw.authType ?? "managed-identity";
233
+ if (authType === "oauth") {
234
+ if (!rw.clientId || !rw.tenantId || !rw.clientSecretRef) {
235
+ errors.push("Azure Monitor remote write (OAuth) requires a client ID, tenant ID, and client secret reference.");
236
+ }
237
+ }
238
+ else if (authType === "workload-identity") {
239
+ if (!rw.clientId || !rw.tenantId) {
240
+ errors.push("Azure Monitor remote write (workload identity) requires a client ID and tenant ID.");
241
+ }
242
+ }
243
+ else if (!rw.clientId) {
244
+ errors.push("Azure Monitor remote write (managed identity) requires a client ID.");
245
+ }
246
+ break;
247
+ }
248
+ case "grafana-cloud":
249
+ if (!rw.usernameSecretRef || !rw.passwordSecretRef) {
250
+ errors.push("Grafana Cloud remote write requires username and password secret references.");
251
+ }
252
+ break;
253
+ case "generic":
254
+ if (rw.authType === "basic" &&
255
+ (!rw.usernameSecretRef || !rw.passwordSecretRef)) {
256
+ errors.push("Basic-auth remote write requires username and password secret references.");
257
+ }
258
+ if (rw.authType === "bearer" && !rw.bearerTokenSecretRef) {
259
+ errors.push("Bearer-token remote write requires a token secret reference.");
260
+ }
261
+ break;
262
+ }
263
+ return errors;
264
+ }
265
+ const RemoteWriteConfigSchema = z
266
+ .object({
267
+ destination: z.enum([
268
+ "aws-amp",
269
+ "azure-monitor",
270
+ "grafana-cloud",
271
+ "generic",
272
+ ]),
391
273
  url: z.string().url(),
392
274
  authType: z
393
275
  .enum([
@@ -410,6 +292,11 @@ const RemoteWriteConfigSchema = z.object({
410
292
  usernameSecretRef: SecretKeyRefSchema.optional(),
411
293
  passwordSecretRef: SecretKeyRefSchema.optional(),
412
294
  bearerTokenSecretRef: SecretKeyRefSchema.optional(),
295
+ })
296
+ .superRefine((rw, ctx) => {
297
+ for (const message of validateRemoteWriteConfig(rw)) {
298
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message });
299
+ }
413
300
  });
414
301
  const MonitoringDestinationSchema = z.enum([
415
302
  "local-grafana",
@@ -418,6 +305,210 @@ const MonitoringDestinationSchema = z.enum([
418
305
  "grafana-cloud",
419
306
  "generic",
420
307
  ]);
308
+ const TracingConfigSchema = z
309
+ .object({
310
+ enabled: z.boolean(),
311
+ // Absent means "elastic" for backward compatibility with existing configs.
312
+ destination: z.enum(["elastic", "otlp", "azure-monitor"]).optional(),
313
+ samplingRatio: z.number().min(0).max(1).optional(),
314
+ elastic: z
315
+ .object({
316
+ endpoint: z.string().url().optional(),
317
+ authMode: z.enum(["secret-token", "api-key", "none"]).optional(),
318
+ secretToken: z.string().optional(),
319
+ apiKey: z.string().optional(),
320
+ })
321
+ .optional(),
322
+ // Generic OTLP/HTTP backend (Grafana Tempo, Honeycomb, Jaeger, vendor
323
+ // gateways, AWS/Azure OTLP ingestion, etc.).
324
+ otlp: z
325
+ .object({
326
+ endpoint: z.string().url().optional(),
327
+ authMode: z.enum(["none", "bearer", "api-key", "header"]).optional(),
328
+ headerName: z.string().optional(),
329
+ token: z.string().optional(),
330
+ apiKey: z.string().optional(),
331
+ headerValue: z.string().optional(),
332
+ headers: z.record(z.string()).optional(),
333
+ tlsInsecureSkipVerify: z.boolean().optional(),
334
+ })
335
+ .optional(),
336
+ // Azure Monitor / Application Insights.
337
+ azureMonitor: z
338
+ .object({
339
+ connectionString: z.string().optional(),
340
+ })
341
+ .optional(),
342
+ })
343
+ .superRefine((t, ctx) => {
344
+ if (!t.enabled)
345
+ return;
346
+ const destination = t.destination ?? "elastic";
347
+ if (destination === "elastic") {
348
+ if (!t.elastic?.endpoint) {
349
+ ctx.addIssue({
350
+ code: z.ZodIssueCode.custom,
351
+ message: "features.tracing.elastic.endpoint is required when tracing destination is 'elastic'",
352
+ path: ["elastic", "endpoint"],
353
+ });
354
+ }
355
+ const mode = t.elastic?.authMode ?? "secret-token";
356
+ if (mode === "secret-token" && !t.elastic?.secretToken) {
357
+ ctx.addIssue({
358
+ code: z.ZodIssueCode.custom,
359
+ message: "features.tracing.elastic.secretToken is required for authMode 'secret-token'",
360
+ path: ["elastic", "secretToken"],
361
+ });
362
+ }
363
+ if (mode === "api-key" && !t.elastic?.apiKey) {
364
+ ctx.addIssue({
365
+ code: z.ZodIssueCode.custom,
366
+ message: "features.tracing.elastic.apiKey is required for authMode 'api-key'",
367
+ path: ["elastic", "apiKey"],
368
+ });
369
+ }
370
+ }
371
+ else if (destination === "otlp") {
372
+ if (!t.otlp?.endpoint) {
373
+ ctx.addIssue({
374
+ code: z.ZodIssueCode.custom,
375
+ message: "features.tracing.otlp.endpoint is required when tracing destination is 'otlp'",
376
+ path: ["otlp", "endpoint"],
377
+ });
378
+ }
379
+ const mode = t.otlp?.authMode ?? "none";
380
+ if (mode === "bearer" && !t.otlp?.token) {
381
+ ctx.addIssue({
382
+ code: z.ZodIssueCode.custom,
383
+ message: "features.tracing.otlp.token is required for authMode 'bearer'",
384
+ path: ["otlp", "token"],
385
+ });
386
+ }
387
+ if (mode === "api-key" && !t.otlp?.apiKey) {
388
+ ctx.addIssue({
389
+ code: z.ZodIssueCode.custom,
390
+ message: "features.tracing.otlp.apiKey is required for authMode 'api-key'",
391
+ path: ["otlp", "apiKey"],
392
+ });
393
+ }
394
+ if (mode === "header" && !t.otlp?.headerValue) {
395
+ ctx.addIssue({
396
+ code: z.ZodIssueCode.custom,
397
+ message: "features.tracing.otlp.headerValue is required for authMode 'header'",
398
+ path: ["otlp", "headerValue"],
399
+ });
400
+ }
401
+ }
402
+ else if (destination === "azure-monitor") {
403
+ if (!t.azureMonitor?.connectionString) {
404
+ ctx.addIssue({
405
+ code: z.ZodIssueCode.custom,
406
+ message: "features.tracing.azureMonitor.connectionString is required when tracing destination is 'azure-monitor'",
407
+ path: ["azureMonitor", "connectionString"],
408
+ });
409
+ }
410
+ }
411
+ });
412
+ // Application/container log shipping to a customer-managed Elasticsearch (BYO)
413
+ // via the Vector agent DaemonSet. Distinct from decision-log sinks.
414
+ const AppLogsConfigSchema = z
415
+ .object({
416
+ enabled: z.boolean(),
417
+ destination: z.enum(["elasticsearch", "loki", "generic"]).optional(),
418
+ elasticsearch: z
419
+ .object({
420
+ endpoint: z.string().url().optional(),
421
+ index: z.string().optional(),
422
+ authMode: z.enum(["basic", "api-key", "none"]).optional(),
423
+ username: z.string().optional(),
424
+ password: z.string().optional(),
425
+ apiKey: z.string().optional(),
426
+ verifyCertificate: z.boolean().optional(),
427
+ })
428
+ .optional(),
429
+ loki: z
430
+ .object({
431
+ endpoint: z.string().url().optional(),
432
+ labels: z.record(z.string()).optional(),
433
+ })
434
+ .optional(),
435
+ generic: z
436
+ .object({
437
+ endpoint: z.string().url().optional(),
438
+ authHeader: z.string().optional(),
439
+ })
440
+ .optional(),
441
+ })
442
+ .superRefine((a, ctx) => {
443
+ if (!a.enabled)
444
+ return;
445
+ const destination = a.destination ?? "elasticsearch";
446
+ if (destination === "loki") {
447
+ if (!a.loki?.endpoint) {
448
+ ctx.addIssue({
449
+ code: z.ZodIssueCode.custom,
450
+ message: "features.logging.appLogs.loki.endpoint is required when appLogs destination is 'loki'",
451
+ path: ["loki", "endpoint"],
452
+ });
453
+ }
454
+ return;
455
+ }
456
+ if (destination === "generic") {
457
+ if (!a.generic?.endpoint) {
458
+ ctx.addIssue({
459
+ code: z.ZodIssueCode.custom,
460
+ message: "features.logging.appLogs.generic.endpoint is required when appLogs destination is 'generic'",
461
+ path: ["generic", "endpoint"],
462
+ });
463
+ }
464
+ return;
465
+ }
466
+ if (!a.elasticsearch?.endpoint) {
467
+ ctx.addIssue({
468
+ code: z.ZodIssueCode.custom,
469
+ message: "features.logging.appLogs.elasticsearch.endpoint is required when appLogs is enabled",
470
+ path: ["elasticsearch", "endpoint"],
471
+ });
472
+ }
473
+ const mode = a.elasticsearch?.authMode ?? "basic";
474
+ if (mode === "basic" &&
475
+ (!a.elasticsearch?.username || !a.elasticsearch?.password)) {
476
+ ctx.addIssue({
477
+ code: z.ZodIssueCode.custom,
478
+ message: "features.logging.appLogs.elasticsearch username and password are required for authMode 'basic'",
479
+ path: ["elasticsearch", "username"],
480
+ });
481
+ }
482
+ if (mode === "api-key" && !a.elasticsearch?.apiKey) {
483
+ ctx.addIssue({
484
+ code: z.ZodIssueCode.custom,
485
+ message: "features.logging.appLogs.elasticsearch.apiKey is required for authMode 'api-key'",
486
+ path: ["elasticsearch", "apiKey"],
487
+ });
488
+ }
489
+ });
490
+ const CacheObservabilityConfigSchema = z.object({
491
+ valkeyAdmin: z
492
+ .object({
493
+ enabled: z.boolean(),
494
+ exposure: z.enum(["internal", "ingress"]).optional(),
495
+ hostname: z.string().optional(),
496
+ basicAuthUsers: z.array(z.string()).optional(),
497
+ basicAuthExistingSecret: z.string().optional(),
498
+ allowedIPs: z.array(z.string()).optional(),
499
+ })
500
+ .optional(),
501
+ redisExporter: z
502
+ .object({
503
+ enabled: z.boolean(),
504
+ })
505
+ .optional(),
506
+ kafkaExporter: z
507
+ .object({
508
+ enabled: z.boolean(),
509
+ })
510
+ .optional(),
511
+ });
421
512
  // Deployment configuration schema
422
513
  export const DeploymentConfigSchema = z.object({
423
514
  name: z
@@ -427,12 +518,24 @@ export const DeploymentConfigSchema = z.object({
427
518
  .regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/),
428
519
  // Infrastructure
429
520
  infrastructure: z.object({
430
- mode: z.enum(["existing", "provision"]),
521
+ mode: z.literal("existing"),
431
522
  provider: z.enum(["aws", "gcp", "azure"]).optional(),
432
523
  region: z.string().optional(),
433
524
  clusterName: z.string().optional(),
434
525
  gcpProjectId: z.string().optional(),
435
526
  azureResourceGroup: z.string().optional(),
527
+ nodeArchitecture: z
528
+ .enum(["amd64", "arm64", "mixed", "unknown"])
529
+ .optional(),
530
+ arm64TolerationRequired: z.boolean().optional(),
531
+ storageClass: z.string().optional(),
532
+ storageProvisioner: z.string().optional(),
533
+ schedulableNodeCount: z.number().optional(),
534
+ totalCpuCores: z.number().optional(),
535
+ totalMemoryGi: z.number().optional(),
536
+ eligibleCpuCores: z.number().optional(),
537
+ eligibleMemoryGi: z.number().optional(),
538
+ totalPersistentStorageGi: z.number().optional(),
436
539
  }),
437
540
  // Domain & TLS
438
541
  domain: z.string().min(1),
@@ -444,8 +547,6 @@ export const DeploymentConfigSchema = z.object({
444
547
  provider: z.enum(["route53", "cloudflare", "google", "azure", "other"]),
445
548
  // Should we auto-manage DNS records? (only applicable for supported providers)
446
549
  autoManage: z.boolean(),
447
- // For existing clusters: does external-dns already exist cluster-wide?
448
- existingExternalDns: z.boolean().optional(),
449
550
  }),
450
551
  // SMTP Configuration
451
552
  smtp: z.object({
@@ -471,8 +572,153 @@ export const DeploymentConfigSchema = z.object({
471
572
  supabaseDashboardUser: z.string().optional(),
472
573
  supabaseDashboardPass: z.string().optional(),
473
574
  }),
474
- // Performance
475
- tier: z.enum(["small", "medium", "large"]),
575
+ // Shared object storage: one provider, one identity, one bucket/container.
576
+ // Decision logs and DB backups are just key prefixes within it.
577
+ storage: z
578
+ .object({
579
+ provider: z.enum(["s3", "azure-blob", "gcs"]),
580
+ cloudAuthMode: z.enum(["workload-identity", "secret"]).optional(),
581
+ // Single bucket (S3/GCS) or storage account (azure-blob) + its region.
582
+ bucket: z.string().min(1),
583
+ region: z.string().min(1),
584
+ awsIamRoleArn: z.string().optional(),
585
+ azureBlobClientId: z.string().optional(),
586
+ azureBlobTenantId: z.string().optional(),
587
+ azureBlobConnectionStringSecretRef: SecretKeyRefSchema.optional(),
588
+ // Single blob container (azure-blob only) holding all prefixes.
589
+ azureBlobContainer: z.string().optional(),
590
+ gcpServiceAccountEmail: z.string().optional(),
591
+ // Per-purpose key prefixes within the single bucket/container.
592
+ paths: z
593
+ .object({
594
+ decisionLogs: z.string().optional(),
595
+ dbBackups: z.string().optional(),
596
+ })
597
+ .optional(),
598
+ })
599
+ .optional(),
600
+ // External/managed Redis and Kafka (for large deployments that prefer managed
601
+ // services over the in-cluster defaults). Omitted/embedded means the chart
602
+ // deploys these in-cluster as usual.
603
+ externalServices: z
604
+ .object({
605
+ redis: z
606
+ .object({
607
+ mode: z.enum(["embedded", "external"]),
608
+ external: z
609
+ .object({
610
+ host: z.string().optional(),
611
+ port: z.number().int().min(1).max(65535).optional(),
612
+ password: z.string().optional(),
613
+ existingSecret: z.string().optional(),
614
+ existingSecretKey: z.string().optional(),
615
+ tls: z.boolean().optional(),
616
+ httpApi: z
617
+ .object({
618
+ enabled: z.boolean(),
619
+ url: z.string().optional(),
620
+ token: z.string().optional(),
621
+ })
622
+ .optional(),
623
+ })
624
+ .optional(),
625
+ })
626
+ .optional(),
627
+ kafka: z
628
+ .object({
629
+ mode: z.enum(["embedded", "external"]),
630
+ external: z
631
+ .object({
632
+ // Preset drives per-cloud auth defaults and whether the Vector
633
+ // bridge sidecar is required.
634
+ preset: z
635
+ .enum([
636
+ "aws-msk-iam",
637
+ "azure-event-hubs",
638
+ "gcp-managed",
639
+ "custom",
640
+ ])
641
+ .optional(),
642
+ brokers: z.string().optional(),
643
+ topic: z.string().optional(),
644
+ // Prefix namespacing all Kafka topics (e.g. "com.rulebricks.").
645
+ topicPrefix: z.string().optional(),
646
+ // Whether the chart creates the required topics on the managed
647
+ // broker (via the kafka-topic-provision Job, MSK IAM only). Set
648
+ // false for a locked-down broker where topics are managed out of
649
+ // band and the workload role has no CreateTopic. Default true.
650
+ provisionTopics: z.boolean().optional(),
651
+ ssl: z.boolean().optional(),
652
+ sasl: z
653
+ .object({
654
+ // "" means no SASL (plaintext/SSL-only).
655
+ mechanism: z.enum([
656
+ "",
657
+ "aws-iam",
658
+ "oauthbearer",
659
+ "scram-sha-256",
660
+ "scram-sha-512",
661
+ "plain",
662
+ ]),
663
+ region: z.string().optional(),
664
+ username: z.string().optional(),
665
+ password: z.string().optional(),
666
+ existingSecret: z.string().optional(),
667
+ })
668
+ .optional(),
669
+ // Cloud workload identity for token mechanisms (MSK IAM / GCP
670
+ // OAUTHBEARER). Applied to the HPS and Vector service accounts.
671
+ identity: z
672
+ .object({
673
+ awsRoleArn: z.string().optional(),
674
+ gcpServiceAccountEmail: z.string().optional(),
675
+ azureClientId: z.string().optional(),
676
+ })
677
+ .optional(),
678
+ })
679
+ .optional(),
680
+ })
681
+ .optional(),
682
+ // Managed external PostgreSQL (AWS RDS/Aurora or Azure Flexible Server).
683
+ // Self-hosted Supabase runs against this instead of the bundled in-cluster
684
+ // database. Maps to the chart's supabase.externalDatabase.* values.
685
+ postgres: z
686
+ .object({
687
+ mode: z.enum(["embedded", "external"]),
688
+ external: z
689
+ .object({
690
+ // Managed provider this database lives on (gates CLI prompts; only
691
+ // aws/azure are supported for the structured/managed flow).
692
+ provider: z.enum(["aws", "azure"]).optional(),
693
+ host: z.string().optional(),
694
+ port: z.number().int().min(1).max(65535).optional(),
695
+ database: z.string().optional(),
696
+ // One-time master/owner credentials the chart's pre-install hook
697
+ // uses to initialize the database (roles, schemas, auth helpers,
698
+ // publication). Inline creds are materialized into a hook-scoped
699
+ // secret; secretRef points at a pre-created secret instead.
700
+ bootstrap: z
701
+ .object({
702
+ enabled: z.boolean().optional(),
703
+ masterUsername: z.string().optional(),
704
+ masterPassword: z.string().optional(),
705
+ secretRef: z.string().optional(),
706
+ appRole: z.string().optional(),
707
+ })
708
+ .optional(),
709
+ })
710
+ .optional(),
711
+ })
712
+ .optional(),
713
+ })
714
+ .optional(),
715
+ backup: z
716
+ .object({
717
+ enabled: z.boolean(),
718
+ schedule: z.string().min(1),
719
+ retentionDays: z.number().int().min(1),
720
+ })
721
+ .optional(),
476
722
  // Optional features
477
723
  features: z.object({
478
724
  ai: z.object({
@@ -495,15 +741,27 @@ export const DeploymentConfigSchema = z.object({
495
741
  remoteWriteUrl: z.string().url().optional(),
496
742
  remoteWrite: RemoteWriteConfigSchema.optional(),
497
743
  }),
744
+ observability: z
745
+ .object({
746
+ clickstack: z.object({
747
+ enabled: z.boolean(),
748
+ telemetryRetentionDays: z.number().int().min(1).optional(),
749
+ decisionLogRetentionDays: z.number().int().min(1).optional(),
750
+ clickHouseStorageSize: z.string().optional(),
751
+ }),
752
+ })
753
+ .optional(),
754
+ // Distributed tracing (optional; self-hosted only). Absent on existing
755
+ // config files, which keeps them valid.
756
+ tracing: TracingConfigSchema.optional(),
757
+ cache: CacheObservabilityConfigSchema.optional(),
498
758
  logging: z.object({
499
- // Logging always happens to console by default
500
- // This configures additional external sinks
759
+ // Console logging is always on. This selects an additional external
760
+ // logging platform. Cloud object storage for decision logs is configured
761
+ // separately under `storage`.
501
762
  sink: z.enum([
502
763
  "console",
503
764
  "pending",
504
- "s3",
505
- "azure-blob",
506
- "gcs",
507
765
  "datadog",
508
766
  "splunk",
509
767
  "elasticsearch",
@@ -511,18 +769,13 @@ export const DeploymentConfigSchema = z.object({
511
769
  "newrelic",
512
770
  "axiom",
513
771
  ]),
514
- // Sink-specific configuration
515
- // For cloud storage: bucket name and region
516
- // For platforms: repurposed for credentials (API key) and extra config
772
+ // For platforms, bucket/region are repurposed to carry the credential
773
+ // (API key/token) and endpoint/site.
517
774
  bucket: z.string().optional(),
518
775
  region: z.string().optional(),
519
- cloudAuthMode: z.enum(["workload-identity", "secret"]).optional(),
520
- awsIamRoleArn: z.string().optional(),
521
- azureBlobContainer: z.string().optional(),
522
- azureBlobClientId: z.string().optional(),
523
- azureBlobTenantId: z.string().optional(),
524
- azureBlobConnectionStringSecretRef: SecretKeyRefSchema.optional(),
525
- gcpServiceAccountEmail: z.string().optional(),
776
+ // Application/container log shipping to Elasticsearch via the Vector
777
+ // agent DaemonSet (distinct from the decision-log `sink` above).
778
+ appLogs: AppLogsConfigSchema.optional(),
526
779
  }),
527
780
  customEmails: z
528
781
  .object({
@@ -548,65 +801,17 @@ export const DeploymentConfigSchema = z.object({
548
801
  }),
549
802
  // Credentials
550
803
  licenseKey: z.string().min(1),
551
- // Version - app and HPS image versions
552
- appVersion: z.string().optional(),
553
- hpsVersion: z.string().optional(),
804
+ // Product version used for app, HPS, and HPS worker images
805
+ version: z.string().min(1),
806
+ // Optional single registry-host override for every image. Empty/unset means
807
+ // the chart default docker.io/rulebricks/*. When set (e.g. "myacr.azurecr.io"
808
+ // for an air-gapped mirror), the CLI rewrites the registry host into
809
+ // global.imageRegistry and into each Tier-2 chart's native image keys, keeping
810
+ // the rulebricks/<name> path. See the helm chart's global.imageRegistry knob.
811
+ imageRegistry: z.string().optional(),
554
812
  // Legacy chart version (deprecated, kept for backwards compatibility)
555
813
  chartVersion: z.string().optional(),
556
814
  });
557
- export const WIZARD_STEPS = [
558
- { id: "mode", title: "Deployment Mode", description: "Choose how to deploy" },
559
- {
560
- id: "cloud",
561
- title: "Cloud Provider",
562
- description: "Select your cloud provider",
563
- },
564
- {
565
- id: "domain",
566
- title: "Domain & DNS",
567
- description: "Configure your domain and DNS",
568
- },
569
- {
570
- id: "smtp",
571
- title: "Email (SMTP)",
572
- description: "Configure email delivery",
573
- },
574
- {
575
- id: "database",
576
- title: "Database",
577
- description: "Choose your database setup",
578
- },
579
- {
580
- id: "database-creds",
581
- title: "Database Credentials",
582
- description: "Configure database access",
583
- },
584
- {
585
- id: "tier",
586
- title: "Performance Tier",
587
- description: "Select your deployment size",
588
- },
589
- {
590
- id: "features",
591
- title: "Optional Features",
592
- description: "Enable additional features",
593
- },
594
- {
595
- id: "feature-config",
596
- title: "Feature Settings",
597
- description: "Configure enabled features",
598
- },
599
- {
600
- id: "credentials",
601
- title: "License & Version",
602
- description: "Enter license and select version",
603
- },
604
- {
605
- id: "review",
606
- title: "Review & Save",
607
- description: "Review your configuration",
608
- },
609
- ];
610
815
  // Helper to check if DNS provider supports external-dns
611
816
  export function isSupportedDnsProvider(provider) {
612
817
  return SUPPORTED_DNS_PROVIDERS.includes(provider);
@@ -635,9 +840,27 @@ export const ProfileConfigSchema = z.object({
635
840
  openaiApiKey: z.string().optional(),
636
841
  licenseKey: z.string().optional(),
637
842
  // Preferences
638
- tier: z.enum(["small", "medium", "large"]).optional(),
639
843
  databaseType: z.enum(["self-hosted", "supabase-cloud"]).optional(),
640
- infrastructureMode: z.enum(["existing", "provision"]).optional(),
844
+ storage: z
845
+ .object({
846
+ provider: z.enum(["s3", "azure-blob", "gcs"]),
847
+ cloudAuthMode: z.enum(["workload-identity", "secret"]).optional(),
848
+ bucket: z.string().optional(),
849
+ region: z.string().optional(),
850
+ awsIamRoleArn: z.string().optional(),
851
+ azureBlobClientId: z.string().optional(),
852
+ azureBlobTenantId: z.string().optional(),
853
+ azureBlobConnectionStringSecretRef: SecretKeyRefSchema.optional(),
854
+ azureBlobContainer: z.string().optional(),
855
+ gcpServiceAccountEmail: z.string().optional(),
856
+ paths: z
857
+ .object({
858
+ decisionLogs: z.string().optional(),
859
+ dbBackups: z.string().optional(),
860
+ })
861
+ .optional(),
862
+ })
863
+ .optional(),
641
864
  // SSO (optional)
642
865
  ssoProvider: z
643
866
  .enum(["azure", "google", "okta", "keycloak", "ory", "other"])