@crossdelta/infrastructure 0.1.35
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/LICENSE +21 -0
- package/README.md +173 -0
- package/dist/env.cjs +53 -0
- package/dist/env.d.ts +9 -0
- package/dist/env.js +21 -0
- package/dist/helpers/config.d.ts +29 -0
- package/dist/helpers/discover-services.d.ts +7 -0
- package/dist/helpers/docker-hub-image.d.ts +14 -0
- package/dist/helpers/image.d.ts +13 -0
- package/dist/helpers/index.d.ts +7 -0
- package/dist/helpers/service-builder.d.ts +27 -0
- package/dist/helpers/service-runtime.d.ts +39 -0
- package/dist/helpers/service-urls.d.ts +27 -0
- package/dist/index.cjs +260 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +231 -0
- package/dist/types/index.d.ts +81 -0
- package/dist/types/service-names.d.ts +9 -0
- package/lib/env.ts +10 -0
- package/lib/helpers/config.ts +45 -0
- package/lib/helpers/discover-services.ts +57 -0
- package/lib/helpers/docker-hub-image.ts +24 -0
- package/lib/helpers/image.ts +54 -0
- package/lib/helpers/index.ts +7 -0
- package/lib/helpers/service-builder.ts +61 -0
- package/lib/helpers/service-runtime.ts +62 -0
- package/lib/helpers/service-urls.ts +82 -0
- package/lib/index.ts +2 -0
- package/lib/types/index.ts +87 -0
- package/lib/types/service-names.ts +10 -0
- package/package.json +48 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __moduleCache = /* @__PURE__ */ new WeakMap;
|
|
6
|
+
var __toCommonJS = (from) => {
|
|
7
|
+
var entry = __moduleCache.get(from), desc;
|
|
8
|
+
if (entry)
|
|
9
|
+
return entry;
|
|
10
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function")
|
|
12
|
+
__getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
|
|
13
|
+
get: () => from[key],
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
}));
|
|
16
|
+
__moduleCache.set(from, entry);
|
|
17
|
+
return entry;
|
|
18
|
+
};
|
|
19
|
+
var __export = (target, all) => {
|
|
20
|
+
for (var name in all)
|
|
21
|
+
__defProp(target, name, {
|
|
22
|
+
get: all[name],
|
|
23
|
+
enumerable: true,
|
|
24
|
+
configurable: true,
|
|
25
|
+
set: (newValue) => all[name] = () => newValue
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// lib/index.ts
|
|
30
|
+
var exports_lib = {};
|
|
31
|
+
__export(exports_lib, {
|
|
32
|
+
getServiceUrl: () => getServiceUrl,
|
|
33
|
+
getServicePort: () => getServicePort2,
|
|
34
|
+
getImage: () => getImage,
|
|
35
|
+
ensureDot: () => ensureDot,
|
|
36
|
+
dockerHubImage: () => dockerHubImage,
|
|
37
|
+
discoverServices: () => discoverServices,
|
|
38
|
+
defaultHealthCheck: () => defaultHealthCheck,
|
|
39
|
+
defaultAlerts: () => defaultAlerts,
|
|
40
|
+
createLogDestinations: () => createLogDestinations,
|
|
41
|
+
buildServices: () => buildServices,
|
|
42
|
+
buildServiceUrlEnvs: () => buildServiceUrlEnvs,
|
|
43
|
+
buildServicePortEnvs: () => buildServicePortEnvs,
|
|
44
|
+
buildLocalUrls: () => buildLocalUrls,
|
|
45
|
+
buildInternalUrls: () => buildInternalUrls,
|
|
46
|
+
buildIngressRules: () => buildIngressRules,
|
|
47
|
+
buildExternalUrls: () => buildExternalUrls,
|
|
48
|
+
RegistryType: () => RegistryType
|
|
49
|
+
});
|
|
50
|
+
module.exports = __toCommonJS(exports_lib);
|
|
51
|
+
|
|
52
|
+
// lib/helpers/config.ts
|
|
53
|
+
var ensureDot = (str) => {
|
|
54
|
+
return str.endsWith(".") ? str : `${str}.`;
|
|
55
|
+
};
|
|
56
|
+
var defaultAlerts = [
|
|
57
|
+
{
|
|
58
|
+
rule: "CPU_UTILIZATION",
|
|
59
|
+
operator: "GREATER_THAN",
|
|
60
|
+
value: 70,
|
|
61
|
+
window: "FIVE_MINUTES"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
rule: "MEM_UTILIZATION",
|
|
65
|
+
operator: "GREATER_THAN",
|
|
66
|
+
value: 70,
|
|
67
|
+
window: "FIVE_MINUTES"
|
|
68
|
+
}
|
|
69
|
+
];
|
|
70
|
+
var createLogDestinations = (logtailToken) => [
|
|
71
|
+
{
|
|
72
|
+
name: "better-stacks-logs",
|
|
73
|
+
logtail: {
|
|
74
|
+
token: logtailToken
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
];
|
|
78
|
+
var defaultHealthCheck = {
|
|
79
|
+
httpPath: "/health"
|
|
80
|
+
};
|
|
81
|
+
// lib/helpers/discover-services.ts
|
|
82
|
+
var import_node_fs = require("node:fs");
|
|
83
|
+
var import_node_path = require("node:path");
|
|
84
|
+
function getServicePort(config) {
|
|
85
|
+
if (config.httpPort)
|
|
86
|
+
return config.httpPort;
|
|
87
|
+
const internalPorts = config.internalPorts;
|
|
88
|
+
if (internalPorts?.[0])
|
|
89
|
+
return internalPorts[0];
|
|
90
|
+
return 8080;
|
|
91
|
+
}
|
|
92
|
+
function validateNoDuplicatePorts(configs) {
|
|
93
|
+
const portMap = new Map;
|
|
94
|
+
for (const config of configs) {
|
|
95
|
+
const port = getServicePort(config);
|
|
96
|
+
const existing = portMap.get(port) || [];
|
|
97
|
+
existing.push(config.name);
|
|
98
|
+
portMap.set(port, existing);
|
|
99
|
+
}
|
|
100
|
+
const conflicts = [...portMap.entries()].filter(([, services]) => services.length > 1).map(([port, services]) => `Port ${port}: ${services.join(", ")}`);
|
|
101
|
+
if (conflicts.length > 0) {
|
|
102
|
+
throw new Error(`Port conflicts detected:
|
|
103
|
+
${conflicts.join(`
|
|
104
|
+
`)}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function discoverServices(servicesDir) {
|
|
108
|
+
const files = import_node_fs.readdirSync(servicesDir).filter((file) => file.endsWith(".ts") && file !== "index.ts");
|
|
109
|
+
const configs = files.map((file) => {
|
|
110
|
+
const module2 = require(import_node_path.join(servicesDir, file));
|
|
111
|
+
return module2.default;
|
|
112
|
+
});
|
|
113
|
+
validateNoDuplicatePorts(configs);
|
|
114
|
+
return configs;
|
|
115
|
+
}
|
|
116
|
+
// lib/types/index.ts
|
|
117
|
+
var RegistryType = {
|
|
118
|
+
DOCKER_HUB: "DOCKER_HUB",
|
|
119
|
+
GHCR: "GHCR",
|
|
120
|
+
DOCR: "DOCR"
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// lib/helpers/docker-hub-image.ts
|
|
124
|
+
var dockerHubImage = (repository, tag, registry = "library") => ({
|
|
125
|
+
registryType: RegistryType.DOCKER_HUB,
|
|
126
|
+
registry,
|
|
127
|
+
repository,
|
|
128
|
+
tag
|
|
129
|
+
});
|
|
130
|
+
// lib/helpers/image.ts
|
|
131
|
+
var scopeImageTagsRaw = process.env.SCOPE_IMAGE_TAGS ?? "";
|
|
132
|
+
var scopeImageTags = (() => {
|
|
133
|
+
if (!scopeImageTagsRaw.trim()) {
|
|
134
|
+
return {};
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
const parsed = JSON.parse(scopeImageTagsRaw);
|
|
138
|
+
const tags = {};
|
|
139
|
+
for (const [key, value] of Object.entries(parsed ?? {})) {
|
|
140
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
141
|
+
tags[key] = value.trim();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return tags;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.warn("Unable to parse scope image tags from environment:", error);
|
|
147
|
+
return {};
|
|
148
|
+
}
|
|
149
|
+
})();
|
|
150
|
+
var resolveImageTag = (scopeName) => {
|
|
151
|
+
const tag = scopeImageTags[scopeName];
|
|
152
|
+
if (!tag) {
|
|
153
|
+
console.warn(`No image tag for "${scopeName}", using "latest" as fallback`);
|
|
154
|
+
return "latest";
|
|
155
|
+
}
|
|
156
|
+
return tag;
|
|
157
|
+
};
|
|
158
|
+
var getImage = (repository, registryCredentials) => {
|
|
159
|
+
const scopeName = repository.split("/").pop();
|
|
160
|
+
if (!scopeName) {
|
|
161
|
+
throw new Error(`Invalid repository name: ${repository}`);
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
registryType: "GHCR",
|
|
165
|
+
registry: "orderboss",
|
|
166
|
+
repository,
|
|
167
|
+
registryCredentials,
|
|
168
|
+
tag: resolveImageTag(scopeName)
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
// lib/helpers/service-builder.ts
|
|
172
|
+
function buildServices(options) {
|
|
173
|
+
const { serviceConfigs, registryCredentials, logtailToken } = options;
|
|
174
|
+
const logDestinations = createLogDestinations(logtailToken);
|
|
175
|
+
return serviceConfigs.map((config) => {
|
|
176
|
+
const {
|
|
177
|
+
ingressPrefix: _ingressPrefix,
|
|
178
|
+
skip: _skip,
|
|
179
|
+
image: configImage,
|
|
180
|
+
internalUrl: _internalUrl,
|
|
181
|
+
...serviceConfig
|
|
182
|
+
} = config;
|
|
183
|
+
const image = configImage ?? getImage(`platform/${config.name}`, registryCredentials);
|
|
184
|
+
return {
|
|
185
|
+
healthCheck: defaultHealthCheck,
|
|
186
|
+
alerts: defaultAlerts,
|
|
187
|
+
logDestinations,
|
|
188
|
+
...serviceConfig,
|
|
189
|
+
image
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
function buildIngressRules(serviceConfigs) {
|
|
194
|
+
return serviceConfigs.filter((config) => config.ingressPrefix !== undefined && config.httpPort !== undefined).map((config) => ({
|
|
195
|
+
component: { name: config.name },
|
|
196
|
+
match: {
|
|
197
|
+
path: {
|
|
198
|
+
prefix: config.ingressPrefix
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
203
|
+
// lib/helpers/service-runtime.ts
|
|
204
|
+
var toEnvKey = (name) => name.toUpperCase().replace(/-/g, "_");
|
|
205
|
+
function getServicePort2(serviceName, defaultPort = 8080) {
|
|
206
|
+
const envKey = `${toEnvKey(serviceName)}_PORT`;
|
|
207
|
+
const envValue = process.env[envKey];
|
|
208
|
+
if (envValue) {
|
|
209
|
+
const parsed = Number(envValue);
|
|
210
|
+
if (!Number.isNaN(parsed)) {
|
|
211
|
+
return parsed;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return defaultPort;
|
|
215
|
+
}
|
|
216
|
+
function getServiceUrl(serviceName) {
|
|
217
|
+
const envKey = `${toEnvKey(serviceName)}_URL`;
|
|
218
|
+
return process.env[envKey];
|
|
219
|
+
}
|
|
220
|
+
// lib/helpers/service-urls.ts
|
|
221
|
+
function getServicePort3(config) {
|
|
222
|
+
if (config.httpPort)
|
|
223
|
+
return config.httpPort;
|
|
224
|
+
const internalPorts = config.internalPorts;
|
|
225
|
+
if (internalPorts?.[0])
|
|
226
|
+
return internalPorts[0];
|
|
227
|
+
return 8080;
|
|
228
|
+
}
|
|
229
|
+
function buildInternalUrls(serviceConfigs) {
|
|
230
|
+
return Object.fromEntries(serviceConfigs.map((config) => {
|
|
231
|
+
const url = config.internalUrl ?? `http://${config.name}:${getServicePort3(config)}`;
|
|
232
|
+
return [config.name, url];
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
function buildExternalUrls(serviceConfigs, baseUrl) {
|
|
236
|
+
return Object.fromEntries(serviceConfigs.filter((config) => config.ingressPrefix !== undefined).map((config) => {
|
|
237
|
+
const path = config.ingressPrefix === "/" ? "" : config.ingressPrefix;
|
|
238
|
+
return [config.name, `${baseUrl}${path}`];
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
function buildLocalUrls(serviceConfigs) {
|
|
242
|
+
return Object.fromEntries(serviceConfigs.map((config) => {
|
|
243
|
+
const port = getServicePort3(config);
|
|
244
|
+
return [config.name, `http://localhost:${port}`];
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
function buildServiceUrlEnvs(serviceConfigs) {
|
|
248
|
+
return serviceConfigs.map((config) => ({
|
|
249
|
+
key: `${config.name.toUpperCase().replace(/-/g, "_")}_URL`,
|
|
250
|
+
scope: "RUN_TIME",
|
|
251
|
+
value: config.internalUrl ?? `http://${config.name}:${getServicePort3(config)}`
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
function buildServicePortEnvs(serviceConfigs) {
|
|
255
|
+
return serviceConfigs.map((config) => ({
|
|
256
|
+
key: `${config.name.toUpperCase().replace(/-/g, "_")}_PORT`,
|
|
257
|
+
scope: "RUN_TIME",
|
|
258
|
+
value: String(getServicePort3(config))
|
|
259
|
+
}));
|
|
260
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// lib/helpers/config.ts
|
|
5
|
+
var ensureDot = (str) => {
|
|
6
|
+
return str.endsWith(".") ? str : `${str}.`;
|
|
7
|
+
};
|
|
8
|
+
var defaultAlerts = [
|
|
9
|
+
{
|
|
10
|
+
rule: "CPU_UTILIZATION",
|
|
11
|
+
operator: "GREATER_THAN",
|
|
12
|
+
value: 70,
|
|
13
|
+
window: "FIVE_MINUTES"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
rule: "MEM_UTILIZATION",
|
|
17
|
+
operator: "GREATER_THAN",
|
|
18
|
+
value: 70,
|
|
19
|
+
window: "FIVE_MINUTES"
|
|
20
|
+
}
|
|
21
|
+
];
|
|
22
|
+
var createLogDestinations = (logtailToken) => [
|
|
23
|
+
{
|
|
24
|
+
name: "better-stacks-logs",
|
|
25
|
+
logtail: {
|
|
26
|
+
token: logtailToken
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
];
|
|
30
|
+
var defaultHealthCheck = {
|
|
31
|
+
httpPath: "/health"
|
|
32
|
+
};
|
|
33
|
+
// lib/helpers/discover-services.ts
|
|
34
|
+
import { readdirSync } from "node:fs";
|
|
35
|
+
import { join } from "node:path";
|
|
36
|
+
function getServicePort(config) {
|
|
37
|
+
if (config.httpPort)
|
|
38
|
+
return config.httpPort;
|
|
39
|
+
const internalPorts = config.internalPorts;
|
|
40
|
+
if (internalPorts?.[0])
|
|
41
|
+
return internalPorts[0];
|
|
42
|
+
return 8080;
|
|
43
|
+
}
|
|
44
|
+
function validateNoDuplicatePorts(configs) {
|
|
45
|
+
const portMap = new Map;
|
|
46
|
+
for (const config of configs) {
|
|
47
|
+
const port = getServicePort(config);
|
|
48
|
+
const existing = portMap.get(port) || [];
|
|
49
|
+
existing.push(config.name);
|
|
50
|
+
portMap.set(port, existing);
|
|
51
|
+
}
|
|
52
|
+
const conflicts = [...portMap.entries()].filter(([, services]) => services.length > 1).map(([port, services]) => `Port ${port}: ${services.join(", ")}`);
|
|
53
|
+
if (conflicts.length > 0) {
|
|
54
|
+
throw new Error(`Port conflicts detected:
|
|
55
|
+
${conflicts.join(`
|
|
56
|
+
`)}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function discoverServices(servicesDir) {
|
|
60
|
+
const files = readdirSync(servicesDir).filter((file) => file.endsWith(".ts") && file !== "index.ts");
|
|
61
|
+
const configs = files.map((file) => {
|
|
62
|
+
const module = __require(join(servicesDir, file));
|
|
63
|
+
return module.default;
|
|
64
|
+
});
|
|
65
|
+
validateNoDuplicatePorts(configs);
|
|
66
|
+
return configs;
|
|
67
|
+
}
|
|
68
|
+
// lib/types/index.ts
|
|
69
|
+
var RegistryType = {
|
|
70
|
+
DOCKER_HUB: "DOCKER_HUB",
|
|
71
|
+
GHCR: "GHCR",
|
|
72
|
+
DOCR: "DOCR"
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// lib/helpers/docker-hub-image.ts
|
|
76
|
+
var dockerHubImage = (repository, tag, registry = "library") => ({
|
|
77
|
+
registryType: RegistryType.DOCKER_HUB,
|
|
78
|
+
registry,
|
|
79
|
+
repository,
|
|
80
|
+
tag
|
|
81
|
+
});
|
|
82
|
+
// lib/helpers/image.ts
|
|
83
|
+
var scopeImageTagsRaw = process.env.SCOPE_IMAGE_TAGS ?? "";
|
|
84
|
+
var scopeImageTags = (() => {
|
|
85
|
+
if (!scopeImageTagsRaw.trim()) {
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const parsed = JSON.parse(scopeImageTagsRaw);
|
|
90
|
+
const tags = {};
|
|
91
|
+
for (const [key, value] of Object.entries(parsed ?? {})) {
|
|
92
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
93
|
+
tags[key] = value.trim();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return tags;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.warn("Unable to parse scope image tags from environment:", error);
|
|
99
|
+
return {};
|
|
100
|
+
}
|
|
101
|
+
})();
|
|
102
|
+
var resolveImageTag = (scopeName) => {
|
|
103
|
+
const tag = scopeImageTags[scopeName];
|
|
104
|
+
if (!tag) {
|
|
105
|
+
console.warn(`No image tag for "${scopeName}", using "latest" as fallback`);
|
|
106
|
+
return "latest";
|
|
107
|
+
}
|
|
108
|
+
return tag;
|
|
109
|
+
};
|
|
110
|
+
var getImage = (repository, registryCredentials) => {
|
|
111
|
+
const scopeName = repository.split("/").pop();
|
|
112
|
+
if (!scopeName) {
|
|
113
|
+
throw new Error(`Invalid repository name: ${repository}`);
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
registryType: "GHCR",
|
|
117
|
+
registry: "orderboss",
|
|
118
|
+
repository,
|
|
119
|
+
registryCredentials,
|
|
120
|
+
tag: resolveImageTag(scopeName)
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
// lib/helpers/service-builder.ts
|
|
124
|
+
function buildServices(options) {
|
|
125
|
+
const { serviceConfigs, registryCredentials, logtailToken } = options;
|
|
126
|
+
const logDestinations = createLogDestinations(logtailToken);
|
|
127
|
+
return serviceConfigs.map((config) => {
|
|
128
|
+
const {
|
|
129
|
+
ingressPrefix: _ingressPrefix,
|
|
130
|
+
skip: _skip,
|
|
131
|
+
image: configImage,
|
|
132
|
+
internalUrl: _internalUrl,
|
|
133
|
+
...serviceConfig
|
|
134
|
+
} = config;
|
|
135
|
+
const image = configImage ?? getImage(`platform/${config.name}`, registryCredentials);
|
|
136
|
+
return {
|
|
137
|
+
healthCheck: defaultHealthCheck,
|
|
138
|
+
alerts: defaultAlerts,
|
|
139
|
+
logDestinations,
|
|
140
|
+
...serviceConfig,
|
|
141
|
+
image
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function buildIngressRules(serviceConfigs) {
|
|
146
|
+
return serviceConfigs.filter((config) => config.ingressPrefix !== undefined && config.httpPort !== undefined).map((config) => ({
|
|
147
|
+
component: { name: config.name },
|
|
148
|
+
match: {
|
|
149
|
+
path: {
|
|
150
|
+
prefix: config.ingressPrefix
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
// lib/helpers/service-runtime.ts
|
|
156
|
+
var toEnvKey = (name) => name.toUpperCase().replace(/-/g, "_");
|
|
157
|
+
function getServicePort2(serviceName, defaultPort = 8080) {
|
|
158
|
+
const envKey = `${toEnvKey(serviceName)}_PORT`;
|
|
159
|
+
const envValue = process.env[envKey];
|
|
160
|
+
if (envValue) {
|
|
161
|
+
const parsed = Number(envValue);
|
|
162
|
+
if (!Number.isNaN(parsed)) {
|
|
163
|
+
return parsed;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return defaultPort;
|
|
167
|
+
}
|
|
168
|
+
function getServiceUrl(serviceName) {
|
|
169
|
+
const envKey = `${toEnvKey(serviceName)}_URL`;
|
|
170
|
+
return process.env[envKey];
|
|
171
|
+
}
|
|
172
|
+
// lib/helpers/service-urls.ts
|
|
173
|
+
function getServicePort3(config) {
|
|
174
|
+
if (config.httpPort)
|
|
175
|
+
return config.httpPort;
|
|
176
|
+
const internalPorts = config.internalPorts;
|
|
177
|
+
if (internalPorts?.[0])
|
|
178
|
+
return internalPorts[0];
|
|
179
|
+
return 8080;
|
|
180
|
+
}
|
|
181
|
+
function buildInternalUrls(serviceConfigs) {
|
|
182
|
+
return Object.fromEntries(serviceConfigs.map((config) => {
|
|
183
|
+
const url = config.internalUrl ?? `http://${config.name}:${getServicePort3(config)}`;
|
|
184
|
+
return [config.name, url];
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
function buildExternalUrls(serviceConfigs, baseUrl) {
|
|
188
|
+
return Object.fromEntries(serviceConfigs.filter((config) => config.ingressPrefix !== undefined).map((config) => {
|
|
189
|
+
const path = config.ingressPrefix === "/" ? "" : config.ingressPrefix;
|
|
190
|
+
return [config.name, `${baseUrl}${path}`];
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
193
|
+
function buildLocalUrls(serviceConfigs) {
|
|
194
|
+
return Object.fromEntries(serviceConfigs.map((config) => {
|
|
195
|
+
const port = getServicePort3(config);
|
|
196
|
+
return [config.name, `http://localhost:${port}`];
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
function buildServiceUrlEnvs(serviceConfigs) {
|
|
200
|
+
return serviceConfigs.map((config) => ({
|
|
201
|
+
key: `${config.name.toUpperCase().replace(/-/g, "_")}_URL`,
|
|
202
|
+
scope: "RUN_TIME",
|
|
203
|
+
value: config.internalUrl ?? `http://${config.name}:${getServicePort3(config)}`
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
function buildServicePortEnvs(serviceConfigs) {
|
|
207
|
+
return serviceConfigs.map((config) => ({
|
|
208
|
+
key: `${config.name.toUpperCase().replace(/-/g, "_")}_PORT`,
|
|
209
|
+
scope: "RUN_TIME",
|
|
210
|
+
value: String(getServicePort3(config))
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
export {
|
|
214
|
+
getServiceUrl,
|
|
215
|
+
getServicePort2 as getServicePort,
|
|
216
|
+
getImage,
|
|
217
|
+
ensureDot,
|
|
218
|
+
dockerHubImage,
|
|
219
|
+
discoverServices,
|
|
220
|
+
defaultHealthCheck,
|
|
221
|
+
defaultAlerts,
|
|
222
|
+
createLogDestinations,
|
|
223
|
+
buildServices,
|
|
224
|
+
buildServiceUrlEnvs,
|
|
225
|
+
buildServicePortEnvs,
|
|
226
|
+
buildLocalUrls,
|
|
227
|
+
buildInternalUrls,
|
|
228
|
+
buildIngressRules,
|
|
229
|
+
buildExternalUrls,
|
|
230
|
+
RegistryType
|
|
231
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { AppSpecService } from '@pulumi/digitalocean/types/input';
|
|
2
|
+
/**
|
|
3
|
+
* Supported container registry types for DigitalOcean App Platform.
|
|
4
|
+
*/
|
|
5
|
+
export declare const RegistryType: {
|
|
6
|
+
/** Docker Hub - public or private images */
|
|
7
|
+
readonly DOCKER_HUB: "DOCKER_HUB";
|
|
8
|
+
/** GitHub Container Registry */
|
|
9
|
+
readonly GHCR: "GHCR";
|
|
10
|
+
/** DigitalOcean Container Registry */
|
|
11
|
+
readonly DOCR: "DOCR";
|
|
12
|
+
};
|
|
13
|
+
export type RegistryType = (typeof RegistryType)[keyof typeof RegistryType];
|
|
14
|
+
/**
|
|
15
|
+
* Image configuration for a service.
|
|
16
|
+
*/
|
|
17
|
+
export interface ImageConfig {
|
|
18
|
+
/** Registry type */
|
|
19
|
+
registryType: RegistryType;
|
|
20
|
+
/** Registry name (e.g., 'library' for official Docker Hub images, 'orderboss' for GHCR) */
|
|
21
|
+
registry: string;
|
|
22
|
+
/** Repository name (e.g., 'nats', 'platform/storefront') */
|
|
23
|
+
repository: string;
|
|
24
|
+
/** Image tag (e.g., '2.10-alpine', 'latest') */
|
|
25
|
+
tag: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Configuration for a service that will be deployed to DigitalOcean App Platform.
|
|
29
|
+
* Each service in infra/services/*.ts should export a default object of this type.
|
|
30
|
+
*
|
|
31
|
+
* ## Port Configuration
|
|
32
|
+
*
|
|
33
|
+
* Services can expose ports in two ways:
|
|
34
|
+
*
|
|
35
|
+
* - `httpPort`: Public HTTP port with ingress routing (requires `ingressPrefix`)
|
|
36
|
+
* - `internalPorts`: Internal-only ports, not publicly accessible
|
|
37
|
+
*
|
|
38
|
+
* The **first port** (httpPort or internalPorts[0]) is used as the primary port for:
|
|
39
|
+
* - `<SERVICE>_PORT` environment variable
|
|
40
|
+
* - `<SERVICE>_URL` environment variable (unless `internalUrl` is set)
|
|
41
|
+
*
|
|
42
|
+
* ## Examples
|
|
43
|
+
*
|
|
44
|
+
* ```ts
|
|
45
|
+
* // Public service (API Gateway)
|
|
46
|
+
* { httpPort: 4000, ingressPrefix: '/api' }
|
|
47
|
+
*
|
|
48
|
+
* // Internal-only service (Orders)
|
|
49
|
+
* { internalPorts: [4001] }
|
|
50
|
+
*
|
|
51
|
+
* // Internal service with custom protocol (NATS)
|
|
52
|
+
* { internalPorts: [4222, 8222], internalUrl: 'nats://nats:4222' }
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* @see https://docs.digitalocean.com/reference/api/digitalocean/#tag/Apps
|
|
56
|
+
*/
|
|
57
|
+
export type ServiceConfig = Partial<Omit<AppSpecService, 'image'>> & {
|
|
58
|
+
/** Unique name of the service (required) */
|
|
59
|
+
name: string;
|
|
60
|
+
/**
|
|
61
|
+
* Ingress path prefix for public routing (e.g., '/api').
|
|
62
|
+
* Only used when `httpPort` is set. Services with only `internalPorts` cannot have ingress.
|
|
63
|
+
*/
|
|
64
|
+
ingressPrefix?: string;
|
|
65
|
+
/** Set to true to exclude this service from deployment */
|
|
66
|
+
skip?: boolean;
|
|
67
|
+
/** Custom image configuration (defaults to GHCR image based on service name) */
|
|
68
|
+
image?: ImageConfig;
|
|
69
|
+
/**
|
|
70
|
+
* Override the internal URL for service-to-service communication.
|
|
71
|
+
* Use this for non-HTTP protocols (e.g., 'nats://nats:4222').
|
|
72
|
+
* If not set, defaults to `http://{name}:{primaryPort}`.
|
|
73
|
+
*/
|
|
74
|
+
internalUrl?: string;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Full service spec after merging with common config.
|
|
78
|
+
* This is what gets passed to the DigitalOcean App spec.
|
|
79
|
+
*/
|
|
80
|
+
export type FullServiceSpec = AppSpecService;
|
|
81
|
+
export * from './service-names';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-generated file - do not edit manually!
|
|
3
|
+
* Generated by: bun run generate-env
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Known service names in the platform.
|
|
7
|
+
* This type is auto-generated from infra/services/*.ts
|
|
8
|
+
*/
|
|
9
|
+
export type ServiceName = 'nats' | 'orders' | 'storefront' | 'api-gateway';
|
package/lib/env.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment helpers for services to read configuration.
|
|
3
|
+
* Use this entry point in production services (Node.js, NestJS, etc.)
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* import { getServicePort, getServiceUrl } from '@crossdelta/infrastructure/env'
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export * from './helpers/service-runtime'
|
|
10
|
+
export type { ServiceName } from './types/service-names'
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Input } from '@pulumi/pulumi'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ensures that a string ends with a dot (useful for DNS CNAME values).
|
|
5
|
+
*/
|
|
6
|
+
export const ensureDot = (str: string) => {
|
|
7
|
+
return str.endsWith('.') ? str : `${str}.`
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Common alert configuration for services
|
|
12
|
+
*/
|
|
13
|
+
export const defaultAlerts = [
|
|
14
|
+
{
|
|
15
|
+
rule: 'CPU_UTILIZATION',
|
|
16
|
+
operator: 'GREATER_THAN',
|
|
17
|
+
value: 70,
|
|
18
|
+
window: 'FIVE_MINUTES',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
rule: 'MEM_UTILIZATION',
|
|
22
|
+
operator: 'GREATER_THAN',
|
|
23
|
+
value: 70,
|
|
24
|
+
window: 'FIVE_MINUTES',
|
|
25
|
+
},
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Creates log destinations config with Logtail token
|
|
30
|
+
*/
|
|
31
|
+
export const createLogDestinations = (logtailToken: Input<string>) => [
|
|
32
|
+
{
|
|
33
|
+
name: 'better-stacks-logs',
|
|
34
|
+
logtail: {
|
|
35
|
+
token: logtailToken,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Default health check configuration
|
|
42
|
+
*/
|
|
43
|
+
export const defaultHealthCheck = {
|
|
44
|
+
httpPath: '/health',
|
|
45
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { readdirSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import type { ServiceConfig } from '../types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get the primary port for a service (for validation).
|
|
7
|
+
* Prefers httpPort, falls back to first internalPort, then 8080.
|
|
8
|
+
*/
|
|
9
|
+
function getServicePort(config: ServiceConfig): number {
|
|
10
|
+
if (config.httpPort) return config.httpPort as number
|
|
11
|
+
const internalPorts = config.internalPorts as number[] | undefined
|
|
12
|
+
if (internalPorts?.[0]) return internalPorts[0]
|
|
13
|
+
return 8080
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Validates that no two services use the same port.
|
|
18
|
+
* @throws Error if duplicate ports are found
|
|
19
|
+
*/
|
|
20
|
+
function validateNoDuplicatePorts(configs: ServiceConfig[]): void {
|
|
21
|
+
const portMap = new Map<number, string[]>()
|
|
22
|
+
|
|
23
|
+
for (const config of configs) {
|
|
24
|
+
const port = getServicePort(config)
|
|
25
|
+
const existing = portMap.get(port) || []
|
|
26
|
+
existing.push(config.name)
|
|
27
|
+
portMap.set(port, existing)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const conflicts = [...portMap.entries()]
|
|
31
|
+
.filter(([, services]) => services.length > 1)
|
|
32
|
+
.map(([port, services]) => `Port ${port}: ${services.join(', ')}`)
|
|
33
|
+
|
|
34
|
+
if (conflicts.length > 0) {
|
|
35
|
+
throw new Error(`Port conflicts detected:\n${conflicts.join('\n')}`)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Auto-discovers all service configurations from a directory.
|
|
41
|
+
* Each .ts file (except index.ts) should export a ServiceConfig as default.
|
|
42
|
+
* @throws Error if duplicate ports are detected
|
|
43
|
+
*/
|
|
44
|
+
export function discoverServices(servicesDir: string): ServiceConfig[] {
|
|
45
|
+
const files = readdirSync(servicesDir).filter(
|
|
46
|
+
(file) => file.endsWith('.ts') && file !== 'index.ts'
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const configs = files.map((file) => {
|
|
50
|
+
const module = require(join(servicesDir, file))
|
|
51
|
+
return module.default as ServiceConfig
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
validateNoDuplicatePorts(configs)
|
|
55
|
+
|
|
56
|
+
return configs
|
|
57
|
+
}
|