@platformatic/runtime 3.71.1-alpha.0 → 4.0.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.
@@ -14,7 +14,8 @@ import { platform, tmpdir } from 'node:os'
14
14
  import { dirname, join } from 'node:path'
15
15
  import { setTimeout as sleep } from 'node:timers/promises'
16
16
  import { createWebSocketStream } from 'ws'
17
- import { prepareApplication } from './config.js'
17
+ import { prepareAddedApplications } from './config.js'
18
+ import { readMemory } from './memory.js'
18
19
 
19
20
  const PLATFORMATIC_TMP_DIR = join(tmpdir(), 'platformatic', 'runtimes')
20
21
 
@@ -99,18 +100,6 @@ export async function managementApiPlugin (app, opts) {
99
100
  return runtime.getRuntimeMetadata()
100
101
  })
101
102
 
102
- app.get('/config', quiet, async request => {
103
- const metadata = request.query.metadata === 'true'
104
- const rawConfig = await runtime.getRuntimeConfig(metadata)
105
-
106
- if (metadata) {
107
- const { [kMetadata]: __metadata, ...config } = rawConfig
108
- return { ...config, __metadata }
109
- }
110
-
111
- return rawConfig
112
- })
113
-
114
103
  app.get('/env', quiet, async () => {
115
104
  return { ...process.env, ...runtime.getRuntimeEnv() }
116
105
  })
@@ -147,6 +136,20 @@ export async function managementApiPlugin (app, opts) {
147
136
  return runtime.getApplications()
148
137
  })
149
138
 
139
+ app.get('/memory', quiet, async () => {
140
+ return readMemory()
141
+ })
142
+
143
+ app.get('/workers/scaler', quiet, async (_request, reply) => {
144
+ const scaler = runtime.getDynamicWorkersScaler()
145
+ if (!scaler) {
146
+ reply.code(404)
147
+ return { statusCode: 404, error: 'Not Found', message: 'Dynamic workers scaling is not enabled.' }
148
+ }
149
+
150
+ return scaler.getDiagnostics()
151
+ })
152
+
150
153
  app.post('/applications', async (request, reply) => {
151
154
  let applications = request.body
152
155
 
@@ -169,11 +172,9 @@ export async function managementApiPlugin (app, opts) {
169
172
  }
170
173
  }
171
174
 
172
- for (let i = 0; i < applications.length; i++) {
173
- applications[i] = await prepareApplication(config, applications[i], config.workers)
174
- }
175
+ const prepared = await prepareAddedApplications(config, applications, runtime.getApplicationsIds())
175
176
 
176
- const created = await runtime.addApplications(applications, request.query.start !== 'false')
177
+ const created = await runtime.addApplications(prepared, request.query.start !== 'false')
177
178
  reply.code(201)
178
179
  return created
179
180
  })
@@ -303,7 +304,10 @@ export async function managementApiPlugin (app, opts) {
303
304
  })
304
305
 
