@crossdelta/infrastructure 0.5.5 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +184 -11
- package/dist/index.js +184 -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();
|
|
@@ -962,6 +1133,8 @@ function createDOKSCluster(config) {
|
|
|
962
1133
|
maxNodes: config.nodePool.autoScale?.maxNodes,
|
|
963
1134
|
labels: config.nodePool.labels
|
|
964
1135
|
}
|
|
1136
|
+
}, {
|
|
1137
|
+
ignoreChanges: ["version"]
|
|
965
1138
|
});
|
|
966
1139
|
const kubeconfig = cluster.kubeConfigs.apply((configs) => {
|
|
967
1140
|
const firstConfig = configs?.[0];
|
|
@@ -970,7 +1143,7 @@ function createDOKSCluster(config) {
|
|
|
970
1143
|
}
|
|
971
1144
|
return firstConfig.rawConfig;
|
|
972
1145
|
});
|
|
973
|
-
const provider = new
|
|
1146
|
+
const provider = new k8s5.Provider(`${config.name}-k8s-provider`, {
|
|
974
1147
|
kubeconfig
|
|
975
1148
|
});
|
|
976
1149
|
return {
|
|
@@ -981,7 +1154,7 @@ function createDOKSCluster(config) {
|
|
|
981
1154
|
};
|
|
982
1155
|
}
|
|
983
1156
|
function createK8sProviderFromKubeconfig(name, kubeconfig) {
|
|
984
|
-
return new
|
|
1157
|
+
return new k8s5.Provider(name, { kubeconfig });
|
|
985
1158
|
}
|
|
986
1159
|
// lib/runtimes/doks/vpc.ts
|
|
987
1160
|
var digitalocean2 = __toESM(require("@pulumi/digitalocean"));
|
|
@@ -997,7 +1170,7 @@ function createVPC(config) {
|
|
|
997
1170
|
});
|
|
998
1171
|
}
|
|
999
1172
|
// lib/runtimes/doks/workloads.ts
|
|
1000
|
-
var
|
|
1173
|
+
var k8s6 = __toESM(require("@pulumi/kubernetes"));
|
|
1001
1174
|
var pulumi6 = __toESM(require("@pulumi/pulumi"));
|
|
1002
1175
|
var normalizeK8sConfig = (config) => {
|
|
1003
1176
|
if (config.ports) {
|
|
@@ -1047,7 +1220,7 @@ var createImagePullSecret = (provider, namespace, name, config) => {
|
|
|
1047
1220
|
}
|
|
1048
1221
|
});
|
|
1049
1222
|
});
|
|
1050
|
-
return new
|
|
1223
|
+
return new k8s6.core.v1.Secret(name, {
|
|
1051
1224
|
metadata: {
|
|
1052
1225
|
name,
|
|
1053
1226
|
namespace,
|
|
@@ -1077,7 +1250,7 @@ var buildEnvVars = (config) => {
|
|
|
1077
1250
|
})) : [];
|
|
1078
1251
|
return [portEnv, ...plainEnvVars, ...secretEnvVars];
|
|
1079
1252
|
};
|
|
1080
|
-
var createServiceSecret = (provider, namespace, config, labels) => !config.secrets || Object.keys(config.secrets).length === 0 ? undefined : new
|
|
1253
|
+
var createServiceSecret = (provider, namespace, config, labels) => !config.secrets || Object.keys(config.secrets).length === 0 ? undefined : new k8s6.core.v1.Secret(`${config.name}-secret`, {
|
|
1081
1254
|
metadata: {
|
|
1082
1255
|
name: `${config.name}-secret`,
|
|
1083
1256
|
namespace,
|
|
@@ -1090,7 +1263,7 @@ var createServiceVolumes = (provider, namespace, config, labels) => {
|
|
|
1090
1263
|
if (!config.volumes) {
|
|
1091
1264
|
return { pvcs: [], volumeMounts: [], volumes: [] };
|
|
1092
1265
|
}
|
|
1093
|
-
const pvcs = config.volumes.map((vol) => new
|
|
1266
|
+
const pvcs = config.volumes.map((vol) => new k8s6.core.v1.PersistentVolumeClaim(`${config.name}-${vol.name}`, {
|
|
1094
1267
|
metadata: {
|
|
1095
1268
|
name: `${config.name}-${vol.name}`,
|
|
1096
1269
|
namespace,
|
|
@@ -1206,7 +1379,7 @@ var createServiceIngress = (provider, namespace, config, labels, service) => {
|
|
|
1206
1379
|
const allHosts = [...primaryHosts, ...additionalHosts];
|
|
1207
1380
|
const ingressRules = allHosts.length > 0 ? allHosts.map(createRule) : [createRule()];
|
|
1208
1381
|
const tlsSecretName = config.ingress.tls?.secretName ?? `${config.name}-tls`;
|
|
1209
|
-
return new
|
|
1382
|
+
return new k8s6.networking.v1.Ingress(`${config.name}-ingress`, {
|
|
1210
1383
|
metadata: {
|
|
1211
1384
|
name: config.name,
|
|
1212
1385
|
namespace,
|
|
@@ -1236,7 +1409,7 @@ var deployK8sService = (provider, namespace, config) => {
|
|
|
1236
1409
|
const { livenessProbe, readinessProbe } = buildHealthProbes(normalizedConfig);
|
|
1237
1410
|
const containerPorts = buildContainerPorts(normalizedConfig);
|
|
1238
1411
|
const servicePorts = buildServicePorts(normalizedConfig);
|
|
1239
|
-
const deployment = new
|
|
1412
|
+
const deployment = new k8s6.apps.v1.Deployment(`${normalizedConfig.name}-deployment`, {
|
|
1240
1413
|
metadata: {
|
|
1241
1414
|
name: normalizedConfig.name,
|
|
1242
1415
|
namespace,
|
|
@@ -1277,7 +1450,7 @@ var deployK8sService = (provider, namespace, config) => {
|
|
|
1277
1450
|
}
|
|
1278
1451
|
}
|
|
1279
1452
|
}, { provider, dependsOn: pvcs.length > 0 ? pvcs : undefined });
|
|
1280
|
-
const service = new
|
|
1453
|
+
const service = new k8s6.core.v1.Service(`${normalizedConfig.name}-service`, {
|
|
1281
1454
|
metadata: {
|
|
1282
1455
|
name: normalizedConfig.name,
|
|
1283
1456
|
namespace,
|
|
@@ -1310,7 +1483,7 @@ var deployK8sServices = (provider, namespace, configs, options) => configs.filte
|
|
|
1310
1483
|
results.set(config.name, deployK8sService(provider, namespace, configWithSecret));
|
|
1311
1484
|
return results;
|
|
1312
1485
|
}, new Map);
|
|
1313
|
-
var createNamespace = (provider, name, labels) => new
|
|
1486
|
+
var createNamespace = (provider, name, labels) => new k8s6.core.v1.Namespace(name, {
|
|
1314
1487
|
metadata: {
|
|
1315
1488
|
name,
|
|
1316
1489
|
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();
|
|
@@ -873,6 +1039,8 @@ function createDOKSCluster(config) {
|
|
|
873
1039
|
maxNodes: config.nodePool.autoScale?.maxNodes,
|
|
874
1040
|
labels: config.nodePool.labels
|
|
875
1041
|
}
|
|
1042
|
+
}, {
|
|
1043
|
+
ignoreChanges: ["version"]
|
|
876
1044
|
});
|
|
877
1045
|
const kubeconfig = cluster.kubeConfigs.apply((configs) => {
|
|
878
1046
|
const firstConfig = configs?.[0];
|
|
@@ -881,7 +1049,7 @@ function createDOKSCluster(config) {
|
|
|
881
1049
|
}
|
|
882
1050
|
return firstConfig.rawConfig;
|
|
883
1051
|
});
|
|
884
|
-
const provider = new
|
|
1052
|
+
const provider = new k8s5.Provider(`${config.name}-k8s-provider`, {
|
|
885
1053
|
kubeconfig
|
|
886
1054
|
});
|
|
887
1055
|
return {
|
|
@@ -892,7 +1060,7 @@ function createDOKSCluster(config) {
|
|
|
892
1060
|
};
|
|
893
1061
|
}
|
|
894
1062
|
function createK8sProviderFromKubeconfig(name, kubeconfig) {
|
|
895
|
-
return new
|
|
1063
|
+
return new k8s5.Provider(name, { kubeconfig });
|
|
896
1064
|
}
|
|
897
1065
|
// lib/runtimes/doks/vpc.ts
|
|
898
1066
|
import * as digitalocean2 from "@pulumi/digitalocean";
|
|
@@ -908,7 +1076,7 @@ function createVPC(config) {
|
|
|
908
1076
|
});
|
|
909
1077
|
}
|
|
910
1078
|
// lib/runtimes/doks/workloads.ts
|
|
911
|
-
import * as
|
|
1079
|
+
import * as k8s6 from "@pulumi/kubernetes";
|
|
912
1080
|
import * as pulumi6 from "@pulumi/pulumi";
|
|
913
1081
|
var normalizeK8sConfig = (config) => {
|
|
914
1082
|
if (config.ports) {
|
|
@@ -958,7 +1126,7 @@ var createImagePullSecret = (provider, namespace, name, config) => {
|
|
|
958
1126
|
}
|
|
959
1127
|
});
|
|
960
1128
|
});
|
|
961
|
-
return new
|
|
1129
|
+
return new k8s6.core.v1.Secret(name, {
|
|
962
1130
|
metadata: {
|
|
963
1131
|
name,
|
|
964
1132
|
namespace,
|
|
@@ -988,7 +1156,7 @@ var buildEnvVars = (config) => {
|
|
|
988
1156
|
})) : [];
|
|
989
1157
|
return [portEnv, ...plainEnvVars, ...secretEnvVars];
|
|
990
1158
|
};
|
|
991
|
-
var createServiceSecret = (provider, namespace, config, labels) => !config.secrets || Object.keys(config.secrets).length === 0 ? undefined : new
|
|
1159
|
+
var createServiceSecret = (provider, namespace, config, labels) => !config.secrets || Object.keys(config.secrets).length === 0 ? undefined : new k8s6.core.v1.Secret(`${config.name}-secret`, {
|
|
992
1160
|
metadata: {
|
|
993
1161
|
name: `${config.name}-secret`,
|
|
994
1162
|
namespace,
|
|
@@ -1001,7 +1169,7 @@ var createServiceVolumes = (provider, namespace, config, labels) => {
|
|
|
1001
1169
|
if (!config.volumes) {
|
|
1002
1170
|
return { pvcs: [], volumeMounts: [], volumes: [] };
|
|
1003
1171
|
}
|
|
1004
|
-
const pvcs = config.volumes.map((vol) => new
|
|
1172
|
+
const pvcs = config.volumes.map((vol) => new k8s6.core.v1.PersistentVolumeClaim(`${config.name}-${vol.name}`, {
|
|
1005
1173
|
metadata: {
|
|
1006
1174
|
name: `${config.name}-${vol.name}`,
|
|
1007
1175
|
namespace,
|
|
@@ -1117,7 +1285,7 @@ var createServiceIngress = (provider, namespace, config, labels, service) => {
|
|
|
1117
1285
|
const allHosts = [...primaryHosts, ...additionalHosts];
|
|
1118
1286
|
const ingressRules = allHosts.length > 0 ? allHosts.map(createRule) : [createRule()];
|
|
1119
1287
|
const tlsSecretName = config.ingress.tls?.secretName ?? `${config.name}-tls`;
|
|
1120
|
-
return new
|
|
1288
|
+
return new k8s6.networking.v1.Ingress(`${config.name}-ingress`, {
|
|
1121
1289
|
metadata: {
|
|
1122
1290
|
name: config.name,
|
|
1123
1291
|
namespace,
|
|
@@ -1147,7 +1315,7 @@ var deployK8sService = (provider, namespace, config) => {
|
|
|
1147
1315
|
const { livenessProbe, readinessProbe } = buildHealthProbes(normalizedConfig);
|
|
1148
1316
|
const containerPorts = buildContainerPorts(normalizedConfig);
|
|
1149
1317
|
const servicePorts = buildServicePorts(normalizedConfig);
|
|
1150
|
-
const deployment = new
|
|
1318
|
+
const deployment = new k8s6.apps.v1.Deployment(`${normalizedConfig.name}-deployment`, {
|
|
1151
1319
|
metadata: {
|
|
1152
1320
|
name: normalizedConfig.name,
|
|
1153
1321
|
namespace,
|
|
@@ -1188,7 +1356,7 @@ var deployK8sService = (provider, namespace, config) => {
|
|
|
1188
1356
|
}
|
|
1189
1357
|
}
|
|
1190
1358
|
}, { provider, dependsOn: pvcs.length > 0 ? pvcs : undefined });
|
|
1191
|
-
const service = new
|
|
1359
|
+
const service = new k8s6.core.v1.Service(`${normalizedConfig.name}-service`, {
|
|
1192
1360
|
metadata: {
|
|
1193
1361
|
name: normalizedConfig.name,
|
|
1194
1362
|
namespace,
|
|
@@ -1221,7 +1389,7 @@ var deployK8sServices = (provider, namespace, configs, options) => configs.filte
|
|
|
1221
1389
|
results.set(config.name, deployK8sService(provider, namespace, configWithSecret));
|
|
1222
1390
|
return results;
|
|
1223
1391
|
}, new Map);
|
|
1224
|
-
var createNamespace = (provider, name, labels) => new
|
|
1392
|
+
var createNamespace = (provider, name, labels) => new k8s6.core.v1.Namespace(name, {
|
|
1225
1393
|
metadata: {
|
|
1226
1394
|
name,
|
|
1227
1395
|
labels: {
|
|
@@ -1595,8 +1763,11 @@ var generateLocalSetupScript = (services, options = {}) => {
|
|
|
1595
1763
|
export {
|
|
1596
1764
|
validateStreamDefinitionsPure,
|
|
1597
1765
|
validateStreamDefinitions,
|
|
1766
|
+
resolveStreams,
|
|
1598
1767
|
registerRuntime,
|
|
1599
1768
|
ports,
|
|
1769
|
+
msToNatsDuration,
|
|
1770
|
+
materializeStreams,
|
|
1600
1771
|
hasPublicPort,
|
|
1601
1772
|
getStreamNames,
|
|
1602
1773
|
getStreamDefinition,
|
|
@@ -1604,6 +1775,7 @@ export {
|
|
|
1604
1775
|
getPublicPorts,
|
|
1605
1776
|
getPrimaryPort,
|
|
1606
1777
|
getAllPorts,
|
|
1778
|
+
generateSetupScript,
|
|
1607
1779
|
generateLocalSetupScript,
|
|
1608
1780
|
generateKubectlApplyCommand,
|
|
1609
1781
|
generateK3dDeleteCommand,
|
|
@@ -1632,6 +1804,7 @@ export {
|
|
|
1632
1804
|
createImagePullSecret,
|
|
1633
1805
|
createDOKSCluster,
|
|
1634
1806
|
convertToComposeService,
|
|
1807
|
+
computeConfigHash,
|
|
1635
1808
|
collectStreamDefinitions,
|
|
1636
1809
|
buildOtelEnv,
|
|
1637
1810
|
buildNatsUrl,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crossdelta/infrastructure",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
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",
|