@rulebricks/cli 2.3.1 → 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.
- package/dist/lib/helmValues.js +83 -4
- package/dist/lib/helmValues.test.js +63 -17
- package/package.json +1 -1
package/dist/lib/helmValues.js
CHANGED
|
@@ -8,8 +8,15 @@ import YAML from "yaml";
|
|
|
8
8
|
// Names of the Kubernetes Secrets the CLI creates in k8s secret mode. Shared by
|
|
9
9
|
// the value generator (which sets the secretRef fields) and src/lib/secrets.ts
|
|
10
10
|
// (which creates the Secrets) so they always agree.
|
|
11
|
+
//
|
|
12
|
+
// The base MUST be the Helm release name, not config.name. Most chart consumers
|
|
13
|
+
// read the secretRef *value* (name-agnostic), but a few templates hardcode the
|
|
14
|
+
// canonical <release>-* name — e.g. templates/migration-job.yaml derives
|
|
15
|
+
// DB_PASSWORD from `{{ .Release.Name }}-supabase-db`. Naming these secrets with
|
|
16
|
+
// the release name keeps the CLI a faithful drop-in for the unmodified chart so
|
|
17
|
+
// we never have to customize the chart to match the CLI.
|
|
11
18
|
export function deploymentSecretNames(config) {
|
|
12
|
-
const base = config.name;
|
|
19
|
+
const base = getReleaseName(config.name);
|
|
13
20
|
return {
|
|
14
21
|
app: `${base}-app-secrets`,
|
|
15
22
|
db: `${base}-supabase-db`,
|
|
@@ -2075,7 +2082,19 @@ export function buildHelmValues(config, options = {}) {
|
|
|
2075
2082
|
ingress: {
|
|
2076
2083
|
enabled: true,
|
|
2077
2084
|
className: "traefik",
|
|
2078
|
-
|
|
2085
|
+
// The supabase subchart's kong ingress does NOT emit Traefik's
|
|
2086
|
+
// router.entrypoints/router.tls annotations the way the app
|
|
2087
|
+
// ingress does — without them Traefik only builds a web (HTTP)
|
|
2088
|
+
// router, so https://supabase.<domain> 404s and the app can't
|
|
2089
|
+
// reach Supabase. Inject them via the subchart's annotations
|
|
2090
|
+
// passthrough (kong/ingress.yaml ranges over these), matching
|
|
2091
|
+
// charts/rulebricks/templates/ingress.yaml.
|
|
2092
|
+
annotations: {
|
|
2093
|
+
"traefik.ingress.kubernetes.io/router.entrypoints": tlsEnabled ? "websecure" : "web",
|
|
2094
|
+
"traefik.ingress.kubernetes.io/router.tls": tlsEnabled
|
|
2095
|
+
? "true"
|
|
2096
|
+
: "false",
|
|
2097
|
+
},
|
|
2079
2098
|
},
|
|
2080
2099
|
},
|
|
2081
2100
|
studio: {
|
|
@@ -2247,6 +2266,35 @@ export function buildHelmValues(config, options = {}) {
|
|
|
2247
2266
|
enabled: false,
|
|
2248
2267
|
},
|
|
2249
2268
|
};
|
|
2269
|
+
// The managed-Postgres migration hook (templates/migration-job.yaml) reads the
|
|
2270
|
+
// DB host/port from .Values.migrations.externalDb — a SEPARATE seam from
|
|
2271
|
+
// supabase.externalDatabase.* — and its `pg_isready -h $DB_HOST` loop hangs
|
|
2272
|
+
// forever (empty host) if it is unset. Wire it for external Postgres. We only
|
|
2273
|
+
// set host/port: DB_PASSWORD falls back to the <release>-supabase-db secret and
|
|
2274
|
+
// DB_USER/DB_NAME default to "postgres", which match deploymentSecretNames()
|
|
2275
|
+
// and the bootstrap app role.
|
|
2276
|
+
const migrationsPgExt = config.database.type === "self-hosted" &&
|
|
2277
|
+
config.externalServices?.postgres?.mode === "external"
|
|
2278
|
+
? config.externalServices.postgres.external
|
|
2279
|
+
: undefined;
|
|
2280
|
+
if (migrationsPgExt) {
|
|
2281
|
+
values.migrations = {
|
|
2282
|
+
externalDb: {
|
|
2283
|
+
host: migrationsPgExt.host ?? "",
|
|
2284
|
+
// Chart schema requires a string here (the template quotes it).
|
|
2285
|
+
port: String(migrationsPgExt.port ?? 5432),
|
|
2286
|
+
// Run migrations as the master/app_role. The bootstrap hook creates the
|
|
2287
|
+
// service login roles (authenticator, supabase_auth_admin, …) with the
|
|
2288
|
+
// service password but deliberately does NOT change the master's
|
|
2289
|
+
// password (bootstrap.sql runs "as the master user (named postgres)").
|
|
2290
|
+
// So the migrate hook must authenticate with the MASTER credential, not
|
|
2291
|
+
// the service password in <release>-supabase-db (that would 401). Point
|
|
2292
|
+
// DB_PASSWORD at the bootstrap Secret's master-password.
|
|
2293
|
+
existingSecret: deploymentSecretNames(config).dbBootstrap,
|
|
2294
|
+
existingSecretKey: "master-password",
|
|
2295
|
+
},
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2250
2298
|
// In k8s secret mode, the CLI creates Kubernetes Secrets and the chart reads
|
|
2251
2299
|
// them by reference. Point the chart's secretRef seams at those Secrets and
|
|
2252
2300
|
// strip every plaintext secret out of the generated values.
|
|
@@ -2277,7 +2325,13 @@ export function redactSecretsToRefs(values, config) {
|
|
|
2277
2325
|
}
|
|
2278
2326
|
if (global.supabase) {
|
|
2279
2327
|
delete global.supabase.jwtSecret;
|
|
2280
|
-
|
|
2328
|
+
// NOTE: anonKey is intentionally NOT stripped. It is the *public* Supabase
|
|
2329
|
+
// key that app-configmap.yaml embeds into the Next.js client bundle
|
|
2330
|
+
// (SUPABASE_PUBLIC_KEY / NEXT_PUBLIC_SUPABASE_PUBLIC_KEY). That ConfigMap
|
|
2331
|
+
// reads global.supabase.anonKey at TEMPLATE time and there is no secretRef
|
|
2332
|
+
// seam for it, so stripping it leaves the browser client with an empty key.
|
|
2333
|
+
// It is a public token (safe in a ConfigMap by design) and never appears in
|
|
2334
|
+
// the k8s-mode secret-leak checks.
|
|
2281
2335
|
delete global.supabase.serviceKey;
|
|
2282
2336
|
delete global.supabase.accessToken;
|
|
2283
2337
|
}
|
|
@@ -2287,7 +2341,16 @@ export function redactSecretsToRefs(values, config) {
|
|
|
2287
2341
|
delete global.sso.clientId;
|
|
2288
2342
|
delete global.sso.clientSecret;
|
|
2289
2343
|
}
|
|
2290
|
-
|
|
2344
|
+
// NOTE: licenseKey is intentionally NOT stripped. The (standard) chart builds
|
|
2345
|
+
// the image-pull secret <release>-regcred from inline global.licenseKey at
|
|
2346
|
+
// TEMPLATE time (templates/registry-secret.yaml -> imagePullSecret helper). A
|
|
2347
|
+
// Kubernetes imagePullSecret cannot be sourced from a secretRef, so the chart
|
|
2348
|
+
// has no k8s-mode seam for it — stripping it makes the chart fall back to the
|
|
2349
|
+
// "evaluation" placeholder -> dckr_pat_evaluation -> 401 on every private
|
|
2350
|
+
// rulebricks/* image. Standalone chart users set global.licenseKey in their own
|
|
2351
|
+
// values for exactly this reason; the CLI must do the same to stay compatible
|
|
2352
|
+
// with the unmodified chart. It is a Docker Hub read-only PAT and already lives
|
|
2353
|
+
// in the deployment's config.yaml, so keeping it inline adds no new exposure.
|
|
2291
2354
|
// Supabase subchart: replace each inline secret block with a secretRef.
|
|
2292
2355
|
if (supabase.secret) {
|
|
2293
2356
|
const dbSecret = { secretRef: names.db };
|
|
@@ -2381,6 +2444,22 @@ export async function updateHelmValuesForTLS(deploymentName, tlsEnabled) {
|
|
|
2381
2444
|
}
|
|
2382
2445
|
}
|
|
2383
2446
|
}
|
|
2447
|
+
// Keep the supabase kong ingress on the right Traefik entrypoint. The
|
|
2448
|
+
// subchart doesn't emit router.entrypoints/tls itself, so on the TLS-toggle
|
|
2449
|
+
// path (not a full regen) HTTPS to supabase.<domain> would 404 without this.
|
|
2450
|
+
// Mirrors what buildHelmValues sets on the kong ingress annotations.
|
|
2451
|
+
const supabase = values.supabase;
|
|
2452
|
+
const kongIngress = supabase?.kong
|
|
2453
|
+
?.ingress;
|
|
2454
|
+
if (kongIngress && typeof kongIngress === "object") {
|
|
2455
|
+
kongIngress.annotations = {
|
|
2456
|
+
...kongIngress.annotations,
|
|
2457
|
+
"traefik.ingress.kubernetes.io/router.entrypoints": tlsEnabled
|
|
2458
|
+
? "websecure"
|
|
2459
|
+
: "web",
|
|
2460
|
+
"traefik.ingress.kubernetes.io/router.tls": tlsEnabled ? "true" : "false",
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2384
2463
|
// Save updated values
|
|
2385
2464
|
await fs.writeFile(valuesPath, YAML.stringify(values), "utf8");
|
|
2386
2465
|
}
|
|
@@ -723,7 +723,7 @@ test("external Postgres k8s secret mode keeps compatibility and uses secret refs
|
|
|
723
723
|
// the Secret keys below.
|
|
724
724
|
assert.equal(sb.externalDatabase.host, "db.cluster-xxxx.us-east-1.rds.amazonaws.com");
|
|
725
725
|
assert.equal(sb.externalDatabase.port, 5432);
|
|
726
|
-
assert.equal(sb.externalDatabase.secretRef, `${config.name}-supabase-db`);
|
|
726
|
+
assert.equal(sb.externalDatabase.secretRef, `${getReleaseName(config.name)}-supabase-db`);
|
|
727
727
|
assert.deepEqual(sb.externalDatabase.secretRefKey, {
|
|
728
728
|
host: "host",
|
|
729
729
|
port: "port",
|
|
@@ -731,9 +731,9 @@ test("external Postgres k8s secret mode keeps compatibility and uses secret refs
|
|
|
731
731
|
password: "password",
|
|
732
732
|
database: "database",
|
|
733
733
|
});
|
|
734
|
-
assert.equal(sb.externalDatabase.bootstrap.secretRef, `${config.name}-supabase-db-bootstrap`);
|
|
734
|
+
assert.equal(sb.externalDatabase.bootstrap.secretRef, `${getReleaseName(config.name)}-supabase-db-bootstrap`);
|
|
735
735
|
assert.equal(sb.externalDatabase.bootstrap.masterPassword, undefined);
|
|
736
|
-
assert.equal(sb.secret.db.secretRef, `${config.name}-supabase-db`);
|
|
736
|
+
assert.equal(sb.secret.db.secretRef, `${getReleaseName(config.name)}-supabase-db`);
|
|
737
737
|
assert.deepEqual(sb.secret.db.secretRefKey, {
|
|
738
738
|
host: "host",
|
|
739
739
|
port: "port",
|
|
@@ -750,7 +750,7 @@ test("embedded Postgres still deploys the bundled database", () => {
|
|
|
750
750
|
});
|
|
751
751
|
import { buildDeploymentSecrets } from "./secrets.js";
|
|
752
752
|
import { deriveRealtimeSecrets } from "./helmValues.js";
|
|
753
|
-
test("k8s secret mode: secretRefs set,
|
|
753
|
+
test("k8s secret mode: secretRefs set, app secrets kept out of values (license stays inline for the pull secret)", () => {
|
|
754
754
|
const config = cloneFixture("aws-self-hosted-minimal");
|
|
755
755
|
config.features.ai = {
|
|
756
756
|
enabled: true,
|
|
@@ -765,22 +765,27 @@ test("k8s secret mode: secretRefs set, zero plaintext secrets in values", () =>
|
|
|
765
765
|
const schemaResult = validateHelmValues(values);
|
|
766
766
|
assert.ok(schemaResult.valid, `k8s secret-mode values should satisfy chart schema:\n${schemaResult.errors.join("\n")}`);
|
|
767
767
|
// secretRef seams point at the CLI-created Secrets
|
|
768
|
-
assert.equal(values.global.secrets.secretRef, `${config.name}-app-secrets`);
|
|
769
|
-
assert.equal(values.supabase.secret.db.secretRef, `${config.name}-supabase-db`);
|
|
770
|
-
assert.equal(values.supabase.secret.jwt.secretRef, `${config.name}-supabase-jwt`);
|
|
771
|
-
assert.equal(values.supabase.secret.dashboard.secretRef, `${config.name}-supabase-dashboard`);
|
|
772
|
-
assert.equal(values.supabase.secret.realtime.secretRef, `${config.name}-supabase-realtime`);
|
|
773
|
-
//
|
|
768
|
+
assert.equal(values.global.secrets.secretRef, `${getReleaseName(config.name)}-app-secrets`);
|
|
769
|
+
assert.equal(values.supabase.secret.db.secretRef, `${getReleaseName(config.name)}-supabase-db`);
|
|
770
|
+
assert.equal(values.supabase.secret.jwt.secretRef, `${getReleaseName(config.name)}-supabase-jwt`);
|
|
771
|
+
assert.equal(values.supabase.secret.dashboard.secretRef, `${getReleaseName(config.name)}-supabase-dashboard`);
|
|
772
|
+
assert.equal(values.supabase.secret.realtime.secretRef, `${getReleaseName(config.name)}-supabase-realtime`);
|
|
773
|
+
// Genuinely-sensitive app secrets are stripped (delivered via secretRef).
|
|
774
774
|
assert.equal(values.global.supabase.jwtSecret, undefined);
|
|
775
775
|
assert.equal(values.global.ai.openaiApiKey, undefined);
|
|
776
|
-
|
|
777
|
-
//
|
|
776
|
+
// These two MUST stay inline: the standard (unmodified) chart consumes them at
|
|
777
|
+
// Helm TEMPLATE time with no secretRef seam.
|
|
778
|
+
// - licenseKey -> registry-secret.yaml builds the <release>-regcred pull
|
|
779
|
+
// secret. Stripping it => dckr_pat_evaluation => 401 on every private image.
|
|
780
|
+
// - anonKey (public) -> app-configmap.yaml NEXT_PUBLIC_SUPABASE_PUBLIC_KEY.
|
|
781
|
+
assert.equal(values.global.licenseKey, license);
|
|
782
|
+
assert.ok(values.global.supabase.anonKey, "public anonKey must remain inline in k8s mode for the app ConfigMap");
|
|
783
|
+
// The genuinely-sensitive app secrets never appear in the generated values.
|
|
778
784
|
const dump = JSON.stringify(values);
|
|
779
785
|
for (const [label, secret] of [
|
|
780
786
|
["db password", dbPw],
|
|
781
787
|
["jwt secret", jwt],
|
|
782
788
|
["dashboard password", dashPw],
|
|
783
|
-
["license key", license],
|
|
784
789
|
["openai key", openai],
|
|
785
790
|
]) {
|
|
786
791
|
assert.ok(!dump.includes(secret), `${label} leaked into k8s-mode values`);
|
|
@@ -797,7 +802,7 @@ test("k8s secret mode: SSO + AI configs validate against the chart schema", () =
|
|
|
797
802
|
assert.equal(values.global.sso.clientId, undefined);
|
|
798
803
|
assert.equal(values.global.sso.clientSecret, undefined);
|
|
799
804
|
assert.equal(values.global.ai.openaiApiKey, undefined);
|
|
800
|
-
assert.equal(values.global.secrets.secretRef, `${config.name}-app-secrets`);
|
|
805
|
+
assert.equal(values.global.secrets.secretRef, `${getReleaseName(config.name)}-app-secrets`);
|
|
801
806
|
});
|
|
802
807
|
test("k8s secret mode: managed Supabase config validates against the chart schema", () => {
|
|
803
808
|
// Managed (Supabase Cloud) redacts the access token into the app Secret; the
|
|
@@ -808,7 +813,7 @@ test("k8s secret mode: managed Supabase config validates against the chart schem
|
|
|
808
813
|
assert.ok(result.valid, `k8s-mode managed-Supabase values should satisfy the chart schema:\n${result.errors.join("\n")}`);
|
|
809
814
|
assert.equal(values.global.supabase.accessToken, undefined);
|
|
810
815
|
assert.ok(values.global.supabase.url);
|
|
811
|
-
assert.equal(values.global.secrets.secretRef, `${config.name}-app-secrets`);
|
|
816
|
+
assert.equal(values.global.secrets.secretRef, `${getReleaseName(config.name)}-app-secrets`);
|
|
812
817
|
});
|
|
813
818
|
test("inline secret mode keeps secrets in values (dev path)", () => {
|
|
814
819
|
const config = cloneFixture("aws-self-hosted-minimal");
|
|
@@ -824,7 +829,7 @@ test("buildDeploymentSecrets: app + supabase secrets with JWT-derived keys", ()
|
|
|
824
829
|
const config = cloneFixture("aws-self-hosted-minimal");
|
|
825
830
|
const jwt = config.database.supabaseJwtSecret;
|
|
826
831
|
const byName = Object.fromEntries(buildDeploymentSecrets(config).map((s) => [s.name, s.stringData]));
|
|
827
|
-
const base = config.name;
|
|
832
|
+
const base = getReleaseName(config.name);
|
|
828
833
|
assert.equal(byName[`${base}-app-secrets`].LICENSE_KEY, config.licenseKey);
|
|
829
834
|
assert.equal(byName[`${base}-supabase-db`].password, config.database.supabaseDbPassword);
|
|
830
835
|
assert.equal(byName[`${base}-supabase-jwt`].anonKey, signSupabaseJwt("anon", jwt));
|
|
@@ -837,7 +842,7 @@ test("buildDeploymentSecrets: app + supabase secrets with JWT-derived keys", ()
|
|
|
837
842
|
test("buildDeploymentSecrets includes external Postgres host/port and bootstrap creds", () => {
|
|
838
843
|
const config = cloneFixture("aws-external-postgres");
|
|
839
844
|
const byName = Object.fromEntries(buildDeploymentSecrets(config).map((s) => [s.name, s.stringData]));
|
|
840
|
-
const base = config.name;
|
|
845
|
+
const base = getReleaseName(config.name);
|
|
841
846
|
assert.deepEqual(byName[`${base}-supabase-db`], {
|
|
842
847
|
username: "postgres",
|
|
843
848
|
password: config.database.supabaseDbPassword,
|
|
@@ -851,6 +856,47 @@ test("buildDeploymentSecrets includes external Postgres host/port and bootstrap
|
|
|
851
856
|
"service-password": config.database.supabaseDbPassword,
|
|
852
857
|
});
|
|
853
858
|
});
|
|
859
|
+
test("external Postgres wires migrations.externalDb host/port for the migration hook", () => {
|
|
860
|
+
// templates/migration-job.yaml reads DB_HOST from .Values.migrations.externalDb
|
|
861
|
+
// (not supabase.externalDatabase). If unset, pg_isready gets an empty host and
|
|
862
|
+
// the migrate hook hangs until Helm times out. Guards that regression.
|
|
863
|
+
const config = cloneFixture("aws-external-postgres");
|
|
864
|
+
const values = buildHelmValues(config, { secretMode: "k8s" });
|
|
865
|
+
assert.ok(values.migrations?.externalDb, "migrations.externalDb must be set for external Postgres");
|
|
866
|
+
assert.ok(values.migrations.externalDb.host, "migration-hook DB_HOST must be non-empty");
|
|
867
|
+
assert.equal(values.migrations.externalDb.host, values.supabase.externalDatabase.host);
|
|
868
|
+
assert.equal(values.migrations.externalDb.port, "5432");
|
|
869
|
+
// Migrations run as the master (bootstrap only sets service-role passwords, not
|
|
870
|
+
// the master's), so DB_PASSWORD must come from the bootstrap Secret's
|
|
871
|
+
// master-password, NOT the service password in <release>-supabase-db.
|
|
872
|
+
assert.equal(values.migrations.externalDb.existingSecret, `${getReleaseName(config.name)}-supabase-db-bootstrap`);
|
|
873
|
+
assert.equal(values.migrations.externalDb.existingSecretKey, "master-password");
|
|
874
|
+
// Bundled-Postgres deploys must NOT set it (chart uses the internal service).
|
|
875
|
+
const internal = buildHelmValues(cloneFixture("aws-self-hosted-minimal"), {
|
|
876
|
+
secretMode: "k8s",
|
|
877
|
+
});
|
|
878
|
+
assert.equal(internal.migrations?.externalDb, undefined);
|
|
879
|
+
});
|
|
880
|
+
test("supabase kong ingress carries Traefik websecure router annotations under TLS", () => {
|
|
881
|
+
// The supabase subchart's kong ingress doesn't emit router.entrypoints/tls
|
|
882
|
+
// itself, so Traefik only builds a web router and https://supabase.<domain>
|
|
883
|
+
// 404s. The CLI must inject them (via the subchart's annotations passthrough).
|
|
884
|
+
const config = cloneFixture("aws-self-hosted-minimal");
|
|
885
|
+
const tls = buildHelmValues(config, {
|
|
886
|
+
tlsEnabled: true,
|
|
887
|
+
secretMode: "k8s",
|
|
888
|
+
});
|
|
889
|
+
const a = tls.supabase.kong.ingress.annotations;
|
|
890
|
+
assert.equal(a["traefik.ingress.kubernetes.io/router.entrypoints"], "websecure");
|
|
891
|
+
assert.equal(a["traefik.ingress.kubernetes.io/router.tls"], "true");
|
|
892
|
+
const notls = buildHelmValues(config, {
|
|
893
|
+
tlsEnabled: false,
|
|
894
|
+
secretMode: "k8s",
|
|
895
|
+
});
|
|
896
|
+
const b = notls.supabase.kong.ingress.annotations;
|
|
897
|
+
assert.equal(b["traefik.ingress.kubernetes.io/router.entrypoints"], "web");
|
|
898
|
+
assert.equal(b["traefik.ingress.kubernetes.io/router.tls"], "false");
|
|
899
|
+
});
|
|
854
900
|
// ===========================================================================
|
|
855
901
|
// Image registry / digest pinning (docker.io/rulebricks/* + global.imageRegistry)
|
|
856
902
|
// ===========================================================================
|