typekro 0.38.2 → 0.40.0

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 (42) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/core/aspects/values-merge.d.ts +14 -0
  3. package/dist/core/aspects/values-merge.d.ts.map +1 -1
  4. package/dist/core/aspects/values-merge.js +27 -0
  5. package/dist/core/aspects/values-merge.js.map +1 -1
  6. package/dist/factories/clickhouse/utils/helm-values-mapper.d.ts.map +1 -1
  7. package/dist/factories/clickhouse/utils/helm-values-mapper.js +7 -8
  8. package/dist/factories/clickhouse/utils/helm-values-mapper.js.map +1 -1
  9. package/dist/factories/clickstack/compositions/clickstack-bootstrap.d.ts +29 -2
  10. package/dist/factories/clickstack/compositions/clickstack-bootstrap.d.ts.map +1 -1
  11. package/dist/factories/clickstack/compositions/clickstack-bootstrap.js +298 -46
  12. package/dist/factories/clickstack/compositions/clickstack-bootstrap.js.map +1 -1
  13. package/dist/factories/clickstack/hyperdx-oidc/index.d.ts +2 -2
  14. package/dist/factories/clickstack/hyperdx-oidc/index.d.ts.map +1 -1
  15. package/dist/factories/clickstack/hyperdx-oidc/index.js +25 -8
  16. package/dist/factories/clickstack/hyperdx-oidc/index.js.map +1 -1
  17. package/dist/factories/clickstack/hyperdx-oidc/plugin-bundle.generated.d.ts.map +1 -1
  18. package/dist/factories/clickstack/hyperdx-oidc/plugin-bundle.generated.js +4 -4
  19. package/dist/factories/clickstack/hyperdx-oidc/plugin-bundle.generated.js.map +1 -1
  20. package/dist/factories/clickstack/types.d.ts +159 -1
  21. package/dist/factories/clickstack/types.d.ts.map +1 -1
  22. package/dist/factories/clickstack/types.js +123 -1
  23. package/dist/factories/clickstack/types.js.map +1 -1
  24. package/dist/factories/clickstack/utils/helm-values-mapper.d.ts +71 -2
  25. package/dist/factories/clickstack/utils/helm-values-mapper.d.ts.map +1 -1
  26. package/dist/factories/clickstack/utils/helm-values-mapper.js +149 -121
  27. package/dist/factories/clickstack/utils/helm-values-mapper.js.map +1 -1
  28. package/dist/factories/clickstack/utils/storage.d.ts +76 -0
  29. package/dist/factories/clickstack/utils/storage.d.ts.map +1 -1
  30. package/dist/factories/clickstack/utils/storage.js +167 -1
  31. package/dist/factories/clickstack/utils/storage.js.map +1 -1
  32. package/dist/factories/clickstack/utils/team-defaults.d.ts +87 -0
  33. package/dist/factories/clickstack/utils/team-defaults.d.ts.map +1 -0
  34. package/dist/factories/clickstack/utils/team-defaults.js +462 -0
  35. package/dist/factories/clickstack/utils/team-defaults.js.map +1 -0
  36. package/dist/factories/inngest/utils/helm-values-mapper.d.ts.map +1 -1
  37. package/dist/factories/inngest/utils/helm-values-mapper.js +16 -10
  38. package/dist/factories/inngest/utils/helm-values-mapper.js.map +1 -1
  39. package/dist/factories/rook/utils/helm-values-mapper.d.ts.map +1 -1
  40. package/dist/factories/rook/utils/helm-values-mapper.js +7 -8
  41. package/dist/factories/rook/utils/helm-values-mapper.js.map +1 -1
  42. package/package.json +2 -1
@@ -88,12 +88,18 @@ import { persistentVolumeClaim } from '../../kubernetes/storage/persistent-volum
88
88
  import { cronJob } from '../../kubernetes/workloads/cron-job.js';
89
89
  import { CLICKSTACK_API_PORT, CLICKSTACK_APP_PORT, CLICKSTACK_GATEWAY_NAME_SUFFIX, CLICKSTACK_OTLP_GRPC_PORT, CLICKSTACK_OTLP_HTTP_PORT, clickstackHelmRelease, DEFAULT_CLICKSTACK_REPO_NAME, DEFAULT_CLICKSTACK_REPO_URL, DEFAULT_CLICKSTACK_VERSION, } from '../resources/helm.js';
90
90
  import { clickstackMongoService, clickstackMongoStatefulSet } from '../resources/mongo.js';
