@platformatic/runtime 3.72.0 → 4.0.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.
@@ -3,18 +3,22 @@ import fastifyWebsocket from '@fastify/websocket'
3
3
  import {
4
4
  applications as applicationSchema,
5
5
  createDirectory,
6
+ createValidator,
6
7
  createSharedTemporaryDirectory,
7
8
  kMetadata,
8
9
  safeRemove,
9
10
  validate
10
11
  } from '@platformatic/foundation'
12
+ import { schemaComponents } from '@platformatic/foundation/schema'
11
13
  import fastify from 'fastify'
12
14
  import { chmod } from 'node:fs/promises'
13
15
  import { platform, tmpdir } from 'node:os'
14
16
  import { dirname, join } from 'node:path'
15
17
  import { setTimeout as sleep } from 'node:timers/promises'
16
18
  import { createWebSocketStream } from 'ws'
17
- import { prepareApplication } from './config.js'
19
+ import { prepareAddedApplications } from './config.js'
20
+ import { readMemory } from './memory.js'
21
+ import { MetricsDisabledError } from './errors.js'
18
22
 
19
23
  const PLATFORMATIC_TMP_DIR = join(tmpdir(), 'platformatic', 'runtimes')
20
24
 
@@ -99,18 +103,6 @@ export async function managementApiPlugin (app, opts) {
99
103
  return runtime.getRuntimeMetadata()
100
104
  })
101
105
 
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
106
  app.get('/env', quiet, async () => {
115
107
  return { ...process.env, ...runtime.getRuntimeEnv() }
116
108
  })
@@ -147,6 +139,20 @@ export async function managementApiPlugin (app, opts) {
147
139
  return runtime.getApplications()
148
140
  })
149
141
 
142
+ app.get('/memory', quiet, async () => {
143
+ return readMemory()
144
+ })
145
+
146
+ app.get('/workers/scaler', quiet, async (_request, reply) => {
147
+ const scaler = runtime.getDynamicWorkersScaler()
148
+ if (!scaler) {
149
+ reply.code(404)
150
+ return { statusCode: 404, error: 'Not Found', message: 'Dynamic workers scaling is not enabled.' }
151
+ }
152
+
153
+ return scaler.getDiagnostics()
154
+ })
155
+
150
156
  app.post('/applications', async (request, reply) => {
151
157
  let applications = request.body
152
158
 
@@ -169,11 +175,9 @@ export async function managementApiPlugin (app, opts) {
169
175
  }
170
176
  }
171
177
 
172
- for (let i = 0; i < applications.length; i++) {
173
- applications[i] = await prepareApplication(config, applications[i], config.workers)
174
- }
178
+ const prepared = await prepareAddedApplications(config, applications, runtime.getApplicationsIds())
175
179
 
176
- const created = await runtime.addApplications(applications, request.query.start !== 'false')
180
+ const created = await runtime.addApplications(prepared, request.query.start !== 'false')
177
181
  reply.code(201)
178
182
  return created
179
183
  })
@@ -302,16 +306,31 @@ export async function managementApiPlugin (app, opts) {
302
306
  return reply
303
307
  })
304
308
 
309
+ const metricsSchema = schemaComponents.runtimeProperties.metrics.anyOf[1]
310
+ app.patch('/metrics', {
311
+ schema: {
312
+ body: {
313
+ ...metricsSchema,
314
+ minProperties: 1,
315
+ properties: { ...metricsSchema.properties, enabled: { type: 'boolean' } }
316
+ }
317
+ },
318
+ // A patch must not inject defaults, coerce values, or silently discard unknown options.
319
+ validatorCompiler: ({ schema }) => createValidator(schema, {
320
+ useDefaults: false,
321
+ coerceTypes: false,
322
+ removeAdditional: false
323
+ }, { root: runtime.getRuntimeConfig(true)[kMetadata].root })
324
+ }, async req => runtime.updateMetricsConfig(req.body))
325
+
305
326
  app.get('/metrics', quiet, async (req, reply) => {
306
- const config = await runtime.getRuntimeConfig()
327
+ // The live read: this needs one boolean, and the public getter builds a frozen snapshot of the
328
+ // whole configuration -- which is the right shape to hand a consumer and the wrong one to pay
329
+ // for on a scrape.
330
+ const config = runtime.getRuntimeConfig(true)
307
331
 
308
- if (config.metrics?.enabled === false) {
309
- reply.code(501)
310
- return {
311
- statusCode: 501,
312
- error: 'Not Implemented',
313
- message: 'Metrics are disabled.'
314
- }
332
+ if (config.metrics === false || config.metrics?.enabled === false) {
333
+ throw new MetricsDisabledError()
315
334
  }
316
335
 
317
336
  const accepts = req.accepts()
@@ -326,53 +345,6 @@ export async function managementApiPlugin (app, opts) {
326
345
  return metrics
327
346
  })
328
347
 
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
348
  app.get('/logs/live', { ...quiet, websocket: true }, async socket => {
377
349
  runtime.addLoggerDestination(createWebSocketStream(socket))
378
350
  })
@@ -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
+ }