@rulebricks/cli 2.1.6 → 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 (114) hide show
  1. package/README.md +75 -14
  2. package/cluster-setup/aws/README.md +123 -0
  3. package/cluster-setup/aws/check-aws-access.sh +242 -0
  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 +141 -0
  7. package/cluster-setup/azure/check-aks-prereqs.sh +276 -0
  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 +189 -0
  11. package/cluster-setup/gcp/check-gke-prereqs.sh +260 -0
  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 -47
  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 +174 -29
  33. package/dist/components/Wizard/WizardContext.js +896 -91
  34. package/dist/components/Wizard/steps/CloudProviderStep.js +192 -102
  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 +959 -248
  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 -7
  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 +1937 -259
  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 +126 -13
  85. package/dist/lib/kubernetes.js +624 -134
  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 +2152 -95
  101. package/dist/types/index.js +554 -286
  102. package/package.json +10 -4
  103. package/schema/values.schema.json +1934 -0
  104. package/dist/components/Wizard/steps/CredentialsStep.d.ts +0 -6
  105. package/dist/components/Wizard/steps/CredentialsStep.js +0 -22
  106. package/dist/components/Wizard/steps/DeploymentModeStep.d.ts +0 -5
  107. package/dist/components/Wizard/steps/DeploymentModeStep.js +0 -26
  108. package/dist/components/Wizard/steps/TierStep.d.ts +0 -6
  109. package/dist/components/Wizard/steps/TierStep.js +0 -29
  110. package/dist/lib/terraform.d.ts +0 -66
  111. package/dist/lib/terraform.js +0 -754
  112. package/terraform/aws/main.tf +0 -355
  113. package/terraform/azure/main.tf +0 -371
  114. 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,24 +195,320 @@ 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)";
198
+ const SecretKeyRefSchema = z.object({
199
+ name: z.string().min(1),
200
+ key: z.string().min(1),
201
+ });
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;
384
264
  }
265
+ const RemoteWriteConfigSchema = z
266
+ .object({
267
+ destination: z.enum([
268
+ "aws-amp",
269
+ "azure-monitor",
270
+ "grafana-cloud",
271
+ "generic",
272
+ ]),
273
+ url: z.string().url(),
274
+ authType: z
275
+ .enum([
276
+ "none",
277
+ "managed-identity",
278
+ "workload-identity",
279
+ "oauth",
280
+ "basic",
281
+ "bearer",
282
+ ])
283
+ .optional(),
284
+ awsRegion: z.string().optional(),
285
+ awsRoleArn: z.string().optional(),
286
+ azureCloud: z
287
+ .enum(["AzurePublic", "AzureChina", "AzureGovernment"])
288
+ .optional(),
289
+ clientId: z.string().optional(),
290
+ tenantId: z.string().optional(),
291
+ clientSecretRef: SecretKeyRefSchema.optional(),
292
+ usernameSecretRef: SecretKeyRefSchema.optional(),
293
+ passwordSecretRef: SecretKeyRefSchema.optional(),
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
+ }
300
+ });
301
+ const MonitoringDestinationSchema = z.enum([
302
+ "local-grafana",
303
+ "aws-amp",
304
+ "azure-monitor",
305
+ "grafana-cloud",
306
+ "generic",
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
+ });
385
512
  // Deployment configuration schema
