@platformatic/watt-extra 1.13.0-alpha.0 → 1.13.0-alpha.2
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/auth.js +7 -9
- package/plugins/env.js +1 -0
- package/plugins/init.js +2 -2
- package/test/auth.test.js +42 -4
- package/test/init.test.js +51 -2
package/package.json
CHANGED
package/plugins/auth.js
CHANGED
|
@@ -23,12 +23,6 @@ function isTokenExpired (token, offset = 0) {
|
|
|
23
23
|
return payload.exp <= Math.floor(Date.now() / 1000) + offset
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
function detectProvider () {
|
|
27
|
-
if (process.env.ECS_CONTAINER_METADATA_URI_V4) return 'ecs'
|
|
28
|
-
if (process.env.KUBERNETES_SERVICE_HOST) return 'k8s'
|
|
29
|
-
return 'k8s' // safe default
|
|
30
|
-
}
|
|
31
|
-
|
|
32
26
|
async function loadK8sToken (log) {
|
|
33
27
|
let token
|
|
34
28
|
try {
|
|
@@ -54,7 +48,12 @@ async function resolveEcsIdentity (log) {
|
|
|
54
48
|
if (!res.ok) throw new Error(`status ${res.status}`)
|
|
55
49
|
const meta = await res.json()
|
|
56
50
|
const id = meta.TaskARN?.split('/').pop()
|
|
57
|
-
|
|
51
|
+
// meta.Cluster may be either the short name or the full cluster ARN
|
|
52
|
+
// (e.g. 'arn:aws:ecs:us-east-1:123456789012:cluster/my-cluster'). We
|
|
53
|
+
// strip down to the short name so callers can interpolate it into
|
|
54
|
+
// URL paths without producing extra path segments.
|
|
55
|
+
const cluster = meta.Cluster
|
|
56
|
+
const namespace = cluster?.includes('/') ? cluster.split('/').pop() : cluster
|
|
58
57
|
if (!id || !namespace) throw new Error('TaskARN or Cluster missing in metadata')
|
|
59
58
|
log.info({ id, namespace }, 'Resolved ECS task identity')
|
|
60
59
|
return { id, namespace }
|
|
@@ -105,9 +104,8 @@ async function createEcsStrategy (app) {
|
|
|
105
104
|
async function authPlugin (app) {
|
|
106
105
|
// 1 min offset to refresh the token before it actually expires.
|
|
107
106
|
const offset = parseInt(process.env.PLT_JWT_EXPIRATION_OFFSET_SEC ?? 0)
|
|
108
|
-
const provider = detectProvider()
|
|
109
107
|
|
|
110
|
-
const getProviderHeaders = provider === 'ecs'
|
|
108
|
+
const getProviderHeaders = app.provider === 'ecs'
|
|
111
109
|
? await createEcsStrategy(app)
|
|
112
110
|
: await createK8sStrategy(app, offset)
|
|
113
111
|
|
package/plugins/env.js
CHANGED
package/plugins/init.js
CHANGED
|
@@ -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,7 +73,7 @@ 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)
|
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: () => {},
|
|
@@ -188,8 +189,6 @@ test('auth plugin sends ECS identity headers when running on ECS', async (t) =>
|
|
|
188
189
|
const metadataUrl = `http://localhost:${metadata.server.address().port}`
|
|
189
190
|
t.after(() => metadata.close())
|
|
190
191
|
|
|
191
|
-
// Make detectProvider() pick ECS.
|
|
192
|
-
delete process.env.KUBERNETES_SERVICE_HOST
|
|
193
192
|
process.env.ECS_CONTAINER_METADATA_URI_V4 = metadataUrl
|
|
194
193
|
|
|
195
194
|
const server = fastify()
|
|
@@ -200,7 +199,7 @@ test('auth plugin sends ECS identity headers when running on ECS', async (t) =>
|
|
|
200
199
|
const url = `http://localhost:${server.server.address().port}`
|
|
201
200
|
t.after(() => server.close())
|
|
202
201
|
|
|
203
|
-
const app = createMockApp()
|
|
202
|
+
const app = createMockApp('ecs')
|
|
204
203
|
await authPlugin(app)
|
|
205
204
|
|
|
206
205
|
equal(app.machineIdentity?.id, 'abcdef0123', 'Task id should be the TaskARN suffix')
|
|
@@ -215,3 +214,42 @@ test('auth plugin sends ECS identity headers when running on ECS', async (t) =>
|
|
|
215
214
|
equal(responseBody.headers['x-ecs-cluster'], 'my-cluster')
|
|
216
215
|
equal(responseBody.headers.authorization, undefined, 'No Authorization header on ECS')
|
|
217
216
|
})
|
|
217
|
+
|
|
218
|
+
test('auth plugin strips cluster ARN to short name in x-ecs-cluster header', async (t) => {
|
|
219
|
+
const originalEnv = { ...process.env }
|
|
220
|
+
|
|
221
|
+
t.after(() => {
|
|
222
|
+
process.env = originalEnv
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
// ECS task metadata sometimes returns the full cluster ARN in the Cluster
|
|
226
|
+
// field. ICC interpolates this value into URL paths, so the short name
|
|
227
|
+
// (which contains no slashes) is the only safe form to send.
|
|
228
|
+
const metadata = fastify()
|
|
229
|
+
metadata.get('/task', async () => ({
|
|
230
|
+
TaskARN: 'arn:aws:ecs:us-east-1:123456789012:task/my-cluster/abcdef0123',
|
|
231
|
+
Cluster: 'arn:aws:ecs:us-east-1:123456789012:cluster/my-cluster'
|
|
232
|
+
}))
|
|
233
|
+
await metadata.listen({ port: 0 })
|
|
234
|
+
const metadataUrl = `http://localhost:${metadata.server.address().port}`
|
|
235
|
+
t.after(() => metadata.close())
|
|
236
|
+
|
|
237
|
+
process.env.ECS_CONTAINER_METADATA_URI_V4 = metadataUrl
|
|
238
|
+
|
|
239
|
+
const server = fastify()
|
|
240
|
+
server.get('/', async (request) => {
|
|
241
|
+
return { headers: request.headers }
|
|
242
|
+
})
|
|
243
|
+
await server.listen({ port: 0 })
|
|
244
|
+
const url = `http://localhost:${server.server.address().port}`
|
|
245
|
+
t.after(() => server.close())
|
|
246
|
+
|
|
247
|
+
const app = createMockApp('ecs')
|
|
248
|
+
await authPlugin(app)
|
|
249
|
+
|
|
250
|
+
equal(app.machineIdentity?.namespace, 'my-cluster', 'Namespace should be stripped to cluster short name')
|
|
251
|
+
|
|
252
|
+
const response = await request(url, { dispatcher: app.dispatcher })
|
|
253
|
+
const responseBody = await response.body.json()
|
|
254
|
+
equal(responseBody.headers['x-ecs-cluster'], 'my-cluster')
|
|
255
|
+
})
|
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: {
|
|
@@ -21,7 +21,9 @@ const createMockApp = (env = {}) => {
|
|
|
21
21
|
error: () => {}
|
|
22
22
|
},
|
|
23
23
|
getAuthorizationHeaders: async () => 'Bearer test-token',
|
|
24
|
-
|
|
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()
|