@rulebricks/cli 2.1.5 → 2.1.7

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.
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
3
+ "contentVersion": "1.0.0.0",
4
+ "parameters": {
5
+ "clusterName": { "value": "rulebricks-cluster" },
6
+ "location": { "value": "eastus" },
7
+ "kubernetesVersion": { "value": "1.34" },
8
+ "nodeCount": { "value": 4 },
9
+ "nodeVmSize": { "value": "Standard_D2ps_v5" },
10
+ "osDiskSizeGB": { "value": 20 },
11
+ "osDiskType": { "value": "Managed" },
12
+
13
+ "rulebricksNamespace": { "value": "rulebricks" },
14
+ "vectorServiceAccountName": { "value": "vector" },
15
+ "prometheusServiceAccountName": { "value": "prometheus" },
16
+ "enableExternalDns": { "value": false },
17
+ "dnsZoneResourceGroup": { "value": "" },
18
+ "enableBlobLogging": { "value": false },
19
+ "loggingStorageAccountName": { "value": "" }
20
+ }
21
+ }
@@ -0,0 +1,172 @@
1
+ # GCP Cluster Setup
2
+
3
+ Use these commands to create a minimum GKE cluster that can run Rulebricks without using the Rulebricks CLI Terraform flow. GCP does not have an `eksctl`-style cluster YAML or a concise Bicep equivalent; the most familiar native interface is `gcloud`.
4
+
5
+ ## Files
6
+
7
+ - `check-gke-prereqs.sh` verifies `gcloud` auth, Application Default Credentials, required APIs, selected-region quota, GKE access, `kubectl`, and Helm.
8
+
9
+ ## Core Cluster Parameters
10
+
11
+ - Cluster name: `rulebricks-cluster` (`Core cluster parameters` block -> `CLUSTER_NAME`)
12
+ - Region / zone: `us-central1` / `us-central1-a` (`Core cluster parameters` block -> `REGION` / `ZONE`)
13
+ - Kubernetes version: `1.34` (`Core cluster parameters` block -> `KUBERNETES_VERSION`)
14
+ - Node count: `4` (`Core cluster parameters` block -> `NODE_COUNT`)
15
+ - Machine type: `c4a-standard-2` (`Core cluster parameters` block -> `MACHINE_TYPE`)
16
+ - Disk size (GB): `20` (`Core cluster parameters` block -> `DISK_SIZE`)
17
+ - Disk type: `hyperdisk-balanced` (`Core cluster parameters` block -> `DISK_TYPE`)
18
+
19
+ ## Check Access
20
+
21
+ ```bash
22
+ gcloud auth login
23
+ gcloud config set project <project-id>
24
+ gcloud auth application-default login
25
+ GCP_REGION=us-central1 bash check-gke-prereqs.sh
26
+ ```
27
+
28
+ If API warnings appear, run the suggested `gcloud services enable` commands and wait for enablement to complete.
29
+
30
+ ## Create The Cluster
31
+
32
+ Set the core cluster parameters. The default example uses `us-central1-a` because it supports C4A ARM64 nodes.
33
+
34
+ ```bash
35
+ PROJECT_ID="$(gcloud config get-value project)"
36
+ CLUSTER_NAME=rulebricks-cluster
37
+ REGION=us-central1
38
+ ZONE=us-central1-a
39
+ KUBERNETES_VERSION="1.34"
40
+ NODE_COUNT=4
41
+ MACHINE_TYPE=c4a-standard-2
42
+ DISK_SIZE=20
43
+ DISK_TYPE=hyperdisk-balanced
44
+ ```
45
+
46
+ Enable required APIs:
47
+
48
+ ```bash
49
+ gcloud services enable \
50
+ compute.googleapis.com \
51
+ container.googleapis.com \
52
+ iam.googleapis.com \
53
+ cloudresourcemanager.googleapis.com \
54
+ --project "$PROJECT_ID"
55
+ ```
56
+
57
+ Create the VPC, subnet, NAT, and internal firewall rule:
58
+
59
+ ```bash
60
+ gcloud compute networks create "${CLUSTER_NAME}-vpc" \
61
+ --project "$PROJECT_ID" \
62
+ --subnet-mode custom
63
+
64
+ gcloud compute networks subnets create "${CLUSTER_NAME}-subnet" \
65
+ --project "$PROJECT_ID" \
66
+ --region "$REGION" \
67
+ --network "${CLUSTER_NAME}-vpc" \
68
+ --range 10.0.0.0/16 \
69
+ --secondary-range pods=10.1.0.0/16,services=10.2.0.0/16 \
70
+ --enable-private-ip-google-access
71
+
72
+ gcloud compute routers create "${CLUSTER_NAME}-router" \
73
+ --project "$PROJECT_ID" \
74
+ --region "$REGION" \
75
+ --network "${CLUSTER_NAME}-vpc"
76
+
77
+ gcloud compute routers nats create "${CLUSTER_NAME}-nat" \
78
+ --project "$PROJECT_ID" \
79
+ --region "$REGION" \
80
+ --router "${CLUSTER_NAME}-router" \
81
+ --auto-allocate-nat-external-ips \
82
+ --nat-all-subnet-ip-ranges
83
+
84
+ gcloud compute firewall-rules create "${CLUSTER_NAME}-allow-internal" \
85
+ --project "$PROJECT_ID" \
86
+ --network "${CLUSTER_NAME}-vpc" \
87
+ --allow tcp:0-65535,udp:0-65535,icmp \
88
+ --source-ranges 10.0.0.0/16,10.1.0.0/16,10.2.0.0/16 \
89
+ --target-tags "gke-${CLUSTER_NAME}"
90
+ ```
91
+
92
+ Create the GKE cluster:
93
+
94
+ ```bash
95
+ gcloud container clusters create "$CLUSTER_NAME" \
96
+ --project "$PROJECT_ID" \
97
+ --region "$REGION" \
98
+ --node-locations "$ZONE" \
99
+ --cluster-version "$KUBERNETES_VERSION" \
100
+ --release-channel regular \
101
+ --network "${CLUSTER_NAME}-vpc" \
102
+ --subnetwork "${CLUSTER_NAME}-subnet" \
103
+ --enable-ip-alias \
104
+ --cluster-secondary-range-name pods \
105
+ --services-secondary-range-name services \
106
+ --enable-private-nodes \
107
+ --master-ipv4-cidr 172.16.0.0/28 \
108
+ --enable-master-authorized-networks \
109
+ --master-authorized-networks 0.0.0.0/0 \
110
+ --workload-pool "${PROJECT_ID}.svc.id.goog" \
111
+ --enable-network-policy \
112
+ --addons HttpLoadBalancing,HorizontalPodAutoscaling,GcePersistentDiskCsiDriver \
113
+ --node-pool rulebricks-nodes \
114
+ --machine-type "$MACHINE_TYPE" \
115
+ --num-nodes "$NODE_COUNT" \
116
+ --disk-type "$DISK_TYPE" \
117
+ --disk-size "$DISK_SIZE" \
118
+ --scopes cloud-platform \
119
+ --workload-metadata GKE_METADATA \
120
+ --enable-autorepair \
121
+ --enable-autoupgrade \
122
+ --node-labels environment=rulebricks \
123
+ --tags "gke-${CLUSTER_NAME}"
124
+ ```
125
+
126
+ Configure kubeconfig:
127
+
128
+ ```bash
129
+ gcloud container clusters get-credentials "$CLUSTER_NAME" \
130
+ --region "$REGION" \
131
+ --project "$PROJECT_ID"
132
+ ```
133
+
134
+ Use `rulebricks init` with **Use existing Kubernetes cluster** after kubeconfig works.
135
+
136
+ ## Optional Identity Setup
137
+
138
+ If you use GCS decision-log export, bind the `vector` Kubernetes service account to a Google service account that can write to the bucket:
139
+
140
+ ```bash
141
+ NAMESPACE=rulebricks-demo
142
+ PROJECT_ID="$(gcloud config get-value project)"
143
+ GSA=rulebricks-vector@"$PROJECT_ID".iam.gserviceaccount.com
144
+
145
+ gcloud iam service-accounts create rulebricks-vector \
146
+ --project "$PROJECT_ID"
147
+
148
+ gcloud storage buckets add-iam-policy-binding gs://<bucket-name> \
149
+ --member "serviceAccount:$GSA" \
150
+ --role roles/storage.objectCreator
151
+
152
+ gcloud iam service-accounts add-iam-policy-binding "$GSA" \
153
+ --project "$PROJECT_ID" \
154
+ --role roles/iam.workloadIdentityUser \
155
+ --member "serviceAccount:$PROJECT_ID.svc.id.goog[$NAMESPACE/vector]"
156
+ ```
157
+
158
+ Annotate the service account after the Rulebricks namespace exists:
159
+
160
+ ```bash
161
+ kubectl annotate serviceaccount vector \
162
+ --namespace "$NAMESPACE" \
163
+ iam.gke.io/gcp-service-account="$GSA"
164
+ ```
165
+
166
+ Enter the Google service account email when prompted by the CLI.
167
+
168
+ ## Notes
169
+
170
+ - The example creates four `c4a-standard-2` ARM64 nodes with `hyperdisk-balanced`, matching the minimum CLI Terraform defaults.
171
+ - C4A availability varies by region and zone. If you change `REGION`, choose a `ZONE` where C4A is available.
172
+ - Regional GKE clusters can multiply node counts across node locations. This example pins one node location to keep the minimum cluster shape predictable.
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ PROJECT_ID="${GOOGLE_CLOUD_PROJECT:-$(gcloud config get-value project 2>/dev/null || true)}"
5
+ REGION="${GCP_REGION:-us-central1}"
6
+ REQUIRED_VCPU=8
7
+ REQUIRED_APIS=(
8
+ compute.googleapis.com
9
+ container.googleapis.com
10
+ iam.googleapis.com
11
+ cloudresourcemanager.googleapis.com
12
+ )
13
+
14
+ require_cmd() {
15
+ command -v "$1" >/dev/null 2>&1 || {
16
+ echo "Missing required command: $1" >&2
17
+ exit 1
18
+ }
19
+ }
20
+
21
+ check_api() {
22
+ local api="$1"
23
+ if gcloud services list \
24
+ --project "$PROJECT_ID" \
25
+ --enabled \
26
+ --filter="name:${api}" \
27
+ --format="value(name)" | awk -v api="$api" '$0 ~ api { found=1 } END { exit found ? 0 : 1 }'; then
28
+ echo "OK: $api is enabled"
29
+ else
30
+ echo "WARN: $api is not enabled. Run: gcloud services enable $api --project $PROJECT_ID"
31
+ fi
32
+ }
33
+
34
+ check_quota() {
35
+ echo "Checking regional CPU quota in $REGION..."
36
+ local quota_line
37
+ quota_line="$(gcloud compute regions describe "$REGION" \
38
+ --project "$PROJECT_ID" \
39
+ --format="csv[no-heading](quotas.metric,quotas.limit,quotas.usage)" 2>/dev/null \
40
+ | awk -F, '$1=="CPUS"{print $2 "," $3; exit}' || true)"
41
+
42
+ if [[ -z "$quota_line" ]]; then
43
+ echo "WARN: Could not read regional CPU quota."
44
+ return
45
+ fi
46
+
47
+ local limit="${quota_line%,*}"
48
+ local usage="${quota_line#*,}"
49
+ local available
50
+ available="$(awk -v limit="$limit" -v usage="$usage" 'BEGIN { printf "%d", limit - usage }')"
51
+
52
+ if (( available < REQUIRED_VCPU )); then
53
+ echo "WARN: ${available}/${limit} CPUs available; ${REQUIRED_VCPU}+ recommended for the included cluster commands."
54
+ else
55
+ echo "OK: ${available}/${limit} CPUs available."
56
+ fi
57
+ }
58
+
59
+ require_cmd gcloud
60
+ require_cmd kubectl
61
+ require_cmd helm
62
+ require_cmd awk
63
+
64
+ if [[ -z "$PROJECT_ID" ]]; then
65
+ echo "No GCP project configured. Run: gcloud config set project PROJECT_ID" >&2
66
+ exit 1
67
+ fi
68
+
69
+ echo "Rulebricks GKE prerequisite checks"
70
+ echo "Project: $PROJECT_ID"
71
+ echo "Region: $REGION"
72
+ echo
73
+
74
+ echo "Checking gcloud account..."
75
+ gcloud auth list --filter=status:ACTIVE --format="value(account)"
76
+ gcloud auth application-default print-access-token >/dev/null
77
+ echo "OK: gcloud auth and Application Default Credentials are available."
78
+ echo
79
+
80
+ for api in "${REQUIRED_APIS[@]}"; do
81
+ check_api "$api"
82
+ done
83
+
84
+ echo
85
+ gcloud compute regions describe "$REGION" --project "$PROJECT_ID" >/dev/null
86
+ echo "OK: region $REGION is accessible."
87
+ gcloud container clusters list --region "$REGION" --project "$PROJECT_ID" >/dev/null
88
+ echo "OK: GKE cluster list access works."
89
+ check_quota
90
+
91
+ echo
92
+ echo "Checking local Kubernetes tools..."
93
+ kubectl version --client=true >/dev/null
94
+ helm version >/dev/null
95
+ echo "OK: kubectl and Helm are installed."
96
+
97
+ echo
98
+ echo "GKE prerequisite checks completed. Warnings may require GCP project-admin review before cluster creation."
@@ -8,7 +8,7 @@ import { loadDeploymentConfig, loadDeploymentState, saveDeploymentState, updateD
8
8
  import { setupTerraformWorkspace, terraformInit, terraformPlan, terraformApply, terraformDestroy, cleanupOrphanedResources, updateKubeconfig, hasTerraformState, isTerraformInstalled, generateTerraformVars, } from "../lib/terraform.js";
9
9
  import { checkGcpApplicationDefaultCredentials, checkAzureResourceProviders, checkAzureVmQuota, AZURE_TIER_CORES, } from "../lib/cloudCli.js";
10
10
  import { installOrUpgradeChart, upgradeChart, isHelmInstalled, } from "../lib/helm.js";
11
- import { isKubectlInstalled, checkClusterAccessible, } from "../lib/kubernetes.js";
11
+ import { isKubectlInstalled, checkClusterAccessible, waitForCertificatesReady, } from "../lib/kubernetes.js";
12
12
  import { generateHelmValues, updateHelmValuesForTLS, } from "../lib/helmValues.js";
13
13
  import { isSupportedDnsProvider, getNamespace, getReleaseName, } from "../types/index.js";
14
14
  function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
@@ -20,11 +20,13 @@ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
20
20
  const [useExternalDns, setUseExternalDns] = useState(false);
21
21
  const infraStartedRef = useRef(false); // Track if we started infra provisioning (ref for sync access)
22
22
  const [cleanupError, setCleanupError] = useState(null);
23
+ const [tlsWarning, setTlsWarning] = useState(null);
23
24
  const [status, setStatus] = useState({
24
25
  preflight: "pending",
25
26
  infrastructure: "pending",
26
27
  kubeconfig: "pending",
27
28
  helmInstall: "pending",
29
+ certCheck: "pending",
28
30
  dnsConfig: "pending",
29
31
  helmUpgradeTls: "pending",
30
32
  });
@@ -83,7 +85,17 @@ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
83
85
  const releaseName = getReleaseName(config.name);
84
86
  // Upgrade the chart with TLS enabled
85
87
  await upgradeChart(name, { releaseName, namespace, version, wait: true });
86
- setStatus((s) => ({ ...s, helmUpgradeTls: "success" }));
88
+ setStatus((s) => ({ ...s, helmUpgradeTls: "success", certCheck: "running" }));
89
+ setStep("cert-check");
90
+ try {
91
+ await waitForCertificatesReady(namespace);
92
+ setStatus((s) => ({ ...s, certCheck: "success" }));
93
+ }
94
+ catch (certErr) {
95
+ setStatus((s) => ({ ...s, certCheck: "error" }));
96
+ setTlsWarning("TLS certificates are still being issued. " +
97
+ "HTTPS may not be available yet.");
98
+ }
87
99
  // Update state
88
100
  await updateDeploymentStatus(name, "running", {
89
101
  application: {
@@ -113,6 +125,7 @@ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
113
125
  ...s,
114
126
  dnsConfig: "skipped",
115
127
  helmUpgradeTls: "skipped",
128
+ certCheck: "skipped",
116
129
  }));
117
130
  const namespace = getNamespace(config.name);
118
131
  // Mark as running without TLS upgrade
@@ -223,7 +236,18 @@ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
223
236
  helmInstall: "success",
224
237
  dnsConfig: "skipped", // External DNS handles this
225
238
  helmUpgradeTls: "skipped", // TLS enabled from start
239
+ certCheck: "running",
226
240
  }));
