@rulebricks/cli 2.1.7 → 2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +51 -16
  2. package/cluster-setup/aws/README.md +96 -47
  3. package/cluster-setup/aws/check-aws-access.sh +216 -52
  4. package/cluster-setup/aws/parameters.json +13 -0
  5. package/cluster-setup/aws/rulebricks-cluster.cfn.yaml +355 -0
  6. package/cluster-setup/azure/README.md +103 -55
  7. package/cluster-setup/azure/check-aks-prereqs.sh +236 -56
  8. package/cluster-setup/azure/parameters.json +30 -0
  9. package/cluster-setup/azure/rulebricks-cluster.bicep +546 -0
  10. package/cluster-setup/gcp/README.md +51 -34
  11. package/cluster-setup/gcp/check-gke-prereqs.sh +222 -60
  12. package/dist/commands/backup.d.ts +5 -0
  13. package/dist/commands/backup.js +104 -0
  14. package/dist/commands/deploy.d.ts +3 -1
  15. package/dist/commands/deploy.js +226 -326
  16. package/dist/commands/destroy.d.ts +1 -1
  17. package/dist/commands/destroy.js +73 -123
  18. package/dist/commands/init.d.ts +5 -1
  19. package/dist/commands/init.js +78 -54
  20. package/dist/commands/list.d.ts +1 -0
  21. package/dist/commands/list.js +74 -0
  22. package/dist/commands/open.d.ts +1 -1
  23. package/dist/commands/open.js +4 -12
  24. package/dist/commands/redeploy.d.ts +6 -0
  25. package/dist/commands/redeploy.js +310 -0
  26. package/dist/commands/restore.d.ts +5 -0
  27. package/dist/commands/restore.js +338 -0
  28. package/dist/commands/status.js +62 -49
  29. package/dist/commands/upgrade.js +74 -51
  30. package/dist/components/DNSWaitScreen.d.ts +5 -1
  31. package/dist/components/DNSWaitScreen.js +47 -41
  32. package/dist/components/Wizard/WizardContext.d.ts +157 -36
  33. package/dist/components/Wizard/WizardContext.js +872 -160
  34. package/dist/components/Wizard/steps/CloudProviderStep.js +192 -107
  35. package/dist/components/Wizard/steps/DomainStep.js +5 -24
  36. package/dist/components/Wizard/steps/ExternalServicesStep.d.ts +6 -0
  37. package/dist/components/Wizard/steps/ExternalServicesStep.js +645 -0
  38. package/dist/components/Wizard/steps/FeatureConfigStep.d.ts +2 -1
  39. package/dist/components/Wizard/steps/FeatureConfigStep.js +739 -425
  40. package/dist/components/Wizard/steps/FeaturesStep.js +31 -35
  41. package/dist/components/Wizard/steps/ObservabilityStep.d.ts +6 -0
  42. package/dist/components/Wizard/steps/ObservabilityStep.js +137 -0
  43. package/dist/components/Wizard/steps/ReviewStep.d.ts +2 -1
  44. package/dist/components/Wizard/steps/ReviewStep.js +56 -12
  45. package/dist/components/Wizard/steps/StorageStep.d.ts +9 -0
  46. package/dist/components/Wizard/steps/StorageStep.js +592 -0
  47. package/dist/components/Wizard/steps/SupabaseCredentialsStep.js +20 -21
  48. package/dist/components/Wizard/steps/VersionStep.js +45 -23
  49. package/dist/components/Wizard/steps/index.d.ts +3 -3
  50. package/dist/components/Wizard/steps/index.js +3 -3
  51. package/dist/components/common/CommandApproval.d.ts +12 -0
  52. package/dist/components/common/CommandApproval.js +91 -0
  53. package/dist/components/common/DeploymentPicker.d.ts +14 -0
  54. package/dist/components/common/DeploymentPicker.js +16 -0
  55. package/dist/components/common/index.d.ts +2 -0
  56. package/dist/components/common/index.js +2 -0
  57. package/dist/index.js +94 -62
  58. package/dist/lib/cloudCli.d.ts +134 -63
  59. package/dist/lib/cloudCli.js +512 -220
  60. package/dist/lib/clusterSetupDefaults.d.ts +30 -0
  61. package/dist/lib/clusterSetupDefaults.js +64 -0
  62. package/dist/lib/commandApproval.d.ts +26 -0
  63. package/dist/lib/commandApproval.js +114 -0
  64. package/dist/lib/config.d.ts +12 -10
  65. package/dist/lib/config.js +91 -33
  66. package/dist/lib/configFixtures.d.ts +5 -0
  67. package/dist/lib/configFixtures.js +513 -0
  68. package/dist/lib/deploymentHealth.d.ts +32 -0
  69. package/dist/lib/deploymentHealth.js +157 -0
  70. package/dist/lib/dns.d.ts +1 -1
  71. package/dist/lib/dns.js +19 -1
  72. package/dist/lib/dns.test.d.ts +1 -0
  73. package/dist/lib/dns.test.js +27 -0
  74. package/dist/lib/dockerHub.d.ts +12 -1
  75. package/dist/lib/dockerHub.js +18 -8
  76. package/dist/lib/helm.d.ts +4 -0
  77. package/dist/lib/helm.js +16 -0
  78. package/dist/lib/helmValues.d.ts +25 -0
  79. package/dist/lib/helmValues.js +1841 -289
  80. package/dist/lib/helmValues.test.d.ts +1 -0
  81. package/dist/lib/helmValues.test.js +1012 -0
  82. package/dist/lib/htpasswd.d.ts +1 -0
  83. package/dist/lib/htpasswd.js +15 -0
  84. package/dist/lib/kubernetes.d.ts +124 -17
  85. package/dist/lib/kubernetes.js +576 -145
  86. package/dist/lib/secrets.d.ts +23 -0
  87. package/dist/lib/secrets.js +158 -0
  88. package/dist/lib/validateValues.d.ts +31 -0
  89. package/dist/lib/validateValues.js +253 -0
  90. package/dist/lib/versions.d.ts +82 -11
  91. package/dist/lib/versions.js +131 -31
  92. package/dist/lib/versions.test.d.ts +1 -0
  93. package/dist/lib/versions.test.js +81 -0
  94. package/dist/lib/wizardSteps.d.ts +14 -0
  95. package/dist/lib/wizardSteps.js +23 -0
  96. package/dist/lib/workloadIdentity.d.ts +26 -0
  97. package/dist/lib/workloadIdentity.js +323 -0
  98. package/dist/lib/workloadIdentity.test.d.ts +1 -0
  99. package/dist/lib/workloadIdentity.test.js +57 -0
  100. package/dist/types/index.d.ts +1860 -164
  101. package/dist/types/index.js +518 -295
  102. package/package.json +9 -4
  103. package/schema/values.schema.json +1934 -0
  104. package/cluster-setup/aws/cluster.yaml +0 -33
  105. package/cluster-setup/azure/main.bicep +0 -282
  106. package/cluster-setup/azure/main.parameters.json +0 -21
  107. package/dist/components/Wizard/steps/CredentialsStep.d.ts +0 -6
  108. package/dist/components/Wizard/steps/CredentialsStep.js +0 -22
  109. package/dist/components/Wizard/steps/DeploymentModeStep.d.ts +0 -5
  110. package/dist/components/Wizard/steps/DeploymentModeStep.js +0 -26
  111. package/dist/components/Wizard/steps/TierStep.d.ts +0 -6
  112. package/dist/components/Wizard/steps/TierStep.js +0 -29
  113. package/dist/lib/terraform.d.ts +0 -66
  114. package/dist/lib/terraform.js +0 -754
  115. package/terraform/aws/main.tf +0 -355
  116. package/terraform/azure/main.tf +0 -371
  117. package/terraform/gcp/main.tf +0 -407
