@platformatic/watt-extra 1.13.0-alpha.1 → 1.13.0-alpha.3

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/lib/watt.js CHANGED
@@ -53,11 +53,15 @@ class Watt {
53
53
  }
54
54
 
55
55
  async spawn () {
56
+ this.#logger.info('[ECS-DEBUG] spawn: enter')
56
57
  try {
57
58
  this.runtime = await this.#createRuntime()
59
+ this.#logger.info('[ECS-DEBUG] spawn: createRuntime returned, about to call runtime.start()')
58
60
  this.#logger.info('Starting runtime -WATT')
59
61
  await this.runtime.start()
62
+ this.#logger.info('[ECS-DEBUG] spawn: runtime.start() returned')
60
63
  await this.updateSharedContext(this.#sharedContext)
64
+ this.#logger.info('[ECS-DEBUG] spawn: updateSharedContext returned')
61
65
  this.#logger.info('Runtime started')
62
66
  } catch (err) {
63
67
  this.#logger.error(
@@ -155,14 +159,17 @@ class Watt {
155
159
  }
156
160
 
157
161
  async #createRuntime () {
162
+ this.#logger.info('[ECS-DEBUG] createRuntime: enter')
158
163
  this.#logger.info('Creating runtime')
159
164
  const { create, transform } = this.#require('@platformatic/runtime')
160
165
 
166
+ this.#logger.info('[ECS-DEBUG] createRuntime: about to call platformatic create()')
161
167
  this.#logger.info('Creating runtime')
162
168
 
163
169
  const runtime = await create(this.#appDir, null, {
164
170
  isProduction: true,
165
171
  transform: async (config) => {
172
+ this.#logger.info('[ECS-DEBUG] createRuntime: transform called')
166
173
  config = await transform(config)
167
174
 
168
175
  this.#config = config
@@ -171,9 +178,11 @@ class Watt {
171
178
  this.#logger.info('Patching runtime config')
172
179
 
173
180
  this.#patchRuntimeConfig(config)
181
+ this.#logger.info('[ECS-DEBUG] createRuntime: transform done')
174
182
  return config
175
183
  }
176
184
  })
185
+ this.#logger.info('[ECS-DEBUG] createRuntime: platformatic create() returned')
177
186
 
178
187
  /* c8 ignore next 3 */
179
188
  const restartListener = restartRuntime.bind(null, runtime)
@@ -182,11 +191,15 @@ class Watt {
182
191
  process.removeListener('SIGUSR2', restartListener)
183
192
  })
184
193
 
194
+ this.#logger.info('[ECS-DEBUG] createRuntime: about to configureServices')
185
195
  await this.#configureServices(runtime)
196
+ this.#logger.info('[ECS-DEBUG] createRuntime: configureServices returned, about to runtime.init()')
186
197
 
187
198
  try {
188
199
  await runtime.init()
200
+ this.#logger.info('[ECS-DEBUG] createRuntime: runtime.init() returned')
189
201
  } catch (e) {
202
+ this.#logger.error({ err: ensureLoggableError(e) }, '[ECS-DEBUG] createRuntime: runtime.init() threw')
190
203
  await runtime.close()
191
204
  throw e
192
205
  }
@@ -508,13 +521,18 @@ class Watt {
508
521
  }
509
522
 
510
523
  async #configureServices (runtime) {
524
+ this.#logger.info('[ECS-DEBUG] configureServices: enter')
511
525
  if (typeof runtime.setApplicationConfigPatch !== 'function') {
526
+ this.#logger.info('[ECS-DEBUG] configureServices: runtime has no setApplicationConfigPatch, skipping')
512
527
  return
513
528
  }
514
529
 
515
530
  const config = runtime.getRuntimeConfig(true)
531
+ const apps = config.applications ?? []
532
+ this.#logger.info({ count: apps.length, apps: apps.map(a => ({ id: a.id, type: a.type, entrypoint: a.entrypoint })) }, '[ECS-DEBUG] configureServices: iterating apps')
516
533
 
517
- for (const app of config.applications ?? []) {
534
+ for (const app of apps) {
535
+ this.#logger.info({ id: app.id, type: app.type }, '[ECS-DEBUG] configureServices: handling app')
518
536
  if (app.type === 'next') {
519
537
  await this.#configureNextService(runtime, app)
520
538
  } else if (
@@ -528,19 +546,24 @@ class Watt {
528
546
  } else if (app.type === '@platformatic/regina') {
529
547
  await this.#configureReginaService(runtime, app)
530
548
  }
549
+ this.#logger.info({ id: app.id }, '[ECS-DEBUG] configureServices: done with app')
531
550
  }
551
+ this.#logger.info('[ECS-DEBUG] configureServices: all apps configured')
532
552
  }
533
553
 
534
554
  async #configureNextService (runtime, service) {
555
+ this.#logger.info({ id: service.id, path: service.path, entrypoint: service.entrypoint }, '[ECS-DEBUG] configureNextService: enter')
535
556
  let nextSchema
536
557
 
537
558
  try {
538
559
  const nextPackage = createRequire(
539
560
  resolve(service.path, 'index.js')
540
561
  ).resolve('@platformatic/next')
562
+ this.#logger.info({ nextPackage }, '[ECS-DEBUG] configureNextService: resolved @platformatic/next')
541
563
  nextSchema = JSON.parse(
542
564
  await readFile(resolve(nextPackage, '../schema.json'), 'utf8')
543
565
  )
566
+ this.#logger.info('[ECS-DEBUG] configureNextService: read next schema')
544
567
  } catch (e) {
545
568
  this.#logger.error(
546
569
  { err: ensureLoggableError(e) },
@@ -589,6 +612,7 @@ class Watt {
589
612
  }
590
613
 
591
614
  async #configurePlatformaticServices (runtime, app) {
615
+ this.#logger.info({ id: app.id, entrypoint: app.entrypoint, type: app.type }, '[ECS-DEBUG] configurePlatformaticServices: enter')
592
616
  if (app.entrypoint) {
593
617
  const config = app
594
618
  const patches = [{ op: 'add', path: '/server/trustProxy', value: true }]
@@ -601,20 +625,27 @@ class Watt {
601
625
 
602
626
  this.#patchService(runtime, app.id, patches)
603
627
  }
628
+ this.#logger.info({ id: app.id }, '[ECS-DEBUG] configurePlatformaticServices: exit')
604
629
  }
605
630
 
606
631
  async #configureReginaService (runtime, app) {
632
+ this.#logger.info({ id: app.id }, '[ECS-DEBUG] configureReginaService: enter')
607
633
  const privateIp = await this.#getPrivateIp()
608
634
  const port = this.#getRuntimePort()
609
635
  const address = `http://${privateIp}:${port}`
636
+ this.#logger.info({ id: app.id, address }, '[ECS-DEBUG] configureReginaService: applying patch')
610
637
 
611
638
  this.#patchService(runtime, app.id, [
612
639
  { op: 'add', path: '/regina/memberAddress', value: address }
613
640
  ])
641
+ this.#logger.info({ id: app.id }, '[ECS-DEBUG] configureReginaService: exit')
614
642
  }
615
643
 
616
644
  async #getPrivateIp () {
617
- const { address } = await dns.lookup(os.hostname())
645
+ const hostname = os.hostname()
646
+ this.#logger.info({ hostname }, '[ECS-DEBUG] getPrivateIp: about to dns.lookup')
647
+ const { address } = await dns.lookup(hostname)
648
+ this.#logger.info({ hostname, address }, '[ECS-DEBUG] getPrivateIp: resolved')
618
649
  return address
619
650
  }