91
- import { ClickStackBootstrapConfigSchema, ClickStackBootstrapStatusSchema, ClickStackExternalMongoBootstrapConfigSchema, ClickStackSecretValuesBootstrapConfigSchema, ClickStackSecretValuesExternalMongoBootstrapConfigSchema, assertClickStackReleaseName, CLICKSTACK_BOOTSTRAP_MARKER_COLLECTION, CLICKSTACK_CONTRACT_CONFIGMAP_SUFFIX, CLICKSTACK_INITIAL_USER_API_BASE_URL_ENV, CLICKSTACK_INITIAL_USER_MARKER_ID, CLICKSTACK_INITIAL_USER_VALIDATED_APP_VERSION, CLICKSTACK_INITIAL_USER_VALIDATED_CHART_VERSIONS, CLICKSTACK_RETENTION_NAME_SUFFIX, CLICKSTACK_TEAM_BOOTSTRAP_NAME_SUFFIX, clickStackInitialUserVersionValidationRule, isClickStackInitialUserValidatedChartVersion, resolveClickStackInitialUser, } from '../types.js';
92
- import { DEFAULT_CLICKSTACK_NAMESPACE, mapClickStackConfigToHelmValues, } from '../utils/helm-values-mapper.js';
91
+ import { ClickStackBootstrapConfigSchema, ClickStackBootstrapStatusSchema, ClickStackExternalMongoBootstrapConfigSchema, ClickStackSecretValuesBootstrapConfigSchema, ClickStackSecretValuesExternalMongoBootstrapConfigSchema, assertClickStackReleaseName, CLICKSTACK_BOOTSTRAP_MARKER_COLLECTION, CLICKSTACK_CLICKHOUSE_HOST_VALIDATION_RULE, CLICKSTACK_CONTRACT_CONFIGMAP_SUFFIX, CLICKSTACK_INITIAL_USER_API_BASE_URL_ENV, CLICKSTACK_INITIAL_USER_MARKER_ID, CLICKSTACK_INITIAL_USER_VALIDATED_APP_VERSION, CLICKSTACK_INITIAL_USER_VALIDATED_CHART_VERSIONS, CLICKSTACK_RETENTION_NAME_SUFFIX, CLICKSTACK_TEAM_BOOTSTRAP_NAME_SUFFIX, clickStackInitialUserVersionValidationRule, DEFAULT_CLICKSTACK_TEAM_NAME, resolveClickStackTeamName, validateClickStackClickhouseHost, isClickStackInitialUserValidatedChartVersion, resolveClickStackInitialUser, } from '../types.js';
92
+ import { CLICKSTACK_DEFAULT_CONNECTIONS_VALUES_KEY, clickStackDefaultConnectionTarget, CLICKSTACK_SECRET_VALUES_DEFAULT_CONNECTIONS_DOCUMENT, DEFAULT_CLICKSTACK_NAMESPACE, mapClickStackConfigToHelmValues, } from '../utils/helm-values-mapper.js';
93
+ import { CLICKSTACK_APP_PASSWORD_SECRET_KEY, CLICKSTACK_CHART_VERSION_ENV, HYPERDX_DEFAULT_CONNECTION_HOST_ENV, HYPERDX_DEFAULT_CONNECTION_PASSWORD_ENV, HYPERDX_DEFAULT_CONNECTION_USERNAME_ENV, HYPERDX_DEFAULT_SOURCES_DATABASE_ENV, CLICKSTACK_LEGACY_TEAM_NAME_SHA256, renderTeamBootstrapHelpers, } from '../utils/team-defaults.js';
93
94
  import { CLICKSTACK_CONFIG_MAP_NAME, CLICKSTACK_SECRET_NAME, assertQueueReplicaCompatible, clickStackQueueClaimName, renderPersistentQueueClaimSpec, renderRetentionScript, resolveClickStackStorage, } from '../utils/storage.js';
94
95
  import { applyHyperdxOidcValues, CLICKSTACK_HYPERDX_OIDC_VALIDATED_APP_VERSION, CLICKSTACK_HYPERDX_OIDC_VALIDATED_CHART_VERSIONS, hyperdxOidcPluginConfigMapData, hyperdxOidcPluginConfigMapName, isClickStackHyperdxOidcValidatedChartVersion, resolveClickStackHyperdxOidc, } from '../hyperdx-oidc/index.js';
95
96
  import { clickstackHelmRepositoryBootstrap } from './clickstack-helm-repository.js';
96
97
  const CLICKSTACK_CHART_PLACEHOLDER_API_KEY = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';
98
+ /**
99
+ * Suffix of the `secretValues`-mode ConfigMap carrying HyperDX's default
100
+ * connection values (`<release>-default-connections`).
101
+ */
102
+ export const CLICKSTACK_DEFAULT_CONNECTIONS_CONFIGMAP_SUFFIX = '-default-connections';
97
103
  /**
98
104
  * Readiness for the collector queue's PersistentVolumeClaim.
99
105
  *
@@ -173,9 +179,16 @@ const CLICKSTACK_HELM_RELEASE_RESOURCE_ID = 'clickstackHelmRelease';
173
179
  * @see https://github.com/yehudacohen/typekro/issues/188
174
180
  */
175
181
  const CLICKSTACK_CONTRACT_RESOURCE_ID = 'clickstackContract';
182
+ /** Every variant: the host the connection URLs are built from. */
183
+ const hostSchemaFieldValidations = {
184
+ 'clickhouse.host': CLICKSTACK_CLICKHOUSE_HOST_VALIDATION_RULE,
185
+ };
176
186
  const inlineSchemaFieldValidations = {
187
+ ...hostSchemaFieldValidations,
177
188
  apiKey: `self != "${CLICKSTACK_CHART_PLACEHOLDER_API_KEY}"`,
178
189
  };
190
+ /** The Team the degraded script creates, and so the one it may rename and seed. */
191
+ const CLICKSTACK_MANAGED_TEAM_HOOK_ID = 'typekro-managed-ingestion';
179
192
  /**
180
193
  * The DEGRADED script: reconcile the ingestion key by creating the Team
181
194
  * outright. Rendered only when `initialUser` is NOT configured.
@@ -189,33 +202,54 @@ const inlineSchemaFieldValidations = {
189
202
  * (#227). This path exists because it is what shipped, and removing it would
190
203
  * break deployments whose ingestion key is already converged; it is not the
191
204
  * supported way to run the composition. Configure `initialUser`.
205
+ *
206
+ * A Team inserted here also skips HyperDX's `setupTeamDefaults`, so the script
207
+ * seeds its connection and sources itself (see `utils/team-defaults.ts`).
192
208
  */
