@fjall/components-infrastructure 1.1.0 → 2.1.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.
@@ -2,7 +2,7 @@ import { Connections, type IConnectable, type IVpc } from "aws-cdk-lib/aws-ec2";
2
2
  import { type TaskDefinition } from "aws-cdk-lib/aws-ecs";
3
3
  import { type IBucket } from "aws-cdk-lib/aws-s3";
4
4
  import { Construct } from "constructs";
5
- import { Secret } from "../../resources/aws/secrets/secret.js";
5
+ import { Secret, type SecretImport } from "../../resources/aws/secrets/secret.js";
6
6
  import { type ClickHouseSchemaAdmin, type ProfileSpec } from "../../resources/aws/database/clickhouseSchemas.js";
7
7
  import { type ISecret } from "aws-cdk-lib/aws-secretsmanager";
8
8
  import type { ClickHouseTlsOptions } from "./clickhouseTls/index.js";
@@ -174,8 +174,14 @@ export declare class ClickHouseDatabase extends Construct implements IClickHouse
174
174
  * Port matches: HTTPS (8443) under TLS, HTTP (8123) otherwise.
175
175
  */
176
176
  getUrl(): string;
177
- /** @deprecated Alias for {@link getUrl}. Removed in a follow-up landing. */
178
- getHttpUrl(): string;
177
+ /**
178
+ * SecretImport for the CA certificate secret, or `undefined` when TLS is
179
+ * disabled (`tls: { mode: "none", … }`). Consumers wire this into their
180
+ * `secretsImport:` block under `CLICKHOUSE_CA_CERT` so `@fjall/clickhouse`
181
+ * `createFjallClickHouseClient()` picks it up from `process.env`. The secret
182
+ * holds raw PEM (no JSON field) so `field` is `undefined`.
183
+ */
184
+ getTlsCaCertImport(): SecretImport | undefined;
179
185
  getNativeUrl(): string;
180
186
  getDatabaseName(): string;
181
187
  getBackupBucket(): IBucket;
@@ -294,6 +294,15 @@ export class ClickHouseDatabase extends Construct {
294
294
  ...OPTIMISE_MV_TABLES.map((table) => `OPTIMIZE TABLE ${CLICKHOUSE_DATABASE_NAME}.${table}`)
295
295
  ].join("; ");
296
296
  const adminSecret = expectDefined(userSecrets.get(schemaAdmin.name), `schemaAdmin '${schemaAdmin.name}' secret not minted.`);
297
+ const sidecarTlsPreamble = tlsActive
298
+ ? `set -eu && printf '%s\\n' "$CLICKHOUSE_CA_CERT" > /tmp/ca.crt && printf '%s' '<?xml version="1.0"?><config><openSSL><client><caConfig>/tmp/ca.crt</caConfig><verificationMode>strict</verificationMode><loadDefaultCAFile>false</loadDefaultCAFile><invalidCertificateHandler><name>RejectCertificateHandler</name></invalidCertificateHandler></client></openSSL></config>' > /tmp/clickhouse-client.xml && `
299
+ : "";
300
+ const sidecarTlsClientArgs = tlsActive
301
+ ? " --config-file=/tmp/clickhouse-client.xml --secure"
302
+ : "";
303
+ const sidecarTlsSecrets = tlsActive && tlsCaSecret !== undefined
304
+ ? { CLICKHOUSE_CA_CERT: EcsSecret.fromSecretsManager(tlsCaSecret) }
305
+ : {};
297
306
  const scheduledTasks = [];