620
651
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/watt-extra",
3
- "version": "1.13.0-alpha.1",
3
+ "version": "1.13.0-alpha.3",
4
4
  "description": "The Platformatic runtime manager",
5
5
  "type": "module",
6
6
  "scripts": {
package/plugins/auth.js CHANGED
@@ -43,12 +43,20 @@ async function loadK8sToken (log) {
43
43
  // endpoint. K8s identity travels via the SA JWT, so no resolution needed there.
44
44
  async function resolveEcsIdentity (log) {
45
45
  const metadataUrl = `${process.env.ECS_CONTAINER_METADATA_URI_V4}/task`
46
+ log.info({ metadataUrl }, '[ECS-DEBUG] resolveEcsIdentity: fetching task metadata')
46
47
  try {
47
48
  const res = await fetch(metadataUrl)
49
+ log.info({ status: res.status, ok: res.ok }, '[ECS-DEBUG] resolveEcsIdentity: metadata fetch returned')
48
50
  if (!res.ok) throw new Error(`status ${res.status}`)
49
51
  const meta = await res.json()
52
+ log.info({ TaskARN: meta.TaskARN, Cluster: meta.Cluster, Family: meta.Family, LaunchType: meta.LaunchType }, '[ECS-DEBUG] resolveEcsIdentity: parsed metadata')
50
53
  const id = meta.TaskARN?.split('/').pop()
51
- const namespace = meta.Cluster
54
+ // meta.Cluster may be either the short name or the full cluster ARN
55
+ // (e.g. 'arn:aws:ecs:us-east-1:123456789012:cluster/my-cluster'). We
56
+ // strip down to the short name so callers can interpolate it into
57
+ // URL paths without producing extra path segments.
58
+ const cluster = meta.Cluster
59
+ const namespace = cluster?.includes('/') ? cluster.split('/').pop() : cluster
52
60
  if (!id || !namespace) throw new Error('TaskARN or Cluster missing in metadata')
53
61
  log.info({ id, namespace }, 'Resolved ECS task identity')
54
62
  return { id, namespace }
package/plugins/init.js CHANGED
@@ -26,6 +26,14 @@ async function initPlugin (app) {
26
26
  const applicationDir = app.env.PLT_APP_DIR
27
27
  const instanceId = app.provider === 'k8s' ? os.hostname() : app.machineIdentity?.id
28
28
 
29
+ app.log.info({
30
+ provider: app.provider,
31
+ hostname: os.hostname(),
32
+ machineIdentity: app.machineIdentity,
33
+ instanceId,
34
+ applicationDir
35
+ }, '[ECS-DEBUG] init: computed instanceId')
36
+
29
37
  app.log.info({ applicationName, applicationDir }, 'Loading watt-extra application')
30
38
 
31
39
  // Skip ICC initialization if PLT_ICC_URL is not set
@@ -39,6 +47,14 @@ async function initPlugin (app) {
39
47
 
40
48
  const instanceConfig = await initApplicationInstance(instanceId, applicationName)
41
49
  app.log.info({ applicationId: instanceConfig.applicationId }, 'Got application info')
50
+ app.log.info({
51
+ iccServices: instanceConfig.iccServices,
52
+ enableSlicerInterceptor: instanceConfig.enableSlicerInterceptor,
53
+ enableTrafficInterceptor: instanceConfig.enableTrafficInterceptor,
54
+ enableOpenTelemetry: instanceConfig.enableOpenTelemetry,
55
+ httpCacheKeys: Object.keys(instanceConfig.httpCache?.clientOpts || {}),
56
+ configKeys: Object.keys(instanceConfig.config || {})
57
+ }, '[ECS-DEBUG] init: ICC instanceConfig details')
42
58
 
43
59
  instanceConfig.scaler ??= { version: 'v1' }
44
60
 
package/test/auth.test.js CHANGED
@@ -214,3 +214,42 @@ test('auth plugin sends ECS identity headers when running on ECS', async (t) =>
214
214
  equal(responseBody.headers['x-ecs-cluster'], 'my-cluster')
215
215
  equal(responseBody.headers.authorization, undefined, 'No Authorization header on ECS')
216
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
+ })