241
+ setStep("cert-check");
242
+ try {
243
+ await waitForCertificatesReady(namespace);
244
+ setStatus((s) => ({ ...s, certCheck: "success" }));
245
+ }
246
+ catch (certErr) {
247
+ setStatus((s) => ({ ...s, certCheck: "error" }));
248
+ setTlsWarning("TLS certificates are still being issued. " +
249
+ "HTTPS may not be available yet.");
250
+ }
227
251
  // Update state to running
228
252
  await updateDeploymentStatus(name, "running", {
229
253
  application: {
@@ -254,6 +278,7 @@ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
254
278
  ...s,
255
279
  dnsConfig: "skipped",
256
280
  helmUpgradeTls: "skipped",
281
+ certCheck: "skipped",
257
282
  }));
258
283
  await updateDeploymentStatus(name, "waiting-dns", {
259
284
  application: {
@@ -421,7 +446,7 @@ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
421
446
  // Complete screen
422
447
  if (step === "complete") {
423
448
  const tlsSkipped = status.helmUpgradeTls === "skipped" && !useExternalDns;
424
- return (_jsx(BorderBox, { title: "Deployment Complete", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: colors.success, bold: true, children: "\u2713 Rulebricks deployed successfully!" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: ["URL:", " ", _jsxs(Text, { color: colors.accent, children: ["https://", config?.domain, "/auth/signup"] })] }), useExternalDns && (_jsx(Text, { color: colors.muted, children: "DNS records will be created automatically by external-dns" })), tlsSkipped && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.warning, children: ["\u26A0 TLS not configured. Run `rulebricks deploy ", name, "` again after DNS setup."] }) }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Next steps:" }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 Visit the URL to complete initial setup"] }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 Run `rulebricks status ", name, "` to check deployment health"] }), tlsSkipped && (_jsxs(Text, { color: colors.muted, children: [" ", "\u2022 Configure DNS and re-run deploy for TLS"] }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.muted, dimColor: true, children: "Tip: If the URL isn't accessible yet, your local DNS may need time to propagate." }), _jsxs(Text, { color: colors.muted, dimColor: true, children: ["Flush DNS cache: ", getDnsFlushCommand()] })] })] }) }));
449
+ return (_jsx(BorderBox, { title: "Deployment Complete", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: colors.success, bold: true, children: "\u2713 Rulebricks deployed successfully!" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: ["URL:", " ", _jsxs(Text, { color: colors.accent, children: ["https://", config?.domain, "/auth/signup"] })] }), useExternalDns && (_jsx(Text, { color: colors.muted, children: "DNS records will be created automatically by external-dns" })), tlsSkipped && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.warning, children: ["\u26A0 TLS not configured. Run `rulebricks deploy ", name, "` again after DNS setup."] }) })), tlsWarning && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.warning, children: ["\u26A0 ", tlsWarning] }) }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Next steps:" }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 Visit the URL to complete initial setup"] }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 Run `rulebricks status ", name, "` to check deployment health"] }), tlsSkipped && (_jsxs(Text, { color: colors.muted, children: [" ", "\u2022 Configure DNS and re-run deploy for TLS"] })), tlsWarning && (_jsxs(Text, { color: colors.muted, children: [" ", "\u2022 Run `rulebricks status ", name, "` to check TLS certificate status"] }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.muted, dimColor: true, children: "Tip: If the URL isn't accessible yet, your local DNS may need time to propagate." }), _jsxs(Text, { color: colors.muted, dimColor: true, children: ["Flush DNS cache: ", getDnsFlushCommand()] })] })] }) }));
425
450
  }
