@crossdelta/infrastructure 0.5.5 → 0.6.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.
@@ -2,4 +2,6 @@ export * from './deploy-streams';
2
2
  export * from './discover-services';
3
3
  export * from './docker-hub-image';
4
4
  export * from './otel';
5
+ export * from './stream-job';
6
+ export * from './stream-setup';
5
7
  export * from './streams';
@@ -0,0 +1,68 @@
1
+ /**
2
+ * NATS JetStream Stream Materialization via K8s Job
3
+ *
4
+ * Creates streams inside the cluster using a K8s Job with nats-box.
5
+ * This solves the problem that Pulumi runs outside the cluster
6
+ * and cannot reach cluster-internal NATS URLs.
7
+ *
8
+ * Architecture:
9
+ * - Pure functions (stream-setup.ts) generate shell scripts
10
+ * - This file creates Pulumi K8s resources (ConfigMap + Job)
11
+ * - nats-box image provides the `nats` CLI (no custom image needed)
12
+ *
13
+ * @module @crossdelta/infrastructure/lib/helpers/stream-job
14
+ */
15
+ import type { Provider } from '@pulumi/kubernetes';
16
+ import * as k8s from '@pulumi/kubernetes';
17
+ import type { Output } from '@pulumi/pulumi';
18
+ import type { StreamPolicy } from './deploy-streams';
19
+ import { type ResolvedStream } from './stream-setup';
20
+ export type { ResolvedStream };
21
+ export { computeConfigHash, generateSetupScript, msToNatsDuration, resolveStreams } from './stream-setup';
22
+ export interface MaterializeStreamsConfig {
23
+ /** NATS connection URL (cluster-internal) */
24
+ natsUrl: Output<string>;
25
+ /** NATS auth credentials (optional) */
26
+ credentials?: {
27
+ user?: Output<string>;
28
+ password?: Output<string>;
29
+ };
30
+ /** Contracts module (e.g., `import * as contracts from '@my-org/contracts'`) */
31
+ contracts: Record<string, unknown>;
32
+ /** Stream-specific policy overrides */
33
+ policies?: Record<string, Partial<StreamPolicy>>;
34
+ /** Default policy for all streams */
35
+ defaults?: Partial<StreamPolicy>;
36
+ /** nats-box image override (default: natsio/nats-box:0.14.5) */
37
+ image?: string;
38
+ /** Job TTL after completion in seconds (default: 300) */
39
+ ttlAfterFinished?: number;
40
+ /** Max retry attempts (default: 3) */
41
+ backoffLimit?: number;
42
+ }
43
+ /**
44
+ * Materialize NATS JetStream streams via a K8s Job.
45
+ *
46
+ * Creates a ConfigMap with a setup script and a Job that runs it
47
+ * inside the cluster where NATS is reachable. Uses nats-box image
48
+ * (no custom image build needed).
49
+ *
50
+ * Validation runs at Pulumi plan time (outside cluster).
51
+ * Materialization runs at deploy time (inside cluster via K8s Job).
52
+ *
53
+ * @example
54
+ * ```typescript
55
+ * import { materializeStreams } from '@crossdelta/infrastructure'
56
+ * import * as contracts from '@my-org/contracts'
57
+ * import { STREAM_POLICIES, DEFAULT_POLICY } from '@my-org/contracts'
58
+ *
59
+ * materializeStreams(provider, 'my-namespace', {
60
+ * natsUrl: runtime.natsUrl,
61
+ * credentials: { user: natsUser, password: natsPassword },
62
+ * contracts,
63
+ * policies: STREAM_POLICIES,
64
+ * defaults: DEFAULT_POLICY,
65
+ * })
66
+ * ```
67
+ */
68
+ export declare const materializeStreams: (provider: Provider, namespace: string, config: MaterializeStreamsConfig) => k8s.batch.v1.Job;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Stream Setup Script Generator
3
+ *
4
+ * Pure functions for generating NATS JetStream setup scripts.
5
+ * No Pulumi or K8s dependencies — fully testable with Bun.
6
+ *
7
+ * Used by:
8
+ * - `stream-job.ts` (K8s Job materialization)
9
+ * - Tests (direct import)
10
+ *
11
+ * @module @crossdelta/infrastructure/lib/helpers/stream-setup
12
+ */
13
+ import type { StreamPolicy } from './deploy-streams';
14
+ import type { StreamDefinition } from './streams';
15
+ export interface ResolvedStream {
16
+ stream: string;
17
+ subjects: string[];
18
+ retention: string;
19
+ storage: string;
20
+ maxAge: string;
21
+ replicas: number;
22
+ maxMsgSize: number;
23
+ }
24
+ /**
25
+ * Convert milliseconds to NATS CLI duration format (e.g., "168h0m0s")
26
+ */
27
+ export declare const msToNatsDuration: (ms: number) => string;
28
+ /**
29
+ * Apply policies and produce resolved stream configs (pure)
30
+ */
31
+ export declare const resolveStreams: (definitions: StreamDefinition[], policies?: Record<string, Partial<StreamPolicy>>, defaults?: Partial<StreamPolicy>) => ResolvedStream[];
32
+ /**
33
+ * Generate a shell script that creates/updates NATS JetStream streams.
34
+ * Uses `nats` CLI from nats-box. Idempotent: creates if missing, updates if exists.
35
+ */
36
+ export declare const generateSetupScript: (streams: ResolvedStream[]) => string;
37
+ /**
38
+ * Compute a short hash from stream config for change detection.
39
+ * When config changes, the Job name changes → Pulumi recreates it.
40
+ */
41
+ export declare const computeConfigHash: (streams: ResolvedStream[]) => string;
package/dist/index.cjs CHANGED
@@ -44,8 +44,11 @@ var exports_lib = {};
44
44
  __export(exports_lib, {
45
45
  validateStreamDefinitionsPure: () => validateStreamDefinitionsPure,
46
46
  validateStreamDefinitions: () => validateStreamDefinitions,
47
+ resolveStreams: () => resolveStreams,
47
48
  registerRuntime: () => registerRuntime,
48
49
  ports: () => ports,
50
+ msToNatsDuration: () => msToNatsDuration,
51
+ materializeStreams: () => materializeStreams,
49
52
  hasPublicPort: () => hasPublicPort,
50
53
  getStreamNames: () => getStreamNames,
51
54
  getStreamDefinition: () => getStreamDefinition,
@@ -53,6 +56,7 @@ __export(exports_lib, {
53
56
  getPublicPorts: () => getPublicPorts,
54
57
  getPrimaryPort: () => getPrimaryPort,
55
58
  getAllPorts: () => getAllPorts,
59
+ generateSetupScript: () => generateSetupScript,
56
60
  generateLocalSetupScript: () => generateLocalSetupScript,
57
61
  generateKubectlApplyCommand: () => generateKubectlApplyCommand,
58
62
  generateK3dDeleteCommand: () => generateK3dDeleteCommand,
@@ -81,6 +85,7 @@ __export(exports_lib, {
81
85
  createImagePullSecret: () => createImagePullSecret,
82
86
  createDOKSCluster: () => createDOKSCluster,
83
87
  convertToComposeService: () => convertToComposeService,
88
+ computeConfigHash: () => computeConfigHash,
84
89
  collectStreamDefinitions: () => collectStreamDefinitions,
85
90
  buildOtelEnv: () => buildOtelEnv,
86
91
  buildNatsUrl: () => buildNatsUrl,
@@ -933,9 +938,175 @@ var buildOtelEnv = (serviceName, config) => {
933
938
  }
934
939
  return env;
935
940
  };
941
+ // lib/helpers/stream-job.ts
942
+ var k8s4 = __toESM(require("@pulumi/kubernetes"));
943
+
944
+ // lib/helpers/stream-setup.ts
945
+ var DEFAULT_POLICY = {
946
+ maxAge: 7 * 24 * 60 * 60 * 1000,
947
+ storage: "file",
948
+ replicas: 1,
949
+ maxMsgSize: 1024 * 1024
950
+ };
951
+ var msToNatsDuration = (ms) => {
952
+ const totalSeconds = Math.floor(ms / 1000);
953
+ const hours = Math.floor(totalSeconds / 3600);
954
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
955
+ const seconds = totalSeconds % 60;
956
+ return `${hours}h${minutes}m${seconds}s`;
957
+ };
958
+ var resolveStreams = (definitions, policies = {}, defaults = DEFAULT_POLICY) => definitions.map(({ stream, subjects }) => {
959
+ const merged = { ...DEFAULT_POLICY, ...defaults, ...policies[stream] };
960
+ return {
961
+ stream,
962
+ subjects,
963
+ retention: "limits",
964
+ storage: merged.storage ?? "file",
965
+ maxAge: msToNatsDuration(merged.maxAge ?? 7 * 24 * 60 * 60 * 1000),
966
+ replicas: merged.replicas ?? 1,
967
+ maxMsgSize: merged.maxMsgSize ?? 1024 * 1024
968
+ };
969
+ });
970
+ var generateSetupScript = (streams) => {
971
+ const streamCommands = streams.map((s) => {
972
+ const subjects = s.subjects.join(",");
973
+ const args = [
974
+ `--retention ${s.retention}`,
975
+ `--storage ${s.storage}`,
976
+ `--max-age ${s.maxAge}`,
977
+ `--replicas ${s.replicas}`,
978
+ `--max-msg-size ${s.maxMsgSize}`
979
+ ].join(" ");
980
+ return [
981
+ `echo "Processing stream: ${s.stream} (${subjects})"`,
982
+ `if nats $NATS_OPTS stream info "${s.stream}" > /dev/null 2>&1; then`,
983
+ ` echo " Updating existing stream..."`,
984
+ ` nats $NATS_OPTS stream edit "${s.stream}" --subjects "${subjects}" ${args} --force`,
985
+ `else`,
986
+ ` echo " Creating new stream..."`,
987
+ ` nats $NATS_OPTS stream add "${s.stream}" --subjects "${subjects}" ${args} --defaults`,
988
+ `fi`,
989
+ `echo " ✅ ${s.stream} ready"`,
990
+ ``
991
+ ].join(`
992
+ `);
993
+ });
994
+ return [
995
+ `#!/bin/sh`,
996
+ `set -e`,
997
+ ``,
998
+ `NATS_OPTS="--server $NATS_URL"`,
999
+ `if [ -n "$NATS_USER" ] && [ -n "$NATS_PASSWORD" ]; then`,
1000
+ ` NATS_OPTS="$NATS_OPTS --user $NATS_USER --password $NATS_PASSWORD"`,
1001
+ `fi`,
1002
+ ``,
1003
+ `echo "\uD83D\uDE80 Materializing ${streams.length} stream(s)..."`,
1004
+ `echo ""`,
1005
+ ``,
1006
+ ...streamCommands,
1007
+ `echo ""`,
1008
+ `echo "✅ All streams materialized successfully"`
1009
+ ].join(`
1010
+ `);
1011
+ };
1012
+ var computeConfigHash = (streams) => {
1013
+ const data = JSON.stringify(streams);
1014
+ let hash = 0;
1015
+ for (let i = 0;i < data.length; i++) {
1016
+ const char = data.charCodeAt(i);
1017
+ hash = (hash << 5) - hash + char | 0;
1018
+ }
1019
+ return Math.abs(hash).toString(36).slice(0, 8);
1020
+ };
1021
+
1022
+ // lib/helpers/stream-job.ts
1023
+ var DEFAULT_POLICY2 = {
1024
+ maxAge: 7 * 24 * 60 * 60 * 1000,
1025
+ storage: "file",
1026
+ replicas: 1,
1027
+ maxMsgSize: 1024 * 1024
1028
+ };
1029
+ var NATS_BOX_IMAGE = "natsio/nats-box:0.14.5";
1030
+ var materializeStreams = (provider, namespace, config) => {
1031
+ const {
1032
+ contracts,
1033
+ policies = {},
1034
+ defaults = DEFAULT_POLICY2,
1035
+ image = NATS_BOX_IMAGE,
1036
+ ttlAfterFinished = 300,
1037
+ backoffLimit = 3
1038
+ } = config;
1039
+ validateStreamDefinitions(contracts);
1040
+ const definitions = collectStreamDefinitions(contracts);
1041
+ const resolved = resolveStreams(definitions, policies, defaults);
1042
+ const script = generateSetupScript(resolved);
1043
+ const configHash = computeConfigHash(resolved);
1044
+ console.log(`\uD83D\uDCCB Collecting stream definitions from contracts...`);
1045
+ console.log(`✅ Found ${definitions.length} stream(s):`);
1046
+ for (const { stream, subjects } of definitions) {
1047
+ console.log(` - ${stream}: ${subjects.join(", ")}`);
1048
+ }
1049
+ const configMap = new k8s4.core.v1.ConfigMap("stream-setup-config", {
1050
+ metadata: {
1051
+ name: `stream-setup-${configHash}`,
1052
+ namespace,
1053
+ labels: { app: "stream-setup", "config-hash": configHash }
1054
+ },
1055
+ data: {
1056
+ "setup.sh": script,
1057
+ "streams.json": JSON.stringify(resolved, null, 2)
1058
+ }
1059
+ }, { provider });
1060
+ const envVars = [{ name: "NATS_URL", value: config.natsUrl }];
1061
+ if (config.credentials?.user) {
1062
+ envVars.push({ name: "NATS_USER", value: config.credentials.user });
1063
+ }
1064
+ if (config.credentials?.password) {
1065
+ envVars.push({ name: "NATS_PASSWORD", value: config.credentials.password });
1066
+ }
1067
+ const job = new k8s4.batch.v1.Job("stream-setup", {
1068
+ metadata: {
1069
+ name: `stream-setup-${configHash}`,
1070
+ namespace,
1071
+ labels: { app: "stream-setup", "config-hash": configHash }
1072
+ },
1073
+ spec: {
1074
+ backoffLimit,
1075
+ ttlSecondsAfterFinished: ttlAfterFinished,
1076
+ template: {
1077
+ metadata: {
1078
+ labels: { app: "stream-setup" }
1079
+ },
1080
+ spec: {
1081
+ restartPolicy: "Never",
1082
+ containers: [
1083
+ {
1084
+ name: "stream-setup",
1085
+ image,
1086
+ command: ["sh", "/config/setup.sh"],
1087
+ env: envVars,
1088
+ volumeMounts: [{ name: "config", mountPath: "/config", readOnly: true }]
1089
+ }
1090
+ ],
1091
+ volumes: [
1092
+ {
1093
+ name: "config",
1094
+ configMap: { name: configMap.metadata.name }
1095
+ }
1096
+ ]
1097
+ }
1098
+ }
1099
+ }
1100
+ }, {
1101
+ provider,
1102
+ dependsOn: [configMap],
1103
+ deleteBeforeReplace: true
1104
+ });
1105
+ return job;
1106
+ };
936
1107
  // lib/runtimes/doks/cluster.ts
937
1108
  var digitalocean = __toESM(require("@pulumi/digitalocean"));
938
- var k8s4 = __toESM(require("@pulumi/kubernetes"));
1109
+ var k8s5 = __toESM(require("@pulumi/kubernetes"));
939
1110
  var pulumi4 = __toESM(require("@pulumi/pulumi"));
940
1111
  function createDOKSCluster(config) {
941
1112
  const stack = pulumi4.getStack();
@@ -970,7 +1141,7 @@ function createDOKSCluster(config) {
970
1141
  }
971
1142
  return firstConfig.rawConfig;
972
1143
  });
973
- const provider = new k8s4.Provider(`${config.name}-k8s-provider`, {
1144
+ const provider = new k8s5.Provider(`${config.name}-k8s-provider`, {
974
1145
  kubeconfig
975
1146
  });
976
1147
  return {
@@ -981,7 +1152,7 @@ function createDOKSCluster(config) {
981
1152
  };
982
1153
  }
983
1154
  function createK8sProviderFromKubeconfig(name, kubeconfig) {
984
- return new k8s4.Provider(name, { kubeconfig });
1155
+ return new k8s5.Provider(name, { kubeconfig });
985
1156
  }
986
1157
  // lib/runtimes/doks/vpc.ts
987
1158
  var digitalocean2 = __toESM(require("@pulumi/digitalocean"));
@@ -997,7 +1168,7 @@ function createVPC(config) {
997
1168
  });
998
1169
  }
999
1170
  // lib/runtimes/doks/workloads.ts
1000
- var k8s5 = __toESM(require("@pulumi/kubernetes"));
1171
+ var k8s6 = __toESM(require("@pulumi/kubernetes"));
1001
1172
  var pulumi6 = __toESM(require("@pulumi/pulumi"));
1002
1173
  var normalizeK8sConfig = (config) => {
1003
1174
  if (config.ports) {
@@ -1047,7 +1218,7 @@ var createImagePullSecret = (provider, namespace, name, config) => {
1047
1218
  }
1048
1219
  });
1049
1220
  });
1050
- return new k8s5.core.v1.Secret(name, {
1221
+ return new k8s6.core.v1.Secret(name, {
1051
1222
  metadata: {
1052
1223
  name,
1053
1224
  namespace,
@@ -1077,7 +1248,7 @@ var buildEnvVars = (config) => {
1077
1248
  })) : [];
1078
1249
  return [portEnv, ...plainEnvVars, ...secretEnvVars];
1079
1250
  };
1080
- var createServiceSecret = (provider, namespace, config, labels) => !config.secrets || Object.keys(config.secrets).length === 0 ? undefined : new k8s5.core.v1.Secret(`${config.name}-secret`, {
1251
+ var createServiceSecret = (provider, namespace, config, labels) => !config.secrets || Object.keys(config.secrets).length === 0 ? undefined : new k8s6.core.v1.Secret(`${config.name}-secret`, {
1081
1252
  metadata: {
1082
1253
  name: `${config.name}-secret`,
1083
1254
  namespace,
@@ -1090,7 +1261,7 @@ var createServiceVolumes = (provider, namespace, config, labels) => {
1090
1261
  if (!config.volumes) {
1091
1262
  return { pvcs: [], volumeMounts: [], volumes: [] };
1092
1263
  }
1093
- const pvcs = config.volumes.map((vol) => new k8s5.core.v1.PersistentVolumeClaim(`${config.name}-${vol.name}`, {
1264
+ const pvcs = config.volumes.map((vol) => new k8s6.core.v1.PersistentVolumeClaim(`${config.name}-${vol.name}`, {
1094
1265
  metadata: {
1095
1266
  name: `${config.name}-${vol.name}`,
1096
1267
  namespace,
@@ -1206,7 +1377,7 @@ var createServiceIngress = (provider, namespace, config, labels, service) => {
1206
1377
  const allHosts = [...primaryHosts, ...additionalHosts];
1207
1378
  const ingressRules = allHosts.length > 0 ? allHosts.map(createRule) : [createRule()];
1208
1379
  const tlsSecretName = config.ingress.tls?.secretName ?? `${config.name}-tls`;
1209
- return new k8s5.networking.v1.Ingress(`${config.name}-ingress`, {
1380
+ return new k8s6.networking.v1.Ingress(`${config.name}-ingress`, {
1210
1381
  metadata: {
1211
1382
  name: config.name,
1212
1383
  namespace,
@@ -1236,7 +1407,7 @@ var deployK8sService = (provider, namespace, config) => {
1236
1407
  const { livenessProbe, readinessProbe } = buildHealthProbes(normalizedConfig);
1237
1408
  const containerPorts = buildContainerPorts(normalizedConfig);
1238
1409
  const servicePorts = buildServicePorts(normalizedConfig);
1239
- const deployment = new k8s5.apps.v1.Deployment(`${normalizedConfig.name}-deployment`, {
1410
+ const deployment = new k8s6.apps.v1.Deployment(`${normalizedConfig.name}-deployment`, {
1240
1411
  metadata: {
1241
1412
  name: normalizedConfig.name,
1242
1413
  namespace,
@@ -1277,7 +1448,7 @@ var deployK8sService = (provider, namespace, config) => {
1277
1448
  }
1278
1449
  }
1279
1450
  }, { provider, dependsOn: pvcs.length > 0 ? pvcs : undefined });
1280
- const service = new k8s5.core.v1.Service(`${normalizedConfig.name}-service`, {
1451
+ const service = new k8s6.core.v1.Service(`${normalizedConfig.name}-service`, {
1281
1452
  metadata: {
1282
1453
  name: normalizedConfig.name,
1283
1454
  namespace,
@@ -1310,7 +1481,7 @@ var deployK8sServices = (provider, namespace, configs, options) => configs.filte
1310
1481
  results.set(config.name, deployK8sService(provider, namespace, configWithSecret));
1311
1482
  return results;
1312
1483
  }, new Map);
1313
- var createNamespace = (provider, name, labels) => new k8s5.core.v1.Namespace(name, {
1484
+ var createNamespace = (provider, name, labels) => new k8s6.core.v1.Namespace(name, {
1314
1485
  metadata: {
1315
1486
  name,
1316
1487
  labels: {
package/dist/index.js CHANGED
@@ -844,9 +844,175 @@ var buildOtelEnv = (serviceName, config) => {
844
844
  }
845
845
  return env;
846
846
  };
847
+ // lib/helpers/stream-job.ts
848
+ import * as k8s4 from "@pulumi/kubernetes";
849
+
850
+ // lib/helpers/stream-setup.ts
851
+ var DEFAULT_POLICY = {
852
+ maxAge: 7 * 24 * 60 * 60 * 1000,
853
+ storage: "file",
854
+ replicas: 1,
855
+ maxMsgSize: 1024 * 1024
856
+ };
857
+ var msToNatsDuration = (ms) => {
858
+ const totalSeconds = Math.floor(ms / 1000);
859
+ const hours = Math.floor(totalSeconds / 3600);
860
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
861
+ const seconds = totalSeconds % 60;
862
+ return `${hours}h${minutes}m${seconds}s`;
863
+ };
864
+ var resolveStreams = (definitions, policies = {}, defaults = DEFAULT_POLICY) => definitions.map(({ stream, subjects }) => {
865
+ const merged = { ...DEFAULT_POLICY, ...defaults, ...policies[stream] };
866
+ return {
867
+ stream,
868
+ subjects,
869
+ retention: "limits",
870
+ storage: merged.storage ?? "file",
871
+ maxAge: msToNatsDuration(merged.maxAge ?? 7 * 24 * 60 * 60 * 1000),
872
+ replicas: merged.replicas ?? 1,
873
+ maxMsgSize: merged.maxMsgSize ?? 1024 * 1024
874
+ };
875
+ });
876
+ var generateSetupScript = (streams) => {
877
+ const streamCommands = streams.map((s) => {
878
+ const subjects = s.subjects.join(",");
879
+ const args = [
880
+ `--retention ${s.retention}`,
881
+ `--storage ${s.storage}`,
882
+ `--max-age ${s.maxAge}`,
883
+ `--replicas ${s.replicas}`,
884
+ `--max-msg-size ${s.maxMsgSize}`
885
+ ].join(" ");
886
+ return [
887
+ `echo "Processing stream: ${s.stream} (${subjects})"`,
888
+ `if nats $NATS_OPTS stream info "${s.stream}" > /dev/null 2>&1; then`,
889
+ ` echo " Updating existing stream..."`,
890
+ ` nats $NATS_OPTS stream edit "${s.stream}" --subjects "${subjects}" ${args} --force`,
891
+ `else`,
892
+ ` echo " Creating new stream..."`,
893
+ ` nats $NATS_OPTS stream add "${s.stream}" --subjects "${subjects}" ${args} --defaults`,
894
+ `fi`,
895
+ `echo " ✅ ${s.stream} ready"`,
896
+ ``
897
+ ].join(`
898
+ `);
899
+ });
900
+ return [
901
+ `#!/bin/sh`,
902
+ `set -e`,
903
+ ``,
904
+ `NATS_OPTS="--server $NATS_URL"`,
905
+ `if [ -n "$NATS_USER" ] && [ -n "$NATS_PASSWORD" ]; then`,
906
+ ` NATS_OPTS="$NATS_OPTS --user $NATS_USER --password $NATS_PASSWORD"`,
907
+ `fi`,
908
+ ``,
909
+ `echo "\uD83D\uDE80 Materializing ${streams.length} stream(s)..."`,
910
+ `echo ""`,
911
+ ``,
912
+ ...streamCommands,
913
+ `echo ""`,
914
+ `echo "✅ All streams materialized successfully"`
915
+ ].join(`
916
+ `);
917
+ };
918
+ var computeConfigHash = (streams) => {
919
+ const data = JSON.stringify(streams);
920
+ let hash = 0;
921
+ for (let i = 0;i < data.length; i++) {
922
+ const char = data.charCodeAt(i);
923
+ hash = (hash << 5) - hash + char | 0;
924
+ }
925
+ return Math.abs(hash).toString(36).slice(0, 8);
926
+ };
927
+
928
+ // lib/helpers/stream-job.ts
929
+ var DEFAULT_POLICY2 = {
930
+ maxAge: 7 * 24 * 60 * 60 * 1000,
931
+ storage: "file",
932
+ replicas: 1,
933
+ maxMsgSize: 1024 * 1024
934
+ };
935
+ var NATS_BOX_IMAGE = "natsio/nats-box:0.14.5";
936
+ var materializeStreams = (provider, namespace, config) => {
937
+ const {
938
+ contracts,
939
+ policies = {},
940
+ defaults = DEFAULT_POLICY2,
941
+ image = NATS_BOX_IMAGE,
942
+ ttlAfterFinished = 300,
943
+ backoffLimit = 3
944
+ } = config;
945
+ validateStreamDefinitions(contracts);
946
+ const definitions = collectStreamDefinitions(contracts);
947
+ const resolved = resolveStreams(definitions, policies, defaults);
948
+ const script = generateSetupScript(resolved);
949
+ const configHash = computeConfigHash(resolved);
950
+ console.log(`\uD83D\uDCCB Collecting stream definitions from contracts...`);
951
+ console.log(`✅ Found ${definitions.length} stream(s):`);
952
+ for (const { stream, subjects } of definitions) {
953
+ console.log(` - ${stream}: ${subjects.join(", ")}`);
954
+ }
955
+ const configMap = new k8s4.core.v1.ConfigMap("stream-setup-config", {
956
+ metadata: {
957
+ name: `stream-setup-${configHash}`,
958
+ namespace,
959
+ labels: { app: "stream-setup", "config-hash": configHash }
960
+ },
961
+ data: {
962
+ "setup.sh": script,
963
+ "streams.json": JSON.stringify(resolved, null, 2)
964
+ }
965
+ }, { provider });
966
+ const envVars = [{ name: "NATS_URL", value: config.natsUrl }];
967
+ if (config.credentials?.user) {
968
+ envVars.push({ name: "NATS_USER", value: config.credentials.user });
969
+ }
970
+ if (config.credentials?.password) {
971
+ envVars.push({ name: "NATS_PASSWORD", value: config.credentials.password });
972
+ }
973
+ const job = new k8s4.batch.v1.Job("stream-setup", {
974
+ metadata: {
975
+ name: `stream-setup-${configHash}`,
976
+ namespace,
977
+ labels: { app: "stream-setup", "config-hash": configHash }
978
+ },
979
+ spec: {
980
+ backoffLimit,
981
+ ttlSecondsAfterFinished: ttlAfterFinished,
982
+ template: {
983
+ metadata: {
984
+ labels: { app: "stream-setup" }
985
+ },
986
+ spec: {
987
+ restartPolicy: "Never",
988
+ containers: [
989
+ {
990
+ name: "stream-setup",
991
+ image,
992
+ command: ["sh", "/config/setup.sh"],
993
+ env: envVars,
994
+ volumeMounts: [{ name: "config", mountPath: "/config", readOnly: true }]
995
+ }
996
+ ],
997
+ volumes: [
998
+ {
999
+ name: "config",
1000
+ configMap: { name: configMap.metadata.name }
1001
+ }
1002
+ ]
1003
+ }
1004
+ }
1005
+ }
1006
+ }, {
1007
+ provider,
1008
+ dependsOn: [configMap],
1009
+ deleteBeforeReplace: true
1010
+ });
1011
+ return job;
1012
+ };
847
1013
  // lib/runtimes/doks/cluster.ts
848
1014
  import * as digitalocean from "@pulumi/digitalocean";
849
- import * as k8s4 from "@pulumi/kubernetes";
1015
+ import * as k8s5 from "@pulumi/kubernetes";
850
1016
  import * as pulumi4 from "@pulumi/pulumi";
851
1017
  function createDOKSCluster(config) {
852
1018
  const stack = pulumi4.getStack();
@@ -881,7 +1047,7 @@ function createDOKSCluster(config) {
881
1047
  }
882
1048
  return firstConfig.rawConfig;
883
1049
  });
884
- const provider = new k8s4.Provider(`${config.name}-k8s-provider`, {
1050
+ const provider = new k8s5.Provider(`${config.name}-k8s-provider`, {
885
1051
  kubeconfig
886
1052
  });
887
1053
  return {
@@ -892,7 +1058,7 @@ function createDOKSCluster(config) {
892
1058
  };
893
1059
  }
894
1060
  function createK8sProviderFromKubeconfig(name, kubeconfig) {
895
- return new k8s4.Provider(name, { kubeconfig });
1061
+ return new k8s5.Provider(name, { kubeconfig });
896
1062
  }
897
1063
  // lib/runtimes/doks/vpc.ts
898
1064
  import * as digitalocean2 from "@pulumi/digitalocean";
@@ -908,7 +1074,7 @@ function createVPC(config) {
908
1074
  });
909
1075
  }
910
1076
  // lib/runtimes/doks/workloads.ts
911
- import * as k8s5 from "@pulumi/kubernetes";
1077
+ import * as k8s6 from "@pulumi/kubernetes";
912
1078
  import * as pulumi6 from "@pulumi/pulumi";
913
1079
  var normalizeK8sConfig = (config) => {
914
1080
  if (config.ports) {
@@ -958,7 +1124,7 @@ var createImagePullSecret = (provider, namespace, name, config) => {
958
1124
  }
959
1125
  });
960
1126
  });
961
- return new k8s5.core.v1.Secret(name, {
1127
+ return new k8s6.core.v1.Secret(name, {
962
1128
  metadata: {
963
1129
  name,
964
1130
  namespace,
@@ -988,7 +1154,7 @@ var buildEnvVars = (config) => {
988
1154
  })) : [];
989
1155
  return [portEnv, ...plainEnvVars, ...secretEnvVars];
990
1156
  };
991
- var createServiceSecret = (provider, namespace, config, labels) => !config.secrets || Object.keys(config.secrets).length === 0 ? undefined : new k8s5.core.v1.Secret(`${config.name}-secret`, {
1157
+ var createServiceSecret = (provider, namespace, config, labels) => !config.secrets || Object.keys(config.secrets).length === 0 ? undefined : new k8s6.core.v1.Secret(`${config.name}-secret`, {
992
1158
  metadata: {
993
1159
  name: `${config.name}-secret`,
994
1160
  namespace,
@@ -1001,7 +1167,7 @@ var createServiceVolumes = (provider, namespace, config, labels) => {
1001
1167
  if (!config.volumes) {
1002
1168
  return { pvcs: [], volumeMounts: [], volumes: [] };
1003
1169
  }
1004
- const pvcs = config.volumes.map((vol) => new k8s5.core.v1.PersistentVolumeClaim(`${config.name}-${vol.name}`, {
1170
+ const pvcs = config.volumes.map((vol) => new k8s6.core.v1.PersistentVolumeClaim(`${config.name}-${vol.name}`, {
1005
1171
  metadata: {
1006
1172
  name: `${config.name}-${vol.name}`,
1007
1173
  namespace,
@@ -1117,7 +1283,7 @@ var createServiceIngress = (provider, namespace, config, labels, service) => {
1117
1283
  const allHosts = [...primaryHosts, ...additionalHosts];
1118
1284
  const ingressRules = allHosts.length > 0 ? allHosts.map(createRule) : [createRule()];
1119
1285
  const tlsSecretName = config.ingress.tls?.secretName ?? `${config.name}-tls`;
1120
- return new k8s5.networking.v1.Ingress(`${config.name}-ingress`, {
1286
+ return new k8s6.networking.v1.Ingress(`${config.name}-ingress`, {
1121
1287
  metadata: {
1122
1288
  name: config.name,
1123
1289
  namespace,
@@ -1147,7 +1313,7 @@ var deployK8sService = (provider, namespace, config) => {
1147
1313
  const { livenessProbe, readinessProbe } = buildHealthProbes(normalizedConfig);
1148
1314
  const containerPorts = buildContainerPorts(normalizedConfig);
1149
1315
  const servicePorts = buildServicePorts(normalizedConfig);
1150
- const deployment = new k8s5.apps.v1.Deployment(`${normalizedConfig.name}-deployment`, {
1316
+ const deployment = new k8s6.apps.v1.Deployment(`${normalizedConfig.name}-deployment`, {
1151
1317
  metadata: {
1152
1318
  name: normalizedConfig.name,
1153
1319
  namespace,
@@ -1188,7 +1354,7 @@ var deployK8sService = (provider, namespace, config) => {
1188
1354
  }
1189
1355
  }
1190
1356
  }, { provider, dependsOn: pvcs.length > 0 ? pvcs : undefined });
1191
- const service = new k8s5.core.v1.Service(`${normalizedConfig.name}-service`, {
1357
+ const service = new k8s6.core.v1.Service(`${normalizedConfig.name}-service`, {
1192
1358
  metadata: {
1193
1359
  name: normalizedConfig.name,
1194
1360
  namespace,
@@ -1221,7 +1387,7 @@ var deployK8sServices = (provider, namespace, configs, options) => configs.filte
1221
1387
  results.set(config.name, deployK8sService(provider, namespace, configWithSecret));
1222
1388
  return results;
1223
1389
  }, new Map);
1224
- var createNamespace = (provider, name, labels) => new k8s5.core.v1.Namespace(name, {
1390
+ var createNamespace = (provider, name, labels) => new k8s6.core.v1.Namespace(name, {
1225
1391
  metadata: {
1226
1392
  name,
1227
1393
  labels: {
@@ -1595,8 +1761,11 @@ var generateLocalSetupScript = (services, options = {}) => {
1595
1761
  export {
1596
1762
  validateStreamDefinitionsPure,
1597
1763
  validateStreamDefinitions,
1764
+ resolveStreams,
1598
1765
  registerRuntime,
1599
1766
  ports,
1767
+ msToNatsDuration,
1768
+ materializeStreams,
1600
1769
  hasPublicPort,
1601
1770
  getStreamNames,
1602
1771
  getStreamDefinition,
@@ -1604,6 +1773,7 @@ export {
1604
1773
  getPublicPorts,
1605
1774
  getPrimaryPort,
1606
1775
  getAllPorts,
1776
+ generateSetupScript,
1607
1777
  generateLocalSetupScript,
1608
1778
  generateKubectlApplyCommand,
1609
1779
  generateK3dDeleteCommand,
@@ -1632,6 +1802,7 @@ export {
1632
1802
  createImagePullSecret,
1633
1803
  createDOKSCluster,
1634
1804
  convertToComposeService,
1805
+ computeConfigHash,
1635
1806
  collectStreamDefinitions,
1636
1807
  buildOtelEnv,
1637
1808
  buildNatsUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crossdelta/infrastructure",
3
- "version": "0.5.5",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -35,7 +35,7 @@
35
35
  }
36
36
  },
37
37
  "dependencies": {
38
- "@crossdelta/cloudevents": "0.6.3"
38
+ "@crossdelta/cloudevents": "0.6.4"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@pulumi/digitalocean": "^4.0.0",