386
513
  export const DeploymentConfigSchema = z.object({
387
514
  name: z
@@ -391,12 +518,24 @@ export const DeploymentConfigSchema = z.object({
391
518
  .regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/),
392
519
  // Infrastructure
393
520
  infrastructure: z.object({
394
- mode: z.enum(["existing", "provision"]),
521
+ mode: z.literal("existing"),
395
522
  provider: z.enum(["aws", "gcp", "azure"]).optional(),
396
523
  region: z.string().optional(),
397
524
  clusterName: z.string().optional(),
398
525
  gcpProjectId: z.string().optional(),
399
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(),
400
539
  }),
401
540
  // Domain & TLS
402
541
  domain: z.string().min(1),
@@ -408,8 +547,6 @@ export const DeploymentConfigSchema = z.object({
408
547
  provider: z.enum(["route53", "cloudflare", "google", "azure", "other"]),
409
548
  // Should we auto-manage DNS records? (only applicable for supported providers)
410
549
  autoManage: z.boolean(),
411
- // For existing clusters: does external-dns already exist cluster-wide?
412
- existingExternalDns: z.boolean().optional(),
413
550
  }),
414
551
  // SMTP Configuration
415
552
  smtp: z.object({
@@ -435,8 +572,153 @@ export const DeploymentConfigSchema = z.object({
435
572
  supabaseDashboardUser: z.string().optional(),
436
573
  supabaseDashboardPass: z.string().optional(),
437
574
  }),
438
- // Performance
439
- 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(),
440
722
  // Optional features
441
723
  features: z.object({
442
724
  ai: z.object({
@@ -454,18 +736,32 @@ export const DeploymentConfigSchema = z.object({
454
736
  }),
455
737
  monitoring: z.object({
456
738
  enabled: z.boolean(),
457
- // Optional: Prometheus remote write URL (Datadog, Grafana Cloud, etc.)
739
+ destination: MonitoringDestinationSchema.optional(),
740
+ // Legacy optional URL retained for existing config files.
458
741
  remoteWriteUrl: z.string().url().optional(),
742
+ remoteWrite: RemoteWriteConfigSchema.optional(),
459
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(),
460
758
  logging: z.object({
461
- // Logging always happens to console by default
462
- // 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`.
463
762
  sink: z.enum([
464
763
  "console",
465
764
  "pending",
466
- "s3",
467
- "azure-blob",
468
- "gcs",
469
765
  "datadog",
470
766
  "splunk",
471
767
  "elasticsearch",
@@ -473,11 +769,13 @@ export const DeploymentConfigSchema = z.object({
473
769
  "newrelic",
474
770
  "axiom",
475
771
  ]),
476
- // Sink-specific configuration
477
- // For cloud storage: bucket name and region
478
- // 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.
479
774
  bucket: z.string().optional(),
480
775
  region: 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(),
481
779
  }),
482
780
  customEmails: z
483
781
  .object({
@@ -503,65 +801,17 @@ export const DeploymentConfigSchema = z.object({
503
801
  }),
504
802
  // Credentials
505
803
  licenseKey: z.string().min(1),
506
- // Version - app and HPS image versions
507
- appVersion: z.string().optional(),
508
- 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(),
509
812
  // Legacy chart version (deprecated, kept for backwards compatibility)
510
813
  chartVersion: z.string().optional(),
511
814
  });
512
- export const WIZARD_STEPS = [
513
- { id: "mode", title: "Deployment Mode", description: "Choose how to deploy" },
514
- {
515
- id: "cloud",
516
- title: "Cloud Provider",
517
- description: "Select your cloud provider",
518
- },
519
- {
520
- id: "domain",
521
- title: "Domain & DNS",
522
- description: "Configure your domain and DNS",
523
- },
524
- {
525
- id: "smtp",
526
- title: "Email (SMTP)",
527
- description: "Configure email delivery",
528
- },
529
- {
530
- id: "database",
531
- title: "Database",
532
- description: "Choose your database setup",
533
- },
534
- {
535
- id: "database-creds",
536
- title: "Database Credentials",
537
- description: "Configure database access",
538
- },
539
- {
540
- id: "tier",
541
- title: "Performance Tier",
542
- description: "Select your deployment size",
543
- },
544
- {
545
- id: "features",
546
- title: "Optional Features",
547
- description: "Enable additional features",
548
- },
549
- {
550
- id: "feature-config",
551
- title: "Feature Settings",
552
- description: "Configure enabled features",
553
- },
554
- {
555
- id: "credentials",
556
- title: "License & Version",
557
- description: "Enter license and select version",
558
- },
559
- {
560
- id: "review",
561
- title: "Review & Save",
562
- description: "Review your configuration",
563
- },
564
- ];
565
815
  // Helper to check if DNS provider supports external-dns
566
816
  export function isSupportedDnsProvider(provider) {
567
817
  return SUPPORTED_DNS_PROVIDERS.includes(provider);
@@ -590,9 +840,27 @@ export const ProfileConfigSchema = z.object({
590
840
  openaiApiKey: z.string().optional(),
591
841
  licenseKey: z.string().optional(),
592
842
  // Preferences
593
- tier: z.enum(["small", "medium", "large"]).optional(),
594
843
  databaseType: z.enum(["self-hosted", "supabase-cloud"]).optional(),
595
- 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(),
596
864
  // SSO (optional)
597
865
  ssoProvider: z
598
866
  .enum(["azure", "google", "okta", "keycloak", "ory", "other"])