@rulebricks/cli 2.1.7 → 2.3.2

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.
Files changed (117) hide show
  1. package/README.md +51 -16
  2. package/cluster-setup/aws/README.md +96 -47
  3. package/cluster-setup/aws/check-aws-access.sh +216 -52
  4. package/cluster-setup/aws/parameters.json +13 -0
  5. package/cluster-setup/aws/rulebricks-cluster.cfn.yaml +355 -0
  6. package/cluster-setup/azure/README.md +103 -55
  7. package/cluster-setup/azure/check-aks-prereqs.sh +236 -56
  8. package/cluster-setup/azure/parameters.json +30 -0
  9. package/cluster-setup/azure/rulebricks-cluster.bicep +546 -0
  10. package/cluster-setup/gcp/README.md +51 -34
  11. package/cluster-setup/gcp/check-gke-prereqs.sh +222 -60
  12. package/dist/commands/backup.d.ts +5 -0
  13. package/dist/commands/backup.js +104 -0
  14. package/dist/commands/deploy.d.ts +3 -1
  15. package/dist/commands/deploy.js +226 -326
  16. package/dist/commands/destroy.d.ts +1 -1
  17. package/dist/commands/destroy.js +73 -123
  18. package/dist/commands/init.d.ts +5 -1
  19. package/dist/commands/init.js +78 -54
  20. package/dist/commands/list.d.ts +1 -0
  21. package/dist/commands/list.js +74 -0
  22. package/dist/commands/open.d.ts +1 -1
  23. package/dist/commands/open.js +4 -12
  24. package/dist/commands/redeploy.d.ts +6 -0
  25. package/dist/commands/redeploy.js +310 -0
  26. package/dist/commands/restore.d.ts +5 -0
  27. package/dist/commands/restore.js +338 -0
  28. package/dist/commands/status.js +62 -49
  29. package/dist/commands/upgrade.js +74 -51
  30. package/dist/components/DNSWaitScreen.d.ts +5 -1
  31. package/dist/components/DNSWaitScreen.js +47 -41
  32. package/dist/components/Wizard/WizardContext.d.ts +157 -36
  33. package/dist/components/Wizard/WizardContext.js +872 -160
  34. package/dist/components/Wizard/steps/CloudProviderStep.js +192 -107
  35. package/dist/components/Wizard/steps/DomainStep.js +5 -24
  36. package/dist/components/Wizard/steps/ExternalServicesStep.d.ts +6 -0
  37. package/dist/components/Wizard/steps/ExternalServicesStep.js +645 -0
  38. package/dist/components/Wizard/steps/FeatureConfigStep.d.ts +2 -1
  39. package/dist/components/Wizard/steps/FeatureConfigStep.js +739 -425
  40. package/dist/components/Wizard/steps/FeaturesStep.js +31 -35
  41. package/dist/components/Wizard/steps/ObservabilityStep.d.ts +6 -0
  42. package/dist/components/Wizard/steps/ObservabilityStep.js +137 -0
  43. package/dist/components/Wizard/steps/ReviewStep.d.ts +2 -1
  44. package/dist/components/Wizard/steps/ReviewStep.js +56 -12
  45. package/dist/components/Wizard/steps/StorageStep.d.ts +9 -0
  46. package/dist/components/Wizard/steps/StorageStep.js +592 -0
  47. package/dist/components/Wizard/steps/SupabaseCredentialsStep.js +20 -21
  48. package/dist/components/Wizard/steps/VersionStep.js +45 -23
  49. package/dist/components/Wizard/steps/index.d.ts +3 -3
  50. package/dist/components/Wizard/steps/index.js +3 -3
  51. package/dist/components/common/CommandApproval.d.ts +12 -0
  52. package/dist/components/common/CommandApproval.js +91 -0
  53. package/dist/components/common/DeploymentPicker.d.ts +14 -0
  54. package/dist/components/common/DeploymentPicker.js +16 -0
  55. package/dist/components/common/index.d.ts +2 -0
  56. package/dist/components/common/index.js +2 -0
  57. package/dist/index.js +94 -62
  58. package/dist/lib/cloudCli.d.ts +134 -63
  59. package/dist/lib/cloudCli.js +512 -220
  60. package/dist/lib/clusterSetupDefaults.d.ts +30 -0
  61. package/dist/lib/clusterSetupDefaults.js +64 -0
  62. package/dist/lib/commandApproval.d.ts +26 -0
  63. package/dist/lib/commandApproval.js +114 -0
  64. package/dist/lib/config.d.ts +12 -10
  65. package/dist/lib/config.js +91 -33
  66. package/dist/lib/configFixtures.d.ts +5 -0
  67. package/dist/lib/configFixtures.js +513 -0
  68. package/dist/lib/deploymentHealth.d.ts +32 -0
  69. package/dist/lib/deploymentHealth.js +157 -0
  70. package/dist/lib/dns.d.ts +1 -1
  71. package/dist/lib/dns.js +19 -1
  72. package/dist/lib/dns.test.d.ts +1 -0
  73. package/dist/lib/dns.test.js +27 -0
  74. package/dist/lib/dockerHub.d.ts +12 -1
  75. package/dist/lib/dockerHub.js +18 -8
  76. package/dist/lib/helm.d.ts +4 -0
  77. package/dist/lib/helm.js +16 -0
  78. package/dist/lib/helmValues.d.ts +25 -0
  79. package/dist/lib/helmValues.js +1841 -289
  80. package/dist/lib/helmValues.test.d.ts +1 -0
  81. package/dist/lib/helmValues.test.js +1012 -0
  82. package/dist/lib/htpasswd.d.ts +1 -0
  83. package/dist/lib/htpasswd.js +15 -0
  84. package/dist/lib/kubernetes.d.ts +124 -17
  85. package/dist/lib/kubernetes.js +576 -145
  86. package/dist/lib/secrets.d.ts +23 -0
  87. package/dist/lib/secrets.js +158 -0
  88. package/dist/lib/validateValues.d.ts +31 -0
  89. package/dist/lib/validateValues.js +253 -0
  90. package/dist/lib/versions.d.ts +82 -11
  91. package/dist/lib/versions.js +131 -31
  92. package/dist/lib/versions.test.d.ts +1 -0
  93. package/dist/lib/versions.test.js +81 -0
  94. package/dist/lib/wizardSteps.d.ts +14 -0
  95. package/dist/lib/wizardSteps.js +23 -0
  96. package/dist/lib/workloadIdentity.d.ts +26 -0
  97. package/dist/lib/workloadIdentity.js +323 -0
  98. package/dist/lib/workloadIdentity.test.d.ts +1 -0
  99. package/dist/lib/workloadIdentity.test.js +57 -0
  100. package/dist/types/index.d.ts +1860 -164
  101. package/dist/types/index.js +518 -295
  102. package/package.json +9 -4
  103. package/schema/values.schema.json +1934 -0
  104. package/cluster-setup/aws/cluster.yaml +0 -33
  105. package/cluster-setup/azure/main.bicep +0 -282
  106. package/cluster-setup/azure/main.parameters.json +0 -21
  107. package/dist/components/Wizard/steps/CredentialsStep.d.ts +0 -6
  108. package/dist/components/Wizard/steps/CredentialsStep.js +0 -22
  109. package/dist/components/Wizard/steps/DeploymentModeStep.d.ts +0 -5
  110. package/dist/components/Wizard/steps/DeploymentModeStep.js +0 -26
  111. package/dist/components/Wizard/steps/TierStep.d.ts +0 -6
  112. package/dist/components/Wizard/steps/TierStep.js +0 -29
  113. package/dist/lib/terraform.d.ts +0 -66
  114. package/dist/lib/terraform.js +0 -754
  115. package/terraform/aws/main.tf +0 -355
  116. package/terraform/azure/main.tf +0 -371
  117. package/terraform/gcp/main.tf +0 -407
