@mesh-tech/mesh-cli 0.35.1 → 0.36.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/bin/mesh.js +94 -18
- package/dist/bin/mesh.js.map +1 -1
- package/dist/build-info.json +2 -2
- package/dist/src/commands/create-app.d.ts +1 -0
- package/dist/src/commands/create-app.js +4 -0
- package/dist/src/commands/temporal.js +90 -18
- package/dist/src/commands/temporal.js.map +1 -1
- package/dist/src/utils/aws-auth.d.ts +22 -1
- package/dist/src/utils/aws-auth.js +96 -18
- package/dist/src/utils/scaffold-versions.d.ts +1 -0
- package/dist/src/utils/scaffold-versions.js +1 -0
- package/fragments/base/index.ts.hbs +13 -4
- package/fragments/base/package.json.hbs +1 -0
- package/fragments/service/api/package.json.hbs +1 -3
- package/fragments/service/api/src/index.ts.hbs +11 -3
- package/package.json +2 -2
- package/templates/api-auth/api/package.json.hbs +1 -0
- package/templates/api-auth/api/src/index.ts.hbs +11 -3
- package/templates/api-auth/index.ts.hbs +3 -0
- package/templates/api-role-gating/api/package.json.hbs +1 -0
- package/templates/api-role-gating/api/src/main.ts.hbs +7 -1
- package/templates/api-role-gating/index.ts.hbs +3 -0
- package/templates/external-service/api/src/index.ts.hbs +11 -3
- package/templates/external-service/index.ts.hbs +3 -0
- package/templates/workflow/api/src/index.ts.hbs +11 -3
- package/templates/workflow/index.ts.hbs +3 -0
|
@@ -151,16 +151,83 @@ function toEnv(credentials) {
|
|
|
151
151
|
AWS_SESSION_TOKEN: SessionToken,
|
|
152
152
|
};
|
|
153
153
|
}
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
export const ROLE_CHAINING_MAX_SESSION_SECONDS = 3600;
|
|
155
|
+
export const DEFAULT_ASSUME_ROLE_SECONDS = 43200;
|
|
156
|
+
export function isRoleChainedCaller(callerArn) {
|
|
157
|
+
return !!callerArn && /^arn:aws[a-z-]*:sts::\d+:assumed-role\//.test(callerArn);
|
|
158
|
+
}
|
|
159
|
+
export function stsErrorDetail(err) {
|
|
160
|
+
if (!err || typeof err !== "object")
|
|
161
|
+
return { text: String(err) };
|
|
162
|
+
const e = err;
|
|
163
|
+
const str = (v) => (typeof v === "string" && v ? v : undefined);
|
|
164
|
+
const name = str(e.name);
|
|
165
|
+
const typeCode = str(str(e.__type)?.split("#").pop());
|
|
166
|
+
const code = str(e.Code) ??
|
|
167
|
+
typeCode ??
|
|
168
|
+
str(e.Error?.Code) ??
|
|
169
|
+
(name && name !== "Error" ? name : undefined);
|
|
170
|
+
const text = str(e.Error?.Message) ?? str(e.Message) ?? str(e.message) ?? code ?? String(err);
|
|
171
|
+
return { code, text };
|
|
172
|
+
}
|
|
173
|
+
export function isSessionDurationError(err) {
|
|
174
|
+
if (!err || typeof err !== "object")
|
|
175
|
+
return false;
|
|
176
|
+
const { code, text } = stsErrorDetail(err);
|
|
177
|
+
if (code !== undefined && code !== "ValidationError")
|
|
156
178
|
return false;
|
|
157
|
-
|
|
158
|
-
return (message.includes("MaxSessionDuration") ||
|
|
159
|
-
message.includes("DurationSeconds"));
|
|
179
|
+
return /DurationSeconds|MaxSessionDuration|role chaining/i.test(text);
|
|
160
180
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
181
|
+
function formatDuration(seconds) {
|
|
182
|
+
return seconds % 3600 === 0 ? `${seconds / 3600}h` : `${seconds}s`;
|
|
183
|
+
}
|
|
184
|
+
export function describeAssumeRoleFailure(opts) {
|
|
185
|
+
const reason = opts.code && opts.code !== opts.text ? `${opts.code}: ${opts.text}` : opts.text;
|
|
186
|
+
const lines = [
|
|
187
|
+
`AssumeRole failed for ${opts.roleArn} — requested a ` +
|
|
188
|
+
`${formatDuration(opts.durationSeconds)} session (DurationSeconds=${opts.durationSeconds}): ${reason}`,
|
|
189
|
+
];
|
|
190
|
+
if (opts.callerArn)
|
|
191
|
+
lines.push(` Caller: ${opts.callerArn}`);
|
|
192
|
+
const zitadel = `mesh login ${opts.loginContext ?? "<platform>.<env>"}`;
|
|
193
|
+
if (isExpiredAwsTokenMessage(`${opts.code ?? ""} ${opts.text}`)) {
|
|
194
|
+
lines.push(` Your AWS SSO session looks expired or missing — refresh with: ${SSO_LOGIN_FIX}`);
|
|
195
|
+
lines.push(` Or assume the role via Zitadel instead: ${zitadel}`);
|
|
196
|
+
}
|
|
197
|
+
else if (opts.code === "AccessDenied" || /not authorized/i.test(opts.text)) {
|
|
198
|
+
lines.push(" The role does not trust this AWS identity. Assume it via Zitadel instead:");
|
|
199
|
+
lines.push(` ${zitadel}`);
|
|
200
|
+
lines.push(" or switch to an AWS profile the role's trust policy allows.");
|
|
201
|
+
}
|
|
202
|
+
else if (isSessionDurationError({ Code: opts.code, message: opts.text })) {
|
|
203
|
+
lines.push(` The role refused even a ${formatDuration(ROLE_CHAINING_MAX_SESSION_SECONDS)} session. ` +
|
|
204
|
+
"Check its MaxSessionDuration, or assume it via Zitadel instead:");
|
|
205
|
+
lines.push(` ${zitadel}`);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
lines.push(` Try assuming the role via Zitadel: ${zitadel}`);
|
|
209
|
+
lines.push(` or refresh the AWS session: ${SSO_LOGIN_FIX}`);
|
|
210
|
+
}
|
|
211
|
+
return lines.join("\n");
|
|
212
|
+
}
|
|
213
|
+
export async function assumeRole(roleArn, sessionName = "mesh-cli", durationSeconds = DEFAULT_ASSUME_ROLE_SECONDS, options = {}) {
|
|
214
|
+
const sts = options.sts ?? newStsClient();
|
|
215
|
+
let callerArn;
|
|
216
|
+
try {
|
|
217
|
+
callerArn = (await sts.send(new GetCallerIdentityCommand({}))).Arn;
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
}
|
|
221
|
+
let requested = durationSeconds;
|
|
222
|
+
if (isRoleChainedCaller(callerArn) && requested > ROLE_CHAINING_MAX_SESSION_SECONDS) {
|
|
223
|
+
requested = ROLE_CHAINING_MAX_SESSION_SECONDS;
|
|
224
|
+
logInfo(`Current AWS identity is an assumed-role session — requesting a ` +
|
|
225
|
+
`${formatDuration(requested)} session (role chaining caps AssumeRole at one hour)`);
|
|
226
|
+
}
|
|
227
|
+
const attempts = requested > ROLE_CHAINING_MAX_SESSION_SECONDS
|
|
228
|
+
? [requested, ROLE_CHAINING_MAX_SESSION_SECONDS]
|
|
229
|
+
: [requested];
|
|
230
|
+
for (const [i, duration] of attempts.entries()) {
|
|
164
231
|
try {
|
|
165
232
|
const resp = await sts.send(new AssumeRoleCommand({
|
|
166
233
|
RoleArn: roleArn,
|
|
@@ -172,14 +239,22 @@ export async function assumeRole(roleArn, sessionName = "mesh-cli", durationSeco
|
|
|
172
239
|
return toEnv(resp.Credentials);
|
|
173
240
|
}
|
|
174
241
|
catch (err) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
return null;
|
|
242
|
+
const isLast = i === attempts.length - 1;
|
|
243
|
+
if (!isLast && isSessionDurationError(err)) {
|
|
244
|
+
logInfo(`STS refused a ${formatDuration(duration)} session for ${roleArn} — ` +
|
|
245
|
+
`retrying with ${formatDuration(ROLE_CHAINING_MAX_SESSION_SECONDS)}`);
|
|
246
|
+
continue;
|
|
182
247
|
}
|
|
248
|
+
const { code, text } = stsErrorDetail(err);
|
|
249
|
+
logWarn(describeAssumeRoleFailure({
|
|
250
|
+
roleArn,
|
|
251
|
+
durationSeconds: duration,
|
|
252
|
+
code,
|
|
253
|
+
text,
|
|
254
|
+
callerArn,
|
|
255
|
+
loginContext: options.loginContext,
|
|
256
|
+
}));
|
|
257
|
+
return null;
|
|
183
258
|
}
|
|
184
259
|
}
|
|
185
260
|
return null;
|
|
@@ -207,8 +282,9 @@ export async function assumeRoleCredentials(roleArn, idToken, sessionName, durat
|
|
|
207
282
|
};
|
|
208
283
|
}
|
|
209
284
|
catch (err) {
|
|
210
|
-
if (!
|
|
211
|
-
|
|
285
|
+
if (!isSessionDurationError(err) || duration === ROLE_CHAINING_MAX_SESSION_SECONDS) {
|
|
286
|
+
const { code, text } = stsErrorDetail(err);
|
|
287
|
+
logWarn(`AssumeRoleWithWebIdentity failed: ${code && code !== text ? `${code}: ${text}` : text}`);
|
|
212
288
|
return null;
|
|
213
289
|
}
|
|
214
290
|
}
|
|
@@ -340,7 +416,9 @@ export async function resolveAwsCredentials(roleArn, appRoot, stack) {
|
|
|
340
416
|
}
|
|
341
417
|
}
|
|
342
418
|
logInfo(`Assuming ${roleName} via AWS SSO`);
|
|
343
|
-
const creds = await assumeRole(roleArn, "mesh-cli"
|
|
419
|
+
const creds = await assumeRole(roleArn, "mesh-cli", DEFAULT_ASSUME_ROLE_SECONDS, {
|
|
420
|
+
loginContext: platformContext ?? undefined,
|
|
421
|
+
});
|
|
344
422
|
if (creds) {
|
|
345
423
|
logSuccess(`Assumed ${roleName}`);
|
|
346
424
|
return { env: creds, method: "sso" };
|
|
@@ -2,6 +2,7 @@ export declare const SCAFFOLD_PACKAGE_LINES: {
|
|
|
2
2
|
readonly "@mesh-tech/app-kit": "^1.45.0";
|
|
3
3
|
readonly "@mesh-tech/esm-bundle": "^0.1.1";
|
|
4
4
|
readonly "@mesh-tech/events-service": "^0.1.0";
|
|
5
|
+
readonly "@mesh-tech/events-ui": "^0.1.0";
|
|
5
6
|
readonly "@mesh-tech/authz": "^1.19.0";
|
|
6
7
|
};
|
|
7
8
|
export type ScaffoldPackage = keyof typeof SCAFFOLD_PACKAGE_LINES;
|
|
@@ -112,8 +112,11 @@ const config = new mesh.apps.AppConfig("config", {
|
|
|
112
112
|
// Events and webhooks: the service, its own database, an app-scoped KMS key
|
|
113
113
|
// for endpoint credentials, the publisher key, the Zitadel introspection app,
|
|
114
114
|
// the delivery task queue, the worker registration that lists the app's
|
|
115
|
-
// Temporal namespace on the Hub's Workflows page
|
|
116
|
-
//
|
|
115
|
+
// Temporal namespace on the Hub's Workflows page (with the service's own
|
|
116
|
+
// housekeeping workflows marked internal, so only staff see them there), the
|
|
117
|
+
// registry surface, and the operator UI (`@mesh-tech/events-ui`, installed in
|
|
118
|
+
// package.json) as its own signed-in Service at `{app}-events-ui` — all
|
|
119
|
+
// children of this one declaration.
|
|
117
120
|
//
|
|
118
121
|
// Its database's schema migration gates the deploy: `pulumi up` waits for the
|
|
119
122
|
// migration Job and fails when it fails, and the service's pods are held until
|
|
@@ -126,7 +129,7 @@ const config = new mesh.apps.AppConfig("config", {
|
|
|
126
129
|
// that as "use the default".
|
|
127
130
|
const events = new mesh.apps.EventsService("events", {
|
|
128
131
|
env,
|
|
129
|
-
identity: { connector, projectId: identity.projectId },
|
|
132
|
+
identity: { connector, projectId: identity.projectId, dependsOn: [identity] },
|
|
130
133
|
config,
|
|
131
134
|
// "thin" sends identifiers only, so nothing the API would mask reaches a
|
|
132
135
|
// third-party endpoint and the consumer follows the identifiers back to the
|
|
@@ -171,6 +174,9 @@ const api = new mesh.apps.Service("api", {
|
|
|
171
174
|
// Drives readiness, liveness and the Hub uptime sampler on BOTH ingress kinds.
|
|
172
175
|
// `ingress.healthCheckPath` sets the ALB target group's path only, and a
|
|
173
176
|
// gateway (HTTPRoute) tenant has no target group — so the path belongs here.
|
|
177
|
+
// A boot that can outlast the liveness window (a busy node, a tight CPU
|
|
178
|
+
// limit) takes `healthCheck: { path: "/health", startup: {} }`: a startup
|
|
179
|
+
// probe that holds liveness for up to five minutes of boot.
|
|
174
180
|
healthCheck: "/health",
|
|
175
181
|
replicas: deploy ? 2 : 1,
|
|
176
182
|
resources: { cpu: "100m", memory: "128Mi" },
|
|
@@ -219,7 +225,10 @@ const worker = new mesh.apps.TemporalWorker("worker", {
|
|
|
219
225
|
// Hub on the ports it published and refuses it on every other one, so a
|
|
220
226
|
// registry entry cannot walk the Hub onto a port the surface never opened.
|
|
221
227
|
// Nothing here declares that — it rides the surface (`hubDialPolicy`, on by
|
|
222
|
-
// default wherever the namespace is in the service mesh).
|
|
228
|
+
// default wherever the namespace is in the service mesh). A surface fronting
|
|
229
|
+
// a vendor the tenant already declares names that declaration with
|
|
230
|
+
// `externalService`; its registry entry publishes the name, and the Hub reads
|
|
231
|
+
// the app's Core connection from the vendor's shared health measurement.
|
|
223
232
|
// =============================================================================
|
|
224
233
|
|
|
225
234
|
new mesh.apps.ApiDocs("docs", {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
{{#if events}}
|
|
13
13
|
"@mesh-tech/authz": "{{#if workspaceDeps}}workspace:*{{else}}{{authzRange}}{{/if}}",
|
|
14
14
|
"@mesh-tech/events-service": "{{#if workspaceDeps}}workspace:*{{else}}{{eventsServiceRange}}{{/if}}",
|
|
15
|
+
"@mesh-tech/events-ui": "{{#if workspaceDeps}}workspace:*{{else}}{{eventsUiRange}}{{/if}}",
|
|
15
16
|
{{/if}}
|
|
16
17
|
"@mesh-tech/infra-components": "{{#if workspaceDeps}}workspace:*{{else}}{{meshRange}}{{/if}}",
|
|
17
18
|
{{#if events}}
|
|
@@ -17,10 +17,8 @@
|
|
|
17
17
|
"@aws-sdk/client-s3": "^3.700.0",
|
|
18
18
|
{{/if}}
|
|
19
19
|
"@hono/node-server": "^1.13.0",
|
|
20
|
-
"@mesh-tech/logger": "{{#if workspaceDeps}}workspace:*{{else}}{{meshRange}}{{/if}}",
|
|
21
|
-
{{#if temporal}}
|
|
22
20
|
"@mesh-tech/app-kit": "{{#if workspaceDeps}}workspace:*{{else}}{{meshRange}}{{/if}}",
|
|
23
|
-
{{/if}}
|
|
21
|
+
"@mesh-tech/logger": "{{#if workspaceDeps}}workspace:*{{else}}{{meshRange}}{{/if}}",
|
|
24
22
|
{{#if database}}
|
|
25
23
|
"@prisma/adapter-pg": "^6.0.0",
|
|
26
24
|
"@prisma/client": "^6.0.0",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { Hono } from "hono";
|
|
21
21
|
import { serve } from "@hono/node-server";
|
|
22
|
+
import { configureServerKeepAlive } from "@mesh-tech/app-kit/outbound-http";
|
|
22
23
|
import { createLogger, requestLogger } from "@mesh-tech/logger";
|
|
23
24
|
{{#if temporal}}
|
|
24
25
|
import { createTemporalClient } from "@mesh-tech/app-kit/temporal-client";
|
|
@@ -82,6 +83,13 @@ app.post("/things", async (c) => {
|
|
|
82
83
|
|
|
83
84
|
{{/if}}
|
|
84
85
|
// Start server
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
// Hold idle keep-alive sockets for 75s rather than Node's 5s default. The
|
|
87
|
+
// proxy in front of this service pools connections to it and keeps them idle
|
|
88
|
+
// longer than 5s (the Istio gateway's Envoy, through ztunnel, or an ALB for
|
|
89
|
+
// 60s), so at the default the server keeps closing sockets the pool may be
|
|
90
|
+
// about to reuse.
|
|
91
|
+
configureServerKeepAlive(
|
|
92
|
+
serve({ fetch: app.fetch, port: PORT, hostname: "0.0.0.0" }, (info) => {
|
|
93
|
+
log.info({ port: info.port }, "{{titleCase name}} API listening");
|
|
94
|
+
}),
|
|
95
|
+
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mesh-tech/mesh-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.0",
|
|
4
4
|
"description": "CLI for Mesh platform development utilities",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@mesh-tech/agent-contracts": "0.2.0",
|
|
45
|
-
"@mesh-tech/tsconfig": "2.
|
|
45
|
+
"@mesh-tech/tsconfig": "2.37.0",
|
|
46
46
|
"@mesh-tech/workflow-viz": "0.3.1",
|
|
47
47
|
"@tanstack/intent": "^0.3.6",
|
|
48
48
|
"@types/node": "^20.0.0",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { Hono } from "hono";
|
|
17
17
|
import { serve } from "@hono/node-server";
|
|
18
|
+
import { configureServerKeepAlive } from "@mesh-tech/app-kit/outbound-http";
|
|
18
19
|
import { createAuthn } from "@mesh-tech/authn";
|
|
19
20
|
import { zitadelJwtScheme } from "@mesh-tech/authn/schemes/zitadel-jwt";
|
|
20
21
|
import { createAuthnMiddleware, type AuthnVariables } from "@mesh-tech/authn/hono";
|
|
@@ -102,6 +103,13 @@ app.get("/api/report", m.require("report", "view", { id: () => "quarterly" }), (
|
|
|
102
103
|
c.json({ id: "quarterly", title: "Quarterly Report", classification: "internal" }),
|
|
103
104
|
);
|
|
104
105
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
// Hold idle keep-alive sockets for 75s rather than Node's 5s default. The
|
|
107
|
+
// proxy in front of this service pools connections to it and keeps them idle
|
|
108
|
+
// longer than 5s (the Istio gateway's Envoy, through ztunnel, or an ALB for
|
|
109
|
+
// 60s), so at the default the server keeps closing sockets the pool may be
|
|
110
|
+
// about to reuse.
|
|
111
|
+
configureServerKeepAlive(
|
|
112
|
+
serve({ fetch: app.fetch, port: PORT }, () => {
|
|
113
|
+
log.info({ port: PORT, issuer: ZITADEL_ISSUER }, "{{name}} api listening");
|
|
114
|
+
}),
|
|
115
|
+
);
|
|
@@ -109,6 +109,9 @@ const api = new mesh.apps.Service("api", {
|
|
|
109
109
|
// Drives readiness, liveness and the Hub uptime sampler on BOTH ingress kinds.
|
|
110
110
|
// `ingress.healthCheckPath` sets the ALB target group's path only, and a
|
|
111
111
|
// gateway (HTTPRoute) tenant has no target group — so the path belongs here.
|
|
112
|
+
// A boot that can outlast the liveness window (a busy node, a tight CPU
|
|
113
|
+
// limit) takes `healthCheck: { path: "/health", startup: {} }`: a startup
|
|
114
|
+
// probe that holds liveness for up to five minutes of boot.
|
|
112
115
|
healthCheck: "/health",
|
|
113
116
|
replicas: 1,
|
|
114
117
|
resources: { cpu: "100m", memory: "128Mi" },
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { serve } from "@hono/node-server";
|
|
10
10
|
import { Hono } from "hono";
|
|
11
|
+
import { configureServerKeepAlive } from "@mesh-tech/app-kit/outbound-http";
|
|
11
12
|
import { createAuthnMiddleware, type AuthnVariables } from "@mesh-tech/authn/hono";
|
|
12
13
|
import { createAuthzMiddleware, type AuthzVariables } from "@mesh-tech/authz/hono";
|
|
13
14
|
import { createLogger, requestLogger } from "@mesh-tech/logger";
|
|
@@ -58,6 +59,11 @@ export const createApp = (deps: { testAuth?: TestAuth } = {}) => {
|
|
|
58
59
|
|
|
59
60
|
if (process.env.NODE_ENV !== "test" && process.argv[1]?.endsWith("main.js")) {
|
|
60
61
|
const port = Number(process.env.PORT ?? 3000);
|
|
61
|
-
|
|
62
|
+
// Hold idle keep-alive sockets for 75s rather than Node's 5s default. The
|
|
63
|
+
// proxy in front of this service pools connections to it and keeps them idle
|
|
64
|
+
// longer than 5s (the Istio gateway's Envoy, through ztunnel, or an ALB for
|
|
65
|
+
// 60s), so at the default the server keeps closing sockets the pool may be
|
|
66
|
+
// about to reuse.
|
|
67
|
+
configureServerKeepAlive(serve({ fetch: createApp().fetch, port }));
|
|
62
68
|
log.info({ port, mode: "role-gating" }, "{{name}} api listening");
|
|
63
69
|
}
|
|
@@ -85,6 +85,9 @@ const api = new mesh.apps.Service("api", {
|
|
|
85
85
|
// Drives readiness, liveness and the Hub uptime sampler on BOTH ingress kinds.
|
|
86
86
|
// `ingress.healthCheckPath` sets the ALB target group's path only, and a
|
|
87
87
|
// gateway (HTTPRoute) tenant has no target group — so the path belongs here.
|
|
88
|
+
// A boot that can outlast the liveness window (a busy node, a tight CPU
|
|
89
|
+
// limit) takes `healthCheck: { path: "/health", startup: {} }`: a startup
|
|
90
|
+
// probe that holds liveness for up to five minutes of boot.
|
|
88
91
|
healthCheck: "/health",
|
|
89
92
|
replicas: 1,
|
|
90
93
|
resources: { cpu: "100m", memory: "128Mi" },
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { serve } from "@hono/node-server";
|
|
18
18
|
import { Hono } from "hono";
|
|
19
19
|
import { resolveCredentials } from "@mesh-tech/app-kit/credentials";
|
|
20
|
+
import { configureServerKeepAlive } from "@mesh-tech/app-kit/outbound-http";
|
|
20
21
|
import { traceExternalCall } from "@mesh-tech/app-kit/tracing";
|
|
21
22
|
import { enhanceFetch } from "@mesh-tech/http-client";
|
|
22
23
|
import { createLogger, requestLogger } from "@mesh-tech/logger";
|
|
@@ -81,6 +82,13 @@ app.post("/payments/quote", async (c) => {
|
|
|
81
82
|
});
|
|
82
83
|
|
|
83
84
|
const port = Number(process.env.PORT ?? 3000);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
// Hold idle keep-alive sockets for 75s rather than Node's 5s default. The
|
|
86
|
+
// proxy in front of this service pools connections to it and keeps them idle
|
|
87
|
+
// longer than 5s (the Istio gateway's Envoy, through ztunnel, or an ALB for
|
|
88
|
+
// 60s), so at the default the server keeps closing sockets the pool may be
|
|
89
|
+
// about to reuse.
|
|
90
|
+
configureServerKeepAlive(
|
|
91
|
+
serve({ fetch: app.fetch, port }, () => {
|
|
92
|
+
log.info({ port }, "{{titleCase name}} API listening");
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
@@ -75,6 +75,9 @@ const api = new mesh.apps.Service("api", {
|
|
|
75
75
|
// Drives readiness, liveness and the Hub uptime sampler on BOTH ingress kinds.
|
|
76
76
|
// `ingress.healthCheckPath` sets the ALB target group's path only, and a
|
|
77
77
|
// gateway (HTTPRoute) tenant has no target group — so the path belongs here.
|
|
78
|
+
// A boot that can outlast the liveness window (a busy node, a tight CPU
|
|
79
|
+
// limit) takes `healthCheck: { path: "/health", startup: {} }`: a startup
|
|
80
|
+
// probe that holds liveness for up to five minutes of boot.
|
|
78
81
|
healthCheck: "/health",
|
|
79
82
|
replicas: 1,
|
|
80
83
|
resources: { cpu: "100m", memory: "128Mi" },
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { Hono } from "hono";
|
|
13
13
|
import { serve } from "@hono/node-server";
|
|
14
|
+
import { configureServerKeepAlive } from "@mesh-tech/app-kit/outbound-http";
|
|
14
15
|
import { createTemporalClient, type WithClient } from "@mesh-tech/app-kit/temporal-client";
|
|
15
16
|
import { createLogger, requestLogger } from "@mesh-tech/logger";
|
|
16
17
|
|
|
@@ -125,9 +126,16 @@ app.get("/workflows/:id", async (c) => {
|
|
|
125
126
|
// Start server
|
|
126
127
|
initClient()
|
|
127
128
|
.then(() => {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
// Hold idle keep-alive sockets for 75s rather than Node's 5s default. The
|
|
130
|
+
// proxy in front of this service pools connections to it and keeps them idle
|
|
131
|
+
// longer than 5s (the Istio gateway's Envoy, through ztunnel, or an ALB for
|
|
132
|
+
// 60s), so at the default the server keeps closing sockets the pool may be
|
|
133
|
+
// about to reuse.
|
|
134
|
+
configureServerKeepAlive(
|
|
135
|
+
serve({ fetch: app.fetch, port: PORT, hostname: "0.0.0.0" }, () => {
|
|
136
|
+
log.info({ port: PORT }, "{{titleCase name}} API listening");
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
131
139
|
})
|
|
132
140
|
.catch((err) => {
|
|
133
141
|
log.error({ err }, "Failed to initialize");
|
|
@@ -47,6 +47,9 @@ const api = new mesh.apps.Service("api", {
|
|
|
47
47
|
// Drives readiness, liveness and the Hub uptime sampler on BOTH ingress kinds.
|
|
48
48
|
// `ingress.healthCheckPath` sets the ALB target group's path only, and a
|
|
49
49
|
// gateway (HTTPRoute) tenant has no target group — so the path belongs here.
|
|
50
|
+
// A boot that can outlast the liveness window (a busy node, a tight CPU
|
|
51
|
+
// limit) takes `healthCheck: { path: "/health", startup: {} }`: a startup
|
|
52
|
+
// probe that holds liveness for up to five minutes of boot.
|
|
50
53
|
healthCheck: "/health",
|
|
51
54
|
replicas: 1,
|
|
52
55
|
resources: { cpu: "100m", memory: "128Mi" },
|