193
- const CLICKSTACK_TEAM_BOOTSTRAP_SCRIPT = [
194
- "const database = db.getSiblingDB('hyperdx');",
195
- 'const apiKey = process.env.HYPERDX_API_KEY;',
196
- "if (typeof apiKey !== 'string' || apiKey.trim().length === 0) throw new Error('HYPERDX_API_KEY is required.');",
197
- `if (apiKey === '${CLICKSTACK_CHART_PLACEHOLDER_API_KEY}') throw new Error('HYPERDX_API_KEY must override the published ClickStack chart placeholder.');`,
198
- "const hookId = 'typekro-managed-ingestion';",
199
- 'const teams = database.teams.find({ hookId }).toArray();',
200
- "if (teams.length > 1) throw new Error('Multiple TypeKro-managed ClickStack Teams exist.');",
201
- 'if (teams.length === 0) {',
202
- ' const now = new Date();',
203
- ' database.teams.insertOne({',
204
- " name: 'Applik8s Observability',",
205
- ' allowedAuthMethods: [],',
206
- ' hookId,',
207
- ' apiKey,',
208
- ' collectorAuthenticationEnforced: true,',
209
- ' isMetricsSeriesTableEnabled: false,',
210
- ' createdAt: now,',
211
- ' updatedAt: now,',
212
- ' });',
213
- '} else if (teams[0].apiKey !== apiKey || teams[0].collectorAuthenticationEnforced !== true) {',
214
- ' database.teams.updateOne({ _id: teams[0]._id }, {',
215
- ' $set: { apiKey, collectorAuthenticationEnforced: true, updatedAt: new Date() },',
216
- ' });',
217
- '}',
218
- ].join('\n');
209
+ function renderDegradedTeamBootstrapScript(teamName, teamDefaults, legacyTeamNameHashes, seedChartVersions) {
210
+ return [
211
+ "const database = db.getSiblingDB('hyperdx');",
212
+ `const bootstrapMarkers = database.${CLICKSTACK_BOOTSTRAP_MARKER_COLLECTION};`,
213
+ 'const apiKey = process.env.HYPERDX_API_KEY;',
214
+ "if (typeof apiKey !== 'string' || apiKey.trim().length === 0) throw new Error('HYPERDX_API_KEY is required.');",
215
+ `if (apiKey === '${CLICKSTACK_CHART_PLACEHOLDER_API_KEY}') throw new Error('HYPERDX_API_KEY must override the published ClickStack chart placeholder.');`,
216
+ ...renderTeamBootstrapHelpers({
217
+ seed: teamDefaults,
218
+ untouchedName: { sha256: legacyTeamNameHashes },
219
+ ...(seedChartVersions !== undefined && { validatedChartVersions: seedChartVersions }),
220
+ }),
221
+ `const hookId = '${CLICKSTACK_MANAGED_TEAM_HOOK_ID}';`,
222
+ `const teamName = ${JSON.stringify(teamName)};`,
223
+ 'const teams = database.teams.find({ hookId }).toArray();',
224
+ "if (teams.length > 1) throw new Error('Multiple TypeKro-managed ClickStack Teams exist.');",
225
+ 'if (teams.length === 0) {',
226
+ ' const now = new Date();',
227
+ // Ownership of the name is recorded BEFORE the Team exists, under the id
228
+ // it is about to get, never inferred later from the name. A crash in
229
+ // between leaves an orphan record for an id nothing uses.
230
+ ' const teamId = new ObjectId();',
231
+ ' recordTeamName(teamId, teamName);',
232
+ ' database.teams.insertOne({',
233
+ ' _id: teamId,',
234
+ ' name: teamName,',
235
+ ' allowedAuthMethods: [],',
236
+ ' hookId,',
237
+ ' apiKey,',
238
+ ' collectorAuthenticationEnforced: true,',
239
+ ' isMetricsSeriesTableEnabled: false,',
240
+ ' createdAt: now,',
241
+ ' updatedAt: now,',
242
+ ' });',
243
+ '} else if (teams[0].apiKey !== apiKey || teams[0].collectorAuthenticationEnforced !== true) {',
244
+ ' database.teams.updateOne({ _id: teams[0]._id }, {',
245
+ ' $set: { apiKey, collectorAuthenticationEnforced: true, updatedAt: new Date() },',
246
+ ' });',
247
+ '}',
248
+ 'const team = database.teams.findOne({ hookId });',
249
+ 'reconcileTeamName(team, teamName);',
250
+ 'seedTeamDefaults(team, true);',
251
+ ].join('\n');
252
+ }
219
253
  /**
220
254
  * Render the mongosh script the bootstrap CronJob runs.
221
255
  *
@@ -231,7 +265,7 @@ const CLICKSTACK_TEAM_BOOTSTRAP_SCRIPT = [
231
265
  * WHAT TYPEKRO USED TO DO TO IT. Creating the Team directly, to pre-seed the
232
266
  * ingestion key, spends the registration without producing the account: one
233
267
  * Team, zero users, a login page nobody can satisfy, and no connections or
234
- * sources either (#227). {@link CLICKSTACK_TEAM_BOOTSTRAP_SCRIPT} is that
268
+ * sources either (#227). The degraded script is that
235
269
  * behaviour, kept for compatibility and documented as degraded.
236
270
  *
237
271
  * WHAT IT DOES NOW, WITH `initialUser`. It spends the registration the way
@@ -276,13 +310,23 @@ const CLICKSTACK_TEAM_BOOTSTRAP_SCRIPT = [
276
310
  * and the HyperDX API may still be starting, so a connection failure is
277
311
  * reported as the transient it is, in those words, instead of as a stack
278
312
  * trace that reads like a broken deployment.
313
+ * 6. TEAM NAME AND DEFAULTS, ON BOTH PATHS. `reconcileTeamName` and
314
+ * `seedTeamDefaults` (utils/team-defaults.ts) rename the Team unless a
315
+ * human renamed it after TypeKro did, and seed the connection and sources
316
+ * once into a Team that has neither. Neither ever overwrites a document.
279
317
  *
280
318
  * @param initialUser - The resolved build-time option, or `undefined`
319
+ * @param options - Team name and defaults (see {@link ClickStackTeamBootstrapScriptOptions})
281
320
  * @returns The complete `--eval` script
282
321
  */