298
307
  if (optimiseEnabled) {
299
308
  scheduledTasks.push({
@@ -303,19 +312,13 @@ export class ClickHouseDatabase extends Construct {
303
312
  cpu: OPTIMISE_TASK_CPU_UNITS,
304
313
  memoryLimitMiB: OPTIMISE_TASK_MEMORY_MIB,
305
314
  command: [
306
- "clickhouse-client",
307
- "--host",
308
- clickHouseHost,
309
- "--port",
310
- String(nativePort),
311
- "--user",
312
- schemaAdmin.name,
313
- ...(tlsActive ? ["--secure", "--accept-invalid-certificate"] : []),
314
- "--query",
315
- `${optimiseQuery};`
315
+ "sh",
316
+ "-c",
317
+ `${sidecarTlsPreamble}clickhouse-client --host ${clickHouseHost} --port ${nativePort} --user ${schemaAdmin.name}${sidecarTlsClientArgs} --query "${optimiseQuery};"`
316
318
  ],
317
319
  secrets: {
318
- CLICKHOUSE_PASSWORD: EcsSecret.fromSecretsManager(adminSecret.secret, "password")
320
+ CLICKHOUSE_PASSWORD: EcsSecret.fromSecretsManager(adminSecret.secret, "password"),
321
+ ...sidecarTlsSecrets
319
322
  },
320
323
  logRetention: RetentionDays.ONE_WEEK,
321
324
  securityGroups: [securityGroup]
@@ -332,10 +335,11 @@ export class ClickHouseDatabase extends Construct {
332
335
  "sh",
333
336
  "-c",
334
337
  // Password via CLICKHOUSE_PASSWORD env, not --password on argv (argv → /proc/<pid>/cmdline).
335
- `STAMP=$(date +%Y%m%d-%H%M%S) && clickhouse-client --host ${clickHouseHost} --port ${nativePort} --user ${schemaAdmin.name}${tlsActive ? " --secure --accept-invalid-certificate" : ""} --query "BACKUP DATABASE ${CLICKHOUSE_DATABASE_NAME} TO S3('${backupDestUrl}weekly-$STAMP/')"`
338
+ `${sidecarTlsPreamble}STAMP=$(date +%Y%m%d-%H%M%S) && clickhouse-client --host ${clickHouseHost} --port ${nativePort} --user ${schemaAdmin.name}${sidecarTlsClientArgs} --query "BACKUP DATABASE ${CLICKHOUSE_DATABASE_NAME} TO S3('${backupDestUrl}weekly-$STAMP/')"`
336
339
  ],
337
340
  secrets: {
338
- CLICKHOUSE_PASSWORD: EcsSecret.fromSecretsManager(adminSecret.secret, "password")
341
+ CLICKHOUSE_PASSWORD: EcsSecret.fromSecretsManager(adminSecret.secret, "password"),
342
+ ...sidecarTlsSecrets
339
343
  },
340
344
  logGroup: backupTaskLogGroup,
341
345
  securityGroups: [securityGroup]
@@ -445,7 +449,7 @@ export class ClickHouseDatabase extends Construct {
445
449
  command: [
446
450
  "CMD-SHELL",
447
451
  tlsActive
448
- ? `wget -q --no-check-certificate -O /dev/null https://127.0.0.1:${httpPort}/ping || exit 1`
452
+ ? `wget -q --ca-certificate=${CLICKHOUSE_TLS_CERT_MOUNT_PATH}/ca.crt -O /dev/null https://127.0.0.1:${httpPort}/ping || exit 1`
449
453
  : `wget -q -O /dev/null http://127.0.0.1:${httpPort}/ping || exit 1`
450
454
  ],
451
455
  interval: CLICKHOUSE_HEALTH_CHECK.INTERVAL_SECONDS,
@@ -489,7 +493,7 @@ export class ClickHouseDatabase extends Construct {
489
493
  `ADMIN_PASSWORD=$(aws secretsmanager get-secret-value --secret-id "${adminSecretName}" --query SecretString --output text | jq -r .password)`,
490
494
  `if [ -z "$ADMIN_PASSWORD" ] || [ "$ADMIN_PASSWORD" = "null" ]; then echo "fjall:reload:status=failed reason=password-fetch" >&2; exit 1; fi`,
491
495
  `mv /var/lib/clickhouse/users.d/fjall.xml.new /var/lib/clickhouse/users.d/fjall.xml`,
492
- `docker exec -i -e CLICKHOUSE_CLIENT_PASSWORD="$ADMIN_PASSWORD" "$CONTAINER" clickhouse-client${tlsActive ? " --secure --accept-invalid-certificate" : ""} --port ${nativePort} --user ${schemaAdmin.name} -q "SYSTEM RELOAD USERS"`
496
+ `docker exec -i -e CLICKHOUSE_CLIENT_PASSWORD="$ADMIN_PASSWORD" "$CONTAINER" clickhouse-client${tlsActive ? ` --config-file=${CLICKHOUSE_TLS_CERT_MOUNT_PATH}/client.xml --secure --host localhost` : ""} --port ${nativePort} --user ${schemaAdmin.name} -q "SYSTEM RELOAD USERS"`
493
497
  ].join("\n");
494
498
  const region = Stack.of(this).region;
495
499
  const account = Stack.of(this).account;
@@ -605,9 +609,21 @@ export class ClickHouseDatabase extends Construct {
605
609
  const scheme = this.tlsCaSecret !== undefined ? "https" : "http";
606
610
  return `${scheme}://${this.getHostEndpoint()}:${this.#httpPort}`;
607
611
  }
608
- /** @deprecated Alias for {@link getUrl}. Removed in a follow-up landing. */
609
- getHttpUrl() {
610
- return this.getUrl();
612
+ /**
613
+ * SecretImport for the CA certificate secret, or `undefined` when TLS is
614
+ * disabled (`tls: { mode: "none", … }`). Consumers wire this into their
615
+ * `secretsImport:` block under `CLICKHOUSE_CA_CERT` so `@fjall/clickhouse`
616
+ * `createFjallClickHouseClient()` picks it up from `process.env`. The secret
617
+ * holds raw PEM (no JSON field) so `field` is `undefined`.
618
+ */
619
+ getTlsCaCertImport() {
620
+ if (this.tlsCaSecret === undefined)
621
+ return undefined;
622
+ return {
623
+ id: `${this.node.id}TlsCaCertImport`,
624
+ name: this.tlsCaSecret.secretName,
625
+ field: undefined
626
+ };
611
627
  }
612
628
  getNativeUrl() {
613
629
  return `tcp://${this.getHostEndpoint()}:${this.#nativePort}`;
@@ -662,12 +678,19 @@ export class ClickHouseDatabase extends Construct {
662
678
  */
663
679
  getMigrationContributions() {
664
680
  const environment = {
665
- CLICKHOUSE_URL: this.getHttpUrl(),
681
+ CLICKHOUSE_URL: this.getUrl(),
666
682
  CLICKHOUSE_DATABASE: this.getDatabaseName()
667
683
  };
668
684
  const secretsImport = {};
669
685
  const adminSecret = this.getUser(this.#schemaAdmin.name);
670
686
  secretsImport.SCHEMA_ADMIN_PASSWORD = adminSecret.getImport("password");
687
+ const caCertImport = this.getTlsCaCertImport();
688
+ if (caCertImport !== undefined) {
689
+ secretsImport.CLICKHOUSE_CA_CERT = caCertImport;
690
+ }
691
+ if (this.caCertSha256 !== undefined) {
692
+ environment.CLICKHOUSE_CA_CERT_SHA256 = this.caCertSha256;
693
+ }
671
694
  for (const name of this.#managedPasswordNames) {
672
695
  const userSecret = this.getUser(name);
673
696
  secretsImport[userPasswordEnvName(name)] =
@@ -707,7 +730,7 @@ export class ClickHouseDatabase extends Construct {
707
730
  if (container === undefined) {
708
731
  throw new Error("ClickHouseDatabase.connectFromTask: task has no default container");
709
732
  }
710
- container.addEnvironment("CLICKHOUSE_URL", this.getHttpUrl());
733
+ container.addEnvironment("CLICKHOUSE_URL", this.getUrl());
711
734
  container.addEnvironment("CLICKHOUSE_DATABASE", CLICKHOUSE_DATABASE_NAME);
712
735
  if (opts?.user !== undefined) {
713
736
  const userSecret = this.getUser(opts.user);
@@ -263,12 +263,14 @@ export interface IClickHouseDatabase extends IDatabase, IConnectable, IMigration
263
263
  /** Native TCP port (typically 9000). */
264
264
  getNativePort(): number;
265
265
  /**
266
- * HTTP URL for clickhouse-client / chproxy (`http://<host>:<httpPort>`).
267
- * No credentials in the URL ClickHouse HTTP uses Basic Auth headers or
268
- * `X-ClickHouse-User`/`X-ClickHouse-Key` headers. Compose creds at runtime
269
- * via `getUser(name).getImport(...)`.
266
+ * Canonical URL for the HTTP(S) interface. Scheme is `https` when TLS is
267
+ * active (default), `http` only when `tls: { mode: "none", }` is set.
268
+ * Port matches: HTTPS (8443) under TLS, HTTP (8123) otherwise. No
269
+ * credentials in the URL — ClickHouse HTTP uses Basic Auth headers or
270
+ * `X-ClickHouse-User`/`X-ClickHouse-Key` headers. Compose creds at
271
+ * runtime via `getUser(name).getImport(...)`.
270
272
  */
271
- getHttpUrl(): string;
273
+ getUrl(): string;
272
274
  /**
273
275
  * Native TCP URL (`tcp://<host>:<nativePort>`).
274
276
  * No credentials in the URL — the native protocol handshake carries them.
@@ -288,8 +288,28 @@ SERVER_JSON=$(aws secretsmanager get-secret-value --secret-id "${options.serverS
288
288
  printf '%s' "$SERVER_JSON" | jq -r .cert > "$MOUNT_POINT/server-certs/server.crt"
289
289
  printf '%s' "$SERVER_JSON" | jq -r .key > "$MOUNT_POINT/server-certs/server.key"
290
290
 
291
+ # Strict-verify client config consumed by docker-exec reload SSM script and any
292
+ # in-container clickhouse-client invocation. RejectCertificateHandler + strict
293
+ # verificationMode together close the trust-anchor-skipped class.
294
+ cat > "$MOUNT_POINT/server-certs/client.xml" <<'CLIENTXML'
295
+ <?xml version="1.0"?>
296
+ <config>
297
+ <openSSL>
298
+ <client>
299
+ <caConfig>/etc/clickhouse-server/certs/ca.crt</caConfig>
300
+ <verificationMode>strict</verificationMode>
301
+ <loadDefaultCAFile>false</loadDefaultCAFile>
302
+ <invalidCertificateHandler>
303
+ <name>RejectCertificateHandler</name>
304
+ </invalidCertificateHandler>
305
+ </client>
306
+ </openSSL>
307
+ </config>
308
+ CLIENTXML
309
+
291
310
  chown -R 101:101 "$MOUNT_POINT/server-certs"
292
311
  chmod 600 "$MOUNT_POINT/server-certs/server.key"
312
+ chmod 644 "$MOUNT_POINT/server-certs/client.xml"
293
313
  `
294
314
  : "";
295
315
  const script = `#!/bin/bash
@@ -17,7 +17,7 @@ import { writeFileSync, readFileSync, existsSync, readdirSync, renameSync, unlin
17
17
  import { join, basename } from "path";
18
18
  import { createHash } from "crypto";
19
19
  import { buildConstructMap, constructMapToRecord, ACCOUNT_CONSTRUCT_GROUPS } from "@fjall/util/constructMap";
20
- import { maskSensitiveOutput } from "@fjall/util";
20
+ import { getErrorMessage, maskSensitiveOutput } from "@fjall/util";
21
21
  import { FJALL_MANIFEST_FILENAME, MANIFEST_SCHEMA_VERSION, FjallManifestSchema } from "@fjall/util/manifest/schemas";
22
22
  import { FjallLogger } from "./validationLogger.js";
23
23
  /**
@@ -117,7 +117,7 @@ function computeTemplateHash(templatePath) {
117
117
  return createHash("sha256").update(normalised).digest("hex");
118
118
  }
119
119
  catch (error) {
120
- const maskedMessage = maskSensitiveOutput(error instanceof Error ? error.message : String(error));
120
+ const maskedMessage = maskSensitiveOutput(getErrorMessage(error));
121
121
  FjallLogger.warn(`Failed to compute hash for ${templatePath}: ${maskedMessage}`);
122
122
  return null;
123
123
  }
@@ -144,7 +144,7 @@ function computeStackHashes(cdkOutPath) {
144
144
  }
145
145
  }
146
146
  catch (error) {
147
- const maskedMessage = maskSensitiveOutput(error instanceof Error ? error.message : String(error));
147
+ const maskedMessage = maskSensitiveOutput(getErrorMessage(error));
148
148
  FjallLogger.warn(`Failed to read cdk.out for hash computation: ${maskedMessage}`);
149
149
  }
150
150
  return stacks;
@@ -194,16 +194,10 @@ export function writeManifest(assembly, collector) {
194
194
  unlinkSync(tmpPath);
195
195
  }
196
196
  catch (cleanupError) {
197
- const cleanupMessage = cleanupError instanceof Error
198
- ? cleanupError.message
199
- : String(cleanupError);
200
- FjallLogger.warn(`Failed to clean up tmp manifest at ${tmpPath}: ${maskSensitiveOutput(cleanupMessage)}`);
197
+ FjallLogger.warn(`Failed to clean up tmp manifest at ${tmpPath}: ${maskSensitiveOutput(getErrorMessage(cleanupError))}`);
201
198
  }
202
199
  }
203
- const errorMessage = error instanceof Error ? error.message : String(error);
204
- throw new Error(`Failed to write Fjall manifest: ${errorMessage}`, {
205
- cause: error
206
- });
200
+ throw new Error(`Failed to write Fjall manifest: ${getErrorMessage(error)}`, { cause: error });
207
201
  }
208
202
  }
209
203
  /**
@@ -224,8 +218,7 @@ export function readExistingManifest(cdkOutPath) {
224
218
  return result.data;
225
219
  }
226
220
  catch (error) {
227
- const message = error instanceof Error ? error.message : String(error);
228
- FjallLogger.warn(`Failed to parse existing manifest: ${maskSensitiveOutput(message)}`);
221
+ FjallLogger.warn(`Failed to parse existing manifest: ${maskSensitiveOutput(getErrorMessage(error))}`);
229
222
  return null;
230
223
  }
231
224
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/components-infrastructure",
3
- "version": "1.1.0",
3
+ "version": "2.1.1",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "type": "module",
6
6
  "bin": {
@@ -59,12 +59,12 @@
59
59
  "eslint": "^10.2.1",
60
60
  "prettier": "^3.8.3",
61
61
  "typescript": "^6.0.3",
62
- "vitest": "^4.1.5"
62
+ "vitest": "^4.1.7"
63
63
  },
64
64
  "dependencies": {
65
65
  "@aws-sdk/client-organizations": "^3.1038.0",
66
- "@fjall/generator": "^1.1.0",
67
- "@fjall/util": "^1.1.0",
66
+ "@fjall/generator": "^2.1.1",
67
+ "@fjall/util": "^2.1.1",
68
68
  "constructs": "^10.0.0",
69
69
  "uuid": "^14.0.0"
70
70
  },
@@ -79,5 +79,5 @@
79
79
  "engines": {
80
80
  "node": ">=18.0.0"
81
81
  },
82
- "gitHead": "437ec726425bd7610b83a57c12c1a4e1980bbb01"
82
+ "gitHead": "971d9ed50c6a21d24e67c9c83b9c471a632bf284"
83
83
  }