426
451
  // Progress screen
427
452
  const helmInstallLabel = useExternalDns
@@ -435,7 +460,7 @@ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
435
460
  ? "Planning changes"
436
461
  : step === "infra-apply"
437
462
  ? "Applying infrastructure"
438
- : undefined }), _jsx(StatusLine, { status: status.kubeconfig, label: "Kubernetes configuration" }), _jsx(StatusLine, { status: status.helmInstall, label: helmInstallLabel }), !useExternalDns && (_jsxs(_Fragment, { children: [_jsx(StatusLine, { status: status.dnsConfig, label: "DNS configuration" }), _jsx(StatusLine, { status: status.helmUpgradeTls, label: "TLS configuration" })] })), step !== "dns-wait" && (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: getStepLabel(step, useExternalDns) }) }))] }) }));
463
+ : undefined }), _jsx(StatusLine, { status: status.kubeconfig, label: "Kubernetes configuration" }), _jsx(StatusLine, { status: status.helmInstall, label: helmInstallLabel }), !useExternalDns && (_jsxs(_Fragment, { children: [_jsx(StatusLine, { status: status.dnsConfig, label: "DNS configuration" }), _jsx(StatusLine, { status: status.helmUpgradeTls, label: "TLS configuration" })] })), _jsx(StatusLine, { status: status.certCheck, label: "TLS certificate verification" }), step !== "dns-wait" && (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: getStepLabel(step, useExternalDns) }) }))] }) }));
439
464
  }