@@ -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,8 +77,12 @@ 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,
58
86
  prometheusMonitoringDestination: null,
59
87
  prometheusRemoteWriteUrl: "",
60
88
  prometheusRemoteWriteDestination: null,
@@ -70,15 +98,28 @@ function getInitialState(profile) {
70
98
  prometheusRemoteWriteBearerTokenSecretRef: "",
71
99
  // Features - Logging
72
100
  loggingSink: "console", // Default to console only
73
- loggingBucket: "",
74
- loggingRegion: "",
75
- loggingCloudAuthMode: "workload-identity",
76
- loggingAwsIamRoleArn: "",
77
- loggingAzureBlobContainer: "rulebricks-logs",
78
- loggingAzureBlobClientId: "",
79
- loggingAzureBlobTenantId: "",
80
- loggingAzureBlobConnectionStringSecretRef: "",
81
- loggingGcpServiceAccountEmail: "",
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: "",
82
123
  // Features - Custom Email Templates
83
124
  customEmailsEnabled: false,
84
125
  emailSubjects: { ...DEFAULT_EMAIL_SUBJECTS },
@@ -88,32 +129,579 @@ function getInitialState(profile) {
88
129
  recovery: "",
89
130
  emailChange: "",
90
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: "",
91
177
  // Credentials - pre-populate from profile
92
178
  licenseKey: profile?.licenseKey ?? "",
93
179
  // Version
94
- appVersion: "",
95
- hpsVersion: "",
180
+ version: "",
96
181
  chartVersion: "",
97
182
  };
98
183
  }
