@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.
- 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/deploy.js +31 -4
- 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 +31 -1
- package/dist/lib/kubernetes.js +157 -5
- package/dist/types/index.d.ts +368 -7
- package/dist/types/index.js +47 -2
- 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/deploy.js
CHANGED
|
@@ -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
|
}
|
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
|
? {
|