@@ -0,0 +1,23 @@
1
+ import { DeploymentConfig } from "../types/index.js";
2
+ export interface K8sSecretManifest {
3
+ name: string;
4
+ stringData: Record<string, string>;
5
+ }
6
+ /**
7
+ * Build the Kubernetes Secret manifests for a deployment. Only includes values
8
+ * that are actually set. Supabase anon/service keys are derived from the JWT
9
+ * secret (HS256), matching self-hosted Supabase.
10
+ */
11
+ export declare function buildDeploymentSecrets(config: DeploymentConfig): K8sSecretManifest[];
12
+ /**
13
+ * Idempotently ensure the namespace exists so Secrets can be applied before Helm
14
+ * runs (`helm upgrade --install --create-namespace` also creates it, but that
15
+ * happens after this step).
16
+ */
17
+ export declare function ensureNamespace(namespace: string): Promise<void>;
18
+ /**
19
+ * Create/update the deployment's Kubernetes Secrets. `kubectl apply` is an
20
+ * upsert, so upgrades and redeploys never wipe or churn the Secrets. Returns the
21
+ * names applied.
22
+ */
23
+ export declare function applyDeploymentSecrets(config: DeploymentConfig, namespace: string): Promise<string[]>;
@@ -0,0 +1,158 @@
1
+ // Kubernetes Secret management for k8s secret mode.
2
+ //
3
+ // In k8s mode the CLI creates the deployment's Secrets directly (idempotent
4
+ // `kubectl apply`) and the generated values.yaml carries only secretRef
5
+ // references — no plaintext secrets on disk or in the Helm release. Secret names
6
+ // come from deploymentSecretNames() so they always match the secretRef seams the
7
+ // value generator writes.
8
+ import { execa } from "execa";
9
+ import { signSupabaseJwt, deriveRealtimeSecrets, deploymentSecretNames, } from "./helmValues.js";
10
+ /**
11
+ * Build the Kubernetes Secret manifests for a deployment. Only includes values
12
+ * that are actually set. Supabase anon/service keys are derived from the JWT
13
+ * secret (HS256), matching self-hosted Supabase.
14
+ */
15
+ export function buildDeploymentSecrets(config) {
16
+ const names = deploymentSecretNames(config);
17
+ const out = [];
18
+ // Consolidated app secret (global.secrets.secretRef).
19
+ const app = {};
20
+ const put = (k, v) => {
21
+ if (v)
22
+ app[k] = v;
23
+ };
24
+ put("LICENSE_KEY", config.licenseKey);
25
+ put("EMAIL", config.adminEmail);
26
+ put("SMTP_USER", config.smtp?.user);
27
+ put("SMTP_PASS", config.smtp?.pass);
28
+ if (config.database.type === "supabase-cloud") {
29
+ put("SUPABASE_ANON_KEY", config.database.supabaseAnonKey);
30
+ put("SUPABASE_SERVICE_KEY", config.database.supabaseServiceKey);
31
+ put("SUPABASE_SECRET_KEY", config.database.supabaseServiceKey);
32
+ put("SUPABASE_ACCESS_TOKEN", config.database.supabaseAccessToken);
33
+ }
34
+ else if (config.database.supabaseJwtSecret) {
35
+ const jwt = config.database.supabaseJwtSecret;
36
+ put("SUPABASE_ANON_KEY", signSupabaseJwt("anon", jwt));
37
+ put("SUPABASE_SERVICE_KEY", signSupabaseJwt("service_role", jwt));
38
+ put("SUPABASE_SECRET_KEY", signSupabaseJwt("service_role", jwt));
39
+ put("JWT_SECRET", jwt);
40
+ }
41
+ if (config.features.ai.enabled) {
42
+ put("OPENAI_API_KEY", config.features.ai.openaiApiKey);
43
+ }
44
+ if (config.features.sso.enabled) {
45
+ put("SSO_CLIENT_ID", config.features.sso.clientId);
46
+ put("SSO_CLIENT_SECRET", config.features.sso.clientSecret);
47
+ }
48
+ const redis = config.externalServices?.redis?.external;
49
+ if (redis?.password)
50
+ put("REDIS_PASSWORD", redis.password);
51
+ const kafkaSasl = config.externalServices?.kafka?.external?.sasl;
52
+ if (kafkaSasl?.username)
53
+ put("KAFKA_SASL_USERNAME", kafkaSasl.username);
54
+ if (kafkaSasl?.password)
55
+ put("KAFKA_SASL_PASSWORD", kafkaSasl.password);
56
+ if (Object.keys(app).length > 0) {
57
+ out.push({ name: names.app, stringData: app });
58
+ }
59
+ // Supabase self-hosted component secrets (each maps to a supabase.secret.*.secretRef).
60
+ if (config.database.type === "self-hosted") {
61
+ const pgExt = config.externalServices?.postgres?.mode === "external"
62
+ ? config.externalServices.postgres.external
63
+ : undefined;
64
+ const dbStringData = {
65
+ username: "postgres",
66
+ password: config.database.supabaseDbPassword ?? "",
67
+ database: pgExt?.database ?? "postgres",
68
+ };
69
+ if (pgExt) {
70
+ dbStringData.host = pgExt.host ?? "";
71
+ dbStringData.port = String(pgExt.port ?? 5432);
72
+ }
73
+ out.push({
74
+ name: names.db,
75
+ stringData: dbStringData,
76
+ });
77
+ if (pgExt) {
78
+ out.push({
79
+ name: names.dbBootstrap,
80
+ stringData: {
81
+ "master-username": pgExt.bootstrap?.masterUsername ?? "postgres",
82
+ "master-password": pgExt.bootstrap?.masterPassword ?? "",
83
+ "service-password": config.database.supabaseDbPassword ?? "",
84
+ },
85
+ });
86
+ }
87
+ const jwt = config.database.supabaseJwtSecret ?? "";
88
+ out.push({
89
+ name: names.jwt,
90
+ stringData: {
91
+ secret: jwt,
92
+ anonKey: jwt ? signSupabaseJwt("anon", jwt) : "",
93
+ serviceKey: jwt ? signSupabaseJwt("service_role", jwt) : "",
94
+ },
95
+ });
96
+ out.push({
97
+ name: names.dashboard,
98
+ stringData: {
99
+ username: config.database.supabaseDashboardUser || "supabase",
100
+ password: config.database.supabaseDashboardPass ?? "",
101
+ },
102
+ });
103
+ const rt = deriveRealtimeSecrets(jwt);
104
+ out.push({
105
+ name: names.realtime,
106
+ stringData: { SECRET_KEY_BASE: rt.secretKeyBase, DB_ENC_KEY: rt.dbEncKey },
107
+ });
108
+ // Supabase auth (GoTrue) SMTP, when configured.
109
+ if (config.smtp?.user || config.smtp?.pass) {
110
+ out.push({
111
+ name: names.smtp,
112
+ stringData: {
113
+ username: config.smtp.user ?? "",
114
+ password: config.smtp.pass ?? "",
115
+ },
116
+ });
117
+ }
118
+ }
119
+ return out;
120
+ }
121
+ function secretManifest(name, namespace, stringData) {
122
+ return {
123
+ apiVersion: "v1",
124
+ kind: "Secret",
125
+ type: "Opaque",
126
+ metadata: { name, namespace },
127
+ stringData,
128
+ };
129
+ }
130
+ /**
131
+ * Idempotently ensure the namespace exists so Secrets can be applied before Helm
132
+ * runs (`helm upgrade --install --create-namespace` also creates it, but that
133
+ * happens after this step).
134
+ */
135
+ export async function ensureNamespace(namespace) {
136
+ const manifest = {
137
+ apiVersion: "v1",
138
+ kind: "Namespace",
139
+ metadata: { name: namespace },
140
+ };
141
+ await execa("kubectl", ["apply", "-f", "-"], {
142
+ input: JSON.stringify(manifest),
143
+ });
144
+ }
145
+ /**
146
+ * Create/update the deployment's Kubernetes Secrets. `kubectl apply` is an
147
+ * upsert, so upgrades and redeploys never wipe or churn the Secrets. Returns the
148
+ * names applied.
149
+ */
150
+ export async function applyDeploymentSecrets(config, namespace) {
151
+ const secrets = buildDeploymentSecrets(config);
152
+ for (const s of secrets) {
153
+ await execa("kubectl", ["apply", "-f", "-"], {
154
+ input: JSON.stringify(secretManifest(s.name, namespace, s.stringData)),
155
+ });
156
+ }
157
+ return secrets.map((s) => s.name);
158
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Validates generated Helm values against the chart's values.schema.json.
3
+ *
4
+ * The schema is bundled with the CLI (schema/values.schema.json) and kept in
5
+ * sync with the Helm chart via `npm run sync-schema`. This gives us a last-line
6
+ * guardrail: the CLI refuses to deploy values the chart would reject at install
7
+ * time, surfacing a readable message instead of a raw Helm/JSON-schema error.
8
+ */
9
+ export interface ValuesValidationResult {
10
+ valid: boolean;
11
+ errors: string[];
12
+ }
13
+ /**
14
+ * Cross-field invariants the JSON schema cannot express. These encode the
15
+ * Kafka sizing model: partitions are the worker-fleet concurrency ceiling,
16
+ * topic names must carry the same prefix HPS/Vector/KEDA use, and worker CPU
17
+ * requests must not exceed their (one-core) burst limit.
18
+ */
19
+ export declare function validateValuesInvariants(values: unknown): string[];
20
+ /**
21
+ * Validates a generated Helm values object against the bundled chart schema
22
+ * plus cross-field invariants the schema cannot express.
23
+ * Values are round-tripped through YAML first so we validate exactly what Helm
24
+ * receives (dropping `undefined`, normalizing numbers, etc.).
25
+ */
26
+ export declare function validateHelmValues(values: unknown): ValuesValidationResult;
27
+ /**
28
+ * Throws a readable error if the values are invalid. Used as a pre-deploy
29
+ * guardrail so we never hand Helm a config the chart would reject.
30
+ */
31
+ export declare function assertValidHelmValues(values: unknown): void;
@@ -0,0 +1,253 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { Ajv } from "ajv";
5
+ import YAML from "yaml";
6
+ let cachedValidator = null;
7
+ function getBundledSchemaPath() {
8
+ // Compiled location: dist/lib/validateValues.js -> ../../schema/...
9
+ const here = path.dirname(fileURLToPath(import.meta.url));
10
+ return path.resolve(here, "../../schema/values.schema.json");
11
+ }
12
+ function loadValidator() {
13
+ if (cachedValidator)
14
+ return cachedValidator;
15
+ const schema = JSON.parse(fs.readFileSync(getBundledSchemaPath(), "utf8"));
16
+ // strict:false tolerates the chart schema's union types and `default`
17
+ // keywords; allErrors collects every problem so we can report them together.
18
+ const ajv = new Ajv({ allErrors: true, strict: false });
19
+ const validate = ajv.compile(schema);
20
+ cachedValidator = validate;
21
+ return validate;
22
+ }
23
+ function pointerToPath(instancePath) {
24
+ if (!instancePath)
25
+ return "";
26
+ return instancePath
27
+ .replace(/^\//, "")
28
+ .split("/")
29
+ .map((seg) => seg.replace(/~1/g, "/").replace(/~0/g, "~"))
30
+ .join(".");
31
+ }
32
+ function describeError(err) {
33
+ const where = pointerToPath(err.instancePath || "");
34
+ const prefix = where ? `${where}` : "values";
35
+ switch (err.keyword) {
36
+ case "required": {
37
+ const missing = err.params
38
+ .missingProperty;
39
+ return `${where ? `${where}.` : ""}${missing} is required`;
40
+ }
41
+ case "minLength": {
42
+ const limit = err.params.limit;
43
+ return limit <= 1
44
+ ? `${prefix} must not be empty`
45
+ : `${prefix} must be at least ${limit} characters`;
46
+ }
47
+ case "enum": {
48
+ const allowed = err.params.allowedValues;
49
+ return `${prefix} must be one of: ${allowed
50
+ .map((v) => JSON.stringify(v))
51
+ .join(", ")}`;
52
+ }
53
+ case "const": {
54
+ const allowed = err.params.allowedValue;
55
+ return `${prefix} must be ${JSON.stringify(allowed)}`;
56
+ }
57
+ case "pattern":
58
+ return `${prefix} has an invalid format`;
59
+ case "type": {
60
+ const type = err.params.type;
61
+ return `${prefix} must be of type ${Array.isArray(type) ? type.join(" or ") : type}`;
62
+ }
63
+ case "minimum": {
64
+ const limit = err.params.limit;
65
+ return `${prefix} must be >= ${limit}`;
66
+ }
67
+ case "additionalProperties": {
68
+ const extra = err.params
69
+ .additionalProperty;
70
+ return `${prefix} has an unexpected property '${extra}'`;
71
+ }
72
+ default:
73
+ return null;
74
+ }
75
+ }
76
+ function formatErrors(errors) {
77
+ if (!errors || errors.length === 0) {
78
+ return ["Generated values failed schema validation."];
79
+ }
80
+ const messages = new Set();
81
+ for (const err of errors) {
82
+ const message = describeError(err);
83
+ if (message)
84
+ messages.add(message);
85
+ }
86
+ if (messages.size === 0) {
87
+ // Only structural (if/then/allOf) errors were present; surface a hint.
88
+ messages.add("Generated values do not satisfy a conditional schema rule (check storage, backup, external services, and monitoring settings).");
89
+ }
90
+ return [...messages];
91
+ }
92
+ /* eslint-disable @typescript-eslint/no-explicit-any */
93
+ function get(obj, path) {
94
+ let cur = obj;
95
+ for (const key of path) {
96
+ if (cur === null || cur === undefined || typeof cur !== "object") {
97
+ return undefined;
98
+ }
99
+ cur = cur[key];
100
+ }
101
+ return cur;
102
+ }
103
+ /**
104
+ * Cross-field invariants the JSON schema cannot express. These encode the
105
+ * Kafka sizing model: partitions are the worker-fleet concurrency ceiling,
106
+ * topic names must carry the same prefix HPS/Vector/KEDA use, and worker CPU
107
+ * requests must not exceed their (one-core) burst limit.
108
+ */
109
+ export function validateValuesInvariants(values) {
110
+ const errors = [];
111
+ const workers = get(values, ["rulebricks", "hps", "workers"]);
112
+ const solutionPartitions = get(workers, ["solutionPartitions"]);
113
+ const maxReplicaCount = get(workers, ["keda", "maxReplicaCount"]);
114
+ // 1. Workers beyond the partition count would sit idle.
115
+ if (typeof solutionPartitions === "number" &&
116
+ typeof maxReplicaCount === "number" &&
117
+ maxReplicaCount > solutionPartitions) {
118
+ errors.push(`rulebricks.hps.workers.keda.maxReplicaCount (${maxReplicaCount}) must be <= solutionPartitions (${solutionPartitions}); partitions are the fleet concurrency ceiling`);
119
+ }
120
+ // 2. Single-threaded CPU-bound workers are Burstable: the request may sit
121
+ // below the limit (tight bin-packing + a cheap warm pool), but it must
122
+ // never exceed the limit. The limit is the per-worker burst ceiling (one
123
+ // core); under genuine node contention a Burstable worker can be
124
+ // CFS-throttled toward its request.
125
+ const parseCpuMillicores = (value) => {
126
+ if (typeof value === "number")
127
+ return value * 1000;
128
+ if (typeof value !== "string")
129
+ return undefined;
130
+ const millicores = value.endsWith("m")
131
+ ? Number(value.slice(0, -1))
132
+ : Number(value) * 1000;
133
+ return Number.isFinite(millicores) ? millicores : undefined;
134
+ };
135
+ const workerCpuRequest = get(workers, ["resources", "requests", "cpu"]);
136
+ const workerCpuLimit = get(workers, ["resources", "limits", "cpu"]);
137
+ const workerCpuRequestM = parseCpuMillicores(workerCpuRequest);
138
+ const workerCpuLimitM = parseCpuMillicores(workerCpuLimit);
139
+ if (workerCpuRequestM !== undefined &&
140
+ workerCpuLimitM !== undefined &&
141
+ workerCpuRequestM > workerCpuLimitM) {
142
+ errors.push(`rulebricks.hps.workers.resources cpu request (${workerCpuRequest}) must not exceed limit (${workerCpuLimit})`);
143
+ }
144
+ // 3. In-cluster provisioning: topic names must carry the SAME prefix the
145
+ // application uses, and the solution topic must match solutionPartitions
146
+ // (which HPS receives as MAX_WORKERS). Mirrors the chart's render guard.
147
+ const kafkaEnabled = get(values, ["kafka", "enabled"]);
148
+ const kafkaTopics = get(values, ["kafka", "topics"]);
149
+ if (kafkaEnabled && Array.isArray(kafkaTopics) && kafkaTopics.length > 0) {
150
+ const logging = get(values, ["rulebricks", "app", "logging"]) ?? {};
151
+ const prefix = Object.prototype.hasOwnProperty.call(logging, "kafkaTopicPrefix")
152
+ ? String(logging.kafkaTopicPrefix ?? "")
153
+ : "com.rulebricks.";
154
+ const topics = kafkaTopics;
155
+ const names = topics.map((t) => t?.name);
156
+ for (const base of ["solution", "solution-response", "logs"]) {
157
+ const expected = `${prefix}${base}`;
158
+ if (!names.includes(expected)) {
159
+ errors.push(`kafka.topics must include "${expected}" (kafkaTopicPrefix is "${prefix}"); found: ${names.join(", ") || "none"}`);
160
+ }
161
+ }
162
+ const solutionTopic = topics.find((t) => t?.name === `${prefix}solution`);
163
+ if (typeof solutionPartitions === "number" &&
164
+ solutionTopic &&
165
+ typeof solutionTopic.partitions === "number" &&
166
+ solutionTopic.partitions !== solutionPartitions) {
167
+ errors.push(`kafka "${prefix}solution" partitions (${solutionTopic.partitions}) must equal rulebricks.hps.workers.solutionPartitions (${solutionPartitions}); HPS derives MAX_WORKERS from it`);
168
+ }
169
+ }
170
+ // 4. Distributed tracing: when enabled, the collector must have a non-empty
171
+ // endpoint for the selected destination (the JSON schema also enforces
172
+ // this, but we surface a clearer message), and the active auth mode must
173
+ // carry its credential.
174
+ const tracing = get(values, ["global", "tracing"]);
175
+ if (tracing && tracing.enabled) {
176
+ const destination = tracing.destination ?? "elastic";
177
+ if (destination === "elastic") {
178
+ const elastic = get(tracing, ["elastic"]) ?? {};
179
+ if (!elastic.endpoint) {
180
+ errors.push("global.tracing.elastic.endpoint must be set when tracing destination is 'elastic'");
181
+ }
182
+ const authMode = elastic.authMode ?? "secret-token";
183
+ if (authMode === "secret-token" &&
184
+ !elastic.secretToken &&
185
+ !elastic.existingSecret?.name) {
186
+ errors.push("global.tracing.elastic.secretToken (or existingSecret.name) is required for authMode 'secret-token'");
187
+ }
188
+ if (authMode === "api-key" &&
189
+ !elastic.apiKey &&
190
+ !elastic.existingSecret?.name) {
191
+ errors.push("global.tracing.elastic.apiKey (or existingSecret.name) is required for authMode 'api-key'");
192
+ }
193
+ }
194
+ else if (destination === "otlp") {
195
+ const otlp = get(tracing, ["otlp"]) ?? {};
196
+ if (!otlp.endpoint) {
197
+ errors.push("global.tracing.otlp.endpoint must be set when tracing destination is 'otlp'");
198
+ }
199
+ }
200
+ else if (destination === "azure-monitor") {
201
+ const azure = get(tracing, ["azureMonitor"]) ?? {};
202
+ if (!azure.connectionString && !azure.existingSecret?.name) {
203
+ errors.push("global.tracing.azureMonitor.connectionString (or existingSecret.name) is required when tracing destination is 'azure-monitor'");
204
+ }
205
+ }
206
+ }
207
+ // 5. Application/container log shipping: when the Vector agent is enabled it
208
+ // must have exactly one configured external sink.
209
+ const vectorAgent = get(values, ["vector-agent"]);
210
+ if (vectorAgent && vectorAgent.enabled) {
211
+ const sinks = get(vectorAgent, ["customConfig", "sinks"]);
212
+ const elasticsearchEndpoints = get(sinks, ["elasticsearch", "endpoints"]);
213
+ const hasElasticsearch = Array.isArray(elasticsearchEndpoints) &&
214
+ elasticsearchEndpoints.some((e) => typeof e === "string" && e.length > 0);
215
+ const lokiEndpoint = get(sinks, ["loki", "endpoint"]);
216
+ const hasLoki = typeof lokiEndpoint === "string" && lokiEndpoint.length > 0;
217
+ const genericUri = get(sinks, ["generic_http", "uri"]);
218
+ const hasGeneric = typeof genericUri === "string" && genericUri.length > 0;
219
+ if (!hasElasticsearch && !hasLoki && !hasGeneric) {
220
+ errors.push("vector-agent is enabled but no app-log sink endpoint is configured; set features.logging.appLogs for elasticsearch, loki, or generic");
221
+ }
222
+ }
223
+ return errors;
224
+ }
225
+ /**
226
+ * Validates a generated Helm values object against the bundled chart schema
227
+ * plus cross-field invariants the schema cannot express.
228
+ * Values are round-tripped through YAML first so we validate exactly what Helm
229
+ * receives (dropping `undefined`, normalizing numbers, etc.).
230
+ */
231
+ export function validateHelmValues(values) {
232
+ const normalized = YAML.parse(YAML.stringify(values));
233
+ const validate = loadValidator();
234
+ const valid = validate(normalized);
235
+ const errors = valid ? [] : formatErrors(validate.errors);
236
+ errors.push(...validateValuesInvariants(normalized));
237
+ if (errors.length === 0)
238
+ return { valid: true, errors: [] };
239
+ return { valid: false, errors };
240
+ }
241
+ /**
242
+ * Throws a readable error if the values are invalid. Used as a pre-deploy
243
+ * guardrail so we never hand Helm a config the chart would reject.
244
+ */
245
+ export function assertValidHelmValues(values) {
246
+ const result = validateHelmValues(values);
247
+ if (result.valid)
248
+ return;
249
+ throw new Error([
250
+ "Generated Helm values are not valid for the Rulebricks chart:",
251
+ ...result.errors.map((e) => ` • ${e}`),
252
+ ].join("\n"));
253
+ }
@@ -1,5 +1,77 @@
1
- import { ChartVersion, AppVersion } from "../types/index.js";
1
+ import { ChartVersion, AppVersion, NodeArchitecture } from "../types/index.js";
2
2
  import { ImageTag } from "./dockerHub.js";
3
+ export declare const DEFAULT_IMAGE_REGISTRY = "docker.io";
4
+ export declare const SUPABASE_POSTGRES_IMAGE_REPOSITORY = "rulebricks/supabase-postgres";
5
+ export declare const SUPABASE_POSTGRES_IMAGE_TAG = "17.6.1.141";
6
+ export declare const RCLONE_IMAGE = "docker.io/rulebricks/rclone:1.71.1";
7
+ export declare const KAFKA_PROXY_IMAGE = "docker.io/rulebricks/kafka-proxy:0.4.3";
8
+ /**
9
+ * Repository defaults (registry + repository + tag) for the rulebricks/* images
10
+ * the CLI sets directly: the app stack, clickstack, the kafka-proxy bridge, and
11
+ * the six Tier-2 upstream charts. The registry HOST is overridable via
12
+ * config.imageRegistry; the repository path (rulebricks/<name>) is never changed.
13
+ * Tags mirror images/manifest.yaml in the helm repo.
14
+ */
15
+ export declare const IMAGE_REPOSITORIES: {
16
+ readonly app: "rulebricks/app";
17
+ readonly hps: "rulebricks/hps";
18
+ readonly hyperdx: {
19
+ readonly repository: "rulebricks/hyperdx";
20
+ readonly tag: "2.19.0";
21
+ };
22
+ readonly clickstackOtelCollector: {
23
+ readonly repository: "rulebricks/clickstack-otel-collector";
24
+ readonly tag: "2.19.0";
25
+ };
26
+ readonly ferretdb: {
27
+ readonly repository: "rulebricks/ferretdb";
28
+ readonly tag: "2.7.0";
29
+ };
30
+ readonly postgresDocumentdb: {
31
+ readonly repository: "rulebricks/postgres-documentdb";
32
+ readonly tag: "17-0.107.0-ferretdb-2.7.0";
33
+ };
34
+ readonly opentelemetryCollector: {
35
+ readonly repository: "rulebricks/opentelemetry-collector";
36
+ readonly tag: "0.155.0-debian13-contrib";
37
+ };
38
+ readonly kafkaProxy: {
39
+ readonly repository: "rulebricks/kafka-proxy";
40
+ readonly tag: "v0.4.3";
41
+ };
42
+ readonly supabasePostgres: {
43
+ readonly repository: "rulebricks/supabase-postgres";
44
+ readonly tag: "17.6.1.141";
45
+ };
46
+ readonly prometheus: "rulebricks/prometheus";
47
+ readonly alertmanager: "rulebricks/alertmanager";
48
+ readonly prometheusOperator: "rulebricks/prometheus-operator";
49
+ readonly prometheusConfigReloader: "rulebricks/prometheus-config-reloader";
50
+ readonly kubeWebhookCertgen: "rulebricks/kube-webhook-certgen";
51
+ readonly grafana: "rulebricks/grafana";
52
+ readonly k8sSidecar: "rulebricks/k8s-sidecar";
53
+ readonly kubeStateMetrics: "rulebricks/kube-state-metrics";
54
+ readonly nodeExporter: "rulebricks/node-exporter";
55
+ readonly certManagerController: "rulebricks/cert-manager-controller";
56
+ readonly certManagerWebhook: "rulebricks/cert-manager-webhook";
57
+ readonly certManagerCainjector: "rulebricks/cert-manager-cainjector";
58
+ readonly certManagerStartupapicheck: "rulebricks/cert-manager-startupapicheck";
59
+ readonly certManagerAcmesolver: "rulebricks/cert-manager-acmesolver";
60
+ readonly traefik: "rulebricks/traefik";
61
+ readonly keda: "rulebricks/keda";
62
+ readonly kedaMetricsApiServer: "rulebricks/keda-metrics-apiserver";
63
+ readonly kedaAdmissionWebhooks: "rulebricks/keda-admission-webhooks";
64
+ readonly vector: "rulebricks/vector";
65
+ readonly externalDns: "rulebricks/external-dns";
66
+ };
67
+ /**
68
+ * Generated name -> sha256 digest map. Populated by the helm repo's
69
+ * scripts/images/render-digests.sh (from images/digests.json); this const is
70
+ * regenerated by that script. Empty until the mirror pipeline runs. When a name
71
+ * is present, the chart image helper pins @sha256 instead of :tag, and the CLI
72
+ * threads this through global.imageDigests so the Tier-2 charts pin too.
73
+ */
74
+ export declare const IMAGE_DIGESTS: Record<string, string>;
3
75
  /**
4
76
  * Gets version information for display (legacy chart-based)
5
77
  */
@@ -11,7 +83,7 @@ export interface VersionInfo {
11
83
  changelogUrl: string;
12
84
  }
13
85
  /**
14
- * App version information with matched HPS version
86
+ * Product version information
15
87
  */
16
88
  export interface AppVersionInfo {
17
89
  current: AppVersion | null;
@@ -20,28 +92,27 @@ export interface AppVersionInfo {
20
92
  hasUpdate: boolean;
21
93
  changelogUrl: string;
22
94
  }
95
+ export declare function hasRegistryDigestMismatch(deployedDigests: string[], registryDigests?: string[]): boolean;
23
96
  /**
24
97
  * Fetches complete version information (legacy chart-based)
25
98
  */
26
99
  export declare function getVersionInfo(deploymentName: string, overrideNamespace?: string): Promise<VersionInfo>;
27
100
  /**
28
- * Matches HPS versions to app versions based on release dates.
29
- * For each app version, finds the latest HPS version released on or before that date.
30
- * Compares dates only (ignoring time), so an HPS released later in the same day
31
- * as the app version will still be matched.
101
+ * Matches app versions to exact HPS server and worker versions.
32
102
  *
33
103
  * @param appTags - Array of app image tags
34
104
  * @param hpsTags - Array of HPS image tags
35
- * @returns Array of AppVersion with matched HPS versions
105
+ * @param hpsWorkerTags - Array of HPS worker image tags
106
+ * @returns Array of product versions with app, HPS, and worker images available
36
107
  */
37
- export declare function matchHpsVersions(appTags: ImageTag[], hpsTags: ImageTag[]): AppVersion[];
108
+ export declare function matchExactHpsVersions(appTags: ImageTag[], hpsTags: ImageTag[], hpsWorkerTags: ImageTag[], architecture?: NodeArchitecture): AppVersion[];
38
109
  /**
39
- * Fetches app versions with matched HPS versions from Docker Hub
110
+ * Fetches product versions with app, HPS, and worker images from Docker Hub
40
111
  *
41
112
  * @param licenseKey - The Rulebricks license key (Docker PAT)
42
113
  * @returns Array of AppVersion objects
43
114
  */
44
- export declare function fetchAppVersions(licenseKey: string): Promise<AppVersion[]>;
115
+ export declare function fetchAppVersions(licenseKey: string, architecture?: NodeArchitecture): Promise<AppVersion[]>;
45
116
  /**
46
117
  * Gets complete app version information for a deployment
47
118
  *
@@ -49,7 +120,7 @@ export declare function fetchAppVersions(licenseKey: string): Promise<AppVersion
49
120
  * @param currentAppVersion - Currently installed app version (if known)
50
121
  * @returns AppVersionInfo with current, latest, and available versions
51
122
  */
52
- export declare function getAppVersionInfo(licenseKey: string, currentAppVersion?: string | null): Promise<AppVersionInfo>;
123
+ export declare function getAppVersionInfo(licenseKey: string, currentAppVersion?: string | null, architecture?: NodeArchitecture): Promise<AppVersionInfo>;
53
124
  /**
54
125
  * Formats a version for display
55
126
  */