99
184
  // Default initial state (for backwards compatibility)
100
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
+ }
101
197
  function parseSecretKeyRef(value) {
102
198
  const [name, key] = value.split(":").map((part) => part.trim());
103
199
  if (!name || !key)
104
200
  return undefined;
105
201
  return { name, key };
106
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
+ }
107
671
  function wizardReducer(state, action) {
108
672
  switch (action.type) {
109
673
  case "SET_STEP":
110
674
  return { ...state, step: action.step };
111
675
  case "SET_NAME":
112
676
  return { ...state, name: action.name };
113
- case "SET_INFRA_MODE":
114
- return { ...state, infrastructureMode: action.mode };
115
677
  case "SET_PROVIDER":
116
- 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
+ };
117
705
  case "SET_REGION":
118
706
  return { ...state, region: action.region };
119
707
  case "SET_CLUSTER_NAME":
@@ -126,8 +714,6 @@ function wizardReducer(state, action) {
126
714
  return { ...state, domain: action.domain };
127
715
  case "SET_ADMIN_EMAIL":
128
716
  return { ...state, adminEmail: action.email };
129
- case "SET_TLS_EMAIL":
130
- return { ...state, tlsEmail: action.email };
131
717
  case "SET_DNS_PROVIDER":
132
718
  // Reset auto-manage if switching to unsupported provider
133
719
  return {
@@ -137,8 +723,6 @@ function wizardReducer(state, action) {
137
723
  };
138
724
  case "SET_DNS_AUTO_MANAGE":
139
725
  return { ...state, dnsAutoManage: action.autoManage };
140
- case "SET_EXISTING_EXTERNAL_DNS":
141
- return { ...state, existingExternalDns: action.exists };
142
726
  case "SET_SMTP":
143
727
  return { ...state, ...action.config };
144
728
  case "SET_DATABASE_TYPE":
@@ -147,8 +731,20 @@ function wizardReducer(state, action) {
147
731
  return { ...state, ...action.config };
148
732
  case "SET_SUPABASE_SELF_HOSTED":
149
733
  return { ...state, ...action.config };
150
- case "SET_TIER":
151
- 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
+ };
152
748
  case "SET_AI_ENABLED":
153
749
  return { ...state, aiEnabled: action.enabled };
154
750
  case "SET_OPENAI_KEY":
@@ -157,22 +753,68 @@ function wizardReducer(state, action) {
157
753
  return { ...state, ssoEnabled: action.enabled };
158
754
  case "SET_SSO_CONFIG":
159
755
  return { ...state, ...action.config };
160
- case "SET_MONITORING":
161
- 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
+ };
162
762
  case "SET_PROMETHEUS_REMOTE_WRITE":
163
763
  return { ...state, prometheusRemoteWriteUrl: action.url };
164
764
  case "SET_PROMETHEUS_REMOTE_WRITE_CONFIG":
165
765
  return { ...state, ...action.config };
166
766
  case "SET_LOGGING_SINK":
167
- // Reset bucket/region if switching to console
767
+ // Reset the platform credential/detail if switching back to console.
168
768
  return {
169
769
  ...state,
170
770
  loggingSink: action.sink,
171
- loggingBucket: action.sink === "console" ? "" : state.loggingBucket,
172
- loggingRegion: action.sink === "console" ? "" : state.loggingRegion,
771
+ loggingPlatformCredential: action.sink === "console" ? "" : state.loggingPlatformCredential,
772
+ loggingPlatformDetail: action.sink === "console" ? "" : state.loggingPlatformDetail,
173
773
  };
774
+ case "SET_STORAGE_CONFIG":
775
+ return { ...state, ...action.config };
174
776
  case "SET_LOGGING_CONFIG":
175
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
+ };
176
818
  case "SET_CUSTOM_EMAILS_ENABLED":
177
819
  return { ...state, customEmailsEnabled: action.enabled };
178
820
  case "SET_EMAIL_SUBJECTS":
@@ -187,11 +829,10 @@ function wizardReducer(state, action) {
187
829
  };
188
830
  case "SET_LICENSE_KEY":
189
831
  return { ...state, licenseKey: action.key };
190
- case "SET_APP_VERSION":
832
+ case "SET_VERSION":
191
833
  return {
192
834
  ...state,
193
- appVersion: action.appVersion,
194
- hpsVersion: action.hpsVersion,
835
+ version: action.version,
195
836
  };
196
837
  case "SET_CHART_VERSION":
197
838
  return { ...state, chartVersion: action.version };
@@ -204,101 +845,58 @@ function wizardReducer(state, action) {
204
845
  }
205
846
  }
206
847
  const WizardContext = createContext(null);
207
- export function WizardProvider({ children, initialName, profile, }) {
848
+ export function WizardProvider({ children, initialName, initialState, profile, }) {
208
849
  // Initialize state with profile values for pre-population
209
850
  const [state, dispatch] = useReducer(wizardReducer, {
210
851
  ...getInitialState(profile),
211
- name: initialName || "",
852
+ ...initialState,
853
+ name: initialState?.name || initialName || "",
212
854
  });
213
855
  const toConfig = (options = {}) => {
214
- // Validate required fields
215
- if (!state.name ||
216
- !state.domain ||
217
- !state.adminEmail ||
218
- !state.tlsEmail ||
219
- !state.licenseKey) {
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) {
220
860
  return null;
221
861
  }
222
- // Validate SMTP
223
- if (!state.smtpHost ||
224
- !state.smtpUser ||
225
- !state.smtpPass ||
226
- !state.smtpFrom) {
862
+ // collectConfigIssues guarantees these, but narrow them for the type checker.
863
+ if (!state.storageProvider) {
227
864
  return null;
228
865
  }
229
- // Validate database config
230
- if (state.databaseType === "supabase-cloud") {
231
- if (!state.supabaseUrl ||
232
- !state.supabaseAnonKey ||
233
- !state.supabaseServiceKey) {
234
- return null;
235
- }
236
- }
237
- else if (state.databaseType === "self-hosted") {
238
- if (!state.supabaseDbPassword) {
239
- return null;
240
- }
241
- }
242
- // Validate logging sink config
243
- if (state.loggingSink !== "console" && !state.loggingBucket) {
244
- return null;
245
- }
246
- if (state.loggingSink === "azure-blob" &&
247
- (!state.loggingAzureBlobContainer ||
248
- (state.loggingCloudAuthMode === "workload-identity" &&
249
- (!state.loggingAzureBlobClientId || !state.loggingAzureBlobTenantId)) ||
250
- (state.loggingCloudAuthMode === "secret" &&
251
- !parseSecretKeyRef(state.loggingAzureBlobConnectionStringSecretRef)))) {
252
- return null;
253
- }
254
- if (state.loggingSink === "s3" && !state.loggingAwsIamRoleArn) {
255
- return null;
256
- }
257
- if (state.loggingSink === "gcs" && !state.loggingGcpServiceAccountEmail) {
258
- return null;
259
- }
260
- const remoteWrite = state.monitoringEnabled &&
261
- state.prometheusMonitoringDestination !== "local-grafana" &&
262
- state.prometheusRemoteWriteDestination &&
263
- state.prometheusRemoteWriteUrl
264
- ? {
265
- destination: state.prometheusRemoteWriteDestination,
266
- url: state.prometheusRemoteWriteUrl,
267
- authType: state.prometheusRemoteWriteAuthType || undefined,
268
- awsRegion: state.prometheusRemoteWriteDestination === "aws-amp"
269
- ? state.prometheusRemoteWriteAwsRegion ||
270
- state.region ||
271
- undefined
272
- : undefined,
273
- awsRoleArn: state.prometheusRemoteWriteDestination === "aws-amp"
274
- ? state.prometheusRemoteWriteAwsRoleArn || undefined
275
- : undefined,
276
- azureCloud: state.prometheusRemoteWriteAzureCloud,
277
- clientId: state.prometheusRemoteWriteClientId || undefined,
278
- tenantId: state.prometheusRemoteWriteTenantId || undefined,
279
- clientSecretRef: parseSecretKeyRef(state.prometheusRemoteWriteSecretRef),
280
- usernameSecretRef: parseSecretKeyRef(state.prometheusRemoteWriteUsernameSecretRef),
281
- passwordSecretRef: parseSecretKeyRef(state.prometheusRemoteWritePasswordSecretRef),
282
- bearerTokenSecretRef: parseSecretKeyRef(state.prometheusRemoteWriteBearerTokenSecretRef),
283
- }
866
+ const externalServices = buildExternalServices(state);
867
+ const remoteWrite = !state.clickStackEnabled && state.metricsExportEnabled
868
+ ? buildRemoteWriteFromState(state)
284
869
  : undefined;
285
870
  return {
286
871
  name: state.name,
287
872
  infrastructure: {
288
- mode: state.infrastructureMode || "existing",
873
+ mode: "existing",
289
874
  provider: state.provider || undefined,
290
875
  region: state.region || undefined,
291
876
  clusterName: state.clusterName || undefined,
292
877
  gcpProjectId: state.gcpProjectId || undefined,
293
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,
294
891
  },
295
892
  domain: state.domain,
296
893
  adminEmail: state.adminEmail,
297
- 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,
298
897
  dns: {
299
898
  provider: state.dnsProvider,
300
899
  autoManage: state.dnsAutoManage,
301
- existingExternalDns: state.existingExternalDns || undefined,
302
900
  },
303
901
  smtp: {
304
902
  host: state.smtpHost,
@@ -314,13 +912,52 @@ export function WizardProvider({ children, initialName, profile, }) {
314
912
  supabaseAnonKey: state.supabaseAnonKey || undefined,
315
913
  supabaseServiceKey: state.supabaseServiceKey || undefined,
316
914
  supabaseAccessToken: state.supabaseAccessToken || undefined,
317
- 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,
318
922
  supabaseJwtSecret: state.supabaseJwtSecret || undefined,
319
923
  supabaseDbPassword: state.supabaseDbPassword || undefined,
320
924
  supabaseDashboardUser: state.supabaseDashboardUser || undefined,
321
925
  supabaseDashboardPass: state.supabaseDashboardPass || undefined,
322
926
  },
323
- tier: options.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,
324
961
  features: {
325
962
  ai: {
326
963
  enabled: state.aiEnabled,
@@ -334,39 +971,129 @@ export function WizardProvider({ children, initialName, profile, }) {
334
971
  clientSecret: state.ssoClientSecret || undefined,
335
972
  },
336
973
  monitoring: {
337
- enabled: state.monitoringEnabled,
338
- destination: state.prometheusMonitoringDestination ||
339
- remoteWrite?.destination ||
340
- undefined,
341
- 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,
342
988
  remoteWrite,
343
989
  },
990
+ observability: {
991
+ clickstack: {
992
+ enabled: state.clickStackEnabled,
993
+ telemetryRetentionDays: state.clickStackTelemetryRetentionDays,
994
+ clickHouseStorageSize: state.clickHouseStorageSize,
995
+ },
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,
344
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.
345
1075
  sink: state.loggingSink,
346
- bucket: state.loggingBucket || undefined,
347
- region: state.loggingRegion || undefined,
348
- cloudAuthMode: state.loggingSink === "s3" ||
349
- state.loggingSink === "azure-blob" ||
350
- state.loggingSink === "gcs"
351
- ? state.loggingCloudAuthMode
352
- : undefined,
353
- awsIamRoleArn: state.loggingSink === "s3"
354
- ? state.loggingAwsIamRoleArn || undefined
355
- : undefined,
356
- azureBlobContainer: state.loggingSink === "azure-blob"
357
- ? state.loggingAzureBlobContainer || undefined
358
- : undefined,
359
- azureBlobClientId: state.loggingSink === "azure-blob"
360
- ? state.loggingAzureBlobClientId || undefined
361
- : undefined,
362
- azureBlobTenantId: state.loggingSink === "azure-blob"
363
- ? state.loggingAzureBlobTenantId || undefined
364
- : undefined,
365
- azureBlobConnectionStringSecretRef: state.loggingSink === "azure-blob"
366
- ? parseSecretKeyRef(state.loggingAzureBlobConnectionStringSecretRef)
367
- : undefined,
368
- gcpServiceAccountEmail: state.loggingSink === "gcs"
369
- ? state.loggingGcpServiceAccountEmail || 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
+ }
370
1097
  : undefined,
371
1098
  },
372
1099
  customEmails: state.customEmailsEnabled
@@ -383,51 +1110,36 @@ export function WizardProvider({ children, initialName, profile, }) {
383
1110
  : undefined,
384
1111
  },
385
1112
  licenseKey: state.licenseKey,
386
- appVersion: state.appVersion || undefined,
387
- hpsVersion: state.hpsVersion || undefined,
1113
+ version: state.version,
388
1114
  chartVersion: state.chartVersion || undefined,
389
1115
  };
390
1116
  };
391
1117
  const skipToStep = (stepId) => {
392
- // For conditional step skipping
393
1118
  const stepIndex = [
394
- "mode",
395
1119
  "cloud",
396
1120
  "domain",
397
1121
  "smtp",
398
1122
  "database",
399
1123
  "database-creds",
400
- "tier",
1124
+ "external-services",
1125
+ "storage",
1126
+ "observability",
401
1127
  "features",
402
1128
  "feature-config",
403
- "credentials",
1129
+ "version",
404
1130
  "review",
405
1131
  ].indexOf(stepId);
406
1132
  if (stepIndex >= 0) {
407
1133
  dispatch({ type: "SET_STEP", step: stepIndex });
408
1134
  }
409
1135
  };
410
- /**
411
- * Suggests a domain based on the profile's domain suffix and a deployment name.
412
- * e.g., if profile has domainSuffix ".example.com" and name is "staging",
413
- * suggests "staging.example.com"
414
- */
415
- const suggestDomain = (name) => {
416
- if (!profile?.domainSuffix || !name)
417
- return "";
418
- // Remove leading dot if present and combine with name
419
- const suffix = profile.domainSuffix.startsWith(".")
420
- ? profile.domainSuffix.slice(1)
421
- : profile.domainSuffix;
422
- return `${name}.${suffix}`;
423
- };
424
1136
  return (_jsx(WizardContext.Provider, { value: {
425
1137
  state,
426
1138
  dispatch,
427
1139
  toConfig,
1140
+ configIssues: () => collectConfigIssues(state),
428
1141
  skipToStep,
429
1142
  profile: profile ?? null,
430
- suggestDomain,
431
1143
  }, children: children }));
432
1144
  }
433
1145
  export function useWizard() {