@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.
- package/dist/helpers/index.d.ts +2 -0
- package/dist/helpers/stream-job.d.ts +68 -0
- package/dist/helpers/stream-setup.d.ts +41 -0
- package/dist/index.cjs +182 -11
- package/dist/index.js +182 -11
- package/package.json +2 -2
package/dist/helpers/index.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
38
|
+
"@crossdelta/cloudevents": "0.6.4"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@pulumi/digitalocean": "^4.0.0",
|