283
- export function renderClickStackTeamBootstrapScript(initialUser) {
284
- if (initialUser === undefined)
285
- return CLICKSTACK_TEAM_BOOTSTRAP_SCRIPT;
322
+ export function renderClickStackTeamBootstrapScript(initialUser, options = {}) {
323
+ const teamDefaults = options.teamDefaults ?? true;
324
+ const seedChartVersions = options.seedChartVersions === null
325
+ ? undefined
326
+ : (options.seedChartVersions ?? CLICKSTACK_INITIAL_USER_VALIDATED_CHART_VERSIONS);
327
+ if (initialUser === undefined) {
328
+ return renderDegradedTeamBootstrapScript(options.teamName ?? DEFAULT_CLICKSTACK_TEAM_NAME, teamDefaults, options.legacyTeamNameHashes ?? [CLICKSTACK_LEGACY_TEAM_NAME_SHA256], seedChartVersions);
329
+ }
286
330
  const passwordVar = initialUser.passwordEnvVarName;
287
331
  // mongosh resolves a promise returned by the LAST expression of `--eval` and
288
332
  // exits non-zero when it rejects, but it does NOT accept top-level `await`.
@@ -301,6 +345,18 @@ export function renderClickStackTeamBootstrapScript(initialUser) {
301
345
  // TypeKro's OWN state, in TypeKro's OWN collection — not an extra field on
302
346
  // an upstream-owned team or user document.
303
347
  ` const bootstrapMarkers = database.${CLICKSTACK_BOOTSTRAP_MARKER_COLLECTION};`,
348
+ // A Team HyperDX's registration created is named `<email>'s Team`, and
349
+ // one that still is has never been renamed by a human. So has a Team
350
+ // still carrying the legacy default, from a deployment that ran the
351
+ // degraded path before initialUser was configured.
352
+ ...renderTeamBootstrapHelpers({
353
+ seed: teamDefaults,
354
+ untouchedName: {
355
+ exact: `${initialUser.email}'s Team`,
356
+ sha256: options.legacyTeamNameHashes ?? [CLICKSTACK_LEGACY_TEAM_NAME_SHA256],
357
+ },
358
+ ...(seedChartVersions !== undefined && { validatedChartVersions: seedChartVersions }),
359
+ }).map((line) => ` ${line}`),
304
360
  ` const initialUserMarkerId = ${JSON.stringify(CLICKSTACK_INITIAL_USER_MARKER_ID)};`,
305
361
  // Marker present => the instance was claimed under TypeKro's watch. Short
306
362
  // -circuit BEFORE looking at teams, the password, or the network at all.
@@ -366,6 +422,17 @@ export function renderClickStackTeamBootstrapScript(initialUser) {
366
422
  ' $set: { apiKey, collectorAuthenticationEnforced: true, updatedAt: new Date() },',
367
423
  ' });',
368
424
  ' }',
425
+ // HyperDX named this Team at registration; rename it only when asked to.
426
+ ...(options.teamName === undefined
427
+ ? []
428
+ : [` reconcileTeamName(teams[0], ${JSON.stringify(options.teamName)});`]),
429
+ // Registration already ran setupTeamDefaults, so this normally finds the
430
+ // defaults in place and records that. It fills an empty Team: one from
431
+ // before initialUser was configured, or one whose defaults HyperDX failed
432
+ // to set up (it logs that and carries on).
433
+ // `false`: this path never creates the Team, so a new one is HyperDX's and
434
+ // gets the grace period (a Team from the degraded path is long past it).
435
+ ' seedTeamDefaults(teams[0], false);',
369
436
  " return 'clickstack-bootstrap-ok';",
370
437
  '};',
371
438
  'main();',
@@ -387,7 +454,8 @@ export function renderClickStackTeamBootstrapScript(initialUser) {
387
454
  * WHY IT IS NOT THE WHOLE GUARD. `version` is a RUNTIME spec field, so in KRO
388
455
  * mode it is a schema reference here and a value a consumer supplies to the CR
389
456
  * at apply time — there is no build to fail. That half is covered by narrowing
390
- * `spec.version` on the generated CRD; see
457
+ * `spec.version` on the generated CRD, on CRDs KRO creates fresh (see the KRO
458
+ * caveat on {@link CLICKSTACK_INITIAL_USER_VALIDATED_CHART_VERSIONS}); see
391
459
  * {@link clickStackInitialUserVersionValidationRule}. This function therefore
392
460
  * treats a non-string as "not mine to check", which is also what the analysis
393
461
  * pass hands it.
@@ -405,7 +473,7 @@ function assertClickStackInitialUserChartVersion(version, initialUser) {
405
473
  throw new Error('ClickStack initialUser is audited only against chart version(s) ' +
406
474
  `${CLICKSTACK_INITIAL_USER_VALIDATED_CHART_VERSIONS.join(', ')} (appVersion ` +
407
475
  `${CLICKSTACK_INITIAL_USER_VALIDATED_APP_VERSION}), but version ${JSON.stringify(version)} ` +
408
- 'was requested. Registration goes through HyperDX\'s own endpoint, but TypeKro still ' +
476
+ "was requested. Registration goes through HyperDX's own endpoint, but TypeKro still " +
409
477
  "patches the Team's apiKey directly in HyperDX's schema — if that field moves the Job " +
410
478
  'still succeeds and ingestion is silently unauthenticated. The list is exact on purpose: ' +
411
479
  'a range such as ">=3.2.0" resolves to a chart nobody audited. Verify the registration ' +
@@ -436,27 +504,72 @@ function assertClickStackHyperdxOidcChartVersion(version, hyperdxOidc) {
436
504
  'models. Verify them on that chart, then set hyperdxOidc.allowUnvalidatedChartVersion: true.');
437
505
  }
438
506
  }
507
+ /**
508
+ * Refuse a concrete `clickhouse.host` that would break the URLs it is built
509
+ * into (see {@link validateClickStackClickhouseHost}); the CRD carries the same
510
+ * rule for KRO mode.
511
+ *
512
+ * @throws Error naming the offending value and the rule it breaks
513
+ */
514
+ function assertClickStackClickhouseHost(host) {
515
+ const reason = validateClickStackClickhouseHost(host);
516
+ if (reason !== undefined) {
517
+ throw new Error(`ClickStack clickhouse.host ${JSON.stringify(host)} ${reason}.`);
518
+ }
519
+ }
520
+ /**
521
+ * Refuse the Team-defaults seed on a chart version nobody has audited — the
522
+ * build-time check. The CRD rule ({@link clickStackSchemaFieldValidations})
523
+ * covers KRO mode on fresh CRDs, and the CronJob's runtime check
524
+ * (`CLICKSTACK_CHART_VERSION`) covers every CRD, including ones KRO created
525
+ * before the rule existed.
526
+ *
527
+ * The seed writes documents in HyperDX 2.35.0's own `connections` / `sources`
528
+ * schema. On a chart whose HyperDX changed that schema, the CronJob would
529
+ * still succeed and the Team would look configured with documents HyperDX may
530
+ * misread, and the seed never runs twice.
531
+ */
532
+ function assertClickStackTeamDefaultsChartVersion(version, teamDefaults) {
533
+ if (teamDefaults === undefined || teamDefaults.allowUnvalidatedChartVersion)
534
+ return;
535
+ if (typeof version !== 'string')
536
+ return;
537
+ if (!isClickStackInitialUserValidatedChartVersion(version)) {
538
+ throw new Error('ClickStack teamDefaults is audited only against chart version(s) ' +
539
+ `${CLICKSTACK_INITIAL_USER_VALIDATED_CHART_VERSIONS.join(', ')} (appVersion ` +
540
+ `${CLICKSTACK_INITIAL_USER_VALIDATED_APP_VERSION}), but version ${JSON.stringify(version)} ` +
541
+ "was requested. The Team-bootstrap CronJob seeds an empty Team's ClickHouse connection " +
542
+ "and sources in HyperDX's own MongoDB schema. Verify the connections and sources schema " +
543
+ 'on that chart, then set teamDefaults: { allowUnvalidatedChartVersion: true }, or turn ' +
544
+ 'the seed off with teamDefaults: false.');
545
+ }
546
+ }
439
547
  /**
440
548
  * Schema field validations for a composition, with the chart-version narrowing
441
- * added when `initialUser` is configured.
549
+ * added when `initialUser`, `hyperdxOidc` or the Team-defaults seed is on.
442
550
  *
443
551
  * This is the KRO-mode half of the version allowlist. `schemaFieldValidations`
444
552
  * becomes `x-kubernetes-validations` on the generated CRD, so a consumer who
445
553
  * sets an unaudited `spec.version` on the custom resource is refused by
446
554
  * ADMISSION — the path a build-time throw structurally cannot reach. It is
447
555
  * applied to the `secretValues` variants too, which previously carried no
448
- * validations at all and so had no KRO-side guard of any kind.
556
+ * validations at all and so had no KRO-side guard of any kind. KRO 0.9.2 puts
557
+ * these rules only on CRDs it creates fresh (it compares no validations on an
558
+ * update), which is why the seed also checks the chart version and the host
559
+ * at runtime.
449
560
  *
450
561
  * @param base - The mode's own validations (inline mode pins `apiKey`)
451
- * @param initialUser - The resolved option; `undefined` adds nothing
562
+ * @param build - The resolved options that write into HyperDX's own schema
452
563
  * @returns Composition options carrying the merged map, or `{}` when empty
453
564
  */
454
- function clickStackSchemaFieldValidations(base, initialUser, hyperdxOidc) {
565
+ function clickStackSchemaFieldValidations(base, build) {
566
+ const { initialUser, hyperdxOidc, teamDefaults } = build;
455
567
  const merged = { ...base };
456
- // Both features are audited against the same chart series, so one rule
457
- // covers either (see CLICKSTACK_HYPERDX_OIDC_VALIDATED_CHART_VERSIONS).
568
+ // All three are audited against the same chart series, so one rule covers
569
+ // any of them (see CLICKSTACK_HYPERDX_OIDC_VALIDATED_CHART_VERSIONS).
458
570
  if ((initialUser !== undefined && !initialUser.allowUnvalidatedChartVersion) ||
459
- (hyperdxOidc !== undefined && !hyperdxOidc.allowUnvalidatedChartVersion)) {
571
+ (hyperdxOidc !== undefined && !hyperdxOidc.allowUnvalidatedChartVersion) ||
572
+ (teamDefaults !== undefined && !teamDefaults.allowUnvalidatedChartVersion)) {
460
573
  merged.version = clickStackInitialUserVersionValidationRule();
461
574
  }
462
575
  return Object.keys(merged).length > 0 ? { schemaFieldValidations: merged } : {};
@@ -487,6 +600,8 @@ function bootstrapBody(spec, build) {
487
600
  assertClickStackInitialUserChartVersion(isKubernetesRef(spec.version) ? undefined : (spec.version ?? DEFAULT_CLICKSTACK_VERSION), build.initialUser);
488
601
  // The OIDC plugin hooks HyperDX internals, so the same audited-chart rule applies.
489
602
  assertClickStackHyperdxOidcChartVersion(isKubernetesRef(spec.version) ? undefined : (spec.version ?? DEFAULT_CLICKSTACK_VERSION), build.hyperdxOidc);
603
+ // The Team-defaults seed writes HyperDX's `connections` / `sources` schema.
604
+ assertClickStackTeamDefaultsChartVersion(isKubernetesRef(spec.version) ? undefined : (spec.version ?? DEFAULT_CLICKSTACK_VERSION), build.teamDefaults);
490
605
  // The schema constrains `name` — DNS-label syntax and the derived length
491
606
  // bound — for KRO admission and direct-mode `deploy`; direct-mode `toYaml`
492
607
  // does not run `validateSpec`, so a concrete malformed or over-long name
@@ -495,6 +610,11 @@ function bootstrapBody(spec, build) {
495
610
  // The guard runs the SAME schema, so the message cannot drift from it.
496
611
  if (!isKubernetesRef(spec.name))
497
612
  assertClickStackReleaseName(spec.name);
613
+ // The host becomes `http://<host>:<port>` and `tcp://<host>:<port>`, so a
614
+ // concrete one must not carry a scheme, port, path or userinfo.
615
+ if (!isKubernetesRef(spec.clickhouse) && !isKubernetesRef(spec.clickhouse.host)) {
616
+ assertClickStackClickhouseHost(spec.clickhouse.host);
617
+ }
498
618
  if (build.credentialSource === 'inline') {
499
619
  const inlineApiKey = spec.apiKey;
500
620
  if (!isKubernetesRef(inlineApiKey) &&
@@ -628,6 +748,13 @@ function bootstrapBody(spec, build) {
628
748
  ...(build.credentialSource === 'secretValues'
629
749
  ? {
630
750
  valuesFrom: [
751
+ // TypeKro's external ClickHouse connection FIRST, so the
752
+ // caller's fragment below can still override it.
753
+ {
754
+ kind: 'ConfigMap',
755
+ name: `${spec.name}${CLICKSTACK_DEFAULT_CONNECTIONS_CONFIGMAP_SUFFIX}`,
756
+ valuesKey: CLICKSTACK_DEFAULT_CONNECTIONS_VALUES_KEY,
757
+ },
631
758
  {
632
759
  kind: 'Secret',
633
760
  // biome-ignore lint/style/noNonNullAssertion: the secretValues schema requires credentialsSecret
@@ -641,6 +768,31 @@ function bootstrapBody(spec, build) {
641
768
  : {}),
642
769
  id: CLICKSTACK_HELM_RELEASE_RESOURCE_ID,
643
770
  });
771
+ // ── secretValues: HyperDX's default connection ───────────────────────
772
+ // Without it the chart falls back to its own "Local ClickHouse" at the
773
+ // bundled Service TypeKro disables, and HyperDX's registration provisions
774
+ // that. The password stays a chart template (see the mapper), so the
775
+ // ConfigMap carries no credential. The HelmRelease reads it through
776
+ // valuesFrom, so it has to exist first.
777
+ if (build.credentialSource === 'secretValues') {
778
+ const defaultConnections = configMap({
779
+ id: 'clickstackDefaultConnections',
780
+ metadata: {
781
+ name: `${spec.name}${CLICKSTACK_DEFAULT_CONNECTIONS_CONFIGMAP_SUFFIX}`,
782
+ namespace: resolvedNamespace,
783
+ labels: {
784
+ 'app.kubernetes.io/name': 'clickstack',
785
+ 'app.kubernetes.io/instance': spec.name,
786
+ 'app.kubernetes.io/component': 'default-connections',
787
+ 'app.kubernetes.io/managed-by': 'typekro',
788
+ },
789
+ },
790
+ data: {
791
+ [CLICKSTACK_DEFAULT_CONNECTIONS_VALUES_KEY]: CLICKSTACK_SECRET_VALUES_DEFAULT_CONNECTIONS_DOCUMENT,
792
+ },
793
+ });
794
+ _clickstackHelmRelease.dependsOn(defaultConnections);
795
+ }
644
796
  // The collector Pod mounts the queue claim by name, so the claim has to
645
797
  // exist before helm-controller creates the Deployment.
646
798
  if (queueClaim !== undefined) {
@@ -681,6 +833,7 @@ function bootstrapBody(spec, build) {
681
833
  const mongoUri = build.mongoMode === 'external'
682
834
  ? spec.mongoUri
683
835
  : Cel.template('mongodb://%s-mongodb.%s.svc.cluster.local:27017/hyperdx', spec.name, resolvedNamespace);
836
+ const seedTarget = clickStackDefaultConnectionTarget(spec);
684
837
  const _teamBootstrap = cronJob({
685
838
  id: 'clickstackTeamBootstrap',
686
839
  metadata: {
@@ -719,7 +872,13 @@ function bootstrapBody(spec, build) {
719
872
  '--quiet',
720
873
  mongoUri,
721
874
  '--eval',
722
- renderClickStackTeamBootstrapScript(build.initialUser),
875
+ renderClickStackTeamBootstrapScript(build.initialUser, {
876
+ ...(build.teamName !== undefined && { teamName: build.teamName }),
877
+ teamDefaults: build.teamDefaults !== undefined,
878
+ ...(build.teamDefaults?.allowUnvalidatedChartVersion === true && {
879
+ seedChartVersions: null,
880
+ }),
881
+ }),
723
882
  ],
724
883
  env: [
725
884
  {
@@ -772,6 +931,41 @@ function bootstrapBody(spec, build) {
772
931
  value: Cel.template(`http://%s.%s.svc.cluster.local:${CLICKSTACK_API_PORT}`, spec.name, resolvedNamespace),
773
932
  },
774
933
  ]),
934
+ // The connection an empty Team is seeded with: the same
935
+ // host, user and database `defaultConnections` /
936
+ // `defaultSources` render, and the UI user's password by
937
+ // reference, never by value. `optional: true` so a
938
+ // missing key cannot stop ingestion-key reconciliation;
939
+ // the script then seeds nothing rather than ''.
940
+ ...(build.teamDefaults !== undefined
941
+ ? [
942
+ { name: HYPERDX_DEFAULT_CONNECTION_HOST_ENV, value: seedTarget.host },
943
+ {
944
+ name: HYPERDX_DEFAULT_CONNECTION_USERNAME_ENV,
945
+ value: seedTarget.username,
946
+ },
947
+ {
948
+ name: HYPERDX_DEFAULT_CONNECTION_PASSWORD_ENV,
949
+ valueFrom: {
950
+ secretKeyRef: {
951
+ ...build.teamDefaults.passwordSecretRef,
952
+ optional: true,
953
+ },
954
+ },
955
+ },
956
+ {
957
+ name: HYPERDX_DEFAULT_SOURCES_DATABASE_ENV,
958
+ value: seedTarget.database,
959
+ },
960
+ // For the seed's runtime version check: the one
961
+ // guard that holds on a KRO CRD created before
962
+ // the version rule existed.
963
+ {
964
+ name: CLICKSTACK_CHART_VERSION_ENV,
965
+ value: resolvedVersion,
966
+ },
967
+ ]
968
+ : []),
775
969
  ],
776
970
  },
777
971
  ],
@@ -990,11 +1184,66 @@ function resolveClickHouseStorageForBuild(options) {
990
1184
  assertQueueReplicaCompatible('makeClickstackBootstrap', resolved, replicaCount);
991
1185
  return resolved;
992
1186
  }
1187
+ function withTeamDefaults(teamDefaults) {
1188
+ return teamDefaults === undefined ? {} : { teamDefaults };
1189
+ }
1190
+ /**
1191
+ * Whether, and how, the bootstrap seeds Team defaults.
1192
+ *
1193
+ * The seed must create the same connection HyperDX's own registration would,
1194
+ * with the same password, or the Team ends up looking configured with a
1195
+ * connection that cannot log in and is never seeded again. In both credential
1196
+ * modes HyperDX's `DEFAULT_CONNECTIONS` password is the chart value
1197
+ * `hyperdx.secrets.CLICKHOUSE_APP_PASSWORD`: inline mode renders it into
1198
+ * `defaultConnections` directly, and `secretValues` mode templates it there
1199
+ * (see `CLICKSTACK_SECRET_VALUES_DEFAULT_CONNECTIONS_DOCUMENT`). The chart renders
1200
+ * that same value into `clickstack-secret.CLICKHOUSE_APP_PASSWORD`, which is
1201
+ * what the seed reads. One value, one connection definition.
1202
+ *
1203
+ * DEFAULT: ON WITH INLINE CREDENTIALS, OFF WITH `secretValues`. Inline, TypeKro
1204
+ * owns the whole connection. With `secretValues`, the caller's values fragment
1205
+ * may replace `defaultConnections`, and the CronJob cannot see that: without
1206
+ * `initialUser` nothing runs `setupTeamDefaults`, so a default-on seed would
1207
+ * put TypeKro's connection into the empty Team instead of the caller's, once
1208
+ * and for good. There, `teamDefaults: true` (or an options object) asks for
1209
+ * TypeKro's typed topology explicitly. (With `initialUser`, HyperDX's own
1210
+ * registration seeds the Team from the fully merged values either way.)
1211
+ *
1212
+ * Off when build-time values replace the chart defaults (or point HyperDX at
1213
+ * an existing config Secret): what HyperDX would seed is the caller's then.
1214
+ */
1215
+ function resolveTeamDefaults(options) {
1216
+ const requested = options.teamDefaults;
1217
+ if (requested === false)
1218
+ return undefined;
1219
+ if (requested === undefined && options.credentials?.source === 'secretValues')
1220
+ return undefined;
1221
+ const hyperdx = options.values?.hyperdx;
1222
+ const deployment = typeof hyperdx === 'object' && hyperdx !== null
1223
+ ? hyperdx.deployment
1224
+ : undefined;
1225
+ if (typeof deployment === 'object' && deployment !== null) {
1226
+ const overrides = deployment;
1227
+ if (overrides.defaultConnections !== undefined ||
1228
+ overrides.defaultSources !== undefined ||
1229
+ (overrides.useExistingConfigSecret !== undefined &&
1230
+ overrides.useExistingConfigSecret !== false)) {
1231
+ return undefined;
1232
+ }
1233
+ }
1234
+ return {
1235
+ passwordSecretRef: { name: CLICKSTACK_SECRET_NAME, key: CLICKSTACK_APP_PASSWORD_SECRET_KEY },
1236
+ allowUnvalidatedChartVersion: typeof requested === 'object' && requested.allowUnvalidatedChartVersion === true,
1237
+ };
1238
+ }
993
1239
  function resolveInternalBuild(options) {
994
1240
  const initialUser = resolveClickStackInitialUser('makeClickstackBootstrap', CLICKSTACK_SECRET_NAME, options.initialUser);
995
1241
  const hyperdxOidc = resolveClickStackHyperdxOidc('makeClickstackBootstrap', options.hyperdxOidc);
1242
+ const teamName = resolveClickStackTeamName('makeClickstackBootstrap', options.teamName, initialUser !== undefined);
996
1243
  return {
997
1244
  mongoMode: 'internal',
1245
+ ...(teamName !== undefined && { teamName }),
1246
+ ...withTeamDefaults(resolveTeamDefaults(options)),
998
1247
  credentialSource: options.credentials?.source ?? 'inline',
999
1248
  ...(options.mongo?.storage !== undefined && { storage: options.mongo.storage }),
1000
1249
  ...(options.values !== undefined && { values: options.values }),
@@ -1007,8 +1256,11 @@ function resolveInternalBuild(options) {
1007
1256
  function resolveExternalBuild(options) {
1008
1257
  const initialUser = resolveClickStackInitialUser('makeClickstackBootstrap', CLICKSTACK_SECRET_NAME, options.initialUser);
1009
1258
  const hyperdxOidc = resolveClickStackHyperdxOidc('makeClickstackBootstrap', options.hyperdxOidc);
1259
+ const teamName = resolveClickStackTeamName('makeClickstackBootstrap', options.teamName, initialUser !== undefined);
1010
1260
  return {
1011
1261
  mongoMode: 'external',
1262
+ ...(teamName !== undefined && { teamName }),
1263
+ ...withTeamDefaults(resolveTeamDefaults(options)),
1012
1264
  credentialSource: options.credentials?.source ?? 'inline',
1013
1265
  ...(options.values !== undefined && { values: options.values }),
1014
1266
  ...(options.postRenderers !== undefined && { postRenderers: options.postRenderers }),
@@ -1024,7 +1276,7 @@ function buildInternalInlineComposition(options) {
1024
1276
  kind: options.kind ?? 'ClickStackBootstrap',
1025
1277
  spec: ClickStackBootstrapConfigSchema,
1026
1278
  status: ClickStackBootstrapStatusSchema,
1027
- }, (spec) => bootstrapBody(spec, build), clickStackSchemaFieldValidations(inlineSchemaFieldValidations, build.initialUser, build.hyperdxOidc));
1279
+ }, (spec) => bootstrapBody(spec, build), clickStackSchemaFieldValidations(inlineSchemaFieldValidations, build));
1028
1280
  }
1029
1281
  function buildInternalSecretValuesComposition(options) {
1030
1282
  const build = {
@@ -1036,7 +1288,7 @@ function buildInternalSecretValuesComposition(options) {
1036
1288
  kind: options.kind ?? 'ClickStackBootstrap',
1037
1289
  spec: ClickStackSecretValuesBootstrapConfigSchema,
1038
1290
  status: ClickStackBootstrapStatusSchema,
1039
- }, (spec) => bootstrapBody(spec, build), clickStackSchemaFieldValidations({}, build.initialUser, build.hyperdxOidc));
1291
+ }, (spec) => bootstrapBody(spec, build), clickStackSchemaFieldValidations(hostSchemaFieldValidations, build));
1040
1292
  }
1041
1293
  function buildExternalInlineComposition(options) {
1042
1294
  const build = resolveExternalBuild(options);
@@ -1045,7 +1297,7 @@ function buildExternalInlineComposition(options) {
1045
1297
  kind: options.kind ?? 'ClickStackExternalMongoBootstrap',
1046
1298
  spec: ClickStackExternalMongoBootstrapConfigSchema,
1047
1299
  status: ClickStackBootstrapStatusSchema,
1048
- }, (spec) => bootstrapBody(spec, build), clickStackSchemaFieldValidations(inlineSchemaFieldValidations, build.initialUser, build.hyperdxOidc));
1300
+ }, (spec) => bootstrapBody(spec, build), clickStackSchemaFieldValidations(inlineSchemaFieldValidations, build));
1049
1301
  }
1050
1302
  function buildExternalSecretValuesComposition(options) {
1051
1303
  const build = {
@@ -1057,7 +1309,7 @@ function buildExternalSecretValuesComposition(options) {
1057
1309
  kind: options.kind ?? 'ClickStackExternalMongoBootstrap',
1058
1310
  spec: ClickStackSecretValuesExternalMongoBootstrapConfigSchema,
1059
1311
  status: ClickStackBootstrapStatusSchema,
1060
- }, (spec) => bootstrapBody(spec, build), clickStackSchemaFieldValidations({}, build.initialUser, build.hyperdxOidc));
1312
+ }, (spec) => bootstrapBody(spec, build), clickStackSchemaFieldValidations(hostSchemaFieldValidations, build));
1061
1313
  }
1062
1314
  function isSecretValuesExternalBuild(options) {
1063
1315
  return options.mongo?.mode === 'external' && options.credentials?.source === 'secretValues';