@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
package/dist/lib/helmValues.js
CHANGED
|
@@ -36,17 +36,39 @@ function generateVectorSinks(config) {
|
|
|
36
36
|
};
|
|
37
37
|
break;
|
|
38
38
|
case "azure-blob":
|
|
39
|
-
|
|
39
|
+
if (!bucket) {
|
|
40
|
+
throw new Error("Azure Blob logging requires a storage account.");
|
|
41
|
+
}
|
|
42
|
+
const azureBlobSink = {
|
|
40
43
|
type: "azure_blob",
|
|
41
44
|
inputs: ["kafka"],
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
account_name: bucket,
|
|
46
|
+
container_name: config.features.logging.azureBlobContainer || "rulebricks-logs",
|
|
44
47
|
blob_prefix: "rulebricks/logs/%Y/%m/%d/",
|
|
45
48
|
compression: "gzip",
|
|
46
49
|
encoding: {
|
|
47
50
|
codec: "json",
|
|
48
51
|
},
|
|
49
52
|
};
|
|
53
|
+
if (config.features.logging.cloudAuthMode === "secret") {
|
|
54
|
+
if (!config.features.logging.azureBlobConnectionStringSecretRef) {
|
|
55
|
+
throw new Error("Azure Blob connection string auth requires a secret ref.");
|
|
56
|
+
}
|
|
57
|
+
azureBlobSink.connection_string = "${AZURE_STORAGE_CONNECTION_STRING}";
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
if (!config.features.logging.azureBlobClientId ||
|
|
61
|
+
!config.features.logging.azureBlobTenantId) {
|
|
62
|
+
throw new Error("Azure Blob workload identity requires client ID and tenant ID.");
|
|
63
|
+
}
|
|
64
|
+
azureBlobSink.auth = {
|
|
65
|
+
azure_credential_kind: "workload_identity",
|
|
66
|
+
client_id: config.features.logging.azureBlobClientId,
|
|
67
|
+
tenant_id: config.features.logging.azureBlobTenantId,
|
|
68
|
+
token_file_path: "/var/run/secrets/azure/tokens/azure-identity-token",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
sinks.azure_blob = azureBlobSink;
|
|
50
72
|
break;
|
|
51
73
|
case "gcs":
|
|
52
74
|
sinks.gcs = {
|
|
@@ -163,6 +185,61 @@ function generateVectorSinks(config) {
|
|
|
163
185
|
}
|
|
164
186
|
return sinks;
|
|
165
187
|
}
|
|
188
|
+
function generateVectorEnv(config) {
|
|
189
|
+
const env = [
|
|
190
|
+
{
|
|
191
|
+
name: "KAFKA_BOOTSTRAP_SERVERS",
|
|
192
|
+
valueFrom: {
|
|
193
|
+
configMapKeyRef: {
|
|
194
|
+
name: "vector-kafka-env",
|
|
195
|
+
key: "KAFKA_BOOTSTRAP_SERVERS",
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
];
|
|
200
|
+
const azureBlobSecretRef = config.features.logging.azureBlobConnectionStringSecretRef;
|
|
201
|
+
if (config.features.logging.sink === "azure-blob" &&
|
|
202
|
+
config.features.logging.cloudAuthMode === "secret" &&
|
|
203
|
+
azureBlobSecretRef) {
|
|
204
|
+
env.push({
|
|
205
|
+
name: "AZURE_STORAGE_CONNECTION_STRING",
|
|
206
|
+
valueFrom: {
|
|
207
|
+
secretKeyRef: secretKeySelector(azureBlobSecretRef),
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return env;
|
|
212
|
+
}
|
|
213
|
+
function generateVectorServiceAccount(config) {
|
|
214
|
+
const annotations = {};
|
|
215
|
+
if (config.features.logging.sink === "s3" && config.features.logging.awsIamRoleArn) {
|
|
216
|
+
annotations["eks.amazonaws.com/role-arn"] =
|
|
217
|
+
config.features.logging.awsIamRoleArn;
|
|
218
|
+
}
|
|
219
|
+
if (config.features.logging.sink === "azure-blob" &&
|
|
220
|
+
config.features.logging.cloudAuthMode !== "secret" &&
|
|
221
|
+
config.features.logging.azureBlobClientId) {
|
|
222
|
+
annotations["azure.workload.identity/client-id"] =
|
|
223
|
+
config.features.logging.azureBlobClientId;
|
|
224
|
+
}
|
|
225
|
+
if (config.features.logging.sink === "gcs" && config.features.logging.gcpServiceAccountEmail) {
|
|
226
|
+
annotations["iam.gke.io/gcp-service-account"] =
|
|
227
|
+
config.features.logging.gcpServiceAccountEmail;
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
create: true,
|
|
231
|
+
name: "vector",
|
|
232
|
+
annotations,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function generateVectorPodLabels(config) {
|
|
236
|
+
const labels = {};
|
|
237
|
+
if (config.features.logging.sink === "azure-blob" &&
|
|
238
|
+
config.features.logging.cloudAuthMode !== "secret") {
|
|
239
|
+
labels["azure.workload.identity/use"] = "true";
|
|
240
|
+
}
|
|
241
|
+
return labels;
|
|
242
|
+
}
|
|
166
243
|
/**
|
|
167
244
|
* Maps DNS provider to external-dns provider name
|
|
168
245
|
*/
|
|
@@ -175,6 +252,145 @@ function getExternalDnsProvider(dnsProvider) {
|
|
|
175
252
|
};
|
|
176
253
|
return mapping[dnsProvider] || "aws";
|
|
177
254
|
}
|
|
255
|
+
function secretKeySelector(ref) {
|
|
256
|
+
return {
|
|
257
|
+
name: ref.name,
|
|
258
|
+
key: ref.key,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function generateRemoteWriteSpec(config) {
|
|
262
|
+
if (config.features.monitoring.destination === "local-grafana") {
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
const remoteWrite = config.features.monitoring.remoteWrite;
|
|
266
|
+
if (!remoteWrite) {
|
|
267
|
+
return config.features.monitoring.remoteWriteUrl
|
|
268
|
+
? [{ url: config.features.monitoring.remoteWriteUrl }]
|
|
269
|
+
: [];
|
|
270
|
+
}
|
|
271
|
+
const base = {
|
|
272
|
+
url: remoteWrite.url,
|
|
273
|
+
};
|
|
274
|
+
switch (remoteWrite.destination) {
|
|
275
|
+
case "aws-amp":
|
|
276
|
+
if (!remoteWrite.awsRegion) {
|
|
277
|
+
throw new Error("AWS Managed Prometheus remote_write requires a region.");
|
|
278
|
+
}
|
|
279
|
+
return [
|
|
280
|
+
{
|
|
281
|
+
...base,
|
|
282
|
+
sigv4: {
|
|
283
|
+
region: remoteWrite.awsRegion,
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
];
|
|
287
|
+
case "azure-monitor":
|
|
288
|
+
return [generateAzureMonitorRemoteWrite(remoteWrite, base)];
|
|
289
|
+
case "grafana-cloud":
|
|
290
|
+
return [generateBasicAuthRemoteWrite(remoteWrite, base)];
|
|
291
|
+
case "generic":
|
|
292
|
+
return [generateGenericRemoteWrite(remoteWrite, base)];
|
|
293
|
+
default:
|
|
294
|
+
return [base];
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function generatePrometheusServiceAccount(config) {
|
|
298
|
+
const annotations = {};
|
|
299
|
+
const remoteWrite = config.features.monitoring.remoteWrite;
|
|
300
|
+
if (remoteWrite?.destination === "aws-amp" && remoteWrite.awsRoleArn) {
|
|
301
|
+
annotations["eks.amazonaws.com/role-arn"] = remoteWrite.awsRoleArn;
|
|
302
|
+
}
|
|
303
|
+
if (remoteWrite?.destination === "azure-monitor" &&
|
|
304
|
+
remoteWrite.authType === "workload-identity" &&
|
|
305
|
+
remoteWrite.clientId) {
|
|
306
|
+
annotations["azure.workload.identity/client-id"] = remoteWrite.clientId;
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
create: true,
|
|
310
|
+
name: "prometheus",
|
|
311
|
+
annotations,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
function generatePrometheusPodMetadata(config) {
|
|
315
|
+
const remoteWrite = config.features.monitoring.remoteWrite;
|
|
316
|
+
if (remoteWrite?.destination === "azure-monitor" &&
|
|
317
|
+
remoteWrite.authType === "workload-identity") {
|
|
318
|
+
return {
|
|
319
|
+
labels: {
|
|
320
|
+
"azure.workload.identity/use": "true",
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
return {};
|
|
325
|
+
}
|
|
326
|
+
function generateAzureMonitorRemoteWrite(remoteWrite, base) {
|
|
327
|
+
const azureAd = {
|
|
328
|
+
cloud: remoteWrite.azureCloud || "AzurePublic",
|
|
329
|
+
};
|
|
330
|
+
if (remoteWrite.authType === "oauth") {
|
|
331
|
+
if (!remoteWrite.clientId ||
|
|
332
|
+
!remoteWrite.tenantId ||
|
|
333
|
+
!remoteWrite.clientSecretRef) {
|
|
334
|
+
throw new Error("Azure Monitor remote_write OAuth requires client ID, tenant ID, and client secret ref.");
|
|
335
|
+
}
|
|
336
|
+
azureAd.oauth = {
|
|
337
|
+
clientId: remoteWrite.clientId,
|
|
338
|
+
tenantId: remoteWrite.tenantId,
|
|
339
|
+
clientSecret: secretKeySelector(remoteWrite.clientSecretRef),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
else if (remoteWrite.authType === "workload-identity") {
|
|
343
|
+
if (!remoteWrite.clientId || !remoteWrite.tenantId) {
|
|
344
|
+
throw new Error("Azure Monitor remote_write workload identity requires client ID and tenant ID.");
|
|
345
|
+
}
|
|
346
|
+
azureAd.workloadIdentity = {
|
|
347
|
+
clientId: remoteWrite.clientId,
|
|
348
|
+
tenantId: remoteWrite.tenantId,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
if (!remoteWrite.clientId) {
|
|
353
|
+
throw new Error("Azure Monitor remote_write managed identity requires client ID.");
|
|
354
|
+
}
|
|
355
|
+
azureAd.managedIdentity = {
|
|
356
|
+
clientId: remoteWrite.clientId,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
...base,
|
|
361
|
+
azureAd,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function generateBasicAuthRemoteWrite(remoteWrite, base) {
|
|
365
|
+
if (!remoteWrite.usernameSecretRef || !remoteWrite.passwordSecretRef) {
|
|
366
|
+
throw new Error("Basic auth remote_write requires username and password secret refs.");
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
...base,
|
|
370
|
+
basicAuth: {
|
|
371
|
+
username: secretKeySelector(remoteWrite.usernameSecretRef),
|
|
372
|
+
password: secretKeySelector(remoteWrite.passwordSecretRef),
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function generateGenericRemoteWrite(remoteWrite, base) {
|
|
377
|
+
if (remoteWrite.authType === "basic") {
|
|
378
|
+
return generateBasicAuthRemoteWrite(remoteWrite, base);
|
|
379
|
+
}
|
|
380
|
+
if (remoteWrite.authType === "bearer") {
|
|
381
|
+
if (!remoteWrite.bearerTokenSecretRef) {
|
|
382
|
+
throw new Error("Bearer remote_write requires a token secret ref.");
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
...base,
|
|
386
|
+
authorization: {
|
|
387
|
+
type: "Bearer",
|
|
388
|
+
credentials: secretKeySelector(remoteWrite.bearerTokenSecretRef),
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
return base;
|
|
393
|
+
}
|
|
178
394
|
/**
|
|
179
395
|
* Generates Kafka extra environment variables for tuning
|
|
180
396
|
*/
|
|
@@ -205,6 +421,7 @@ function generateKafkaExtraEnvVars() {
|
|
|
205
421
|
export async function generateHelmValues(config, options = {}) {
|
|
206
422
|
const tierConfig = TIER_CONFIGS[config.tier];
|
|
207
423
|
const { tlsEnabled = true } = options;
|
|
424
|
+
const useLocalGrafana = config.features.monitoring.destination === "local-grafana";
|
|
208
425
|
// Determine if external-dns should be enabled
|
|
209
426
|
const externalDnsEnabled = config.dns.autoManage && isSupportedDnsProvider(config.dns.provider);
|
|
210
427
|
// Determine storage class based on provider
|
|
@@ -501,22 +718,14 @@ export async function generateHelmValues(config, options = {}) {
|
|
|
501
718
|
replicas: tierConfig.vectorReplicas,
|
|
502
719
|
resources: tierConfig.vectorResources,
|
|
503
720
|
tolerations: arm64Tolerations,
|
|
721
|
+
serviceAccount: generateVectorServiceAccount(config),
|
|
722
|
+
podLabels: generateVectorPodLabels(config),
|
|
504
723
|
service: {
|
|
505
724
|
enabled: true,
|
|
506
725
|
ports: [{ name: "api", port: 8686, protocol: "TCP", targetPort: 8686 }],
|
|
507
726
|
},
|
|
508
727
|
// Load KAFKA_BOOTSTRAP_SERVERS from templated ConfigMap
|
|
509
|
-
env:
|
|
510
|
-
{
|
|
511
|
-
name: "KAFKA_BOOTSTRAP_SERVERS",
|
|
512
|
-
valueFrom: {
|
|
513
|
-
configMapKeyRef: {
|
|
514
|
-
name: "vector-kafka-env",
|
|
515
|
-
key: "KAFKA_BOOTSTRAP_SERVERS",
|
|
516
|
-
},
|
|
517
|
-
},
|
|
518
|
-
},
|
|
519
|
-
],
|
|
728
|
+
env: generateVectorEnv(config),
|
|
520
729
|
customConfig: {
|
|
521
730
|
sources: {
|
|
522
731
|
kafka: {
|
|
@@ -598,12 +807,14 @@ export async function generateHelmValues(config, options = {}) {
|
|
|
598
807
|
enabled: false,
|
|
599
808
|
},
|
|
600
809
|
grafana: {
|
|
601
|
-
enabled:
|
|
810
|
+
enabled: useLocalGrafana,
|
|
602
811
|
},
|
|
603
812
|
prometheus: {
|
|
604
813
|
enabled: config.features.monitoring.enabled,
|
|
814
|
+
serviceAccount: generatePrometheusServiceAccount(config),
|
|
605
815
|
prometheusSpec: {
|
|
606
816
|
retention: "30d",
|
|
817
|
+
podMetadata: generatePrometheusPodMetadata(config),
|
|
607
818
|
storageSpec: {
|
|
608
819
|
volumeClaimTemplate: {
|
|
609
820
|
spec: {
|
|
@@ -617,13 +828,7 @@ export async function generateHelmValues(config, options = {}) {
|
|
|
617
828
|
},
|
|
618
829
|
},
|
|
619
830
|
},
|
|
620
|
-
|
|
621
|
-
? {
|
|
622
|
-
remoteWrite: [
|
|
623
|
-
{ url: config.features.monitoring.remoteWriteUrl },
|
|
624
|
-
],
|
|
625
|
-
}
|
|
626
|
-
: { remoteWrite: [] }),
|
|
831
|
+
remoteWrite: generateRemoteWriteSpec(config),
|
|
627
832
|
},
|
|
628
833
|
},
|
|
629
834
|
},
|
package/dist/lib/kubernetes.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CloudProvider } from "../types/index.js";
|
|
1
|
+
import { CloudProvider, PerformanceTier } from "../types/index.js";
|
|
2
2
|
/**
|
|
3
3
|
* Checks if kubectl is installed
|
|
4
4
|
*/
|
|
@@ -20,6 +20,12 @@ export declare function checkClusterAccessible(): Promise<string | null>;
|
|
|
20
20
|
* Gets the current kubectl context
|
|
21
21
|
*/
|
|
22
22
|
export declare function getCurrentContext(): Promise<string | null>;
|
|
23
|
+
/**
|
|
24
|
+
* Infers the closest internal Rulebricks sizing tier from the current cluster.
|
|
25
|
+
* This is used only for existing clusters, where the CLI is not responsible for
|
|
26
|
+
* provisioning node pools but still needs app/Kafka/worker Helm sizing values.
|
|
27
|
+
*/
|
|
28
|
+
export declare function inferClusterTier(): Promise<PerformanceTier | null>;
|
|
23
29
|
/**
|
|
24
30
|
* Gets pod status for the Rulebricks namespace
|
|
25
31
|
*/
|
package/dist/lib/kubernetes.js
CHANGED
|
@@ -165,6 +165,65 @@ export async function getCurrentContext() {
|
|
|
165
165
|
return null;
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
|
+
function parseCpuToCores(cpu) {
|
|
169
|
+
if (cpu.endsWith("n"))
|
|
170
|
+
return Number(cpu.slice(0, -1)) / 1_000_000_000;
|
|
171
|
+
if (cpu.endsWith("u"))
|
|
172
|
+
return Number(cpu.slice(0, -1)) / 1_000_000;
|
|
173
|
+
if (cpu.endsWith("m"))
|
|
174
|
+
return Number(cpu.slice(0, -1)) / 1_000;
|
|
175
|
+
return Number(cpu);
|
|
176
|
+
}
|
|
177
|
+
function parseMemoryToGi(memory) {
|
|
178
|
+
const match = memory.match(/^(\d+(?:\.\d+)?)([KMGTP]i?|[kMGTPE])?$/);
|
|
179
|
+
if (!match)
|
|
180
|
+
return 0;
|
|
181
|
+
const value = Number(match[1]);
|
|
182
|
+
const unit = match[2] || "";
|
|
183
|
+
const multipliers = {
|
|
184
|
+
Ki: 1 / 1024 / 1024,
|
|
185
|
+
Mi: 1 / 1024,
|
|
186
|
+
Gi: 1,
|
|
187
|
+
Ti: 1024,
|
|
188
|
+
Pi: 1024 * 1024,
|
|
189
|
+
K: 1000 / 1024 / 1024 / 1024,
|
|
190
|
+
M: 1000 ** 2 / 1024 ** 3,
|
|
191
|
+
G: 1000 ** 3 / 1024 ** 3,
|
|
192
|
+
T: 1000 ** 4 / 1024 ** 3,
|
|
193
|
+
P: 1000 ** 5 / 1024 ** 3,
|
|
194
|
+
};
|
|
195
|
+
return value * (multipliers[unit] ?? 1 / 1024 ** 3);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Infers the closest internal Rulebricks sizing tier from the current cluster.
|
|
199
|
+
* This is used only for existing clusters, where the CLI is not responsible for
|
|
200
|
+
* provisioning node pools but still needs app/Kafka/worker Helm sizing values.
|
|
201
|
+
*/
|
|
202
|
+
export async function inferClusterTier() {
|
|
203
|
+
try {
|
|
204
|
+
const { stdout } = await execa("kubectl", ["get", "nodes", "-o", "json"], {
|
|
205
|
+
timeout: 15000,
|
|
206
|
+
});
|
|
207
|
+
const data = JSON.parse(stdout);
|
|
208
|
+
const schedulableNodes = data.items?.filter((node) => !node.spec?.unschedulable) ?? [];
|
|
209
|
+
let totalCpu = 0;
|
|
210
|
+
let totalMemoryGi = 0;
|
|
211
|
+
for (const node of schedulableNodes) {
|
|
212
|
+
totalCpu += parseCpuToCores(node.status?.allocatable?.cpu || "0");
|
|
213
|
+
totalMemoryGi += parseMemoryToGi(node.status?.allocatable?.memory || "0");
|
|
214
|
+
}
|
|
215
|
+
if (totalCpu >= 40 && totalMemoryGi >= 80)
|
|
216
|
+
return "large";
|
|
217
|
+
if (totalCpu >= 16 && totalMemoryGi >= 32)
|
|
218
|
+
return "medium";
|
|
219
|
+
if (totalCpu > 0 && totalMemoryGi > 0)
|
|
220
|
+
return "small";
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
168
227
|
/**
|
|
169
228
|
* Gets pod status for the Rulebricks namespace
|
|
170
229
|
*/
|