@platformatic/watt-extra 1.12.0 → 1.13.0-alpha.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/package.json +1 -1
- package/plugins/alerts.js +1 -1
- package/plugins/auth.js +80 -42
- package/plugins/compliancy.js +1 -1
- package/plugins/env.js +1 -0
- package/plugins/flamegraphs.js +2 -2
- package/plugins/health-signals.js +2 -2
- package/plugins/init.js +4 -4
- package/plugins/metadata.js +2 -2
- package/plugins/scheduler.js +1 -1
- package/plugins/update.js +1 -1
- package/test/alerts.test.js +24 -24
- package/test/auth.test.js +45 -1
- package/test/health-signals.test.js +2 -2
- package/test/init.test.js +52 -3
- package/test/trigger-flamegraphs.test.js +1 -1
- package/test/update.test.js +1 -1
- package/.claude/settings.local.json +0 -7
package/package.json
CHANGED
package/plugins/alerts.js
CHANGED
|
@@ -130,7 +130,7 @@ async function alerts (app, _opts) {
|
|
|
130
130
|
lastServicesAlertTime[serviceId] = currentTime
|
|
131
131
|
delete healthInfo.healthConfig
|
|
132
132
|
|
|
133
|
-
const authHeaders = await app.
|
|
133
|
+
const authHeaders = await app.getAuthorizationHeaders()
|
|
134
134
|
|
|
135
135
|
const { statusCode, body } = await request(`${scalerUrl}/alerts`, {
|
|
136
136
|
method: 'POST',
|
package/plugins/auth.js
CHANGED
|
@@ -3,6 +3,8 @@ import { Agent } from 'undici'
|
|
|
3
3
|
|
|
4
4
|
const K8S_TOKEN_PATH = '/var/run/secrets/kubernetes.io/serviceaccount/token'
|
|
5
5
|
|
|
6
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
6
8
|
function decodeJwtPayload (token) {
|
|
7
9
|
try {
|
|
8
10
|
if (!token) return null
|
|
@@ -10,7 +12,7 @@ function decodeJwtPayload (token) {
|
|
|
10
12
|
if (!base64Payload) return null
|
|
11
13
|
const payload = Buffer.from(base64Payload, 'base64').toString('utf8')
|
|
12
14
|
return JSON.parse(payload)
|
|
13
|
-
} catch
|
|
15
|
+
} catch {
|
|
14
16
|
return null
|
|
15
17
|
}
|
|
16
18
|
}
|
|
@@ -18,72 +20,108 @@ function decodeJwtPayload (token) {
|
|
|
18
20
|
function isTokenExpired (token, offset = 0) {
|
|
19
21
|
const payload = decodeJwtPayload(token)
|
|
20
22
|
if (!payload || !payload.exp) return true
|
|
23
|
+
return payload.exp <= Math.floor(Date.now() / 1000) + offset
|
|
24
|
+
}
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
async function loadK8sToken (log) {
|
|
27
|
+
let token
|
|
28
|
+
try {
|
|
29
|
+
await stat(K8S_TOKEN_PATH)
|
|
30
|
+
log.info('Loading JWT token from K8s service account')
|
|
31
|
+
token = await readFile(K8S_TOKEN_PATH, 'utf8')
|
|
32
|
+
} catch {
|
|
33
|
+
log.warn('Failed to load JWT token from K8s service account')
|
|
34
|
+
}
|
|
35
|
+
if (!token) {
|
|
36
|
+
log.warn('K8s token not found, falling back to environment variable')
|
|
37
|
+
token = process.env.PLT_TEST_TOKEN
|
|
38
|
+
}
|
|
39
|
+
return token
|
|
25
40
|
}
|
|
26
41
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
42
|
+
// Read ECS task identity (TaskARN suffix + cluster) from the task metadata
|
|
43
|
+
// endpoint. K8s identity travels via the SA JWT, so no resolution needed there.
|
|
44
|
+
async function resolveEcsIdentity (log) {
|
|
45
|
+
const metadataUrl = `${process.env.ECS_CONTAINER_METADATA_URI_V4}/task`
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetch(metadataUrl)
|
|
48
|
+
if (!res.ok) throw new Error(`status ${res.status}`)
|
|
49
|
+
const meta = await res.json()
|
|
50
|
+
const id = meta.TaskARN?.split('/').pop()
|
|
51
|
+
const namespace = meta.Cluster
|
|
52
|
+
if (!id || !namespace) throw new Error('TaskARN or Cluster missing in metadata')
|
|
53
|
+
log.info({ id, namespace }, 'Resolved ECS task identity')
|
|
54
|
+
return { id, namespace }
|
|
55
|
+
} catch (err) {
|
|
56
|
+
log.error({ err, metadataUrl }, 'Failed to read ECS task metadata')
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
}
|
|
31
60
|
|
|
32
|
-
|
|
33
|
-
let token
|
|
34
|
-
try {
|
|
35
|
-
await stat(K8S_TOKEN_PATH)
|
|
36
|
-
app.log.info('Loading JWT token from K8s service account')
|
|
37
|
-
token = await readFile(K8S_TOKEN_PATH, 'utf8')
|
|
38
|
-
} catch (err) {
|
|
39
|
-
app.log.warn('Failed to load JWT token from K8s service account')
|
|
40
|
-
}
|
|
61
|
+
// ── Provider strategies ────────────────────────────────────────────────────
|
|
41
62
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
63
|
+
// K8s strategy: load the SA JWT once, refresh on expiry, send as Bearer.
|
|
64
|
+
async function createK8sStrategy (app, offset) {
|
|
65
|
+
app.token = await loadK8sToken(app.log)
|
|
46
66
|
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const getAuthorizationHeader = async (headers = {}) => {
|
|
67
|
+
return async function getHeaders () {
|
|
51
68
|
if (app.token && isTokenExpired(app.token, offset)) {
|
|
52
69
|
app.log.info('JWT token expired, reloading')
|
|
53
|
-
app.token = await
|
|
70
|
+
app.token = await loadK8sToken(app.log)
|
|
54
71
|
|
|
55
72
|
app.watt?.updateSharedContext({
|
|
56
73
|
iccAuthHeaders: { authorization: `Bearer ${app.token}` }
|
|
57
|
-
}).catch(
|
|
74
|
+
}).catch(err => {
|
|
58
75
|
app.log.error({ err }, 'Failed to update jwt token in shared context')
|
|
59
76
|
})
|
|
60
77
|
}
|
|
78
|
+
return { authorization: `Bearer ${app.token}` }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
61
81
|
|
|
82
|
+
// ECS strategy: resolve task identity once and send as explicit headers.
|
|
83
|
+
// Unauthenticated for now; future hardening will replace this with a Sigv4-
|
|
84
|
+
// presigned sts:GetCallerIdentity proof carried in an Authorization header.
|
|
85
|
+
async function createEcsStrategy (app) {
|
|
86
|
+
app.machineIdentity = await resolveEcsIdentity(app.log)
|
|
87
|
+
|
|
88
|
+
return async function getHeaders () {
|
|
89
|
+
if (!app.machineIdentity) return {}
|
|
62
90
|
return {
|
|
63
|
-
|
|
64
|
-
|
|
91
|
+
'x-ecs-task-id': app.machineIdentity.id,
|
|
92
|
+
'x-ecs-cluster': app.machineIdentity.namespace
|
|
65
93
|
}
|
|
66
94
|
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Plugin ─────────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
async function authPlugin (app) {
|
|
100
|
+
// 1 min offset to refresh the token before it actually expires.
|
|
101
|
+
const offset = parseInt(process.env.PLT_JWT_EXPIRATION_OFFSET_SEC ?? 0)
|
|
102
|
+
|
|
103
|
+
const getProviderHeaders = app.provider === 'ecs'
|
|
104
|
+
? await createEcsStrategy(app)
|
|
105
|
+
: await createK8sStrategy(app, offset)
|
|
106
|
+
|
|
107
|
+
async function getAuthorizationHeaders (headers = {}) {
|
|
108
|
+
return { ...headers, ...(await getProviderHeaders()) }
|
|
109
|
+
}
|
|
67
110
|
|
|
68
|
-
|
|
111
|
+
function authorizationTokenInterceptor (dispatch) {
|
|
69
112
|
return async function InterceptedDispatch (opts, handler) {
|
|
70
|
-
opts.headers = await
|
|
113
|
+
opts.headers = await getAuthorizationHeaders(opts.headers)
|
|
71
114
|
return dispatch(opts, handler)
|
|
72
115
|
}
|
|
73
116
|
}
|
|
74
117
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
setInterval(
|
|
78
|
-
// Check if token is expired to propagate it to the runtime
|
|
79
|
-
// via the shared context
|
|
80
|
-
await getAuthorizationHeader()
|
|
81
|
-
}, offset ? offset * 1000 / 2 : 30000).unref()
|
|
118
|
+
// Periodically call the strategy so K8s token refresh propagates to the
|
|
119
|
+
// runtime shared context before requests need it. No-op for ECS.
|
|
120
|
+
setInterval(getAuthorizationHeaders, offset ? offset * 1000 / 2 : 30000).unref()
|
|
82
121
|
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
app.
|
|
86
|
-
app.getAuthorizationHeader = getAuthorizationHeader
|
|
122
|
+
// Can't replace the global dispatcher (shared with the runtime main thread).
|
|
123
|
+
app.dispatcher = new Agent().compose(authorizationTokenInterceptor)
|
|
124
|
+
app.getAuthorizationHeaders = getAuthorizationHeaders
|
|
87
125
|
}
|
|
88
126
|
|
|
89
127
|
export default authPlugin
|
package/plugins/compliancy.js
CHANGED
|
@@ -30,7 +30,7 @@ async function compliancy (app, _opts) {
|
|
|
30
30
|
// There is a better way? We need to set the default headers for the client
|
|
31
31
|
// every time, because the token might be expired
|
|
32
32
|
// And we cannot set the global dispatcher because it's shared with the runtime main thread.
|
|
33
|
-
setDefaultHeaders(await app.
|
|
33
|
+
setDefaultHeaders(await app.getAuthorizationHeaders())
|
|
34
34
|
const compliancyMetadata = await getCompliancyMetadata({
|
|
35
35
|
projectDir: appDir,
|
|
36
36
|
runtime
|
package/plugins/env.js
CHANGED
package/plugins/flamegraphs.js
CHANGED
|
@@ -281,7 +281,7 @@ async function flamegraphs (app, _opts) {
|
|
|
281
281
|
query.alertId = alertId
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
-
const authHeaders = await app.
|
|
284
|
+
const authHeaders = await app.getAuthorizationHeaders()
|
|
285
285
|
const { statusCode, body } = await request(url, {
|
|
286
286
|
method: 'POST',
|
|
287
287
|
headers: {
|
|
@@ -350,7 +350,7 @@ async function flamegraphs (app, _opts) {
|
|
|
350
350
|
const url = `${scalerUrl}/flamegraphs/${flamegraphId}/alerts`
|
|
351
351
|
app.log.info({ flamegraphId, alerts: alertIds }, 'Attaching flamegraph to alerts')
|
|
352
352
|
|
|
353
|
-
const authHeaders = await app.
|
|
353
|
+
const authHeaders = await app.getAuthorizationHeaders()
|
|
354
354
|
const { statusCode, body } = await request(url, {
|
|
355
355
|
method: 'POST',
|
|
356
356
|
headers: {
|
|
@@ -187,7 +187,7 @@ async function healthSignals (app, _opts) {
|
|
|
187
187
|
const applicationId = app.instanceConfig?.applicationId
|
|
188
188
|
const runtimeId = app.getRuntimeId()
|
|
189
189
|
const timestamp = Date.now()
|
|
190
|
-
const authHeaders = await app.
|
|
190
|
+
const authHeaders = await app.getAuthorizationHeaders()
|
|
191
191
|
|
|
192
192
|
const { statusCode, body } = await request(`${scalerUrl}/ready`, {
|
|
193
193
|
method: 'POST',
|
|
@@ -207,7 +207,7 @@ async function healthSignals (app, _opts) {
|
|
|
207
207
|
async function sendHealthSignals (rawSignals, batchStartedAt) {
|
|
208
208
|
const scalerUrl = app.instanceConfig?.iccServices?.scaler?.url
|
|
209
209
|
const applicationId = app.instanceConfig?.applicationId
|
|
210
|
-
const authHeaders = await app.
|
|
210
|
+
const authHeaders = await app.getAuthorizationHeaders()
|
|
211
211
|
|
|
212
212
|
// Transform signals to the format expected by ICC LoadPredictor
|
|
213
213
|
// Format: { serviceId: { elu: { options, workers: { workerId: { values: [[ts, val], ...] } } } } }
|
package/plugins/init.js
CHANGED
|
@@ -8,7 +8,7 @@ async function initPlugin (app) {
|
|
|
8
8
|
// There is a better way? We need to set the default headers for the client
|
|
9
9
|
// every time, because the token might be expired
|
|
10
10
|
// And we cannot set the global dispatcher because it's shared with the runtime main thread.
|
|
11
|
-
setDefaultHeaders(await app.
|
|
11
|
+
setDefaultHeaders(await app.getAuthorizationHeaders())
|
|
12
12
|
const request = {
|
|
13
13
|
podId,
|
|
14
14
|
apiVersion: 'v3'
|
|
@@ -24,7 +24,7 @@ async function initPlugin (app) {
|
|
|
24
24
|
|
|
25
25
|
let applicationName = app.env.PLT_APP_NAME
|
|
26
26
|
const applicationDir = app.env.PLT_APP_DIR
|
|
27
|
-
const instanceId = os.hostname()
|
|
27
|
+
const instanceId = app.provider === 'k8s' ? os.hostname() : app.machineIdentity?.id
|
|
28
28
|
|
|
29
29
|
app.log.info({ applicationName, applicationDir }, 'Loading watt-extra application')
|
|
30
30
|
|
|
@@ -73,14 +73,14 @@ async function initPlugin (app) {
|
|
|
73
73
|
if (!app.applicationName) {
|
|
74
74
|
app.applicationName = app.env.PLT_APP_NAME
|
|
75
75
|
app.instanceConfig = null
|
|
76
|
-
app.instanceId = os.hostname()
|
|
76
|
+
app.instanceId = app.provider === 'k8s' ? os.hostname() : app.machineIdentity?.id
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
const watt = new Watt(app)
|
|
80
80
|
app.watt = watt
|
|
81
81
|
app.initApplication = initApplication
|
|
82
82
|
|
|
83
|
-
const headers = await app.
|
|
83
|
+
const headers = await app.getAuthorizationHeaders()
|
|
84
84
|
await app.watt.updateSharedContext({ iccAuthHeaders: headers })
|
|
85
85
|
}
|
|
86
86
|
|
package/plugins/metadata.js
CHANGED
|
@@ -60,13 +60,13 @@ async function metadata (app, _opts) {
|
|
|
60
60
|
// There is a better way? We need to set the default headers for the client
|
|
61
61
|
// every time, because the token might be expired
|
|
62
62
|
// And we cannot set the global dispatcher because it's shared with the runtime main thread.
|
|
63
|
-
setDefaultHeaders(await app.
|
|
63
|
+
setDefaultHeaders(await app.getAuthorizationHeaders())
|
|
64
64
|
await controlPlaneClient.saveApplicationInstanceState({
|
|
65
65
|
id: app.instanceId,
|
|
66
66
|
services,
|
|
67
67
|
metadata: runtimeMetadata
|
|
68
68
|
}, {
|
|
69
|
-
headers: await app.
|
|
69
|
+
headers: await app.getAuthorizationHeaders()
|
|
70
70
|
})
|
|
71
71
|
} catch (error) {
|
|
72
72
|
app.log.error('Failed to save application state to Control Plane', error)
|
package/plugins/scheduler.js
CHANGED
package/plugins/update.js
CHANGED
|
@@ -72,7 +72,7 @@ async function updatePlugin (app) {
|
|
|
72
72
|
app.log.info({ runtimeId }, `Connecting to updates websocket at ${wsUrl}`)
|
|
73
73
|
|
|
74
74
|
try {
|
|
75
|
-
const headers = await app.
|
|
75
|
+
const headers = await app.getAuthorizationHeaders()
|
|
76
76
|
|
|
77
77
|
socket = new WebSocket(wsUrl, { headers })
|
|
78
78
|
await once(socket, 'open')
|
package/test/alerts.test.js
CHANGED
|
@@ -52,7 +52,7 @@ test('should send alert when service becomes unhealthy', async (t) => {
|
|
|
52
52
|
let alertReceived = null
|
|
53
53
|
let flamegraphReceived = null
|
|
54
54
|
|
|
55
|
-
const
|
|
55
|
+
const getAuthorizationHeaders = async (headers) => {
|
|
56
56
|
return { ...headers, authorization: 'Bearer test-token' }
|
|
57
57
|
}
|
|
58
58
|
|
|
@@ -83,7 +83,7 @@ test('should send alert when service becomes unhealthy', async (t) => {
|
|
|
83
83
|
})
|
|
84
84
|
|
|
85
85
|
const app = await start()
|
|
86
|
-
app.
|
|
86
|
+
app.getAuthorizationHeaders = getAuthorizationHeaders
|
|
87
87
|
|
|
88
88
|
t.after(async () => {
|
|
89
89
|
await app.close()
|
|
@@ -146,7 +146,7 @@ test('should not send alert when application is healthy', async (t) => {
|
|
|
146
146
|
|
|
147
147
|
let alertReceived = null
|
|
148
148
|
|
|
149
|
-
const
|
|
149
|
+
const getAuthorizationHeaders = async (headers) => {
|
|
150
150
|
return { ...headers, authorization: 'Bearer test-token' }
|
|
151
151
|
}
|
|
152
152
|
|
|
@@ -168,7 +168,7 @@ test('should not send alert when application is healthy', async (t) => {
|
|
|
168
168
|
})
|
|
169
169
|
|
|
170
170
|
const app = await start()
|
|
171
|
-
app.
|
|
171
|
+
app.getAuthorizationHeaders = getAuthorizationHeaders
|
|
172
172
|
|
|
173
173
|
t.after(async () => {
|
|
174
174
|
await app.close()
|
|
@@ -211,7 +211,7 @@ test('should cache health data and include it in alerts', async (t) => {
|
|
|
211
211
|
|
|
212
212
|
let alertReceived = null
|
|
213
213
|
|
|
214
|
-
const
|
|
214
|
+
const getAuthorizationHeaders = async (headers) => {
|
|
215
215
|
return { ...headers, authorization: 'Bearer test-token' }
|
|
216
216
|
}
|
|
217
217
|
|
|
@@ -234,7 +234,7 @@ test('should cache health data and include it in alerts', async (t) => {
|
|
|
234
234
|
})
|
|
235
235
|
|
|
236
236
|
const app = await start()
|
|
237
|
-
app.
|
|
237
|
+
app.getAuthorizationHeaders = getAuthorizationHeaders
|
|
238
238
|
|
|
239
239
|
t.after(async () => {
|
|
240
240
|
await app.close()
|
|
@@ -328,7 +328,7 @@ test('should not fail when health info is missing', async (t) => {
|
|
|
328
328
|
|
|
329
329
|
let alertReceived = null
|
|
330
330
|
|
|
331
|
-
const
|
|
331
|
+
const getAuthorizationHeaders = async (headers) => {
|
|
332
332
|
return { ...headers, authorization: 'Bearer test-token' }
|
|
333
333
|
}
|
|
334
334
|
|
|
@@ -350,7 +350,7 @@ test('should not fail when health info is missing', async (t) => {
|
|
|
350
350
|
})
|
|
351
351
|
|
|
352
352
|
const app = await start()
|
|
353
|
-
app.
|
|
353
|
+
app.getAuthorizationHeaders = getAuthorizationHeaders
|
|
354
354
|
|
|
355
355
|
t.after(async () => {
|
|
356
356
|
await app.close()
|
|
@@ -371,7 +371,7 @@ test('should respect alert retention window', async (t) => {
|
|
|
371
371
|
|
|
372
372
|
const alertsReceived = []
|
|
373
373
|
|
|
374
|
-
const
|
|
374
|
+
const getAuthorizationHeaders = async (headers) => {
|
|
375
375
|
return { ...headers, authorization: 'Bearer test-token' }
|
|
376
376
|
}
|
|
377
377
|
|
|
@@ -397,7 +397,7 @@ test('should respect alert retention window', async (t) => {
|
|
|
397
397
|
|
|
398
398
|
const app = await start()
|
|
399
399
|
|
|
400
|
-
app.
|
|
400
|
+
app.getAuthorizationHeaders = getAuthorizationHeaders
|
|
401
401
|
|
|
402
402
|
t.after(async () => {
|
|
403
403
|
await app.close()
|
|
@@ -495,7 +495,7 @@ test('should send alert when flamegraphs are disabled', async (t) => {
|
|
|
495
495
|
|
|
496
496
|
let alertReceived = null
|
|
497
497
|
|
|
498
|
-
const
|
|
498
|
+
const getAuthorizationHeaders = async (headers) => {
|
|
499
499
|
return { ...headers, authorization: 'Bearer test-token' }
|
|
500
500
|
}
|
|
501
501
|
|
|
@@ -519,7 +519,7 @@ test('should send alert when flamegraphs are disabled', async (t) => {
|
|
|
519
519
|
})
|
|
520
520
|
|
|
521
521
|
const app = await start()
|
|
522
|
-
app.
|
|
522
|
+
app.getAuthorizationHeaders = getAuthorizationHeaders
|
|
523
523
|
|
|
524
524
|
t.after(async () => {
|
|
525
525
|
await app.close()
|
|
@@ -577,7 +577,7 @@ test('should send alert when failed to send a flamegraph', async (t) => {
|
|
|
577
577
|
|
|
578
578
|
let alertReceived = null
|
|
579
579
|
|
|
580
|
-
const
|
|
580
|
+
const getAuthorizationHeaders = async (headers) => {
|
|
581
581
|
return { ...headers, authorization: 'Bearer test-token' }
|
|
582
582
|
}
|
|
583
583
|
|
|
@@ -604,7 +604,7 @@ test('should send alert when failed to send a flamegraph', async (t) => {
|
|
|
604
604
|
})
|
|
605
605
|
|
|
606
606
|
const app = await start()
|
|
607
|
-
app.
|
|
607
|
+
app.getAuthorizationHeaders = getAuthorizationHeaders
|
|
608
608
|
|
|
609
609
|
t.after(async () => {
|
|
610
610
|
await app.close()
|
|
@@ -662,7 +662,7 @@ test('should handle old runtime (< 3.18.0) health events', async (t) => {
|
|
|
662
662
|
|
|
663
663
|
let alertReceived = null
|
|
664
664
|
|
|
665
|
-
const
|
|
665
|
+
const getAuthorizationHeaders = async (headers) => {
|
|
666
666
|
return { ...headers, authorization: 'Bearer test-token' }
|
|
667
667
|
}
|
|
668
668
|
|
|
@@ -684,7 +684,7 @@ test('should handle old runtime (< 3.18.0) health events', async (t) => {
|
|
|
684
684
|
})
|
|
685
685
|
|
|
686
686
|
const app = await start()
|
|
687
|
-
app.
|
|
687
|
+
app.getAuthorizationHeaders = getAuthorizationHeaders
|
|
688
688
|
|
|
689
689
|
// Mock the runtime version check to simulate old runtime
|
|
690
690
|
const originalFn = app.watt.runtimeSupportsNewHealthMetrics
|
|
@@ -748,7 +748,7 @@ test('should attach one flamegraph to multiple alerts', async (t) => {
|
|
|
748
748
|
const receivedFlamegraphs = []
|
|
749
749
|
const receivedAttachedFlamegraphs = []
|
|
750
750
|
|
|
751
|
-
const
|
|
751
|
+
const getAuthorizationHeaders = async (headers) => {
|
|
752
752
|
return { ...headers, authorization: 'Bearer test-token' }
|
|
753
753
|
}
|
|
754
754
|
|
|
@@ -792,7 +792,7 @@ test('should attach one flamegraph to multiple alerts', async (t) => {
|
|
|
792
792
|
})
|
|
793
793
|
|
|
794
794
|
const app = await start()
|
|
795
|
-
app.
|
|
795
|
+
app.getAuthorizationHeaders = getAuthorizationHeaders
|
|
796
796
|
|
|
797
797
|
t.after(async () => {
|
|
798
798
|
await app.close()
|
|
@@ -855,7 +855,7 @@ test('should send flamegraphs if attaching fails', async (t) => {
|
|
|
855
855
|
const receivedAlerts = []
|
|
856
856
|
const receivedFlamegraphs = []
|
|
857
857
|
|
|
858
|
-
const
|
|
858
|
+
const getAuthorizationHeaders = async (headers) => {
|
|
859
859
|
return { ...headers, authorization: 'Bearer test-token' }
|
|
860
860
|
}
|
|
861
861
|
|
|
@@ -895,7 +895,7 @@ test('should send flamegraphs if attaching fails', async (t) => {
|
|
|
895
895
|
})
|
|
896
896
|
|
|
897
897
|
const app = await start()
|
|
898
|
-
app.
|
|
898
|
+
app.getAuthorizationHeaders = getAuthorizationHeaders
|
|
899
899
|
|
|
900
900
|
t.after(async () => {
|
|
901
901
|
await app.close()
|
|
@@ -956,7 +956,7 @@ test('should skip alerts during grace period but still cache health data', async
|
|
|
956
956
|
|
|
957
957
|
let alertReceived = null
|
|
958
958
|
|
|
959
|
-
const
|
|
959
|
+
const getAuthorizationHeaders = async (headers) => {
|
|
960
960
|
return { ...headers, authorization: 'Bearer test-token' }
|
|
961
961
|
}
|
|
962
962
|
|
|
@@ -979,7 +979,7 @@ test('should skip alerts during grace period but still cache health data', async
|
|
|
979
979
|
})
|
|
980
980
|
|
|
981
981
|
const app = await start()
|
|
982
|
-
app.
|
|
982
|
+
app.getAuthorizationHeaders = getAuthorizationHeaders
|
|
983
983
|
|
|
984
984
|
t.after(async () => {
|
|
985
985
|
await app.close()
|
|
@@ -1033,7 +1033,7 @@ test('should reset grace period when worker restarts', async (t) => {
|
|
|
1033
1033
|
|
|
1034
1034
|
const alertsReceived = []
|
|
1035
1035
|
|
|
1036
|
-
const
|
|
1036
|
+
const getAuthorizationHeaders = async (headers) => {
|
|
1037
1037
|
return { ...headers, authorization: 'Bearer test-token' }
|
|
1038
1038
|
}
|
|
1039
1039
|
|
|
@@ -1059,7 +1059,7 @@ test('should reset grace period when worker restarts', async (t) => {
|
|
|
1059
1059
|
})
|
|
1060
1060
|
|
|
1061
1061
|
const app = await start()
|
|
1062
|
-
app.
|
|
1062
|
+
app.getAuthorizationHeaders = getAuthorizationHeaders
|
|
1063
1063
|
|
|
1064
1064
|
t.after(async () => {
|
|
1065
1065
|
await app.close()
|
package/test/auth.test.js
CHANGED
|
@@ -5,8 +5,9 @@ import { request } from 'undici'
|
|
|
5
5
|
import { setUpEnvironment, createJwtToken } from './helper.js'
|
|
6
6
|
import authPlugin from '../plugins/auth.js'
|
|
7
7
|
|
|
8
|
-
const createMockApp = () => {
|
|
8
|
+
const createMockApp = (provider = 'k8s') => {
|
|
9
9
|
return {
|
|
10
|
+
provider,
|
|
10
11
|
log: {
|
|
11
12
|
info: () => {},
|
|
12
13
|
warn: () => {},
|
|
@@ -170,3 +171,46 @@ test('auth plugin does not reload when token is undefined', async (t) => {
|
|
|
170
171
|
const reloadLogMessage = logMessages.find(msg => msg === 'JWT token expired, reloading')
|
|
171
172
|
equal(reloadLogMessage, undefined, 'Should not attempt to reload undefined token')
|
|
172
173
|
})
|
|
174
|
+
|
|
175
|
+
test('auth plugin sends ECS identity headers when running on ECS', async (t) => {
|
|
176
|
+
const originalEnv = { ...process.env }
|
|
177
|
+
|
|
178
|
+
t.after(() => {
|
|
179
|
+
process.env = originalEnv
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
// Mock ECS task metadata endpoint.
|
|
183
|
+
const metadata = fastify()
|
|
184
|
+
metadata.get('/task', async () => ({
|
|
185
|
+
TaskARN: 'arn:aws:ecs:us-east-1:123456789012:task/my-cluster/abcdef0123',
|
|
186
|
+
Cluster: 'my-cluster'
|
|
187
|
+
}))
|
|
188
|
+
await metadata.listen({ port: 0 })
|
|
189
|
+
const metadataUrl = `http://localhost:${metadata.server.address().port}`
|
|
190
|
+
t.after(() => metadata.close())
|
|
191
|
+
|
|
192
|
+
process.env.ECS_CONTAINER_METADATA_URI_V4 = metadataUrl
|
|
193
|
+
|
|
194
|
+
const server = fastify()
|
|
195
|
+
server.get('/', async (request) => {
|
|
196
|
+
return { headers: request.headers }
|
|
197
|
+
})
|
|
198
|
+
await server.listen({ port: 0 })
|
|
199
|
+
const url = `http://localhost:${server.server.address().port}`
|
|
200
|
+
t.after(() => server.close())
|
|
201
|
+
|
|
202
|
+
const app = createMockApp('ecs')
|
|
203
|
+
await authPlugin(app)
|
|
204
|
+
|
|
205
|
+
equal(app.machineIdentity?.id, 'abcdef0123', 'Task id should be the TaskARN suffix')
|
|
206
|
+
equal(app.machineIdentity?.namespace, 'my-cluster', 'Namespace should be the ECS cluster')
|
|
207
|
+
equal(app.token, undefined, 'No K8s JWT should be loaded on ECS')
|
|
208
|
+
|
|
209
|
+
const response = await request(url, { dispatcher: app.dispatcher })
|
|
210
|
+
const responseBody = await response.body.json()
|
|
211
|
+
|
|
212
|
+
equal(response.statusCode, 200)
|
|
213
|
+
equal(responseBody.headers['x-ecs-task-id'], 'abcdef0123')
|
|
214
|
+
equal(responseBody.headers['x-ecs-cluster'], 'my-cluster')
|
|
215
|
+
equal(responseBody.headers.authorization, undefined, 'No Authorization header on ECS')
|
|
216
|
+
})
|
|
@@ -21,7 +21,7 @@ test('should send health signals when service becomes unhealthy', async (t) => {
|
|
|
21
21
|
const receivedFlamegraphReqs = []
|
|
22
22
|
let receivedReadyReq = null
|
|
23
23
|
|
|
24
|
-
const
|
|
24
|
+
const getAuthorizationHeaders = async (headers) => {
|
|
25
25
|
return { ...headers, authorization: 'Bearer test-token' }
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -61,7 +61,7 @@ test('should send health signals when service becomes unhealthy', async (t) => {
|
|
|
61
61
|
})
|
|
62
62
|
|
|
63
63
|
const app = await start()
|
|
64
|
-
app.
|
|
64
|
+
app.getAuthorizationHeaders = getAuthorizationHeaders
|
|
65
65
|
|
|
66
66
|
t.after(async () => {
|
|
67
67
|
await app.close()
|
package/test/init.test.js
CHANGED
|
@@ -5,7 +5,7 @@ import { randomUUID } from 'node:crypto'
|
|
|
5
5
|
import { startICC } from './helper.js'
|
|
6
6
|
import initPlugin from '../plugins/init.js'
|
|
7
7
|
|
|
8
|
-
const createMockApp = (env = {}) => {
|
|
8
|
+
const createMockApp = (env = {}, overrides = {}) => {
|
|
9
9
|
const logMessages = []
|
|
10
10
|
return {
|
|
11
11
|
env: {
|
|
@@ -20,8 +20,10 @@ const createMockApp = (env = {}) => {
|
|
|
20
20
|
warn: () => {},
|
|
21
21
|
error: () => {}
|
|
22
22
|
},
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
getAuthorizationHeaders: async () => 'Bearer test-token',
|
|
24
|
+
provider: 'k8s',
|
|
25
|
+
logMessages,
|
|
26
|
+
...overrides
|
|
25
27
|
}
|
|
26
28
|
}
|
|
27
29
|
|
|
@@ -194,6 +196,53 @@ test('init plugin sends correct request structure when PLT_APP_NAME provided', a
|
|
|
194
196
|
equal(capturedRequest.body.apiVersion, 'v3')
|
|
195
197
|
})
|
|
196
198
|
|
|
199
|
+
test('init plugin uses machineIdentity.id as podId when provider is ecs', async (t) => {
|
|
200
|
+
const applicationName = 'test-app-ecs'
|
|
201
|
+
const applicationId = randomUUID()
|
|
202
|
+
const taskId = 'abcdef0123456789'
|
|
203
|
+
|
|
204
|
+
let capturedRequest = null
|
|
205
|
+
|
|
206
|
+
const icc = await startICC(t, {
|
|
207
|
+
applicationId,
|
|
208
|
+
controlPlaneResponse: (req) => {
|
|
209
|
+
capturedRequest = {
|
|
210
|
+
params: req.params,
|
|
211
|
+
body: req.body
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
applicationId,
|
|
215
|
+
applicationName,
|
|
216
|
+
iccServices: {
|
|
217
|
+
riskEngine: { url: 'http://127.0.0.1:3000/risk-service' },
|
|
218
|
+
trafficInspector: { url: 'http://127.0.0.1:3000/traffic-inspector' },
|
|
219
|
+
compliance: { url: 'http://127.0.0.1:3000/compliance' },
|
|
220
|
+
cron: { url: 'http://127.0.0.1:3000/cron' },
|
|
221
|
+
scaler: { url: 'http://127.0.0.1:3000/scaler' }
|
|
222
|
+
},
|
|
223
|
+
config: {}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
t.after(async () => {
|
|
229
|
+
await icc.close()
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
const app = createMockApp(
|
|
233
|
+
{ PLT_APP_NAME: applicationName, PLT_APP_DIR: '/test/dir' },
|
|
234
|
+
{ provider: 'ecs', machineIdentity: { id: taskId, namespace: 'my-cluster' } }
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
await initPlugin(app)
|
|
238
|
+
|
|
239
|
+
// The podId in URL and body must be the ECS task id, not os.hostname()
|
|
240
|
+
// (which on ECS Fargate contains dots and would break ICC's URL routing).
|
|
241
|
+
equal(capturedRequest.params.podId, taskId)
|
|
242
|
+
equal(capturedRequest.body.podId, taskId)
|
|
243
|
+
equal(app.instanceId, taskId)
|
|
244
|
+
})
|
|
245
|
+
|
|
197
246
|
test('init plugin sends request without applicationName when not provided', async (t) => {
|
|
198
247
|
const applicationName = 'test-app-no-name'
|
|
199
248
|
const applicationId = randomUUID()
|
|
@@ -91,7 +91,7 @@ function createMockApp (port, includeScalerUrl = true, env = {}) {
|
|
|
91
91
|
applicationId: 'test-application-id'
|
|
92
92
|
},
|
|
93
93
|
instanceId: 'test-pod-123',
|
|
94
|
-
|
|
94
|
+
getAuthorizationHeaders: async () => {
|
|
95
95
|
return { Authorization: 'Bearer test-token' }
|
|
96
96
|
},
|
|
97
97
|
getRuntimeId: () => {
|
package/test/update.test.js
CHANGED
|
@@ -18,7 +18,7 @@ function createMockApp (port, options = {}) {
|
|
|
18
18
|
instanceConfig: {
|
|
19
19
|
applicationId: 'test-application-id',
|
|
20
20
|
},
|
|
21
|
-
|
|
21
|
+
getAuthorizationHeaders: async () => {
|
|
22
22
|
return { Authorization: 'Bearer test-token' }
|
|
23
23
|
},
|
|
24
24
|
getRuntimeId: () => {
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(node -e \"const schema = require\\(''''@platformatic/runtime/lib/schema.js''''\\); console.log\\(JSON.stringify\\(schema.default?.properties?.services?.items?.properties || schema.properties?.services?.items?.properties, null, 2\\)\\)\")"
|
|
5
|
-
]
|
|
6
|
-
}
|
|
7
|
-
}
|