305
306
  app.get('/metrics', quiet, async (req, reply) => {
306
- const config = await runtime.getRuntimeConfig()
307
+ // The live read: this needs one boolean, and the public getter builds a frozen snapshot of the
308
+ // whole configuration -- which is the right shape to hand a consumer and the wrong one to pay
309
+ // for on a scrape.
310
+ const config = runtime.getRuntimeConfig(true)
307
311
 
308
312
  if (config.metrics?.enabled === false) {
309
313
  reply.code(501)
@@ -326,53 +330,6 @@ export async function managementApiPlugin (app, opts) {
326
330
  return metrics
327
331
  })
328
332
 
329
- // TODO: Remove in next major version - deprecated endpoint
330
- app.get('/metrics/live', { ...quiet, websocket: true }, async socket => {
331
- const config = await runtime.getRuntimeConfig()
332
-
333
- if (config.metrics?.enabled === false) {
334
- socket.send(
335
- JSON.stringify({
336
- statusCode: 501,
337
- error: 'Not Implemented',
338
- message: 'Metrics are disabled.'
339
- }),
340
- () => {
341
- socket.close()
342
- }
343
- )
344
-
345
- return
346
- }
347
-
348
- const pollAndSendMetrics = async () => {
349
- try {
350
- const metrics = await runtime.getFormattedMetrics()
351
- if (metrics) {
352
- const serializedMetrics = JSON.stringify(metrics)
353
- socket.send(serializedMetrics + '\n')
354
- }
355
- } catch (error) {
356
- // If there's an error, stop polling and close the connection
357
- clearInterval(pollingInterval)
358
- socket.close()
359
- }
360
- }
361
-
362
- // Poll every second
363
- const pollingInterval = setInterval(pollAndSendMetrics, 1000)
364
-
365
- // Send initial metrics immediately
366
- await pollAndSendMetrics()
367
-
368
- const cleanup = () => {
369
- clearInterval(pollingInterval)
370
- }
371
-
372
- socket.on('error', cleanup)
373
- socket.on('close', cleanup)
374
- })
375
-
376
333
  app.get('/logs/live', { ...quiet, websocket: true }, async socket => {
377
334
  runtime.addLoggerDestination(createWebSocketStream(socket))
378
335
  })
@@ -3,7 +3,7 @@ import {
3
3
  kMetadata,
4
4
  validate
5
5
  } from '@platformatic/foundation'
6
- import { prepareApplication } from './config.js'
6
+ import { prepareAddedApplications } from './config.js'
7
7
 
8
8
  const allOperations = [
9
9
  'getRuntimeStatus',
@@ -138,11 +138,9 @@ export function createManagementHandlers (runtime, allowedOperations) {
138
138
 
139
139
  validate(applicationSchema, applications, {}, true, config[kMetadata].root)
140
140
 
141
- for (let i = 0; i < applications.length; i++) {
142
- applications[i] = await prepareApplication(config, applications[i], config.workers)
143
- }
141
+ const prepared = await prepareAddedApplications(config, applications, runtime.getApplicationsIds())
144
142
 
145
- return runtime.addApplications(applications, start)
143
+ return runtime.addApplications(prepared, start)
146
144
  })
147
145
 
148
146
  register('removeApplications', async ({ ids }) => {
package/lib/memory.js ADDED
@@ -0,0 +1,29 @@
1
+ import { getMemoryInfo } from './metrics.js'
2
+
3
+ // Memory readings are shared for ttlMs, and concurrent callers share one read.
4
+ // The scope (host or cgroup) is detected on the first read and then reused.
5
+ export function createMemoryReader ({ read = getMemoryInfo, ttlMs = 1000 } = {}) {
6
+ let scope
7
+ let reading = null
8
+ let readAt = 0
9
+ let pending = null
10
+
11
+ return function readMemory () {
12
+ if (reading && Date.now() - readAt < ttlMs) return Promise.resolve(reading)
13
+
14
+ pending ??= read({ scope })
15
+ .then(info => {
16
+ scope = info.scope
17
+ reading = info
18
+ readAt = Date.now()
19
+ return info
20
+ })
21
+ .finally(() => {
22
+ pending = null
23
+ })
24
+ return pending
25
+ }
26
+ }
27
+
28
+ // Shared by every caller in the process, so they share one cached reading.
29
+ export const readMemory = createMemoryReader()
package/lib/policies.js CHANGED
@@ -21,3 +21,30 @@ export function createChannelCreationHook (config) {
21
21
  return !forbidden.has(`${first.toLowerCase()}:${second.toLowerCase()}`)
22
22
  }
23
23
  }
24
+
25
+ export function createTargetPermissionHook (config) {
26
+ const denyList = config.policies?.deny
27
+
28
+ if (typeof denyList === 'undefined') {
29
+ return undefined
30
+ }
31
+
32
+ const forbidden = new Set()
33
+
34
+ for (let [first, unalloweds] of Object.entries(denyList)) {
35
+ for (let second of unalloweds) {
36
+ first = first.toLowerCase()
37
+ second = second.toLowerCase()
38
+
39
+ forbidden.add(`${first}:${second}`)
40
+ forbidden.add(`${second}:${first}`)
41
+ }
42
+ }
43
+
44
+ return function targetPermissionHook (req, target) {
45
+ const source = config.applicationId ?? req?.headers?.['x-plt-application-id']
46
+ const destination = target?.metadata?.applicationId
47
+
48
+ return !source || !destination || !forbidden.has(`${source.toLowerCase()}:${destination.toLowerCase()}`)
49
+ }
50
+ }