@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.
@@ -36,17 +36,39 @@ function generateVectorSinks(config) {
36
36
  };
37
37
  break;
38
38
  case "azure-blob":
39
- sinks.azure_blob = {
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
- container_name: bucket,
43
- storage_account: "rulebrickslogs", // Will be configured via env var
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: false,
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
- ...(config.features.monitoring.remoteWriteUrl
621
- ? {
622
- remoteWrite: [
623
- { url: config.features.monitoring.remoteWriteUrl },
624
- ],
625
- }
626
- : { remoteWrite: [] }),
831
+ remoteWrite: generateRemoteWriteSpec(config),
627
832
  },
628
833
  },
629
834
  },
@@ -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
  */
@@ -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
  */