@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
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { createContext, useContext, useReducer } from "react";
3
- import { DEFAULT_EMAIL_SUBJECTS, } from "../../types/index.js";
3
+ import { DEFAULT_EMAIL_SUBJECTS, validateRemoteWriteConfig, } from "../../types/index.js";
4
+ import { generateSecureSecret } from "../../lib/validation.js";
4
5
  /**
5
6
  * Creates the initial wizard state, optionally pre-populated from a user profile.
6
7
  * Profile values are used as defaults that the user can still modify.
@@ -10,7 +11,6 @@ function getInitialState(profile) {
10
11
  step: 0,
11
12
  name: "",
12
13
  // Infrastructure - pre-populate from profile
13
- infrastructureMode: profile?.infrastructureMode ?? null,
14
14
  provider: profile?.provider ?? null,
15
15
  region: profile?.region ?? "",
16
16
  clusterName: profile?.clusterName ?? "",
@@ -19,11 +19,13 @@ function getInitialState(profile) {
19
19
  // Domain & Email - pre-populate from profile
20
20
  domain: "", // Domain is intentionally left empty - user should enter unique domain per deployment
21
21
  adminEmail: profile?.adminEmail ?? "",
22
- tlsEmail: profile?.tlsEmail ?? "",
22
+ // The TLS email is no longer asked in the wizard; it defaults to the admin
23
+ // email in toConfig. Only an existing config (redeploy) can carry a custom
24
+ // value, so it is intentionally not pre-populated from the profile.
25
+ tlsEmail: "",
23
26
  // DNS Configuration - pre-populate from profile
24
27
  dnsProvider: profile?.dnsProvider ?? "other",
25
28
  dnsAutoManage: false,
26
- existingExternalDns: false,
27
29
  // SMTP - pre-populate from profile
28
30
  smtpHost: profile?.smtpHost ?? "",
29
31
  smtpPort: profile?.smtpPort ?? 587,
@@ -38,12 +40,34 @@ function getInitialState(profile) {
38
40
  supabaseServiceKey: "",
39
41
  supabaseAccessToken: "",
40
42
  supabaseProjectRef: "",
41
- supabaseJwtSecret: "",
43
+ supabaseJwtSecret: generateSecureSecret(64),
42
44
  supabaseDbPassword: "",
43
45
  supabaseDashboardUser: "supabase",
44
46
  supabaseDashboardPass: "",
45
- // Performance - pre-populate from profile
46
- tier: profile?.tier ?? null,
47
+ // Cluster capabilities (populated by the cluster scan)
48
+ nodeArchitecture: null,
49
+ arm64TolerationRequired: false,
50
+ storageClass: "",
51
+ storageProvisioner: "",
52
+ schedulableNodeCount: 0,
53
+ totalCpuCores: 0,
54
+ totalMemoryGi: 0,
55
+ eligibleCpuCores: 0,
56
+ eligibleMemoryGi: 0,
57
+ totalPersistentStorageGi: 0,
58
+ // Shared object storage
59
+ storageProvider: profile?.storage?.provider ?? null,
60
+ storageBucket: profile?.storage?.bucket ?? "",
61
+ storageRegion: profile?.storage?.region ?? "",
62
+ storageCloudAuthMode: profile?.storage?.cloudAuthMode ?? "workload-identity",
63
+ storageAwsIamRoleArn: profile?.storage?.awsIamRoleArn ?? "",
64
+ storageAzureBlobContainer: profile?.storage?.azureBlobContainer ?? "rulebricks",
65
+ storageAzureBlobClientId: profile?.storage?.azureBlobClientId ?? "",
66
+ storageAzureBlobTenantId: profile?.storage?.azureBlobTenantId ?? "",
67
+ storageAzureBlobConnectionStringSecretRef: profile?.storage?.azureBlobConnectionStringSecretRef
68
+ ? `${profile.storage.azureBlobConnectionStringSecretRef.name}:${profile.storage.azureBlobConnectionStringSecretRef.key}`
69
+ : "",
70
+ storageGcpServiceAccountEmail: profile?.storage?.gcpServiceAccountEmail ?? "",
47
71
  // Features - AI - pre-populate from profile
48
72
  aiEnabled: !!profile?.openaiApiKey,
49
73
  openaiApiKey: profile?.openaiApiKey ?? "",
@@ -53,13 +77,49 @@ function getInitialState(profile) {
53
77
  ssoUrl: profile?.ssoUrl ?? "",
54
78
  ssoClientId: profile?.ssoClientId ?? "",
55
79
  ssoClientSecret: profile?.ssoClientSecret ?? "",
56
- // Features - Monitoring
57
- monitoringEnabled: false,
80
+ // Features - Monitoring (metrics export is opt-in; in-cluster Prometheus
81
+ // is always installed)
82
+ clickStackEnabled: true,
83
+ clickStackTelemetryRetentionDays: 7,
84
+ clickHouseStorageSize: "100Gi",
85
+ metricsExportEnabled: false,
86
+ prometheusMonitoringDestination: null,
58
87
  prometheusRemoteWriteUrl: "",
88
+ prometheusRemoteWriteDestination: null,
89
+ prometheusRemoteWriteAuthType: null,
90
+ prometheusRemoteWriteAwsRegion: "",
91
+ prometheusRemoteWriteAwsRoleArn: "",
92
+ prometheusRemoteWriteAzureCloud: "AzurePublic",
93
+ prometheusRemoteWriteClientId: "",
94
+ prometheusRemoteWriteTenantId: "",
95
+ prometheusRemoteWriteSecretRef: "",
96
+ prometheusRemoteWriteUsernameSecretRef: "",
97
+ prometheusRemoteWritePasswordSecretRef: "",
98
+ prometheusRemoteWriteBearerTokenSecretRef: "",
59
99
  // Features - Logging
60
100
  loggingSink: "console", // Default to console only
61
- loggingBucket: "",
62
- loggingRegion: "",
101
+ loggingPlatformCredential: "",
102
+ loggingPlatformDetail: "",
103
+ // Features - Distributed Tracing
104
+ tracingEnabled: false,
105
+ tracingDestination: "elastic",
106
+ tracingElasticEndpoint: "",
107
+ tracingElasticAuthMode: "secret-token",
108
+ tracingElasticSecretToken: "",
109
+ tracingElasticApiKey: "",
110
+ tracingOtlpEndpoint: "",
111
+ tracingOtlpAuthMode: "none",
112
+ tracingOtlpHeaderName: "Authorization",
113
+ tracingOtlpToken: "",
114
+ tracingAzureConnectionString: "",
115
+ // Features - Application log shipping
116
+ appLogsEnabled: false,
117
+ appLogsElasticEndpoint: "",
118
+ appLogsElasticIndex: "rulebricks-app-logs",
119
+ appLogsElasticAuthMode: "basic",
120
+ appLogsElasticUsername: "",
121
+ appLogsElasticPassword: "",
122
+ appLogsElasticApiKey: "",
63
123
  // Features - Custom Email Templates
64
124
  customEmailsEnabled: false,
65
125
  emailSubjects: { ...DEFAULT_EMAIL_SUBJECTS },
@@ -69,26 +129,579 @@ function getInitialState(profile) {
69
129
  recovery: "",
70
130
  emailChange: "",
71
131
  },
132
+ // Database backups
133
+ backupEnabled: false,
134
+ backupSchedule: "0 2 * * *",
135
+ backupRetentionDays: 7,
136
+ // External services - Redis (default to in-cluster)
137
+ redisMode: "embedded",
138
+ redisHost: "",
139
+ redisPort: 6379,
140
+ redisPassword: "",
141
+ redisExistingSecret: "",
142
+ redisExistingSecretKey: "redis-password",
143
+ redisTls: false,
144
+ redisHttpApiEnabled: false,
145
+ redisHttpApiUrl: "",
146
+ redisHttpApiToken: "",
147
+ valkeyAdminEnabled: false,
148
+ valkeyAdminExposure: "internal",
149
+ valkeyAdminHostname: "",
150
+ valkeyAdminBasicAuthUsers: [],
151
+ valkeyAdminAllowedIPs: [],
152
+ redisExporterEnabled: false,
153
+ kafkaExporterEnabled: false,
154
+ // External services - Kafka (default to in-cluster)
155
+ kafkaMode: "embedded",
156
+ kafkaPreset: null,
157
+ kafkaBrokers: "",
158
+ kafkaTopic: "logs",
159
+ kafkaTopicPrefix: "com.rulebricks.",
160
+ kafkaProvisionTopics: true,
161
+ kafkaSsl: false,
162
+ kafkaSaslMechanism: "",
163
+ kafkaSaslRegion: "",
164
+ kafkaSaslUsername: "",
165
+ kafkaSaslPassword: "",
166
+ kafkaSaslExistingSecret: "",
167
+ kafkaIdentityAwsRoleArn: "",
168
+ kafkaIdentityGcpServiceAccountEmail: "",
169
+ kafkaIdentityAzureClientId: "",
170
+ // External services - Postgres
171
+ postgresMode: "embedded",
172
+ postgresHost: "",
173
+ postgresPort: 5432,
174
+ postgresDatabase: "postgres",
175
+ postgresMasterUsername: "postgres",
176
+ postgresMasterPassword: "",
72
177
  // Credentials - pre-populate from profile
73
178
  licenseKey: profile?.licenseKey ?? "",
74
179
  // Version
75
- appVersion: "",
76
- hpsVersion: "",
180
+ version: "",
77
181
  chartVersion: "",
78
182
  };
79
183
  }
80
184
  // Default initial state (for backwards compatibility)
81
185
  const initialState = getInitialState();
186
+ /**
187
+ * Derives the Supabase project ref from a managed-Supabase project URL
188
+ * (https://<ref>.supabase.co), so the wizard never has to ask for it.
189
+ * Returns undefined for custom domains, where the ref can't be inferred.
190
+ */
191
+ function deriveSupabaseProjectRef(url) {
192
+ const match = url
193
+ .trim()
194
+ .match(/^https?:\/\/([a-z0-9-]+)\.supabase\.(?:co|com|in)(?:[/:]|$)/i);
195
+ return match ? match[1].toLowerCase() : undefined;
196
+ }
197
+ function parseSecretKeyRef(value) {
198
+ const [name, key] = value.split(":").map((part) => part.trim());
199
+ if (!name || !key)
200
+ return undefined;
201
+ return { name, key };
202
+ }
203
+ function formatSecretKeyRef(ref) {
204
+ return ref ? `${ref.name}:${ref.key}` : "";
205
+ }
206
+ /**
207
+ * Builds the externalServices config from wizard state. Returns undefined when
208
+ * both Redis and Kafka use the in-cluster defaults so configs stay clean.
209
+ */
210
+ function buildExternalServices(state) {
211
+ const redisExternal = state.redisMode === "external";
212
+ const kafkaExternal = state.kafkaMode === "external";
213
+ const postgresExternal = state.postgresMode === "external";
214
+ if (!redisExternal && !kafkaExternal && !postgresExternal) {
215
+ return undefined;
216
+ }
217
+ return {
218
+ redis: {
219
+ mode: state.redisMode,
220
+ external: redisExternal
221
+ ? {
222
+ host: state.redisHost.trim() || undefined,
223
+ port: state.redisPort || undefined,
224
+ password: state.redisPassword || undefined,
225
+ existingSecret: state.redisExistingSecret.trim() || undefined,
226
+ existingSecretKey: state.redisExistingSecret.trim() && state.redisExistingSecretKey
227
+ ? state.redisExistingSecretKey
228
+ : undefined,
229
+ tls: state.redisTls,
230
+ httpApi: state.redisHttpApiEnabled
231
+ ? {
232
+ enabled: true,
233
+ url: state.redisHttpApiUrl.trim() || undefined,
234
+ token: state.redisHttpApiToken || undefined,
235
+ }
236
+ : undefined,
237
+ }
238
+ : undefined,
239
+ },
240
+ kafka: {
241
+ mode: state.kafkaMode,
242
+ external: kafkaExternal
243
+ ? {
244
+ preset: state.kafkaPreset ?? "custom",
245
+ brokers: state.kafkaBrokers.trim() || undefined,
246
+ topic: state.kafkaTopic.trim() || undefined,
247
+ // Always emit the prefix (incl. "") so the choice round-trips and the
248
+ // chart doesn't silently fall back to its default.
249
+ topicPrefix: state.kafkaTopicPrefix,
250
+ provisionTopics: state.kafkaProvisionTopics,
251
+ ssl: state.kafkaSsl,
252
+ sasl: state.kafkaSaslMechanism
253
+ ? {
254
+ mechanism: state.kafkaSaslMechanism,
255
+ region: state.kafkaSaslRegion.trim() || undefined,
256
+ username: state.kafkaSaslUsername || undefined,
257
+ password: state.kafkaSaslPassword || undefined,
258
+ existingSecret: state.kafkaSaslExistingSecret.trim() || undefined,
259
+ }
260
+ : undefined,
261
+ identity: buildKafkaIdentity(state),
262
+ }
263
+ : undefined,
264
+ },
265
+ postgres: {
266
+ mode: state.postgresMode,
267
+ external: postgresExternal
268
+ ? {
269
+ provider: state.provider === "aws" || state.provider === "azure"
270
+ ? state.provider
271
+ : undefined,
272
+ host: state.postgresHost.trim() || undefined,
273
+ port: state.postgresPort || undefined,
274
+ database: state.postgresDatabase.trim() || undefined,
275
+ // One-time creds for the chart's bootstrap hook. The shared
276
+ // service-role password is sourced separately from the Supabase DB
277
+ // password (secret.db) so it matches what the services present.
278
+ bootstrap: {
279
+ enabled: true,
280
+ masterUsername: state.postgresMasterUsername.trim() || undefined,
281
+ masterPassword: state.postgresMasterPassword || undefined,
282
+ appRole: "postgres",
283
+ },
284
+ }
285
+ : undefined,
286
+ },
287
+ };
288
+ }
289
+ /**
290
+ * Collects the cloud workload identity for token-based Kafka auth. Returns
291
+ * undefined when no identity is provided (e.g. Azure Event Hubs PLAIN).
292
+ */
293
+ function buildKafkaIdentity(state) {
294
+ const awsRoleArn = state.kafkaIdentityAwsRoleArn.trim();
295
+ const gcpServiceAccountEmail = state.kafkaIdentityGcpServiceAccountEmail.trim();
296
+ const azureClientId = state.kafkaIdentityAzureClientId.trim();
297
+ if (!awsRoleArn && !gcpServiceAccountEmail && !azureClientId) {
298
+ return undefined;
299
+ }
300
+ return {
301
+ awsRoleArn: awsRoleArn || undefined,
302
+ gcpServiceAccountEmail: gcpServiceAccountEmail || undefined,
303
+ azureClientId: azureClientId || undefined,
304
+ };
305
+ }
306
+ /**
307
+ * Builds the Prometheus remote_write config from wizard state. Metrics reuse the
308
+ * single Rulebricks identity chosen in the Storage step, so Azure/AWS principals
309
+ * fall back to the storage identity when not set explicitly.
310
+ */
311
+ function buildRemoteWriteFromState(state) {
312
+ if (state.prometheusMonitoringDestination === "local-grafana" ||
313
+ !state.prometheusRemoteWriteDestination ||
314
+ !state.prometheusRemoteWriteUrl) {
315
+ return undefined;
316
+ }
317
+ return {
318
+ destination: state.prometheusRemoteWriteDestination,
319
+ url: state.prometheusRemoteWriteUrl,
320
+ authType: state.prometheusRemoteWriteAuthType ||
321
+ (state.prometheusRemoteWriteDestination === "azure-monitor"
322
+ ? "managed-identity"
323
+ : undefined),
324
+ awsRegion: state.prometheusRemoteWriteDestination === "aws-amp"
325
+ ? state.prometheusRemoteWriteAwsRegion || state.region || undefined
326
+ : undefined,
327
+ awsRoleArn: state.prometheusRemoteWriteDestination === "aws-amp"
328
+ ? state.prometheusRemoteWriteAwsRoleArn ||
329
+ state.storageAwsIamRoleArn ||
330
+ undefined
331
+ : undefined,
332
+ azureCloud: state.prometheusRemoteWriteAzureCloud,
333
+ clientId: state.prometheusRemoteWriteDestination === "azure-monitor"
334
+ ? state.prometheusRemoteWriteClientId ||
335
+ state.storageAzureBlobClientId ||
336
+ undefined
337
+ : state.prometheusRemoteWriteClientId || undefined,
338
+ tenantId: state.prometheusRemoteWriteDestination === "azure-monitor"
339
+ ? state.prometheusRemoteWriteTenantId ||
340
+ state.storageAzureBlobTenantId ||
341
+ undefined
342
+ : state.prometheusRemoteWriteTenantId || undefined,
343
+ clientSecretRef: parseSecretKeyRef(state.prometheusRemoteWriteSecretRef),
344
+ usernameSecretRef: parseSecretKeyRef(state.prometheusRemoteWriteUsernameSecretRef),
345
+ passwordSecretRef: parseSecretKeyRef(state.prometheusRemoteWritePasswordSecretRef),
346
+ bearerTokenSecretRef: parseSecretKeyRef(state.prometheusRemoteWriteBearerTokenSecretRef),
347
+ };
348
+ }
349
+ /**
350
+ * Returns a list of human-readable reasons the wizard state can't be saved as a
351
+ * valid DeploymentConfig. Empty array means it's good to save. Shared by toConfig
352
+ * (which returns null when non-empty) and the review screen (which shows them),
353
+ * so users get a specific reason instead of a generic "invalid configuration".
354
+ */
355
+ export function collectConfigIssues(state) {
356
+ const issues = [];
357
+ if (!state.name)
358
+ issues.push("Deployment name is required.");
359
+ if (!state.domain)
360
+ issues.push("Domain is required.");
361
+ if (!state.adminEmail)
362
+ issues.push("Admin email is required.");
363
+ if (!state.licenseKey)
364
+ issues.push("License key is required.");
365
+ if (!state.smtpHost || !state.smtpUser || !state.smtpPass || !state.smtpFrom) {
366
+ issues.push("SMTP host, user, password, and from address are required.");
367
+ }
368
+ if (state.databaseType === "supabase-cloud") {
369
+ if (!state.supabaseUrl ||
370
+ !state.supabaseAnonKey ||
371
+ !state.supabaseServiceKey ||
372
+ !state.supabaseAccessToken) {
373
+ issues.push("Managed Supabase requires a project URL, anon key, service key, and access token.");
374
+ }
375
+ }
376
+ else if (state.databaseType === "self-hosted") {
377
+ if (!state.supabaseJwtSecret) {
378
+ issues.push("Self-hosted Supabase requires a JWT secret.");
379
+ }
380
+ if (!state.supabaseDbPassword) {
381
+ issues.push("Self-hosted Supabase requires a database password.");
382
+ }
383
+ }
384
+ if (state.aiEnabled && !state.openaiApiKey) {
385
+ issues.push("AI is enabled but the OpenAI API key is missing.");
386
+ }
387
+ if (state.ssoEnabled &&
388
+ (!state.ssoProvider || !state.ssoClientId || !state.ssoClientSecret)) {
389
+ issues.push("SSO is enabled but the provider, client ID, or client secret is missing.");
390
+ }
391
+ if (state.loggingSink !== "console" &&
392
+ state.loggingSink !== "pending" &&
393
+ !state.loggingPlatformCredential) {
394
+ issues.push("The selected logging platform is missing its credentials/endpoint.");
395
+ }
396
+ if (!state.clickStackEnabled && state.tracingEnabled) {
397
+ if (state.tracingDestination === "elastic") {
398
+ if (!state.tracingElasticEndpoint) {
399
+ issues.push("Distributed tracing is enabled but the Elastic APM endpoint is missing.");
400
+ }
401
+ if (state.tracingElasticAuthMode === "secret-token" &&
402
+ !state.tracingElasticSecretToken) {
403
+ issues.push("Distributed tracing (secret-token auth) is missing the Elastic APM secret token.");
404
+ }
405
+ if (state.tracingElasticAuthMode === "api-key" &&
406
+ !state.tracingElasticApiKey) {
407
+ issues.push("Distributed tracing (API key auth) is missing the Elastic APM API key.");
408
+ }
409
+ }
410
+ else if (state.tracingDestination === "otlp") {
411
+ if (!state.tracingOtlpEndpoint) {
412
+ issues.push("Distributed tracing is enabled but the OTLP endpoint is missing.");
413
+ }
414
+ if (state.tracingOtlpAuthMode !== "none" && !state.tracingOtlpToken) {
415
+ issues.push("Distributed tracing (OTLP) is missing its authentication credential.");
416
+ }
417
+ }
418
+ else if (state.tracingDestination === "azure-monitor") {
419
+ if (!state.tracingAzureConnectionString) {
420
+ issues.push("Distributed tracing is enabled but the Azure Monitor connection string is missing.");
421
+ }
422
+ }
423
+ }
424
+ if (!state.clickStackEnabled && state.appLogsEnabled) {
425
+ if (!state.appLogsElasticEndpoint) {
426
+ issues.push("Application log shipping is enabled but the Elasticsearch endpoint is missing.");
427
+ }
428
+ if (state.appLogsElasticAuthMode === "basic" &&
429
+ (!state.appLogsElasticUsername || !state.appLogsElasticPassword)) {
430
+ issues.push("Application log shipping (basic auth) is missing the Elasticsearch username or password.");
431
+ }
432
+ if (state.appLogsElasticAuthMode === "api-key" &&
433
+ !state.appLogsElasticApiKey) {
434
+ issues.push("Application log shipping (API key auth) is missing the Elasticsearch API key.");
435
+ }
436
+ }
437
+ if (!state.storageProvider || !state.storageBucket || !state.storageRegion) {
438
+ issues.push("Object storage provider, bucket/account, and region are required.");
439
+ }
440
+ if (state.storageProvider === "s3" && !state.storageAwsIamRoleArn) {
441
+ issues.push("S3 storage requires an IAM role (IRSA).");
442
+ }
443
+ if (state.storageProvider === "azure-blob") {
444
+ if (!state.storageAzureBlobContainer) {
445
+ issues.push("Azure Blob storage requires a container name.");
446
+ }
447
+ if (state.storageCloudAuthMode === "workload-identity" &&
448
+ (!state.storageAzureBlobClientId || !state.storageAzureBlobTenantId)) {
449
+ issues.push("Azure Blob workload identity requires a client ID and tenant ID.");
450
+ }
451
+ if (state.storageCloudAuthMode === "secret" &&
452
+ !parseSecretKeyRef(state.storageAzureBlobConnectionStringSecretRef)) {
453
+ issues.push("Azure Blob connection-string auth requires a secret reference (name:key).");
454
+ }
455
+ }
456
+ if (state.storageProvider === "gcs" && !state.storageGcpServiceAccountEmail) {
457
+ issues.push("GCS storage requires a Google service account email.");
458
+ }
459
+ if (state.redisMode === "external" && !state.redisHost.trim()) {
460
+ issues.push("External Redis requires a host.");
461
+ }
462
+ if (state.kafkaMode === "external" && !state.kafkaBrokers.trim()) {
463
+ issues.push("External Kafka requires brokers.");
464
+ }
465
+ if (state.postgresMode === "external") {
466
+ if (!state.postgresHost.trim()) {
467
+ issues.push("External Postgres requires a host.");
468
+ }
469
+ if (!state.postgresMasterPassword) {
470
+ issues.push("External Postgres requires master credentials to initialize the database.");
471
+ }
472
+ }
473
+ if (!state.clickStackEnabled && state.metricsExportEnabled) {
474
+ const remoteWrite = buildRemoteWriteFromState(state);
475
+ if (!remoteWrite) {
476
+ issues.push("Metrics export is enabled but the Prometheus remote_write destination is not configured.");
477
+ }
478
+ else {
479
+ for (const message of validateRemoteWriteConfig(remoteWrite)) {
480
+ issues.push(message);
481
+ }
482
+ }
483
+ }
484
+ if (state.valkeyAdminEnabled &&
485
+ state.valkeyAdminExposure === "ingress" &&
486
+ state.valkeyAdminBasicAuthUsers.length === 0) {
487
+ issues.push("Valkey Admin ingress exposure requires at least one htpasswd BasicAuth user.");
488
+ }
489
+ return issues;
490
+ }
491
+ export function configToWizardState(config, profile) {
492
+ const base = getInitialState(profile);
493
+ const remoteWrite = config.features.monitoring.remoteWrite;
494
+ const storage = config.storage;
495
+ const customEmails = config.features.customEmails;
496
+ const externalRedis = config.externalServices?.redis;
497
+ const externalKafka = config.externalServices?.kafka;
498
+ const externalPostgres = config.externalServices?.postgres;
499
+ return {
500
+ ...base,
501
+ name: config.name,
502
+ provider: config.infrastructure.provider ?? base.provider,
503
+ region: config.infrastructure.region ?? base.region,
504
+ clusterName: config.infrastructure.clusterName ?? base.clusterName,
505
+ gcpProjectId: config.infrastructure.gcpProjectId ?? "",
506
+ azureResourceGroup: config.infrastructure.azureResourceGroup ?? "",
507
+ domain: config.domain,
508
+ adminEmail: config.adminEmail,
509
+ tlsEmail: config.tlsEmail,
510
+ dnsProvider: config.dns.provider,
511
+ dnsAutoManage: config.dns.autoManage,
512
+ smtpHost: config.smtp.host,
513
+ smtpPort: config.smtp.port,
514
+ smtpUser: config.smtp.user,
515
+ smtpPass: config.smtp.pass,
516
+ smtpFrom: config.smtp.from,
517
+ smtpFromName: config.smtp.fromName,
518
+ databaseType: config.database.type,
519
+ supabaseUrl: config.database.supabaseUrl ?? "",
520
+ supabaseAnonKey: config.database.supabaseAnonKey ?? "",
521
+ supabaseServiceKey: config.database.supabaseServiceKey ?? "",
522
+ supabaseAccessToken: config.database.supabaseAccessToken ?? "",
523
+ supabaseProjectRef: config.database.supabaseProjectRef ?? "",
524
+ supabaseJwtSecret: config.database.type === "self-hosted"
525
+ ? config.database.supabaseJwtSecret || base.supabaseJwtSecret
526
+ : config.database.supabaseJwtSecret ?? "",
527
+ supabaseDbPassword: config.database.supabaseDbPassword ?? "",
528
+ supabaseDashboardUser: config.database.supabaseDashboardUser ?? base.supabaseDashboardUser,
529
+ supabaseDashboardPass: config.database.supabaseDashboardPass ?? "",
530
+ nodeArchitecture: config.infrastructure.nodeArchitecture ?? null,
531
+ arm64TolerationRequired: config.infrastructure.arm64TolerationRequired ?? false,
532
+ storageClass: config.infrastructure.storageClass ?? "",
533
+ storageProvisioner: config.infrastructure.storageProvisioner ?? "",
534
+ schedulableNodeCount: config.infrastructure.schedulableNodeCount ?? 0,
535
+ totalCpuCores: config.infrastructure.totalCpuCores ?? 0,
536
+ totalMemoryGi: config.infrastructure.totalMemoryGi ?? 0,
537
+ eligibleCpuCores: config.infrastructure.eligibleCpuCores ?? 0,
538
+ eligibleMemoryGi: config.infrastructure.eligibleMemoryGi ?? 0,
539
+ totalPersistentStorageGi: config.infrastructure.totalPersistentStorageGi ?? 0,
540
+ storageProvider: storage?.provider ?? base.storageProvider,
541
+ storageBucket: storage?.bucket ?? "",
542
+ storageRegion: storage?.region ?? "",
543
+ storageCloudAuthMode: storage?.cloudAuthMode ?? base.storageCloudAuthMode,
544
+ storageAwsIamRoleArn: storage?.awsIamRoleArn ?? "",
545
+ storageAzureBlobContainer: storage?.azureBlobContainer ?? base.storageAzureBlobContainer,
546
+ storageAzureBlobClientId: storage?.azureBlobClientId ?? "",
547
+ storageAzureBlobTenantId: storage?.azureBlobTenantId ?? "",
548
+ storageAzureBlobConnectionStringSecretRef: formatSecretKeyRef(storage?.azureBlobConnectionStringSecretRef),
549
+ storageGcpServiceAccountEmail: storage?.gcpServiceAccountEmail ?? "",
550
+ aiEnabled: config.features.ai.enabled,
551
+ openaiApiKey: config.features.ai.openaiApiKey ?? "",
552
+ ssoEnabled: config.features.sso.enabled,
553
+ ssoProvider: config.features.sso.provider ?? null,
554
+ ssoUrl: config.features.sso.url ?? "",
555
+ ssoClientId: config.features.sso.clientId ?? "",
556
+ ssoClientSecret: config.features.sso.clientSecret ?? "",
557
+ clickStackEnabled: config.features.observability?.clickstack?.enabled ?? true,
558
+ clickStackTelemetryRetentionDays: config.features.observability?.clickstack?.telemetryRetentionDays ??
559
+ base.clickStackTelemetryRetentionDays,
560
+ clickHouseStorageSize: config.features.observability?.clickstack?.clickHouseStorageSize ??
561
+ base.clickHouseStorageSize,
562
+ // The toggle reflects whether remote_write is actually configured, so a
563
+ // redeploy resumes with the metrics-export sub-flow only when in use.
564
+ metricsExportEnabled: !!(remoteWrite || config.features.monitoring.remoteWriteUrl),
565
+ prometheusMonitoringDestination: config.features.monitoring.destination ?? null,
566
+ prometheusRemoteWriteUrl: remoteWrite?.url ?? config.features.monitoring.remoteWriteUrl ?? "",
567
+ prometheusRemoteWriteDestination: remoteWrite?.destination ?? null,
568
+ prometheusRemoteWriteAuthType: remoteWrite?.authType ?? null,
569
+ prometheusRemoteWriteAwsRegion: remoteWrite?.awsRegion ?? "",
570
+ prometheusRemoteWriteAwsRoleArn: remoteWrite?.awsRoleArn ?? "",
571
+ prometheusRemoteWriteAzureCloud: remoteWrite?.azureCloud ?? base.prometheusRemoteWriteAzureCloud,
572
+ prometheusRemoteWriteClientId: remoteWrite?.clientId ?? "",
573
+ prometheusRemoteWriteTenantId: remoteWrite?.tenantId ?? "",
574
+ prometheusRemoteWriteSecretRef: formatSecretKeyRef(remoteWrite?.clientSecretRef),
575
+ prometheusRemoteWriteUsernameSecretRef: formatSecretKeyRef(remoteWrite?.usernameSecretRef),
576
+ prometheusRemoteWritePasswordSecretRef: formatSecretKeyRef(remoteWrite?.passwordSecretRef),
577
+ prometheusRemoteWriteBearerTokenSecretRef: formatSecretKeyRef(remoteWrite?.bearerTokenSecretRef),
578
+ loggingSink: config.features.logging.sink,
579
+ loggingPlatformCredential: config.features.logging.bucket ?? "",
580
+ loggingPlatformDetail: config.features.logging.region ?? "",
581
+ // Distributed tracing (Elastic APM / generic OTLP / Azure Monitor)
582
+ tracingEnabled: config.features.tracing?.enabled ?? false,
583
+ tracingDestination: config.features.tracing?.destination ?? "elastic",
584
+ tracingElasticEndpoint: config.features.tracing?.elastic?.endpoint ?? "",
585
+ tracingElasticAuthMode: config.features.tracing?.elastic?.authMode === "api-key"
586
+ ? "api-key"
587
+ : "secret-token",
588
+ tracingElasticSecretToken: config.features.tracing?.elastic?.secretToken ?? "",
589
+ tracingElasticApiKey: config.features.tracing?.elastic?.apiKey ?? "",
590
+ tracingOtlpEndpoint: config.features.tracing?.otlp?.endpoint ?? "",
591
+ tracingOtlpAuthMode: config.features.tracing?.otlp?.authMode ?? "none",
592
+ tracingOtlpHeaderName: config.features.tracing?.otlp?.headerName ?? "Authorization",
593
+ tracingOtlpToken: config.features.tracing?.otlp?.token ??
594
+ config.features.tracing?.otlp?.apiKey ??
595
+ config.features.tracing?.otlp?.headerValue ??
596
+ "",
597
+ tracingAzureConnectionString: config.features.tracing?.azureMonitor?.connectionString ?? "",
598
+ // Application log shipping (Elasticsearch via Vector agent)
599
+ appLogsEnabled: config.features.logging.appLogs?.enabled ?? false,
600
+ appLogsElasticEndpoint: config.features.logging.appLogs?.elasticsearch?.endpoint ?? "",
601
+ appLogsElasticIndex: config.features.logging.appLogs?.elasticsearch?.index ??
602
+ base.appLogsElasticIndex,
603
+ appLogsElasticAuthMode: config.features.logging.appLogs?.elasticsearch?.authMode === "api-key"
604
+ ? "api-key"
605
+ : "basic",
606
+ appLogsElasticUsername: config.features.logging.appLogs?.elasticsearch?.username ?? "",
607
+ appLogsElasticPassword: config.features.logging.appLogs?.elasticsearch?.password ?? "",
608
+ appLogsElasticApiKey: config.features.logging.appLogs?.elasticsearch?.apiKey ?? "",
609
+ customEmailsEnabled: customEmails?.enabled ?? false,
610
+ emailSubjects: customEmails?.subjects ?? base.emailSubjects,
611
+ emailTemplates: customEmails?.templates ?? base.emailTemplates,
612
+ backupEnabled: config.backup?.enabled ?? false,
613
+ backupSchedule: config.backup?.schedule ?? base.backupSchedule,
614
+ backupRetentionDays: config.backup?.retentionDays ?? base.backupRetentionDays,
615
+ // External services - Redis
616
+ redisMode: externalRedis?.mode ?? base.redisMode,
617
+ redisHost: externalRedis?.external?.host ?? base.redisHost,
618
+ redisPort: externalRedis?.external?.port ?? base.redisPort,
619
+ redisPassword: externalRedis?.external?.password ?? base.redisPassword,
620
+ redisExistingSecret: externalRedis?.external?.existingSecret ?? base.redisExistingSecret,
621
+ redisExistingSecretKey: externalRedis?.external?.existingSecretKey ?? base.redisExistingSecretKey,
622
+ redisTls: externalRedis?.external?.tls ?? base.redisTls,
623
+ redisHttpApiEnabled: externalRedis?.external?.httpApi?.enabled ?? base.redisHttpApiEnabled,
624
+ redisHttpApiUrl: externalRedis?.external?.httpApi?.url ?? base.redisHttpApiUrl,
625
+ redisHttpApiToken: externalRedis?.external?.httpApi?.token ?? base.redisHttpApiToken,
626
+ valkeyAdminEnabled: config.features.cache?.valkeyAdmin?.enabled ?? base.valkeyAdminEnabled,
627
+ valkeyAdminExposure: config.features.cache?.valkeyAdmin?.exposure ?? base.valkeyAdminExposure,
628
+ valkeyAdminHostname: config.features.cache?.valkeyAdmin?.hostname ?? base.valkeyAdminHostname,
629
+ valkeyAdminBasicAuthUsers: config.features.cache?.valkeyAdmin?.basicAuthUsers ??
630
+ base.valkeyAdminBasicAuthUsers,
631
+ valkeyAdminAllowedIPs: config.features.cache?.valkeyAdmin?.allowedIPs ??
632
+ base.valkeyAdminAllowedIPs,
633
+ redisExporterEnabled: config.features.cache?.redisExporter?.enabled ??
634
+ base.redisExporterEnabled,
635
+ kafkaExporterEnabled: config.features.cache?.kafkaExporter?.enabled ??
636
+ base.kafkaExporterEnabled,
637
+ // External services - Kafka
638
+ kafkaMode: externalKafka?.mode ?? base.kafkaMode,
639
+ kafkaPreset: externalKafka?.external?.preset ?? base.kafkaPreset,
640
+ kafkaBrokers: externalKafka?.external?.brokers ?? base.kafkaBrokers,
641
+ kafkaTopic: externalKafka?.external?.topic ?? base.kafkaTopic,
642
+ kafkaTopicPrefix: externalKafka?.external?.topicPrefix ?? base.kafkaTopicPrefix,
643
+ kafkaProvisionTopics: externalKafka?.external?.provisionTopics ?? base.kafkaProvisionTopics,
644
+ kafkaSsl: externalKafka?.external?.ssl ?? base.kafkaSsl,
645
+ kafkaSaslMechanism: externalKafka?.external?.sasl?.mechanism ?? base.kafkaSaslMechanism,
646
+ kafkaSaslRegion: externalKafka?.external?.sasl?.region ?? base.kafkaSaslRegion,
647
+ kafkaSaslUsername: externalKafka?.external?.sasl?.username ?? base.kafkaSaslUsername,
648
+ kafkaSaslPassword: externalKafka?.external?.sasl?.password ?? base.kafkaSaslPassword,
649
+ kafkaSaslExistingSecret: externalKafka?.external?.sasl?.existingSecret ??
650
+ base.kafkaSaslExistingSecret,
651
+ kafkaIdentityAwsRoleArn: externalKafka?.external?.identity?.awsRoleArn ??
652
+ base.kafkaIdentityAwsRoleArn,
653
+ kafkaIdentityGcpServiceAccountEmail: externalKafka?.external?.identity?.gcpServiceAccountEmail ??
654
+ base.kafkaIdentityGcpServiceAccountEmail,
655
+ kafkaIdentityAzureClientId: externalKafka?.external?.identity?.azureClientId ??
656
+ base.kafkaIdentityAzureClientId,
657
+ // External services - Postgres
658
+ postgresMode: externalPostgres?.mode ?? base.postgresMode,
659
+ postgresHost: externalPostgres?.external?.host ?? base.postgresHost,
660
+ postgresPort: externalPostgres?.external?.port ?? base.postgresPort,
661
+ postgresDatabase: externalPostgres?.external?.database ?? base.postgresDatabase,
662
+ postgresMasterUsername: externalPostgres?.external?.bootstrap?.masterUsername ??
663
+ base.postgresMasterUsername,
664
+ postgresMasterPassword: externalPostgres?.external?.bootstrap?.masterPassword ??
665
+ base.postgresMasterPassword,
666
+ licenseKey: config.licenseKey,
667
+ version: config.version,
668
+ chartVersion: config.chartVersion ?? "",
669
+ };
670
+ }
82
671
  function wizardReducer(state, action) {
83
672
  switch (action.type) {
84
673
  case "SET_STEP":
85
674
  return { ...state, step: action.step };
86
675
  case "SET_NAME":
87
676
  return { ...state, name: action.name };
88
- case "SET_INFRA_MODE":
89
- return { ...state, infrastructureMode: action.mode };
90
677
  case "SET_PROVIDER":
91
- return { ...state, provider: action.provider, region: "" };
678
+ if (action.provider === state.provider) {
679
+ return {
680
+ ...state,
681
+ provider: action.provider,
682
+ region: "",
683
+ clusterName: "",
684
+ gcpProjectId: "",
685
+ azureResourceGroup: "",
686
+ };
687
+ }
688
+ return {
689
+ ...state,
690
+ provider: action.provider,
691
+ region: "",
692
+ clusterName: "",
693
+ gcpProjectId: "",
694
+ azureResourceGroup: "",
695
+ storageProvider: null,
696
+ storageBucket: "",
697
+ storageRegion: "",
698
+ storageAwsIamRoleArn: "",
699
+ storageAzureBlobContainer: "",
700
+ storageAzureBlobClientId: "",
701
+ storageAzureBlobTenantId: "",
702
+ storageAzureBlobConnectionStringSecretRef: "",
703
+ storageGcpServiceAccountEmail: "",
704
+ };
92
705
  case "SET_REGION":
93
706
  return { ...state, region: action.region };
94
707
  case "SET_CLUSTER_NAME":
@@ -101,8 +714,6 @@ function wizardReducer(state, action) {
101
714
  return { ...state, domain: action.domain };
102
715
  case "SET_ADMIN_EMAIL":
103
716
  return { ...state, adminEmail: action.email };
104
- case "SET_TLS_EMAIL":
105
- return { ...state, tlsEmail: action.email };
106
717
  case "SET_DNS_PROVIDER":
107
718
  // Reset auto-manage if switching to unsupported provider
108
719
  return {
@@ -112,8 +723,6 @@ function wizardReducer(state, action) {
112
723
  };
113
724
  case "SET_DNS_AUTO_MANAGE":
114
725
  return { ...state, dnsAutoManage: action.autoManage };
115
- case "SET_EXISTING_EXTERNAL_DNS":
116
- return { ...state, existingExternalDns: action.exists };
117
726
  case "SET_SMTP":
118
727
  return { ...state, ...action.config };
119
728
  case "SET_DATABASE_TYPE":
@@ -122,8 +731,20 @@ function wizardReducer(state, action) {
122
731
  return { ...state, ...action.config };
123
732
  case "SET_SUPABASE_SELF_HOSTED":
124
733
  return { ...state, ...action.config };
125
- case "SET_TIER":
126
- return { ...state, tier: action.tier };
734
+ case "SET_CLUSTER_CAPABILITIES":
735
+ return {
736
+ ...state,
737
+ nodeArchitecture: action.nodeArchitecture,
738
+ arm64TolerationRequired: action.arm64TolerationRequired,
739
+ storageClass: action.storageClass ?? state.storageClass,
740
+ storageProvisioner: action.storageProvisioner ?? state.storageProvisioner,
741
+ schedulableNodeCount: action.schedulableNodeCount ?? state.schedulableNodeCount,
742
+ totalCpuCores: action.totalCpuCores ?? state.totalCpuCores,
743
+ totalMemoryGi: action.totalMemoryGi ?? state.totalMemoryGi,
744
+ eligibleCpuCores: action.eligibleCpuCores ?? state.eligibleCpuCores,
745
+ eligibleMemoryGi: action.eligibleMemoryGi ?? state.eligibleMemoryGi,
746
+ totalPersistentStorageGi: action.totalPersistentStorageGi ?? state.totalPersistentStorageGi,
747
+ };
127
748
  case "SET_AI_ENABLED":
128
749
  return { ...state, aiEnabled: action.enabled };
129
750
  case "SET_OPENAI_KEY":
@@ -132,20 +753,68 @@ function wizardReducer(state, action) {
132
753
  return { ...state, ssoEnabled: action.enabled };
133
754
  case "SET_SSO_CONFIG":
134
755
  return { ...state, ...action.config };
135
- case "SET_MONITORING":
136
- return { ...state, monitoringEnabled: action.enabled };
756
+ case "SET_METRICS_EXPORT":
757
+ return {
758
+ ...state,
759
+ clickStackEnabled: action.enabled ? false : state.clickStackEnabled,
760
+ metricsExportEnabled: action.enabled,
761
+ };
137
762
  case "SET_PROMETHEUS_REMOTE_WRITE":
138
763
  return { ...state, prometheusRemoteWriteUrl: action.url };
764
+ case "SET_PROMETHEUS_REMOTE_WRITE_CONFIG":
765
+ return { ...state, ...action.config };
139
766
  case "SET_LOGGING_SINK":
140
- // Reset bucket/region if switching to console
767
+ // Reset the platform credential/detail if switching back to console.
141
768
  return {
142
769
  ...state,
143
770
  loggingSink: action.sink,
144
- loggingBucket: action.sink === "console" ? "" : state.loggingBucket,
145
- loggingRegion: action.sink === "console" ? "" : state.loggingRegion,
771
+ loggingPlatformCredential: action.sink === "console" ? "" : state.loggingPlatformCredential,
772
+ loggingPlatformDetail: action.sink === "console" ? "" : state.loggingPlatformDetail,
146
773
  };
774
+ case "SET_STORAGE_CONFIG":
775
+ return { ...state, ...action.config };
147
776
  case "SET_LOGGING_CONFIG":
148
777
  return { ...state, ...action.config };
778
+ case "SET_CLICKSTACK_ENABLED":
779
+ return {
780
+ ...state,
781
+ clickStackEnabled: action.enabled,
782
+ metricsExportEnabled: action.enabled ? false : state.metricsExportEnabled,
783
+ tracingEnabled: action.enabled ? false : state.tracingEnabled,
784
+ appLogsEnabled: action.enabled ? false : state.appLogsEnabled,
785
+ };
786
+ case "SET_CLICKSTACK_CONFIG":
787
+ return { ...state, ...action.config };
788
+ case "SET_TRACING_ENABLED":
789
+ return {
790
+ ...state,
791
+ clickStackEnabled: action.enabled ? false : state.clickStackEnabled,
792
+ tracingEnabled: action.enabled,
793
+ };
794
+ case "SET_TRACING_CONFIG":
795
+ return { ...state, ...action.config };
796
+ case "SET_APP_LOGS_ENABLED":
797
+ return {
798
+ ...state,
799
+ clickStackEnabled: action.enabled ? false : state.clickStackEnabled,
800
+ appLogsEnabled: action.enabled,
801
+ };
802
+ case "SET_APP_LOGS_CONFIG":
803
+ return { ...state, ...action.config };
804
+ case "SET_BACKUP_ENABLED":
805
+ return { ...state, backupEnabled: action.enabled };
806
+ case "SET_BACKUP_SCHEDULE":
807
+ return { ...state, backupSchedule: action.schedule };
808
+ case "SET_BACKUP_RETENTION_DAYS":
809
+ return { ...state, backupRetentionDays: action.retentionDays };
810
+ case "SET_EXTERNAL_SERVICES":
811
+ return {
812
+ ...state,
813
+ ...action.config,
814
+ ...(action.config.postgresMode === "external"
815
+ ? { backupEnabled: false }
816
+ : {}),
817
+ };
149
818
  case "SET_CUSTOM_EMAILS_ENABLED":
150
819
  return { ...state, customEmailsEnabled: action.enabled };
151
820
  case "SET_EMAIL_SUBJECTS":
@@ -160,11 +829,10 @@ function wizardReducer(state, action) {
160
829
  };
161
830
  case "SET_LICENSE_KEY":
162
831
  return { ...state, licenseKey: action.key };
163
- case "SET_APP_VERSION":
832
+ case "SET_VERSION":
164
833
  return {
165
834
  ...state,
166
- appVersion: action.appVersion,
167
- hpsVersion: action.hpsVersion,
835
+ version: action.version,
168
836
  };
169
837
  case "SET_CHART_VERSION":
170
838
  return { ...state, chartVersion: action.version };
@@ -177,62 +845,58 @@ function wizardReducer(state, action) {
177
845
  }
178
846
  }
179
847
  const WizardContext = createContext(null);
180
- export function WizardProvider({ children, initialName, profile, }) {
848
+ export function WizardProvider({ children, initialName, initialState, profile, }) {
181
849
  // Initialize state with profile values for pre-population
182
850
  const [state, dispatch] = useReducer(wizardReducer, {
183
851
  ...getInitialState(profile),
184
- name: initialName || "",
852
+ ...initialState,
853
+ name: initialState?.name || initialName || "",
185
854
  });
186
- const toConfig = () => {
187
- // Validate required fields
188
- if (!state.name ||
189
- !state.domain ||
190
- !state.adminEmail ||
191
- !state.tlsEmail ||
192
- !state.licenseKey) {
193
- return null;
194
- }
195
- // Validate SMTP
196
- if (!state.smtpHost ||
197
- !state.smtpUser ||
198
- !state.smtpPass ||
199
- !state.smtpFrom) {
855
+ const toConfig = (options = {}) => {
856
+ // All field/credential gates (including the remote_write checks) live in one
857
+ // place so the review screen can show the specific reason a config is
858
+ // invalid instead of a generic message.
859
+ if (collectConfigIssues(state).length > 0) {
200
860
  return null;
201
861
  }
202
- // Validate database config
203
- if (state.databaseType === "supabase-cloud") {
204
- if (!state.supabaseUrl ||
205
- !state.supabaseAnonKey ||
206
- !state.supabaseServiceKey) {
207
- return null;
208
- }
209
- }
210
- else if (state.databaseType === "self-hosted") {
211
- if (!state.supabaseDbPassword) {
212
- return null;
213
- }
214
- }
215
- // Validate logging sink config
216
- if (state.loggingSink !== "console" && !state.loggingBucket) {
862
+ // collectConfigIssues guarantees these, but narrow them for the type checker.
863
+ if (!state.storageProvider) {
217
864
  return null;
218
865
  }
866
+ const externalServices = buildExternalServices(state);
867
+ const remoteWrite = !state.clickStackEnabled && state.metricsExportEnabled
868
+ ? buildRemoteWriteFromState(state)
869
+ : undefined;
219
870
  return {
220
871
  name: state.name,
221
872
  infrastructure: {
222
- mode: state.infrastructureMode || "existing",
873
+ mode: "existing",
223
874
  provider: state.provider || undefined,
224
875
  region: state.region || undefined,
225
876
  clusterName: state.clusterName || undefined,
226
877
  gcpProjectId: state.gcpProjectId || undefined,
227
878
  azureResourceGroup: state.azureResourceGroup || undefined,
879
+ nodeArchitecture: options.nodeArchitecture || state.nodeArchitecture || undefined,
880
+ arm64TolerationRequired: options.arm64TolerationRequired ?? state.arm64TolerationRequired,
881
+ storageClass: options.storageClass || state.storageClass || undefined,
882
+ storageProvisioner: options.storageProvisioner || state.storageProvisioner || undefined,
883
+ schedulableNodeCount: options.schedulableNodeCount || state.schedulableNodeCount || undefined,
884
+ totalCpuCores: options.totalCpuCores || state.totalCpuCores || undefined,
885
+ totalMemoryGi: options.totalMemoryGi || state.totalMemoryGi || undefined,
886
+ eligibleCpuCores: options.eligibleCpuCores || state.eligibleCpuCores || undefined,
887
+ eligibleMemoryGi: options.eligibleMemoryGi || state.eligibleMemoryGi || undefined,
888
+ totalPersistentStorageGi: options.totalPersistentStorageGi ||
889
+ state.totalPersistentStorageGi ||
890
+ undefined,
228
891
  },
229
892
  domain: state.domain,
230
893
  adminEmail: state.adminEmail,
231
- tlsEmail: state.tlsEmail,
894
+ // Not asked in the wizard; defaults to the admin email. A custom value
895
+ // survives redeploys via configToWizardState / config.yaml edits.
896
+ tlsEmail: state.tlsEmail || state.adminEmail,
232
897
  dns: {
233
898
  provider: state.dnsProvider,
234
899
  autoManage: state.dnsAutoManage,
235
- existingExternalDns: state.existingExternalDns || undefined,
236
900
  },
237
901
  smtp: {
238
902
  host: state.smtpHost,
@@ -248,13 +912,52 @@ export function WizardProvider({ children, initialName, profile, }) {
248
912
  supabaseAnonKey: state.supabaseAnonKey || undefined,
249
913
  supabaseServiceKey: state.supabaseServiceKey || undefined,
250
914
  supabaseAccessToken: state.supabaseAccessToken || undefined,
251
- supabaseProjectRef: state.supabaseProjectRef || undefined,
915
+ // Derived from the project URL when not set explicitly (config.yaml
916
+ // edits and redeploy merges still win).
917
+ supabaseProjectRef: state.supabaseProjectRef ||
918
+ (state.databaseType === "supabase-cloud" && state.supabaseUrl
919
+ ? deriveSupabaseProjectRef(state.supabaseUrl)
920
+ : undefined) ||
921
+ undefined,
252
922
  supabaseJwtSecret: state.supabaseJwtSecret || undefined,
253
923
  supabaseDbPassword: state.supabaseDbPassword || undefined,
254
924
  supabaseDashboardUser: state.supabaseDashboardUser || undefined,
255
925
  supabaseDashboardPass: state.supabaseDashboardPass || undefined,
256
926
  },
257
- tier: state.tier || "small",
927
+ storage: {
928
+ provider: state.storageProvider,
929
+ cloudAuthMode: state.storageCloudAuthMode,
930
+ bucket: state.storageBucket,
931
+ region: state.storageRegion,
932
+ awsIamRoleArn: state.storageProvider === "s3"
933
+ ? state.storageAwsIamRoleArn || undefined
934
+ : undefined,
935
+ azureBlobClientId: state.storageProvider === "azure-blob"
936
+ ? state.storageAzureBlobClientId || undefined
937
+ : undefined,
938
+ azureBlobTenantId: state.storageProvider === "azure-blob"
939
+ ? state.storageAzureBlobTenantId || undefined
940
+ : undefined,
941
+ azureBlobConnectionStringSecretRef: state.storageProvider === "azure-blob"
942
+ ? parseSecretKeyRef(state.storageAzureBlobConnectionStringSecretRef)
943
+ : undefined,
944
+ azureBlobContainer: state.storageProvider === "azure-blob"
945
+ ? state.storageAzureBlobContainer || undefined
946
+ : undefined,
947
+ gcpServiceAccountEmail: state.storageProvider === "gcs"
948
+ ? state.storageGcpServiceAccountEmail || undefined
949
+ : undefined,
950
+ paths: {
951
+ decisionLogs: "decision-logs",
952
+ dbBackups: "db-backups",
953
+ },
954
+ },
955
+ backup: {
956
+ enabled: state.databaseType === "self-hosted" ? state.backupEnabled : false,
957
+ schedule: state.backupSchedule || "0 2 * * *",
958
+ retentionDays: state.backupRetentionDays || 7,
959
+ },
960
+ externalServices,
258
961
  features: {
259
962
  ai: {
260
963
  enabled: state.aiEnabled,
@@ -268,13 +971,130 @@ export function WizardProvider({ children, initialName, profile, }) {
268
971
  clientSecret: state.ssoClientSecret || undefined,
269
972
  },
270
973
  monitoring: {
271
- enabled: state.monitoringEnabled,
272
- remoteWriteUrl: state.prometheusRemoteWriteUrl || undefined,
974
+ // In-cluster Prometheus is always installed.
975
+ enabled: true,
976
+ destination: !state.clickStackEnabled && state.metricsExportEnabled
977
+ ? state.prometheusMonitoringDestination ||
978
+ remoteWrite?.destination ||
979
+ undefined
980
+ : // "local-grafana" is a config-file-only option (in-cluster
981
+ // Grafana, no remote write) and must survive redeploys.
982
+ state.prometheusMonitoringDestination === "local-grafana"
983
+ ? "local-grafana"
984
+ : undefined,
985
+ remoteWriteUrl: !state.clickStackEnabled && state.metricsExportEnabled
986
+ ? state.prometheusRemoteWriteUrl || undefined
987
+ : undefined,
988
+ remoteWrite,
989
+ },
990
+ observability: {
991
+ clickstack: {
992
+ enabled: state.clickStackEnabled,
993
+ telemetryRetentionDays: state.clickStackTelemetryRetentionDays,
994
+ clickHouseStorageSize: state.clickHouseStorageSize,
995
+ },
273
996
  },
997
+ // Distributed tracing (self-hosted only). Omitted when disabled. The
998
+ // destination selects which backend sub-block is emitted.
999
+ tracing: !state.clickStackEnabled && state.tracingEnabled
1000
+ ? state.tracingDestination === "otlp"
1001
+ ? {
1002
+ enabled: true,
1003
+ destination: "otlp",
1004
+ otlp: {
1005
+ endpoint: state.tracingOtlpEndpoint || undefined,
1006
+ authMode: state.tracingOtlpAuthMode,
1007
+ headerName: state.tracingOtlpAuthMode === "header"
1008
+ ? state.tracingOtlpHeaderName || undefined
1009
+ : undefined,
1010
+ token: state.tracingOtlpAuthMode === "bearer"
1011
+ ? state.tracingOtlpToken || undefined
1012
+ : undefined,
1013
+ apiKey: state.tracingOtlpAuthMode === "api-key"
1014
+ ? state.tracingOtlpToken || undefined
1015
+ : undefined,
1016
+ headerValue: state.tracingOtlpAuthMode === "header"
1017
+ ? state.tracingOtlpToken || undefined
1018
+ : undefined,
1019
+ },
1020
+ }
1021
+ : state.tracingDestination === "azure-monitor"
1022
+ ? {
1023
+ enabled: true,
1024
+ destination: "azure-monitor",
1025
+ azureMonitor: {
1026
+ connectionString: state.tracingAzureConnectionString || undefined,
1027
+ },
1028
+ }
1029
+ : {
1030
+ enabled: true,
1031
+ destination: "elastic",
1032
+ elastic: {
1033
+ endpoint: state.tracingElasticEndpoint || undefined,
1034
+ authMode: state.tracingElasticAuthMode,
1035
+ secretToken: state.tracingElasticAuthMode === "secret-token"
1036
+ ? state.tracingElasticSecretToken || undefined
1037
+ : undefined,
1038
+ apiKey: state.tracingElasticAuthMode === "api-key"
1039
+ ? state.tracingElasticApiKey || undefined
1040
+ : undefined,
1041
+ },
1042
+ }
1043
+ : undefined,
1044
+ cache: state.valkeyAdminEnabled ||
1045
+ state.redisExporterEnabled ||
1046
+ state.kafkaExporterEnabled
1047
+ ? {
1048
+ valkeyAdmin: state.valkeyAdminEnabled
1049
+ ? {
1050
+ enabled: true,
1051
+ exposure: state.valkeyAdminExposure,
1052
+ hostname: state.valkeyAdminHostname || undefined,
1053
+ basicAuthUsers: state.valkeyAdminBasicAuthUsers.length > 0
1054
+ ? state.valkeyAdminBasicAuthUsers
1055
+ : undefined,
1056
+ allowedIPs: state.valkeyAdminAllowedIPs.length > 0
1057
+ ? state.valkeyAdminAllowedIPs
1058
+ : undefined,
1059
+ }
1060
+ : undefined,
1061
+ redisExporter: state.redisExporterEnabled
1062
+ ? { enabled: true }
1063
+ : undefined,
1064
+ kafkaExporter: state.kafkaExporterEnabled
1065
+ ? { enabled: true }
1066
+ : undefined,
1067
+ }
1068
+ : undefined,
274
1069
  logging: {
1070
+ // External logging is now a platform-only sink (Datadog, Splunk,
1071
+ // etc.). The persisted bucket/region keys carry the platform
1072
+ // credential and endpoint/detail (not an object-storage bucket).
1073
+ // Cloud object storage for decision logs is configured separately
1074
+ // under `storage` above.
275
1075
  sink: state.loggingSink,
276
- bucket: state.loggingBucket || undefined,
277
- region: state.loggingRegion || undefined,
1076
+ bucket: state.loggingPlatformCredential || undefined,
1077
+ region: state.loggingPlatformDetail || undefined,
1078
+ // Application/container log shipping to Elasticsearch (Vector agent).
1079
+ appLogs: !state.clickStackEnabled && state.appLogsEnabled
1080
+ ? {
1081
+ enabled: true,
1082
+ elasticsearch: {
1083
+ endpoint: state.appLogsElasticEndpoint || undefined,
1084
+ index: state.appLogsElasticIndex || undefined,
1085
+ authMode: state.appLogsElasticAuthMode,
1086
+ username: state.appLogsElasticAuthMode === "basic"
1087
+ ? state.appLogsElasticUsername || undefined
1088
+ : undefined,
1089
+ password: state.appLogsElasticAuthMode === "basic"
1090
+ ? state.appLogsElasticPassword || undefined
1091
+ : undefined,
1092
+ apiKey: state.appLogsElasticAuthMode === "api-key"
1093
+ ? state.appLogsElasticApiKey || undefined
1094
+ : undefined,
1095
+ },
1096
+ }
1097
+ : undefined,
278
1098
  },
279
1099
  customEmails: state.customEmailsEnabled
280
1100
  ? {
@@ -290,51 +1110,36 @@ export function WizardProvider({ children, initialName, profile, }) {
290
1110
  : undefined,
291
1111
  },
292
1112
  licenseKey: state.licenseKey,
293
- appVersion: state.appVersion || undefined,
294
- hpsVersion: state.hpsVersion || undefined,
1113
+ version: state.version,
295
1114
  chartVersion: state.chartVersion || undefined,
296
1115
  };
297
1116
  };
298
1117
  const skipToStep = (stepId) => {
299
- // For conditional step skipping
300
1118
  const stepIndex = [
301
- "mode",
302
1119
  "cloud",
303
1120
  "domain",
304
1121
  "smtp",
305
1122
  "database",
306
1123
  "database-creds",
307
- "tier",
1124
+ "external-services",
1125
+ "storage",
1126
+ "observability",
308
1127
  "features",
309
1128
  "feature-config",
310
- "credentials",
1129
+ "version",
311
1130
  "review",
312
1131
  ].indexOf(stepId);
313
1132
  if (stepIndex >= 0) {
314
1133
  dispatch({ type: "SET_STEP", step: stepIndex });
315
1134
  }
316
1135
  };
317
- /**
318
- * Suggests a domain based on the profile's domain suffix and a deployment name.
319
- * e.g., if profile has domainSuffix ".example.com" and name is "staging",
320
- * suggests "staging.example.com"
321
- */
322
- const suggestDomain = (name) => {
323
- if (!profile?.domainSuffix || !name)
324
- return "";
325
- // Remove leading dot if present and combine with name
326
- const suffix = profile.domainSuffix.startsWith(".")
327
- ? profile.domainSuffix.slice(1)
328
- : profile.domainSuffix;
329
- return `${name}.${suffix}`;
330
- };
331
1136
  return (_jsx(WizardContext.Provider, { value: {
332
1137
  state,
333
1138
  dispatch,
334
1139
  toConfig,
1140
+ configIssues: () => collectConfigIssues(state),
335
1141
  skipToStep,
336
1142
  profile: profile ?? null,
337
- suggestDomain,
338
1143
  }, children: children }));
339
1144
  }
340
1145
  export function useWizard() {