@rulebricks/cli 2.1.6 → 2.3.1
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 +75 -14
- package/cluster-setup/aws/README.md +123 -0
- package/cluster-setup/aws/check-aws-access.sh +242 -0
- package/cluster-setup/aws/parameters.json +13 -0
- package/cluster-setup/aws/rulebricks-cluster.cfn.yaml +355 -0
- package/cluster-setup/azure/README.md +141 -0
- package/cluster-setup/azure/check-aks-prereqs.sh +276 -0
- package/cluster-setup/azure/parameters.json +30 -0
- package/cluster-setup/azure/rulebricks-cluster.bicep +546 -0
- package/cluster-setup/gcp/README.md +189 -0
- package/cluster-setup/gcp/check-gke-prereqs.sh +260 -0
- package/dist/commands/backup.d.ts +5 -0
- package/dist/commands/backup.js +104 -0
- package/dist/commands/deploy.d.ts +3 -1
- package/dist/commands/deploy.js +226 -326
- package/dist/commands/destroy.d.ts +1 -1
- package/dist/commands/destroy.js +73 -123
- package/dist/commands/init.d.ts +5 -1
- package/dist/commands/init.js +78 -47
- package/dist/commands/list.d.ts +1 -0
- package/dist/commands/list.js +74 -0
- package/dist/commands/open.d.ts +1 -1
- package/dist/commands/open.js +4 -12
- package/dist/commands/redeploy.d.ts +6 -0
- package/dist/commands/redeploy.js +310 -0
- package/dist/commands/restore.d.ts +5 -0
- package/dist/commands/restore.js +338 -0
- package/dist/commands/status.js +62 -49
- package/dist/commands/upgrade.js +74 -51
- package/dist/components/DNSWaitScreen.d.ts +5 -1
- package/dist/components/DNSWaitScreen.js +47 -41
- package/dist/components/Wizard/WizardContext.d.ts +174 -29
- package/dist/components/Wizard/WizardContext.js +896 -91
- package/dist/components/Wizard/steps/CloudProviderStep.js +192 -102
- package/dist/components/Wizard/steps/DomainStep.js +5 -24
- package/dist/components/Wizard/steps/ExternalServicesStep.d.ts +6 -0
- package/dist/components/Wizard/steps/ExternalServicesStep.js +645 -0
- package/dist/components/Wizard/steps/FeatureConfigStep.d.ts +2 -1
- package/dist/components/Wizard/steps/FeatureConfigStep.js +959 -248
- package/dist/components/Wizard/steps/FeaturesStep.js +31 -35
- package/dist/components/Wizard/steps/ObservabilityStep.d.ts +6 -0
- package/dist/components/Wizard/steps/ObservabilityStep.js +137 -0
- package/dist/components/Wizard/steps/ReviewStep.d.ts +2 -1
- package/dist/components/Wizard/steps/ReviewStep.js +56 -7
- package/dist/components/Wizard/steps/StorageStep.d.ts +9 -0
- package/dist/components/Wizard/steps/StorageStep.js +592 -0
- package/dist/components/Wizard/steps/SupabaseCredentialsStep.js +20 -21
- package/dist/components/Wizard/steps/VersionStep.js +45 -23
- package/dist/components/Wizard/steps/index.d.ts +3 -3
- package/dist/components/Wizard/steps/index.js +3 -3
- package/dist/components/common/CommandApproval.d.ts +12 -0
- package/dist/components/common/CommandApproval.js +91 -0
- package/dist/components/common/DeploymentPicker.d.ts +14 -0
- package/dist/components/common/DeploymentPicker.js +16 -0
- package/dist/components/common/index.d.ts +2 -0
- package/dist/components/common/index.js +2 -0
- package/dist/index.js +94 -62
- package/dist/lib/cloudCli.d.ts +134 -63
- package/dist/lib/cloudCli.js +512 -220
- package/dist/lib/clusterSetupDefaults.d.ts +30 -0
- package/dist/lib/clusterSetupDefaults.js +64 -0
- package/dist/lib/commandApproval.d.ts +26 -0
- package/dist/lib/commandApproval.js +114 -0
- package/dist/lib/config.d.ts +12 -10
- package/dist/lib/config.js +91 -33
- package/dist/lib/configFixtures.d.ts +5 -0
- package/dist/lib/configFixtures.js +513 -0
- package/dist/lib/deploymentHealth.d.ts +32 -0
- package/dist/lib/deploymentHealth.js +157 -0
- package/dist/lib/dns.d.ts +1 -1
- package/dist/lib/dns.js +19 -1
- package/dist/lib/dns.test.d.ts +1 -0
- package/dist/lib/dns.test.js +27 -0
- package/dist/lib/dockerHub.d.ts +12 -1
- package/dist/lib/dockerHub.js +18 -8
- package/dist/lib/helm.d.ts +4 -0
- package/dist/lib/helm.js +16 -0
- package/dist/lib/helmValues.d.ts +25 -0
- package/dist/lib/helmValues.js +1937 -259
- package/dist/lib/helmValues.test.d.ts +1 -0
- package/dist/lib/helmValues.test.js +966 -0
- package/dist/lib/htpasswd.d.ts +1 -0
- package/dist/lib/htpasswd.js +15 -0
- package/dist/lib/kubernetes.d.ts +126 -13
- package/dist/lib/kubernetes.js +624 -134
- package/dist/lib/secrets.d.ts +23 -0
- package/dist/lib/secrets.js +158 -0
- package/dist/lib/validateValues.d.ts +31 -0
- package/dist/lib/validateValues.js +253 -0
- package/dist/lib/versions.d.ts +82 -11
- package/dist/lib/versions.js +131 -31
- package/dist/lib/versions.test.d.ts +1 -0
- package/dist/lib/versions.test.js +81 -0
- package/dist/lib/wizardSteps.d.ts +14 -0
- package/dist/lib/wizardSteps.js +23 -0
- package/dist/lib/workloadIdentity.d.ts +26 -0
- package/dist/lib/workloadIdentity.js +323 -0
- package/dist/lib/workloadIdentity.test.d.ts +1 -0
- package/dist/lib/workloadIdentity.test.js +57 -0
- package/dist/types/index.d.ts +2152 -95
- package/dist/types/index.js +554 -286
- package/package.json +10 -4
- package/schema/values.schema.json +1934 -0
- package/dist/components/Wizard/steps/CredentialsStep.d.ts +0 -6
- package/dist/components/Wizard/steps/CredentialsStep.js +0 -22
- package/dist/components/Wizard/steps/DeploymentModeStep.d.ts +0 -5
- package/dist/components/Wizard/steps/DeploymentModeStep.js +0 -26
- package/dist/components/Wizard/steps/TierStep.d.ts +0 -6
- package/dist/components/Wizard/steps/TierStep.js +0 -29
- package/dist/lib/terraform.d.ts +0 -66
- package/dist/lib/terraform.js +0 -754
- package/terraform/aws/main.tf +0 -355
- package/terraform/azure/main.tf +0 -371
- 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
|
+
}
|
package/dist/lib/versions.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
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
|
-
* @
|
|
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
|
|
108
|
+
export declare function matchExactHpsVersions(appTags: ImageTag[], hpsTags: ImageTag[], hpsWorkerTags: ImageTag[], architecture?: NodeArchitecture): AppVersion[];
|
|
38
109
|
/**
|
|
39
|
-
* Fetches
|
|
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
|
*/
|