440
465
  function getDnsFlushCommand() {
441
466
  switch (platform()) {
@@ -471,6 +496,8 @@ function getStepLabel(step, useExternalDns) {
471
496
  return "Waiting for DNS configuration...";
472
497
  case "helm-upgrade-tls":
473
498
  return "Enabling TLS certificates...";
499
+ case "cert-check":
500
+ return "Verifying TLS certificates...";
474
501
  default:
475
502
  return "Processing...";
476
503
  }
@@ -6,6 +6,7 @@ import { DeploymentModeStep, CloudProviderStep, DomainStep, SMTPStep, DatabaseSt
6
6
  import { AppShell, ProgressHeader, ThemeProvider, useTheme, Logo, LOGO_LINES, } from "../components/common/index.js";
7
7
  import { saveDeploymentConfig, deploymentExists, loadProfile, updateProfile, extractProfileFromConfig, } from "../lib/config.js";
8
8
  import { generateHelmValues } from "../lib/helmValues.js";
9
+ import { inferClusterTier } from "../lib/kubernetes.js";
9
10
  const STEP_INFO = {
10
11
  mode: { title: "Deployment Mode", description: "Choose how to deploy" },
11
12
  cloud: { title: "Cloud Provider", description: "Select your cloud provider" },
@@ -65,7 +66,10 @@ function WizardStepController({ onSaveComplete }) {
65
66
  if (state.databaseType === "self-hosted") {
66
67
  steps.push("database-creds");
67
68
  }
68
- steps.push("tier", "features");
69
+ if (state.infrastructureMode === "provision") {
70
+ steps.push("tier");
71
+ }
72
+ steps.push("features");
69
73
  // Feature config only if AI, SSO, monitoring, external logging, or custom emails enabled
70
74
  if (state.aiEnabled ||
71
75
  state.ssoEnabled ||
@@ -107,7 +111,10 @@ function WizardStepController({ onSaveComplete }) {
107
111
  setPendingNav("back");
108
112
  }, []);
109
113
  const handleSave = useCallback(async () => {
110
- const config = toConfig();
114
+ const inferredTier = state.infrastructureMode === "existing"
115
+ ? (await inferClusterTier()) || state.tier || "small"
116
+ : state.tier || undefined;
117
+ const config = toConfig({ tier: inferredTier });
111
118
  if (!config) {
112
119
  setError("Invalid configuration - please check all required fields");
113
120
  return;
@@ -1,5 +1,5 @@
1
1
  import React, { ReactNode } from "react";
2
- import { DeploymentConfig, CloudProvider, DatabaseType, PerformanceTier, SSOProvider, DnsProvider, LoggingSink, EmailSubjects, EmailTemplates, ProfileConfig } from "../../types/index.js";
2
+ import { DeploymentConfig, CloudProvider, DatabaseType, PerformanceTier, SSOProvider, DnsProvider, LoggingSink, CloudLoggingAuthMode, MonitoringDestination, RemoteWriteAuthType, RemoteWriteDestination, EmailSubjects, EmailTemplates, ProfileConfig } from "../../types/index.js";
3
3
  export interface WizardState {
4
4
  step: number;
5
5
  name: string;
@@ -40,10 +40,29 @@ export interface WizardState {
40
40
  ssoClientId: string;
41
41
  ssoClientSecret: string;
42
42
  monitoringEnabled: boolean;
43
+ prometheusMonitoringDestination: MonitoringDestination | null;
43
44
  prometheusRemoteWriteUrl: string;
45
+ prometheusRemoteWriteDestination: RemoteWriteDestination | null;
46
+ prometheusRemoteWriteAuthType: RemoteWriteAuthType | null;
47
+ prometheusRemoteWriteAwsRegion: string;
48
+ prometheusRemoteWriteAwsRoleArn: string;
49
+ prometheusRemoteWriteAzureCloud: "AzurePublic" | "AzureChina" | "AzureGovernment";
50
+ prometheusRemoteWriteClientId: string;
51
+ prometheusRemoteWriteTenantId: string;
52
+ prometheusRemoteWriteSecretRef: string;
53
+ prometheusRemoteWriteUsernameSecretRef: string;
54
+ prometheusRemoteWritePasswordSecretRef: string;
55
+ prometheusRemoteWriteBearerTokenSecretRef: string;
44
56
  loggingSink: LoggingSink;
45
57
  loggingBucket: string;
46
58
  loggingRegion: string;
59
+ loggingCloudAuthMode: CloudLoggingAuthMode;
60
+ loggingAwsIamRoleArn: string;
61
+ loggingAzureBlobContainer: string;
62
+ loggingAzureBlobClientId: string;
63
+ loggingAzureBlobTenantId: string;
64
+ loggingAzureBlobConnectionStringSecretRef: string;
65
+ loggingGcpServiceAccountEmail: string;
47
66
  customEmailsEnabled: boolean;
48
67
  emailSubjects: EmailSubjects;
49
68
  emailTemplates: EmailTemplates;
@@ -127,12 +146,15 @@ type WizardAction = {
127
146
  } | {
128
147
  type: "SET_PROMETHEUS_REMOTE_WRITE";
129
148
  url: string;
149
+ } | {
150
+ type: "SET_PROMETHEUS_REMOTE_WRITE_CONFIG";
151
+ config: Partial<Pick<WizardState, "prometheusMonitoringDestination" | "prometheusRemoteWriteDestination" | "prometheusRemoteWriteAuthType" | "prometheusRemoteWriteAwsRegion" | "prometheusRemoteWriteAwsRoleArn" | "prometheusRemoteWriteAzureCloud" | "prometheusRemoteWriteClientId" | "prometheusRemoteWriteTenantId" | "prometheusRemoteWriteSecretRef" | "prometheusRemoteWriteUsernameSecretRef" | "prometheusRemoteWritePasswordSecretRef" | "prometheusRemoteWriteBearerTokenSecretRef">>;
130
152
  } | {
131
153
  type: "SET_LOGGING_SINK";
132
154
  sink: LoggingSink;
133
155
  } | {
134
156
  type: "SET_LOGGING_CONFIG";
135
- config: Partial<Pick<WizardState, "loggingBucket" | "loggingRegion">>;
157
+ config: Partial<Pick<WizardState, "loggingBucket" | "loggingRegion" | "loggingCloudAuthMode" | "loggingAwsIamRoleArn" | "loggingAzureBlobContainer" | "loggingAzureBlobClientId" | "loggingAzureBlobTenantId" | "loggingAzureBlobConnectionStringSecretRef" | "loggingGcpServiceAccountEmail">>;
136
158
  } | {
137
159
  type: "SET_CUSTOM_EMAILS_ENABLED";
138
160
  enabled: boolean;
@@ -160,7 +182,9 @@ type WizardAction = {
160
182
  interface WizardContextValue {
161
183
  state: WizardState;
162
184
  dispatch: React.Dispatch<WizardAction>;
163
- toConfig: () => DeploymentConfig | null;
185
+ toConfig: (options?: {
186
+ tier?: PerformanceTier;
187
+ }) => DeploymentConfig | null;
164
188
  skipToStep: (stepId: string) => void;
165
189
  profile: ProfileConfig | null;
166
190
  /** Suggests a domain based on the profile's domain suffix and deployment name */
@@ -55,11 +55,30 @@ function getInitialState(profile) {
55
55
  ssoClientSecret: profile?.ssoClientSecret ?? "",
56
56
  // Features - Monitoring
57
57
  monitoringEnabled: false,
58
+ prometheusMonitoringDestination: null,
58
59
  prometheusRemoteWriteUrl: "",
60
+ prometheusRemoteWriteDestination: null,
61
+ prometheusRemoteWriteAuthType: null,
62
+ prometheusRemoteWriteAwsRegion: "",
63
+ prometheusRemoteWriteAwsRoleArn: "",
64
+ prometheusRemoteWriteAzureCloud: "AzurePublic",
65
+ prometheusRemoteWriteClientId: "",
66
+ prometheusRemoteWriteTenantId: "",
67
+ prometheusRemoteWriteSecretRef: "",
68
+ prometheusRemoteWriteUsernameSecretRef: "",
69
+ prometheusRemoteWritePasswordSecretRef: "",
70
+ prometheusRemoteWriteBearerTokenSecretRef: "",
59
71
  // Features - Logging
60
72
  loggingSink: "console", // Default to console only
61
73
  loggingBucket: "",
62
74
  loggingRegion: "",
75
+ loggingCloudAuthMode: "workload-identity",
76
+ loggingAwsIamRoleArn: "",
77
+ loggingAzureBlobContainer: "rulebricks-logs",
78
+ loggingAzureBlobClientId: "",
79
+ loggingAzureBlobTenantId: "",
80
+ loggingAzureBlobConnectionStringSecretRef: "",
81
+ loggingGcpServiceAccountEmail: "",
63
82
  // Features - Custom Email Templates
64
83
  customEmailsEnabled: false,
65
84
  emailSubjects: { ...DEFAULT_EMAIL_SUBJECTS },
@@ -79,6 +98,12 @@ function getInitialState(profile) {
79
98
  }
80
99
  // Default initial state (for backwards compatibility)
81
100
  const initialState = getInitialState();
101
+ function parseSecretKeyRef(value) {
102
+ const [name, key] = value.split(":").map((part) => part.trim());
103
+ if (!name || !key)
104
+ return undefined;
105
+ return { name, key };
106
+ }
82
107
  function wizardReducer(state, action) {
83
108
  switch (action.type) {
84
109
  case "SET_STEP":
@@ -136,6 +161,8 @@ function wizardReducer(state, action) {
136
161
  return { ...state, monitoringEnabled: action.enabled };
137
162
  case "SET_PROMETHEUS_REMOTE_WRITE":
138
163
  return { ...state, prometheusRemoteWriteUrl: action.url };
164
+ case "SET_PROMETHEUS_REMOTE_WRITE_CONFIG":
165
+ return { ...state, ...action.config };
139
166
  case "SET_LOGGING_SINK":
140
167
  // Reset bucket/region if switching to console
141
168
  return {
@@ -183,7 +210,7 @@ export function WizardProvider({ children, initialName, profile, }) {
183
210
  ...getInitialState(profile),
184
211
  name: initialName || "",
185
212
  });
186
- const toConfig = () => {
213
+ const toConfig = (options = {}) => {
187
214
  // Validate required fields
188
215
  if (!state.name ||
189
216
  !state.domain ||
@@ -216,6 +243,45 @@ export function WizardProvider({ children, initialName, profile, }) {
216
243
  if (state.loggingSink !== "console" && !state.loggingBucket) {
217
244
  return null;
218
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
+ }
284
+ : undefined;
219
285
  return {
220
286
  name: state.name,
221
287
  infrastructure: {
@@ -254,7 +320,7 @@ export function WizardProvider({ children, initialName, profile, }) {
254
320
  supabaseDashboardUser: state.supabaseDashboardUser || undefined,
255
321
  supabaseDashboardPass: state.supabaseDashboardPass || undefined,
256
322
  },
257
- tier: state.tier || "small",
323
+ tier: options.tier || state.tier || "small",
258
324
  features: {
259
325
  ai: {
260
326
  enabled: state.aiEnabled,
@@ -269,12 +335,39 @@ export function WizardProvider({ children, initialName, profile, }) {
269
335
  },
270
336
  monitoring: {
271
337
  enabled: state.monitoringEnabled,
338
+ destination: state.prometheusMonitoringDestination ||
339
+ remoteWrite?.destination ||
340
+ undefined,
272
341
  remoteWriteUrl: state.prometheusRemoteWriteUrl || undefined,
342
+ remoteWrite,
273
343
  },
274
344
  logging: {
275
345
  sink: state.loggingSink,
276
346
  bucket: state.loggingBucket || undefined,
277
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
370
+ : undefined,
278
371
  },
279
372
  customEmails: state.customEmailsEnabled
280
373
  ? {