@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/watt-extra",
3
- "version": "1.13.0-alpha.0",
3
+ "version": "1.13.0-alpha.2",
4
4
  "description": "The Platformatic runtime manager",
5
5
  "type": "module",
6
6
  "scripts": {
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
- const namespace = meta.Cluster
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
@@ -63,6 +63,7 @@ async function envPlugin (app) {
63
63
  ...env
64
64
  }
65
65
 
66
+ app.provider = process.env.ECS_CONTAINER_METADATA_URI_V4 ? 'ecs' : 'k8s'
66
67
  app.log.info('Environment variables set up')
67
68
  }
68
69
 
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
- logMessages
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()