@rulebricks/cli 2.1.6 → 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.
- package/README.md +26 -0
- package/cluster-setup/aws/README.md +74 -0
- package/cluster-setup/aws/check-aws-access.sh +78 -0
- package/cluster-setup/aws/cluster.yaml +33 -0
- package/cluster-setup/azure/README.md +93 -0
- package/cluster-setup/azure/check-aks-prereqs.sh +96 -0
- package/cluster-setup/azure/main.bicep +282 -0
- package/cluster-setup/azure/main.parameters.json +21 -0
- package/cluster-setup/gcp/README.md +172 -0
- package/cluster-setup/gcp/check-gke-prereqs.sh +98 -0
- package/dist/commands/init.js +9 -2
- package/dist/components/Wizard/WizardContext.d.ts +27 -3
- package/dist/components/Wizard/WizardContext.js +95 -2
- package/dist/components/Wizard/steps/CloudProviderStep.js +7 -2
- package/dist/components/Wizard/steps/FeatureConfigStep.js +407 -10
- package/dist/components/Wizard/steps/ReviewStep.js +7 -2
- package/dist/lib/helmValues.js +227 -22
- package/dist/lib/kubernetes.d.ts +7 -1
- package/dist/lib/kubernetes.js +59 -0
- package/dist/types/index.d.ts +367 -6
- package/dist/types/index.js +46 -1
- package/package.json +2 -1
|
@@ -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."
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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: (
|
|
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
|
? {
|
|
@@ -78,7 +78,12 @@ export function CloudProviderStep({ onComplete, onBack, }) {
|
|
|
78
78
|
];
|
|
79
79
|
return providers.map((p) => ({
|
|
80
80
|
...p,
|
|
81
|
-
|
|
81
|
+
// Existing clusters only need a selectable provider so we can record
|
|
82
|
+
// kubeconfig refresh details. Strict auth/quota checks are only required
|
|
83
|
+
// when the CLI will provision infrastructure.
|
|
84
|
+
disabled: needsTerraform
|
|
85
|
+
? !p.status.installed || !p.status.authenticated
|
|
86
|
+
: !p.status.installed,
|
|
82
87
|
}));
|
|
83
88
|
};
|
|
84
89
|
const providerItems = getProviderItems();
|
|
@@ -215,7 +220,7 @@ export function CloudProviderStep({ onComplete, onBack, }) {
|
|
|
215
220
|
? colors.accent
|
|
216
221
|
: undefined;
|
|
217
222
|
return (_jsxs(Box, { children: [_jsxs(Text, { color: textColor, dimColor: item.disabled, children: [isSelected && !item.disabled ? "❯ " : " ", label] }), renderStatusIndicator(item.status), item.status.identity && item.status.authenticated && (_jsxs(Text, { color: "gray", dimColor: true, children: [" ", "(", item.status.identity, ")"] }))] }));
|
|
218
|
-
} })] })), subStep === "gcp-project" && (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Enter your GCP Project ID:" }), gcpProject && (_jsxs(Text, { color: "gray", dimColor: true, children: ["Detected project: ", gcpProject] })), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: colors.accent, children: "\u276F " }), _jsx(TextInput, { value: gcpProject, onChange: setGcpProject, onSubmit: handleGcpProjectSubmit, placeholder: "my-gcp-project" })] })] })), subStep === "azure-rg" && (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Enter your Azure Resource Group name:" }), _jsx(Text, { color: "gray", dimColor: true, children: "This resource group
|
|
223
|
+
} })] })), subStep === "gcp-project" && (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Enter your GCP Project ID:" }), gcpProject && (_jsxs(Text, { color: "gray", dimColor: true, children: ["Detected project: ", gcpProject] })), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: colors.accent, children: "\u276F " }), _jsx(TextInput, { value: gcpProject, onChange: setGcpProject, onSubmit: handleGcpProjectSubmit, placeholder: "my-gcp-project" })] })] })), subStep === "azure-rg" && (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Enter your Azure Resource Group name:" }), _jsx(Text, { color: "gray", dimColor: true, children: "This should be the resource group containing your AKS cluster" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: colors.accent, children: "\u276F " }), _jsx(TextInput, { value: azureRg, onChange: setAzureRg, onSubmit: handleAzureRgSubmit, placeholder: "rulebricks-rg" })] })] })), subStep === "region-loading" && (_jsx(Box, { flexDirection: "column", marginY: 1, children: _jsx(Spinner, { label: `Fetching ${state.provider?.toUpperCase()} regions...` }) })), subStep === "region" && (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Text, { children: ["Select a region for ", state.provider?.toUpperCase(), ":"] }), _jsxs(Text, { color: "gray", dimColor: true, children: [regions.length, " regions available"] })] }), _jsx(Box, { height: 10, flexDirection: "column", overflowY: "hidden", children: _jsx(SelectInput, { items: regionItems, onSelect: handleRegionSelect, limit: 8, indicatorComponent: () => null, itemComponent: ({ isSelected, label }) => (_jsxs(Text, { color: isSelected ? colors.accent : undefined, children: [isSelected ? "❯ " : " ", label] })) }) })] })), subStep === "cluster-loading" && (_jsx(Box, { flexDirection: "column", marginY: 1, children: _jsx(Spinner, { label: `Fetching ${state.provider?.toUpperCase()} clusters in ${state.region}...` }) })), subStep === "cluster-select" && (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Select your Kubernetes cluster:" }), _jsxs(Text, { color: "gray", dimColor: true, children: [clusters.length, " cluster", clusters.length !== 1 ? "s" : "", " found in ", state.region] })] }), _jsx(Box, { height: 10, flexDirection: "column", overflowY: "hidden", children: _jsx(SelectInput, { items: clusters.map((c) => ({ label: c, value: c })), onSelect: handleClusterSelect, limit: 8, indicatorComponent: () => null, itemComponent: ({ isSelected, label }) => (_jsxs(Text, { color: isSelected ? colors.accent : undefined, children: [isSelected ? "❯ " : " ", label] })) }) }), state.provider && state.region && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: colors.success, children: "\u2713" }), _jsxs(Text, { color: "gray", children: [" ", state.provider?.toUpperCase(), " \u2022 ", state.region] })] }))] })), subStep === "cluster" && (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Enter the Kubernetes cluster name:" }), _jsx(Text, { color: "gray", dimColor: true, children: state.infrastructureMode === "provision"
|
|
219
224
|
? "This cluster will be created"
|
|
220
225
|
: clusters.length === 0 && state.infrastructureMode === "existing"
|
|
221
226
|
? "No clusters found in this region - enter the name manually"
|